diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000000..bc0b17da2f8 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,39 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/dotnet +{ + "name": "C# (.NET)", + "image": "mcr.microsoft.com/devcontainers/dotnet:0-6.0", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/dhoeric/features/act:1": {}, + "ghcr.io/devcontainers/features/dotnet:1": { + "version": "7" + } + }, + "customizations": { + "vscode": { + "extensions": [ + "ms-dotnettools.csharp", + "k--kato.docomment", + "formulahendry.dotnet-test-explorer", + "leo-labs.dotnet", + "github.vscode-pull-request-github" + ] + } + } + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [5000, 5001], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "dotnet restore", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/.editorconfig b/.editorconfig index 6bd1ca1b5e4..e8366c4a0f4 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,6 +15,7 @@ indent_size = 2 [*.cs] end_of_line = lf +dotnet_diagnostic.NUnit1032.severity = suggestion [*.md] trim_trailing_whitespace = false @@ -24,3 +25,4 @@ end_of_line = lf [*.{cmd, bat}] end_of_line = crlf + diff --git a/.gitattributes b/.gitattributes index 68ead8618fa..6e127ce325c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,18 +1,6 @@ # Set default behavior to automatically normalize line endings. * text=auto -*.doc diff=astextplain -*.DOC diff=astextplain -*.docx diff=astextplain -*.DOCX diff=astextplain -*.dot diff=astextplain -*.DOT diff=astextplain -*.pdf diff=astextplain -*.PDF diff=astextplain -*.rtf diff=astextplain -*.RTF diff=astextplain -*.md diff=astextplain - *.jpg binary *.png binary *.gif binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index f277a2db967..bdf43b8594e 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -1,5 +1,5 @@ name: Bug Report -description: Create a bug report for an issue found in MassTransit +description: Create a bug report for an issue found in MassTransit. Should not be used for questions on the use of MassTransit. body: - type: markdown attributes: diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 1b620096adc..656ec73dfd5 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,14 +1,14 @@ blank_issues_enabled: false contact_links: - name: Commercial Support - url: https://masstransit-project.com/learn/support.html - about: For commerial support services, including architectural guidance, code review, and troubleshooting + url: https://masstransit.io/support + about: Commercial support, the official support services for MassTransit customers - name: GitHub Discussions url: https://github.com/MassTransit/MassTransit/discussions - about: Submit questions, ideas, or general conversation topics - - name: Live Chat (on Discord) + about: Questions on the use of MassTransit, use search to find existing discussions + - name: Discord url: https://discord.gg/rNpQgYn - about: Live chat room for questions and conversation + about: Active chat room for MassTransit-related conversations. Should not be used for posting code snippets (use GitHub Discusssions) - name: Stack Overflow url: https://stackoverflow.com/questions/tagged/masstransit - about: Questions and answers, search to see if your question has already been answered \ No newline at end of file + about: Search for existing questions and answers, should not be used for asking support-related questions. \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f3e505f8672..6d2ab8f48f9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,6 @@ name: MassTransit env: - MASSTRANSIT_VERSION: 8.0.13 - DOTNET_CLI_TELEMETRY_OPTOUT: 1 - DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: true + MASSTRANSIT_VERSION: 8.3.1 on: push: paths: @@ -10,6 +8,7 @@ on: - 'tests/**' - 'MassTransit.sln' - 'Directory.Build.props' + - 'Directory.Packages.props' - '**/build.yml' pull_request: paths: @@ -17,13 +16,14 @@ on: - 'tests/**' - 'MassTransit.sln' - 'Directory.Build.props' + - 'Directory.Packages.props' - '**/build.yml' workflow_dispatch: jobs: compile: name: Build - timeout-minutes: 10 + timeout-minutes: 15 strategy: max-parallel: 2 matrix: @@ -31,12 +31,12 @@ jobs: runs-on: ${{ matrix.os }} steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Restore NuGet packages run: dotnet restore @@ -47,7 +47,7 @@ jobs: working-directory: ./ - name: Test Analyzers - run: dotnet test -c Release --logger:"console;verbosity=normal" --no-build --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --no-build --filter Category!=Flaky working-directory: tests/MassTransit.Analyzers.Tests test-ubuntu: @@ -56,38 +56,21 @@ jobs: timeout-minutes: 10 steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Unit Tests - run: dotnet test -c Release -f net6.0 --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release -f net8.0 --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.Tests - name: Test Abstractions - run: dotnet test -c Release -f net6.0 --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release -f net8.0 --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.Abstractions.Tests - test-containers: - name: Container Tests - runs-on: ubuntu-latest - timeout-minutes: 10 - steps: - - name: Check out code - uses: actions/checkout@v3 - - - name: Install .NET Core SDK 6.0 - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '6.0.x' - - - name: Test Containers - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky - working-directory: tests/MassTransit.Containers.Tests - test-activemq: name: "Transports: ActiveMQ" timeout-minutes: 10 @@ -100,51 +83,70 @@ jobs: - "8161:8161" steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test ActiveMQ - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.ActiveMqTransport.Tests - test-grpc: - name: "Transports: gRPC" + test-sql-transport: + name: "Transport: SQL" timeout-minutes: 10 runs-on: ubuntu-latest + services: + mssql: + image: mcr.microsoft.com/azure-sql-edge + env: + ACCEPT_EULA: Y + SA_PASSWORD: "Password12!" + ports: + - 1433:1433 + postgres: + image: postgres:14.7 + env: + POSTGRES_PASSWORD: "Password12!" + ports: + - 5432:5432 + options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 + env: + DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: false steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' + + - name: Test Database Transport + run: dotnet test -c Release --logger GitHubActions --filter "Category!=Flaky&Category!=Integration" + working-directory: tests/MassTransit.SqlTransport.Tests + - - name: Test gRPC - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky - working-directory: tests/MassTransit.GrpcTransport.Tests test-azure-service-bus: name: "Transports: Azure Service Bus" if: false # too flaky at this point runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test Azure Service Bus env: MT_ASB_KEYVALUE: ${{ secrets.AZURE_SERVICEBUS }} MT_AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE }} - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.Azure.ServiceBus.Core.Tests test-rabbitmq: name: "Transports: RabbitMQ" @@ -159,58 +161,66 @@ jobs: options: --health-cmd "rabbitmqctl node_health_check" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test RabbitMQ - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.RabbitMqTransport.Tests test-sqs: - name: "Transports: SQS" + name: "Transports: SQS (+S3)" timeout-minutes: 10 runs-on: ubuntu-latest services: localstack: - image: localstack/localstack:0.12.17.5 + image: localstack/localstack:3.0.2 ports: - "4566:4566" - "4571:4571" - - "8080:8080" - options: --health-cmd "curl --fail http://localhost:4566/health || exit 1" --health-interval 10s --health-timeout 5s --health-retries 5 + options: --health-cmd "curl --fail http://localhost:4566/health || exit 1" --health-interval 10s --health-timeout 5s --health-retries 5 --health-start-period 15s steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test SQS - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.AmazonSqsTransport.Tests test-azure-table: name: "Storage: Azure Table" - if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/v8') && github.repository == 'MassTransit/MassTransit' timeout-minutes: 10 runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' + + - name: Spin up test environment + run: | + docker compose -f docker-compose.yml up -d + working-directory: tests/MassTransit.Azure.Table.Tests - name: Test Azure Table env: MT_AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE }} - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter "Category!=Flaky&Category!=Integration" + run: dotnet test -c Release --logger GitHubActions --filter "Category!=Flaky&Category!=Integration" + working-directory: tests/MassTransit.Azure.Table.Tests + + - name: Stop test environment + run: | + docker compose -f docker-compose.yml down working-directory: tests/MassTransit.Azure.Table.Tests test-cosmosdb: name: "Storage: CosmosDB" @@ -219,18 +229,18 @@ jobs: if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/v8') && github.repository == 'MassTransit/MassTransit' steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test CosmosDB env: MT_COSMOS_ENDPOINT: ${{ secrets.AZURE_COSMOSENDPOINT }} MT_COSMOS_KEY: ${{ secrets.AZURE_COSMOSKEY }} - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter "Category!=Flaky&Category!=Integration" + run: dotnet test -c Release --logger GitHubActions --filter "Category!=Flaky&Category!=Integration" working-directory: tests/MassTransit.Azure.Cosmos.Tests test-dapper: name: "Storage: Dapper" @@ -238,7 +248,7 @@ jobs: runs-on: ubuntu-latest services: mssql: - image: mcr.microsoft.com/mssql/server:2017-latest + image: mcr.microsoft.com/azure-sql-edge env: ACCEPT_EULA: Y SA_PASSWORD: "Password12!" @@ -249,15 +259,15 @@ jobs: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: false steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test Dapper - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter "Category!=Flaky&Category!=Integration" + run: dotnet test -c Release --logger GitHubActions --filter "Category!=Flaky&Category!=Integration" working-directory: tests/MassTransit.DapperIntegration.Tests test-entity-framework: name: "Storage: EntityFramework" @@ -265,7 +275,7 @@ jobs: runs-on: ubuntu-latest services: mssql: - image: mcr.microsoft.com/mssql/server:2017-latest + image: mcr.microsoft.com/azure-sql-edge env: ACCEPT_EULA: Y SA_PASSWORD: "Password12!" @@ -283,19 +293,19 @@ jobs: DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: false steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - - name: Test EntityFrameworkCore 6.0 - run: dotnet test -c Release --logger:"console;verbosity=normal" -f net6.0 --filter "Category!=Flaky&Category!=Integration" + - name: Test EntityFrameworkCore + run: dotnet test -c Release --logger GitHubActions --filter "Category!=Flaky&Category!=Integration" working-directory: tests/MassTransit.EntityFrameworkCoreIntegration.Tests - name: Test EntityFramework - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter "Category!=Flaky&Category!=Integration" + run: dotnet test -c Release --logger GitHubActions --filter "Category!=Flaky&Category!=Integration" working-directory: tests/MassTransit.EntityFrameworkIntegration.Tests test-marten: name: "Storage: Marten" @@ -311,36 +321,41 @@ jobs: options: --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test Marten - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.MartenIntegration.Tests test-mongo: name: "Storage: MongoDB" timeout-minutes: 10 runs-on: ubuntu-latest - services: - mongo: - image: mongo - ports: - - '27017-27019:27017-27019' steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' + + - name: Spin up test environment + run: | + docker compose -f docker-compose.yml up -d + working-directory: tests/MassTransit.MongoDbIntegration.Tests - name: Test MongoDB - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky + working-directory: tests/MassTransit.MongoDbIntegration.Tests + + - name: Stop test environment + run: | + docker compose -f docker-compose.yml down working-directory: tests/MassTransit.MongoDbIntegration.Tests test-nhibernate: name: "Storage: NHibernate" @@ -348,15 +363,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test NHibernate - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.NHibernateIntegration.Tests test-redis: name: "Storage: Redis" @@ -370,15 +385,37 @@ jobs: options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test Redis - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky + working-directory: tests/MassTransit.RedisIntegration.Tests + test-valkey: + name: "Storage: Valkey" + timeout-minutes: 10 + runs-on: ubuntu-latest + services: + valkey: + image: valkey/valkey:8 + ports: + - '6379:6379' + options: --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 + steps: + - name: Check out code + uses: actions/checkout@v4 + + - name: Install .NET Core SDK + uses: actions/setup-dotnet@v4 + with: + dotnet-version: '8.0.x' + + - name: Test Valkey + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.RedisIntegration.Tests test-hangfire: name: "Scheduler: Hangfire" @@ -386,15 +423,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test Hangfire - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.HangfireIntegration.Tests test-quartz: name: "Scheduler: Quartz" @@ -402,91 +439,68 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test Quartz - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.QuartzIntegration.Tests test-eventhub: name: "Rider: EventHub" runs-on: ubuntu-latest - if: false # turned off for flakey steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' + + - name: Spin up test environment + run: | + docker compose -f docker-compose.yml up -d + working-directory: tests/MassTransit.EventHubIntegration.Tests - name: Test EventHub - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky env: MT_EH_NAMESPACE: ${{ secrets.AZURE_EVENTHUB }} MT_AZURE_STORAGE_ACCOUNT: ${{ secrets.AZURE_STORAGE }} working-directory: tests/MassTransit.EventHubIntegration.Tests + + - name: Stop test environment + run: | + docker compose -f docker-compose.yml down + working-directory: tests/MassTransit.EventHubIntegration.Tests test-kafka: name: "Rider: Kafka" runs-on: ubuntu-latest - if: true # turned off for flakey - services: - zookeeper: - image: confluentinc/cp-zookeeper:latest - ports: - - "2181:2181" - env: - ZOOKEEPER_CLIENT_PORT: 2181 - ZOOKEEPER_TICK_TIME: 2000 - broker: - image: confluentinc/cp-kafka:latest - ports: - - "29092:29092" - - "9092:9092" - - "9101:9101" - env: - KAFKA_BROKER_ID: 1 - KAFKA_ZOOKEEPER_CONNECT: 'zookeeper:2181' - KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT - KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://broker:29092,PLAINTEXT_HOST://localhost:9092 - KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_GROUP_INITIAL_REBALANCE_DELAY_MS: 0 - KAFKA_CONFLUENT_LICENSE_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_CONFLUENT_BALANCER_TOPIC_REPLICATION_FACTOR: 1 - KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1 - KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1 - KAFKA_JMX_PORT: 9101 - KAFKA_JMX_HOSTNAME: localhost - KAFKA_CONFLUENT_SCHEMA_REGISTRY_URL: http://schema-registry:8081 - CONFLUENT_METRICS_REPORTER_BOOTSTRAP_SERVERS: broker:29092 - CONFLUENT_METRICS_REPORTER_TOPIC_REPLICAS: 1 - CONFLUENT_METRICS_ENABLE: 'false' - options: --health-cmd "nc -vz localhost 9092 || exit 1" --health-interval 10s --health-timeout 5s --health-retries 5 - schema-registry: - image: confluentinc/cp-schema-registry:latest - ports: - - "8081:8081" - env: - SCHEMA_REGISTRY_HOST_NAME: schema-registry - SCHEMA_REGISTRY_KAFKASTORE_BOOTSTRAP_SERVERS: 'broker:29092' - SCHEMA_REGISTRY_LISTENERS: http://0.0.0.0:8081 - options: --health-cmd "curl --fail http://localhost:8081/subjects || exit 1" --health-interval 10s --health-timeout 5s --health-retries 5 steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' + + - name: Spin up test environment + run: | + docker compose -f docker-compose.yml up -d + working-directory: tests/MassTransit.KafkaIntegration.Tests - name: Test Kafka - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky + working-directory: tests/MassTransit.KafkaIntegration.Tests + + - name: Stop test environment + run: | + docker compose -f docker-compose.yml down working-directory: tests/MassTransit.KafkaIntegration.Tests test-signalr: @@ -495,15 +509,15 @@ jobs: runs-on: ubuntu-latest steps: - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Test SignalR - run: dotnet test -c Release --logger:"console;verbosity=normal" --filter Category!=Flaky + run: dotnet test -c Release --logger GitHubActions --filter Category!=Flaky working-directory: tests/MassTransit.SignalR.Tests calc-version: @@ -512,12 +526,11 @@ jobs: needs: - compile - test-ubuntu - - test-containers - test-activemq # - test-azure-service-bus - - test-grpc - test-rabbitmq - test-sqs + - test-sql-transport - test-azure-table - test-cosmosdb - test-dapper @@ -526,6 +539,7 @@ jobs: - test-mongo - test-nhibernate - test-redis + - test-valkey - test-hangfire - test-quartz # - test-eventhub @@ -555,12 +569,12 @@ jobs: echo "${{ needs.calc-version.outputs.version }}" - name: Check out code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install .NET Core SDK - uses: actions/setup-dotnet@v3 + uses: actions/setup-dotnet@v4 with: - dotnet-version: '6.0.x' + dotnet-version: '8.0.x' - name: Build and Publish MassTransit # was: brandedoutcast/publish-nuget@v2.5.5 @@ -586,25 +600,26 @@ jobs: version: ${{ needs.calc-version.outputs.version }} tag-commit: false nuget-key: ${{secrets.NUGET_API_KEY}} - - name: Build and Publish MassTransit.Analyzers + - name: Build and Publish MassTransit MessagePack + # was: brandedoutcast/publish-nuget@v2.5.5 uses: drusellers/publish-nuget@master with: - project-file-path: src/MassTransit.Analyzers/MassTransit.Analyzers.csproj + project-file-path: src/MassTransit.MessagePack/MassTransit.MessagePack.csproj version: ${{ needs.calc-version.outputs.version }} tag-commit: false nuget-key: ${{secrets.NUGET_API_KEY}} - include-symbols: false - - name: Build and Publish MassTransit.Interop.NServiceBus + - name: Build and Publish MassTransit.Analyzers uses: drusellers/publish-nuget@master with: - project-file-path: src/MassTransit.Interop.NServiceBus/MassTransit.Interop.NServiceBus.csproj + project-file-path: src/MassTransit.Analyzers/MassTransit.Analyzers.csproj version: ${{ needs.calc-version.outputs.version }} tag-commit: false nuget-key: ${{secrets.NUGET_API_KEY}} - - name: Build and Publish MassTransit.PrometheusIntegration + include-symbols: false + - name: Build and Publish MassTransit.Interop.NServiceBus uses: drusellers/publish-nuget@master with: - project-file-path: src/MassTransit.PrometheusIntegration/MassTransit.PrometheusIntegration.csproj + project-file-path: src/MassTransit.Interop.NServiceBus/MassTransit.Interop.NServiceBus.csproj version: ${{ needs.calc-version.outputs.version }} tag-commit: false nuget-key: ${{secrets.NUGET_API_KEY}} @@ -657,6 +672,13 @@ jobs: version: ${{ needs.calc-version.outputs.version }} tag-commit: false nuget-key: ${{secrets.NUGET_API_KEY}} + - name: Build and Publish MassTransit.AmazonS3 + uses: drusellers/publish-nuget@master + with: + project-file-path: src/Persistence/MassTransit.AmazonS3/MassTransit.AmazonS3.csproj + version: ${{ needs.calc-version.outputs.version }} + tag-commit: false + nuget-key: ${{secrets.NUGET_API_KEY}} - name: Build and Publish MassTransit.EntityFrameworkCoreIntegration uses: drusellers/publish-nuget@master with: @@ -734,17 +756,24 @@ jobs: version: ${{ needs.calc-version.outputs.version }} tag-commit: false nuget-key: ${{secrets.NUGET_API_KEY}} - - name: Build and Publish MassTransit.EventHubIntegration + - name: Build and Publish MassTransit.SqlTransport.PostreSql uses: drusellers/publish-nuget@master with: - project-file-path: src/Transports/MassTransit.EventHubIntegration/MassTransit.EventHubIntegration.csproj + project-file-path: src/Transports/MassTransit.SqlTransport.PostgreSql/MassTransit.SqlTransport.PostgreSql.csproj + version: ${{ needs.calc-version.outputs.version }} + tag-commit: false + nuget-key: ${{secrets.NUGET_API_KEY}} + - name: Build and Publish MassTransit.SqlTransport.SqlServer + uses: drusellers/publish-nuget@master + with: + project-file-path: src/Transports/MassTransit.SqlTransport.SqlServer/MassTransit.SqlTransport.SqlServer.csproj version: ${{ needs.calc-version.outputs.version }} tag-commit: false nuget-key: ${{secrets.NUGET_API_KEY}} - - name: Build and Publish MassTransit.Grpc + - name: Build and Publish MassTransit.EventHubIntegration uses: drusellers/publish-nuget@master with: - project-file-path: src/Transports/MassTransit.GrpcTransport/MassTransit.GrpcTransport.csproj + project-file-path: src/Transports/MassTransit.EventHubIntegration/MassTransit.EventHubIntegration.csproj version: ${{ needs.calc-version.outputs.version }} tag-commit: false nuget-key: ${{secrets.NUGET_API_KEY}} diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml deleted file mode 100644 index 33e8d01d4cd..00000000000 --- a/.github/workflows/docs.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: MassTransit Documentation -on: - push: - branches: - - develop - paths: - - 'docs/**' - - 'package.json' - - '**/docs.yml' -jobs: - build: - name: Build and Deploy - runs-on: ubuntu-latest - if: (github.ref == 'refs/heads/develop') && github.repository == 'MassTransit/MassTransit' - steps: - - name: Check out code - uses: actions/checkout@v2 - - - name: Build - run: | - npm install - npm run docs:build - - - name: Deploy - working-directory: ./docs/.vuepress/dist - run: | - git init . - git config --global user.name "MassTransit" - git config --global user.email "mtproj@phatboyg.com" - git fetch https://github.com/MassTransit/masstransit.github.io.git - git checkout 220443cd2ab45d486fcee10a65669aff0bda31ab - git checkout -b master - git add . - git commit -am "Deploy Documentation" - git push --force --set-upstream https://${{secrets.PHATBOYG_PAT}}:x-oauth-basic@github.com/MassTransit/masstransit.github.io.git master - - - diff --git a/.github/workflows/nightly-transports.yml b/.github/workflows/nightly-transports.yml index 0a712b8d4b6..53d2297fb8e 100644 --- a/.github/workflows/nightly-transports.yml +++ b/.github/workflows/nightly-transports.yml @@ -42,15 +42,15 @@ jobs: DOCKER_HOST: "unix:///var/run/docker.sock" steps: - name: Check out code - uses: actions/checkout@v2 + uses: actions/checkout@v4 - name: Install .NET 3.1 - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: '3.1.x' - name: Install .NET 5 - uses: actions/setup-dotnet@v1 + uses: actions/setup-dotnet@v4 with: dotnet-version: '5.0.x' diff --git a/COPYRIGHT b/COPYRIGHT index 03e1cb9ac15..c86b7364da8 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -1,4 +1,4 @@ -Copyright 2007-2019 Chris Patterson +Copyright 2007-2024 Chris Patterson Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the diff --git a/Directory.Build.props b/Directory.Build.props index 75680cb2973..3ade404e6b2 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -7,9 +7,9 @@ - 8 + 12 4 - CS1587,CS1591,CS1998,NU5105 + CS1591,CS1998 diff --git a/Directory.Packages.props b/Directory.Packages.props index 6d55fcd8576..951564e7c96 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,87 +3,109 @@ true - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - - + + + + + + - + - - + + + - - - - - - + + + + + - - - + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + - - - - - - - - + + + + + + + - + + + + + + \ No newline at end of file diff --git a/MassTransit.sln b/MassTransit.sln index e81100fb674..b54f864917b 100644 --- a/MassTransit.sln +++ b/MassTransit.sln @@ -21,8 +21,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.RabbitMqTranspo EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.RabbitMqTransport.Tests", "tests\MassTransit.RabbitMqTransport.Tests\MassTransit.RabbitMqTransport.Tests.csproj", "{E4C3F51F-B7CE-4521-80BC-E91A9B0F6FFD}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.Containers.Tests", "tests\MassTransit.Containers.Tests\MassTransit.Containers.Tests.csproj", "{9CE51963-9E51-48EE-A75F-62C9CB0CF23C}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".solution", ".solution", "{2C8A15FA-A445-4916-AAC2-3BE53AA247A7}" ProjectSection(SolutionItems) = preProject Directory.Build.props = Directory.Build.props @@ -89,8 +87,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.SignalR.Tests", EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.WebJobs.EventHubsIntegration", "src\Transports\MassTransit.WebJobs.EventHubsIntegration\MassTransit.WebJobs.EventHubsIntegration.csproj", "{56E9DB90-C063-49CA-BAF7-FFCFE963E44C}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Monitoring", "Monitoring", "{DE40BDEC-6277-4E14-8EB6-D0B6863571BC}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.Azure.Storage", "src\Persistence\MassTransit.Azure.Storage\MassTransit.Azure.Storage.csproj", "{C81070F9-908B-4A76-A96F-CFD4F67A93AB}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Analyzers", "Analyzers", "{393B7B0C-52F4-41E8-A9E9-EE867A0E6448}" @@ -103,10 +99,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.HangfireIntegra EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.HangfireIntegration.Tests", "tests\MassTransit.HangfireIntegration.Tests\MassTransit.HangfireIntegration.Tests.csproj", "{D4ED7C0B-DADE-45FA-BCCB-9E5D6C27D714}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.PrometheusIntegration", "src\MassTransit.PrometheusIntegration\MassTransit.PrometheusIntegration.csproj", "{1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.PrometheusIntegration.Tests", "tests\MassTransit.PrometheusIntegration.Tests\MassTransit.PrometheusIntegration.Tests.csproj", "{A29B1D94-6F11-487E-B4A1-3460928EC2C0}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Interoperability", "Interoperability", "{4F40E08B-7C24-4D2A-8476-B7F93D0A2910}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MassTransit.Interop.NServiceBus", "src\MassTransit.Interop.NServiceBus\MassTransit.Interop.NServiceBus.csproj", "{F7F32EF2-0586-4FD8-90AD-AD7C792DF5E7}" @@ -125,30 +117,28 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.Azure.Table", " EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.Azure.Table.Tests", "tests\MassTransit.Azure.Table.Tests\MassTransit.Azure.Table.Tests.csproj", "{17E04A9B-5DB5-4857-99CD-E9AF22DA00E4}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.GrpcTransport", "src\Transports\MassTransit.GrpcTransport\MassTransit.GrpcTransport.csproj", "{AF56509B-0B95-4ACE-9CFC-F9A79756C357}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.GrpcTransport.Tests", "tests\MassTransit.GrpcTransport.Tests\MassTransit.GrpcTransport.Tests.csproj", "{082488F6-6322-462A-85D7-9E893A67B8CD}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.Transports.Tests", "tests\MassTransit.Transports.Tests\MassTransit.Transports.Tests.csproj", "{7632EDAF-29A1-4E69-952F-C75D3BF34B3B}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.Abstractions", "src\MassTransit.Abstractions\MassTransit.Abstractions.csproj", "{2CAF0C51-2F64-4EB0-8E27-AE3E0085CA75}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.StateMachineVisualizer", "src\MassTransit.StateMachineVisualizer\MassTransit.StateMachineVisualizer.csproj", "{8CD4298E-962B-4D04-821D-274343099E83}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.Abstractions.Tests", "tests\MassTransit.Abstractions.Tests\MassTransit.Abstractions.Tests.csproj", "{AB188378-1BAC-4ECB-98A7-91E12C861381}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.BenchmarkConsole", "tests\MassTransit.BenchmarkConsole\MassTransit.BenchmarkConsole.csproj", "{2B6ACCF2-0CAF-4152-901F-0048390971E4}" -EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.Benchmark", "tests\MassTransit.Benchmark\MassTransit.Benchmark.csproj", "{667E52D5-E1D9-49EE-B364-3CB7E43EE160}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.Newtonsoft", "src\MassTransit.Newtonsoft\MassTransit.Newtonsoft.csproj", "{388085C8-1BC8-48F8-8EA2-3698FCB385E5}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Benchmarking", "Benchmarking", "{BF384860-70ED-47F0-B276-13D2DA9ECD87}" -EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.DynamoDbIntegration", "src\Persistence\MassTransit.DynamoDbIntegration\MassTransit.DynamoDbIntegration.csproj", "{186D6491-ECCD-49EB-8E99-AFD7AD6037D2}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.DynamoDbIntegration.Tests", "tests\MassTransit.DynamoDbIntegration.Tests\MassTransit.DynamoDbIntegration.Tests.csproj", "{694E06CF-2842-4E71-8CD2-81FA7C1B3D13}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.AmazonS3", "src\Persistence\MassTransit.AmazonS3\MassTransit.AmazonS3.csproj", "{86905425-64C4-4EB0-8884-F2BD27782310}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.SqlTransport.PostgreSql", "src\Transports\MassTransit.SqlTransport.PostgreSql\MassTransit.SqlTransport.PostgreSql.csproj", "{AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.SqlTransport.SqlServer", "src\Transports\MassTransit.SqlTransport.SqlServer\MassTransit.SqlTransport.SqlServer.csproj", "{757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.SqlTransport.Tests", "tests\MassTransit.SqlTransport.Tests\MassTransit.SqlTransport.Tests.csproj", "{814826C2-6F89-4BF2-BDA9-D6116F2F98A2}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MassTransit.MessagePack", "src\MassTransit.MessagePack\MassTransit.MessagePack.csproj", "{EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -216,14 +206,6 @@ Global {E4C3F51F-B7CE-4521-80BC-E91A9B0F6FFD}.Release|x86.ActiveCfg = Release|Any CPU {E4C3F51F-B7CE-4521-80BC-E91A9B0F6FFD}.ReleaseUnsigned|Any CPU.ActiveCfg = Release|Any CPU {E4C3F51F-B7CE-4521-80BC-E91A9B0F6FFD}.ReleaseUnsigned|x86.ActiveCfg = Release|Any CPU - {9CE51963-9E51-48EE-A75F-62C9CB0CF23C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {9CE51963-9E51-48EE-A75F-62C9CB0CF23C}.Debug|Any CPU.Build.0 = Debug|Any CPU - {9CE51963-9E51-48EE-A75F-62C9CB0CF23C}.Debug|x86.ActiveCfg = Debug|Any CPU - {9CE51963-9E51-48EE-A75F-62C9CB0CF23C}.Release|Any CPU.ActiveCfg = Release|Any CPU - {9CE51963-9E51-48EE-A75F-62C9CB0CF23C}.Release|Any CPU.Build.0 = Release|Any CPU - {9CE51963-9E51-48EE-A75F-62C9CB0CF23C}.Release|x86.ActiveCfg = Release|Any CPU - {9CE51963-9E51-48EE-A75F-62C9CB0CF23C}.ReleaseUnsigned|Any CPU.ActiveCfg = Release|Any CPU - {9CE51963-9E51-48EE-A75F-62C9CB0CF23C}.ReleaseUnsigned|x86.ActiveCfg = Release|Any CPU {2F7DB49B-B650-4B57-BB9D-5D1B913CCACC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2F7DB49B-B650-4B57-BB9D-5D1B913CCACC}.Debug|Any CPU.Build.0 = Debug|Any CPU {2F7DB49B-B650-4B57-BB9D-5D1B913CCACC}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -584,30 +566,6 @@ Global {D4ED7C0B-DADE-45FA-BCCB-9E5D6C27D714}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU {D4ED7C0B-DADE-45FA-BCCB-9E5D6C27D714}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU {D4ED7C0B-DADE-45FA-BCCB-9E5D6C27D714}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.Debug|Any CPU.Build.0 = Debug|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.Debug|x86.ActiveCfg = Debug|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.Debug|x86.Build.0 = Debug|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.Release|Any CPU.ActiveCfg = Release|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.Release|Any CPU.Build.0 = Release|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.Release|x86.ActiveCfg = Release|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.Release|x86.Build.0 = Release|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.Debug|Any CPU.Build.0 = Debug|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.Debug|x86.ActiveCfg = Debug|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.Debug|x86.Build.0 = Debug|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.Release|Any CPU.ActiveCfg = Release|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.Release|Any CPU.Build.0 = Release|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.Release|x86.ActiveCfg = Release|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.Release|x86.Build.0 = Release|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU - {A29B1D94-6F11-487E-B4A1-3460928EC2C0}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU {F7F32EF2-0586-4FD8-90AD-AD7C792DF5E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {F7F32EF2-0586-4FD8-90AD-AD7C792DF5E7}.Debug|Any CPU.Build.0 = Debug|Any CPU {F7F32EF2-0586-4FD8-90AD-AD7C792DF5E7}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -692,42 +650,6 @@ Global {17E04A9B-5DB5-4857-99CD-E9AF22DA00E4}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU {17E04A9B-5DB5-4857-99CD-E9AF22DA00E4}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU {17E04A9B-5DB5-4857-99CD-E9AF22DA00E4}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.Debug|Any CPU.Build.0 = Debug|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.Debug|x86.ActiveCfg = Debug|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.Debug|x86.Build.0 = Debug|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.Release|Any CPU.ActiveCfg = Release|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.Release|Any CPU.Build.0 = Release|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.Release|x86.ActiveCfg = Release|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.Release|x86.Build.0 = Release|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU - {AF56509B-0B95-4ACE-9CFC-F9A79756C357}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.Debug|Any CPU.Build.0 = Debug|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.Debug|x86.ActiveCfg = Debug|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.Debug|x86.Build.0 = Debug|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.Release|Any CPU.ActiveCfg = Release|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.Release|Any CPU.Build.0 = Release|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.Release|x86.ActiveCfg = Release|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.Release|x86.Build.0 = Release|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU - {082488F6-6322-462A-85D7-9E893A67B8CD}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.Debug|Any CPU.Build.0 = Debug|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.Debug|x86.ActiveCfg = Debug|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.Debug|x86.Build.0 = Debug|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.Release|Any CPU.ActiveCfg = Release|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.Release|Any CPU.Build.0 = Release|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.Release|x86.ActiveCfg = Release|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.Release|x86.Build.0 = Release|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU {2CAF0C51-2F64-4EB0-8E27-AE3E0085CA75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {2CAF0C51-2F64-4EB0-8E27-AE3E0085CA75}.Debug|Any CPU.Build.0 = Debug|Any CPU {2CAF0C51-2F64-4EB0-8E27-AE3E0085CA75}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -764,30 +686,6 @@ Global {AB188378-1BAC-4ECB-98A7-91E12C861381}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU {AB188378-1BAC-4ECB-98A7-91E12C861381}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU {AB188378-1BAC-4ECB-98A7-91E12C861381}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.Debug|x86.ActiveCfg = Debug|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.Debug|x86.Build.0 = Debug|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.Release|Any CPU.Build.0 = Release|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.Release|x86.ActiveCfg = Release|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.Release|x86.Build.0 = Release|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU - {2B6ACCF2-0CAF-4152-901F-0048390971E4}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.Debug|Any CPU.Build.0 = Debug|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.Debug|x86.ActiveCfg = Debug|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.Debug|x86.Build.0 = Debug|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.Release|Any CPU.ActiveCfg = Release|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.Release|Any CPU.Build.0 = Release|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.Release|x86.ActiveCfg = Release|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.Release|x86.Build.0 = Release|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU - {667E52D5-E1D9-49EE-B364-3CB7E43EE160}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU {388085C8-1BC8-48F8-8EA2-3698FCB385E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {388085C8-1BC8-48F8-8EA2-3698FCB385E5}.Debug|Any CPU.Build.0 = Debug|Any CPU {388085C8-1BC8-48F8-8EA2-3698FCB385E5}.Debug|x86.ActiveCfg = Debug|Any CPU @@ -824,6 +722,66 @@ Global {694E06CF-2842-4E71-8CD2-81FA7C1B3D13}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU {694E06CF-2842-4E71-8CD2-81FA7C1B3D13}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU {694E06CF-2842-4E71-8CD2-81FA7C1B3D13}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.Debug|Any CPU.Build.0 = Debug|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.Debug|x86.ActiveCfg = Debug|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.Debug|x86.Build.0 = Debug|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.Release|Any CPU.ActiveCfg = Release|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.Release|Any CPU.Build.0 = Release|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.Release|x86.ActiveCfg = Release|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.Release|x86.Build.0 = Release|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU + {86905425-64C4-4EB0-8884-F2BD27782310}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.Debug|x86.ActiveCfg = Debug|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.Debug|x86.Build.0 = Debug|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.Release|Any CPU.Build.0 = Release|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.Release|x86.ActiveCfg = Release|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.Release|x86.Build.0 = Release|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.Debug|x86.ActiveCfg = Debug|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.Debug|x86.Build.0 = Debug|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.Release|Any CPU.Build.0 = Release|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.Release|x86.ActiveCfg = Release|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.Release|x86.Build.0 = Release|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.Debug|Any CPU.Build.0 = Debug|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.Debug|x86.ActiveCfg = Debug|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.Debug|x86.Build.0 = Debug|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.Release|Any CPU.ActiveCfg = Release|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.Release|Any CPU.Build.0 = Release|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.Release|x86.ActiveCfg = Release|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.Release|x86.Build.0 = Release|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.Debug|x86.Build.0 = Debug|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.Release|Any CPU.Build.0 = Release|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.Release|x86.ActiveCfg = Release|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.Release|x86.Build.0 = Release|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.ReleaseUnsigned|Any CPU.ActiveCfg = Debug|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.ReleaseUnsigned|Any CPU.Build.0 = Debug|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.ReleaseUnsigned|x86.ActiveCfg = Debug|Any CPU + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD}.ReleaseUnsigned|x86.Build.0 = Debug|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -864,8 +822,6 @@ Global {1F71C6D2-17AE-4C7B-878D-BB7B295D3B3D} = {393B7B0C-52F4-41E8-A9E9-EE867A0E6448} {CF2D2B2E-2C34-4E8C-970F-4CB07DB0457B} = {FDC1A760-D4E5-44F4-B0D9-C19F2E14253A} {D4ED7C0B-DADE-45FA-BCCB-9E5D6C27D714} = {FDC1A760-D4E5-44F4-B0D9-C19F2E14253A} - {1AF687AB-41DE-4B2A-97F5-E9BBF5E51C75} = {DE40BDEC-6277-4E14-8EB6-D0B6863571BC} - {A29B1D94-6F11-487E-B4A1-3460928EC2C0} = {DE40BDEC-6277-4E14-8EB6-D0B6863571BC} {F7F32EF2-0586-4FD8-90AD-AD7C792DF5E7} = {4F40E08B-7C24-4D2A-8476-B7F93D0A2910} {1FC221F1-E49D-473C-9869-4452B2C4EBEE} = {F4CD08DB-40C0-4476-A135-DB8E0BCB79F8} {555EC658-5EF6-4C10-ABA0-B717896398BE} = {F4CD08DB-40C0-4476-A135-DB8E0BCB79F8} @@ -874,14 +830,14 @@ Global {24CC1B47-AE01-4871-A939-2BF6EB789860} = {F4CD08DB-40C0-4476-A135-DB8E0BCB79F8} {338E2B7E-4301-4547-9172-4415F739C029} = {56F516D7-BC3C-49E1-A639-83C5F14953F8} {17E04A9B-5DB5-4857-99CD-E9AF22DA00E4} = {56F516D7-BC3C-49E1-A639-83C5F14953F8} - {AF56509B-0B95-4ACE-9CFC-F9A79756C357} = {0006D6BB-1382-4B32-AD32-CA037F5CD4F6} - {082488F6-6322-462A-85D7-9E893A67B8CD} = {0006D6BB-1382-4B32-AD32-CA037F5CD4F6} - {7632EDAF-29A1-4E69-952F-C75D3BF34B3B} = {0006D6BB-1382-4B32-AD32-CA037F5CD4F6} {388085C8-1BC8-48F8-8EA2-3698FCB385E5} = {4F40E08B-7C24-4D2A-8476-B7F93D0A2910} - {667E52D5-E1D9-49EE-B364-3CB7E43EE160} = {BF384860-70ED-47F0-B276-13D2DA9ECD87} - {2B6ACCF2-0CAF-4152-901F-0048390971E4} = {BF384860-70ED-47F0-B276-13D2DA9ECD87} {186D6491-ECCD-49EB-8E99-AFD7AD6037D2} = {56F516D7-BC3C-49E1-A639-83C5F14953F8} {694E06CF-2842-4E71-8CD2-81FA7C1B3D13} = {56F516D7-BC3C-49E1-A639-83C5F14953F8} + {86905425-64C4-4EB0-8884-F2BD27782310} = {56F516D7-BC3C-49E1-A639-83C5F14953F8} + {AB7996F8-8877-4FE6-AFC1-58A6DF41FAB4} = {0006D6BB-1382-4B32-AD32-CA037F5CD4F6} + {757A9FD1-099A-4CF5-BCF9-7615FCD78CF1} = {0006D6BB-1382-4B32-AD32-CA037F5CD4F6} + {814826C2-6F89-4BF2-BDA9-D6116F2F98A2} = {0006D6BB-1382-4B32-AD32-CA037F5CD4F6} + {EF197CD3-EE31-44F5-95C4-76DCAC3E10DD} = {4F40E08B-7C24-4D2A-8476-B7F93D0A2910} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {43D3A7D5-0945-435E-8D03-1E631E5CDBA8} diff --git a/MassTransit.sln.DotSettings b/MassTransit.sln.DotSettings index f30e4ae292b..00bee3f9ac2 100644 --- a/MassTransit.sln.DotSettings +++ b/MassTransit.sln.DotSettings @@ -5,6 +5,7 @@ DO_NOT_SHOW WARNING DO_NOT_SHOW + DO_NOT_SHOW OFF <?xml version="1.0" encoding="utf-16"?><Profile name="Normal"><HtmlReformatCode>True</HtmlReformatCode><JsReformatCode>True</JsReformatCode><XMLReformatCode>True</XMLReformatCode><CSCodeStyleAttributes ArrangeTypeAccessModifier="True" ArrangeTypeMemberAccessModifier="True" SortModifiers="True" RemoveRedundantParentheses="True" AddMissingParentheses="True" ArrangeBraces="True" ArrangeAttributes="True" ArrangeArgumentsStyle="True" ArrangeCodeBodyStyle="True" ArrangeVarStyle="True" /><CssReformatCode>True</CssReformatCode><VBReformatCode>True</VBReformatCode><CSOptimizeUsings><OptimizeUsings>True</OptimizeUsings><EmbraceInRegion>False</EmbraceInRegion><RegionName></RegionName></CSOptimizeUsings><CSReformatCode>True</CSReformatCode><CSReorderTypeMembers>True</CSReorderTypeMembers><CSUpdateFileHeader>True</CSUpdateFileHeader><CSEnforceVarKeywordUsageSettings>True</CSEnforceVarKeywordUsageSettings><RemoveCodeRedundancies>True</RemoveCodeRedundancies><CSArrangeQualifiers>True</CSArrangeQualifiers><CSFixBuiltinTypeReferences>True</CSFixBuiltinTypeReferences><CSShortenReferences>True</CSShortenReferences><IDEA_SETTINGS>&lt;profile version="1.0"&gt; &lt;option name="myName" value="Normal" /&gt; @@ -35,8 +36,9 @@ True True OUTDENT - OUTDENT + USUAL_INDENT OUTDENT + INDENT 1 False False @@ -214,6 +216,10 @@ <Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_aaBb" /><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="_" Suffix="" Style="aaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb_aaBb" /><ExtraRule Prefix="" Suffix="" Style="AaBb_AaBb" /></Policy></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Interfaces"><ElementKinds><Kind Name="INTERFACE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb"><ExtraRule Prefix="" Suffix="" Style="AaBb" /></Policy></Policy> BOTH_SIDES OUTLINE @@ -241,6 +247,7 @@ True True True + True True True @@ -277,6 +284,7 @@ public async System.Threading.Tasks.Task $TESTNAME$() True True True + True True True True diff --git a/NOTICE b/NOTICE index 32386aa35b0..6b3752b3b66 100644 --- a/NOTICE +++ b/NOTICE @@ -4,4 +4,4 @@ ========================================================================= MassTransit -Copyright 2007-2019 Chris Patterson +Copyright 2007-2024 Chris Patterson diff --git a/NuGet.README.md b/NuGet.README.md index b3fc1d63ae4..09dbb29f882 100644 --- a/NuGet.README.md +++ b/NuGet.README.md @@ -2,10 +2,10 @@ MassTransit provides a developer-focused, modern platform for creating distributed applications without complexity. - - First class testing support - - Write once, then deploy using RabbitMQ, Azure Service Bus, and Amazon SQS - - Observability via Open Telemetry (OTEL) - - Fully-supported, widely-adopted, a complete end-to-end solution +- First class testing support +- Write once, then deploy using RabbitMQ, Azure Service Bus, and Amazon SQS +- Observability via Open Telemetry (OTEL) +- Fully-supported, widely-adopted, a complete end-to-end solution ## Documentation @@ -41,6 +41,7 @@ The following NuGet packages are the currently supported. ### Saga Persistence +* [MassTransit.AmazonS3](https://nuget.org/packages/MassTransit.AmazonS3/) * [MassTransit.Azure.Cosmos](https://nuget.org/packages/MassTransit.Azure.Cosmos/) * [MassTransit.Azure.Cosmos.Table](https://nuget.org/packages/MassTransit.Azure.Cosmos.Table/) * [MassTransit.DapperIntegration](https://nuget.org/packages/MassTransit.DapperIntegration/) @@ -111,7 +112,7 @@ The following packages from earlier versions of MassTransit are no longer suppor * MassTransit.StructureMapSigned * MassTransit.Unity -## Discord +## Discord Get help live at the MassTransit Discord server. @@ -119,6 +120,7 @@ Get help live at the MassTransit Discord server. ## GitHub Issues -> Please do not open an issue on GitHub, unless you have spotted an actual bug in MassTransit. +> Please do not open an issue on GitHub, unless you have spotted an actual bug in MassTransit. -Use [GitHub Discussions](https://github.com/MassTransit/MassTransit/discussions) to ask questions, bring up ideas, or other general items. Issues are not the place for questions, and will either be converted to a discussion or closed. +Use [GitHub Discussions](https://github.com/MassTransit/MassTransit/discussions) to ask questions, bring up ideas, or other general items. Issues are not the +place for questions, and will either be converted to a discussion or closed. diff --git a/README.md b/README.md index e1925597284..5249fda910e 100644 --- a/README.md +++ b/README.md @@ -18,51 +18,53 @@ Build Status |---------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------:| | master | [![master](https://github.com/MassTransit/MassTransit/actions/workflows/build.yml/badge.svg?branch=master&event=push)](https://github.com/MassTransit/MassTransit/actions/workflows/build.yml) | | develop | [![develop](https://github.com/MassTransit/MassTransit/actions/workflows/build.yml/badge.svg?branch=develop&event=push)](https://github.com/MassTransit/MassTransit/actions/workflows/build.yml) | -| documentation | [![documentation](https://github.com/MassTransit/MassTransit/actions/workflows/docs.yml/badge.svg?branch=develop&event=push)](https://github.com/MassTransit/MassTransit/actions/workflows/docs.yml) | MassTransit NuGet Packages --------------------------- -| Package Name | .NET | .NET Standard | .NET Framework | -|-----------------------------------------------------------------|:----:|:-------------:|:--------------:| -| **Main** | | | | -| [MassTransit][MassTransit.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.Abstractions][MassTransitAbstractions.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.Newtonsoft][MassTransitNewtonsoft.nuget] | 6.0 | 2.0 | 4.6.1 | -| **Other** | | | | -| [MassTransit.Analyzers][Analyzers.nuget] | | 2.0 | | -| [MassTransit.Templates][Templates.nuget] | 5.0 | | | -| [MassTransit.SignalR][SignalR.nuget] | 6.0 | | 4.6.1 | -| [MassTransit.Interop.NServiceBus][MassTransitNServiceBus.nuget] | 6.0 | 2.0 | 4.6.1 | -| [MassTransit.TestFramework][TestFramework.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| **Monitoring** | | | | -| [MassTransit.Prometheus][Prometheus.nuget] | 6.0 | 2.0 | 4.6.2 | -| **Persistence** | | | | -| [MassTransit.Azure.Cosmos][Cosmos.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.Azure.Storage][AzureStorage.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.Azure.Table][AzureTable.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.Dapper][Dapper.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.DynamoDb][DynamoDb.nuget] | 6.0 | 2.0 | 4.6.1 | -| [MassTransit.EntityFrameworkCore][EFCore.nuget] | 6.0 | 2.0 | | -| [MassTransit.EntityFramework][EF.nuget] | | 2.1 | 4.6.1 | -| [MassTransit.Marten][Marten.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.MongoDb][MongoDb.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.NHibernate][NHibernate.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.Redis][Redis.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| **Scheduling** | | | | -| [MassTransit.Hangfire][Hangfire.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.Quartz][Quartz.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| **Transports** | | | | -| [MassTransit.ActiveMQ][ActiveMQ.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.AmazonSQS][AmazonSQS.nuget] | 6.0 | 2.0 | 4.6.1 | -| [MassTransit.Azure.ServiceBus.Core][AzureSbCore.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.Grpc][Grpc.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.RabbitMQ][RabbitMQ.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.WebJobs.EventHubs][EventHubs.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.WebJobs.ServiceBus][AzureFunc.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| **Riders** | | | | -| [MassTransit.Kafka][Kafka.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | -| [MassTransit.EventHub][EventHub.nuget] | 6.0 | 2.0, 2.1 | 4.6.1 | +| Package Name | .NET | .NET Standard | .NET Framework | +|-----------------------------------------------------------------|:--------:|:-------------:|:--------------:| +| **Main** | | | | +| [MassTransit][MassTransit.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.Abstractions][MassTransitAbstractions.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.Newtonsoft][MassTransitNewtonsoft.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.MessagePack][MassTransitMessagePack.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| **Other** | | | | +| [MassTransit.Analyzers][Analyzers.nuget] | | 2.0 | | +| [MassTransit.Templates][Templates.nuget] | 6.0 | | | +| [MassTransit.SignalR][SignalR.nuget] | 6.0, 8.0 | | 4.7.2 | +| [MassTransit.Interop.NServiceBus][MassTransitNServiceBus.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.TestFramework][TestFramework.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| **Monitoring** | | | | +| [MassTransit.Prometheus][Prometheus.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| **Persistence** | | | | +| [MassTransit.AmazonS3][AmazonS3.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.Azure.Cosmos][Cosmos.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.Azure.Storage][AzureStorage.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.Azure.Table][AzureTable.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.Dapper][Dapper.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.DynamoDb][DynamoDb.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.EntityFrameworkCore][EFCore.nuget] | 6.0, 8.0 | 2.0 | | +| [MassTransit.EntityFramework][EF.nuget] | | 2.1 | 4.7.2 | +| [MassTransit.Marten][Marten.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.MongoDb][MongoDb.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.NHibernate][NHibernate.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.Redis][Redis.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| **Scheduling** | | | | +| [MassTransit.Hangfire][Hangfire.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.Quartz][Quartz.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| **Transports** | | | | +| [MassTransit.ActiveMQ][ActiveMQ.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.AmazonSQS][AmazonSQS.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.Azure.ServiceBus.Core][AzureSbCore.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.RabbitMQ][RabbitMQ.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.SqlTransport.PostgreSQL][PostgreSQL.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.SqlTransport.SqlServer][SqlServer.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.WebJobs.EventHubs][EventHubs.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.WebJobs.ServiceBus][AzureFunc.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| **Riders** | | | | +| [MassTransit.Kafka][Kafka.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | +| [MassTransit.EventHub][EventHub.nuget] | 6.0, 8.0 | 2.0 | 4.7.2 | ## Discord @@ -83,7 +85,7 @@ enhancements and calls for help from people who forget to check back if they get ## Building from Source - 1. Install the latest [.NET 6 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) + 1. Install the latest [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) 2. Clone the source down to your machine
```bash git clone https://github.com/MassTransit/MassTransit.git @@ -100,7 +102,7 @@ enhancements and calls for help from people who forget to check back if they get 3. Make a pull request # REQUIREMENTS -* .NET 6 SDK +* .NET 8 SDK # CREDITS Logo Design by _The Agile Badger_ @@ -108,6 +110,7 @@ Logo Design by _The Agile Badger_ [MassTransit.nuget]: https://www.nuget.org/packages/MassTransit [MassTransitAbstractions.nuget]: https://www.nuget.org/packages/MassTransit.Abstractions [MassTransitNewtonsoft.nuget]: https://www.nuget.org/packages/MassTransit.Newtonsoft +[MassTransitMessagePack.nuget]: https://www.nuget.org/packages/MassTransit.MessagePack [MassTransitNServiceBus.nuget]: https://www.nuget.org/packages/MassTransit.Interop.NServiceBus [Analyzers.nuget]: https://www.nuget.org/packages/MassTransit.Analyzers [Templates.nuget]: https://www.nuget.org/packages/MassTransit.Templates @@ -132,10 +135,12 @@ Logo Design by _The Agile Badger_ [Quartz.nuget]: https://www.nuget.org/packages/MassTransit.Quartz [ActiveMQ.nuget]: https://www.nuget.org/packages/MassTransit.ActiveMQ +[AmazonS3.nuget]: https://www.nuget.org/packages/MassTransit.AmazonS3 [AmazonSQS.nuget]: https://www.nuget.org/packages/MassTransit.AmazonSQS [AzureSbCore.nuget]: https://www.nuget.org/packages/MassTransit.Azure.ServiceBus.Core -[Grpc.nuget]: https://www.nuget.org/packages/MassTransit.Grpc [RabbitMQ.nuget]: https://www.nuget.org/packages/MassTransit.RabbitMQ +[PostgreSQL.nuget]: https://nuget.org/packages/MassTransit.SqlTransport.PostgreSQL/ +[SqlServer.nuget]: https://nuget.org/packages/MassTransit.SqlTransport.SqlServer/ [EventHubs.nuget]: https://www.nuget.org/packages/MassTransit.WebJobs.EventHubs [AzureFunc.nuget]: https://www.nuget.org/packages/MassTransit.WebJobs.ServiceBus diff --git a/SECURITY.md b/SECURITY.md index 370ed6bde95..7b2681454f2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -6,8 +6,8 @@ MassTransit supports the current major version only. If issues or vulnerabilitie | Version | Supported | | ------- | ------------------ | -| 7.x | :white_check_mark: | -| < 7.0 | :x: | +| 8.x | :white_check_mark: | +| < 8.0 | :x: | ## Reporting a Vulnerability diff --git a/doc/app.config.ts b/doc/app.config.ts index 77206442745..ff8c61eee17 100644 --- a/doc/app.config.ts +++ b/doc/app.config.ts @@ -23,7 +23,7 @@ export default defineAppConfig({ }, footer: { credits: { - text: 'Copyright 2023 Chris Patterson', + text: 'Copyright 2024 Chris Patterson', href: 'https://masstransit.io', icon: 'IconMassTransit' }, diff --git a/doc/components/global/Mermaid.vue b/doc/components/global/Mermaid.vue deleted file mode 100644 index bb74ceb011d..00000000000 --- a/doc/components/global/Mermaid.vue +++ /dev/null @@ -1,22 +0,0 @@ - - - diff --git a/doc/content/2.quick-starts/0.index.md b/doc/content/2.quick-starts/0.index.md index 34c6ebe647c..d094ce73cf8 100755 --- a/doc/content/2.quick-starts/0.index.md +++ b/doc/content/2.quick-starts/0.index.md @@ -37,5 +37,12 @@ Transports #description Requires an AWS account :: + + ::card + #title + [PostgreSQL](/quick-starts/postgresql) + #description + For smaller setups + :: :: diff --git a/doc/content/2.quick-starts/1.in-memory.md b/doc/content/2.quick-starts/1.in-memory.md index 63a50530978..f1661060581 100644 --- a/doc/content/2.quick-starts/1.in-memory.md +++ b/doc/content/2.quick-starts/1.in-memory.md @@ -13,7 +13,7 @@ This example requires a functioning installation of the .NET Runtime and SDK (at MassTransit includes project and item [templates](/quick-starts/templates) simplifying the creation of new projects. Install the templates by executing the command below in the console. A video introducing the templates is available on [YouTube](https://youtu.be/nYKq61-DFBQ). ``` -dotnet new --install MassTransit.Templates +dotnet new install MassTransit.Templates ``` ## Create the Project diff --git a/doc/content/2.quick-starts/2.rabbitmq.md b/doc/content/2.quick-starts/2.rabbitmq.md index b6b135c0985..4809e227a70 100644 --- a/doc/content/2.quick-starts/2.rabbitmq.md +++ b/doc/content/2.quick-starts/2.rabbitmq.md @@ -16,7 +16,10 @@ This tutorial will get you from zero to up and running with [RabbitMQ](/document The following instructions assume you are starting from a completed [In-Memory Quick Start](/quick-starts/in-memory) :: -This example requires a functioning installation of the .NET Runtime and SDK (at least 6.0) and a functioning installation of _Docker_ with _Docker Compose_ support enabled. +This example requires the following: + +- a functioning installation of the .NET Runtime and SDK (at least 6.0) +- a functioning installation of _Docker_ with _Docker Compose_ support enabled. ## Run RabbitMQ @@ -99,3 +102,8 @@ info: GettingStarted.MessageConsumer[0] At this point the service is connecting to RabbitMQ on _localhost_ and publishing messages which are received by the consumer. :tada: + +## What is next + +- [RabbitMQ Transport overview](/documentation/transports/rabbitmq) +- [RabbitMQ Transport configuration](/documentation/configuration/transports/rabbitmq) diff --git a/doc/content/2.quick-starts/3.azure-service-bus.md b/doc/content/2.quick-starts/3.azure-service-bus.md index ab667d9921b..e5717cb996a 100644 --- a/doc/content/2.quick-starts/3.azure-service-bus.md +++ b/doc/content/2.quick-starts/3.azure-service-bus.md @@ -94,3 +94,9 @@ info: GettingStarted.MessageConsumer[0] At this point, the service is connecting to Azure Service Bus and publishing messages which are received by the consumer. :tada: + +## What is next + +- [Azure Service Bus transport overview](/documentation/transports/azure-service-bus) +- [Azure Service Bus transport configuration](/documentation/configuration/transports/azure-service-bus) +- [Azure Service Bus sample](https://github.com/MassTransit/Sample-AzureServiceBus) diff --git a/doc/content/2.quick-starts/4.amazon-sqs.md b/doc/content/2.quick-starts/4.amazon-sqs.md index 13f4a3c9f5d..b453ab2b7c0 100644 --- a/doc/content/2.quick-starts/4.amazon-sqs.md +++ b/doc/content/2.quick-starts/4.amazon-sqs.md @@ -137,3 +137,8 @@ info: GettingStarted.MessageConsumer[0] At this point the service is connecting to Amazon SQS/SNS in the region `us-east-1` and publishing messages which are received by the consumer. :tada: + +## What is next + +- [AmazonSQS transport overview](/documentation/transports/amazon-sqs) +- [AmazonSQS transport configuration](/documentation/configuration/transports/amazon-sqs) diff --git a/doc/content/2.quick-starts/5.postgresql.md b/doc/content/2.quick-starts/5.postgresql.md new file mode 100644 index 00000000000..8bdfaf1776f --- /dev/null +++ b/doc/content/2.quick-starts/5.postgresql.md @@ -0,0 +1,130 @@ +--- +navigation.title: PostgreSQL +--- + +# PostgreSQL Quick Start + +> This tutorial will get you from zero to up and running with [SQL](/documentation/transports/sql) and MassTransit. + +> Walkthrough Video TBD + +- The source for this sample is available [on GitHub](https://github.com/MassTransit/Sample-GettingStarted). + +## Prerequisites + +::alert{type="info"} +The following instructions assume you are starting from a completed [In-Memory Quick Start](/quick-starts/in-memory) +:: + +This example requires the following: + +- a functioning installation of the dotnet runtime and sdk (at least 6.0) +- a functioning installation of _Docker_ with _Docker Compose_ support enabled. + +## Run PostgreSQL + +For this quick start, we recommend running the preconfigured [official Docker image of Postgres](https://hub.docker.com/_/postgres). + +```bash +$ docker run -p 5432:5432 postgres +``` + +If you are running on an ARM platform + +```bash +$ docker run --platform linux/arm64 -p 5432:5432 postgres +``` + +Once its up and running you can use your preferred tool to browse into the database. + +## Configure PostgreSQL + +Add the _MassTransit.SqlTransport.PostgreSQL_ package to the project. + +```bash +$ dotnet add package MassTransit.SqlTransport.PostgreSQL +``` + +### Edit Program.cs + +Change _UsingInMemory_ to _UsingPostgres_ as shown below. + +```csharp +public static IHostBuilder CreateHostBuilder(string[] args) => + Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddOptions().Configure(options => + { + options.Host = "localhost"; + options.Database = "sample"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "masstransit"; + options.Password = "H4rd2Gu3ss!"; + + // credentials to run migrations + options.AdminUsername = "migration-user"; + options.AdminPassword = "H4rderTooGu3ss!!"; + }); + // MassTransit will run the migrations on start up + services.AddPostgresMigrationHostedService(); + services.AddMassTransit(x => + { + // elided... + + x.UsingPostgres((context,cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }); + + services.AddHostedService(); + }); +``` + +| Setting | Description | +|-----------------|-------------------------------------------------------------------------------------------| +| `Host` | The host to connect to. We are using `localhost` to connect to the docker container | +| `Port` | We are using the default `5432`, so we aren't setting it. | +| `Database` | The name of the database to connect to | +| `Schema` | The schema to place the tables and functions inside of | +| `Role` | the role to assign for all created tables, functions, etc. | +| `Username` | The username of the user to login as for normal operations | +| `Password` | The password of the user to login as for normal operations | +| `AdminUsername` | The username of the admin user to login as when running migration commands | +| `AdminPassword` | The password of the admin user to login as when running migration commands | + + +## Run the Project + +```bash +$ dotnet run +``` + +The output should have changed to show the message consumer generating the output (again, press Control+C to exit). Notice that the bus address now starts with _db_. + +``` +Building... +info: MassTransit[0] + Configured endpoint Message, Consumer: GettingStarted.MessageConsumer +info: Microsoft.Hosting.Lifetime[0] + Application started. Press Ctrl+C to shut down. +info: Microsoft.Hosting.Lifetime[0] + Hosting environment: Development +info: Microsoft.Hosting.Lifetime[0] + Content root path: /Users/chris/Garbage/start/GettingStarted +info: MassTransit[0] + Bus started: db://localhost/ +info: GettingStarted.MessageConsumer[0] + Received Text: The time is 3/24/2021 12:11:10 PM -05:00 +``` + +At this point the service is connecting to PostgreSQL on _localhost_ and publishing messages which are received by the consumer. + +:tada: + +## What is next + +- [SQL transport overview](/documentation/transports/sql) +- [SQL transport sample](https://github.com/MassTransit/Sample-DbTransport) diff --git a/doc/content/2.quick-starts/templates.md b/doc/content/2.quick-starts/templates.md index 5a3173cd337..0abd1368f2c 100644 --- a/doc/content/2.quick-starts/templates.md +++ b/doc/content/2.quick-starts/templates.md @@ -7,7 +7,7 @@ A video introducing the templates is available on [YouTube](https://youtu.be/nYK ## Installation ```sh -dotnet new --install MassTransit.Templates +dotnet new install MassTransit.Templates ``` One installed, typing `dotnet new` will display the available templates: diff --git a/doc/content/3.documentation/1.concepts/0.index.md b/doc/content/3.documentation/1.concepts/0.index.md index 43c9532bbdb..5bdf786e0e6 100644 --- a/doc/content/3.documentation/1.concepts/0.index.md +++ b/doc/content/3.documentation/1.concepts/0.index.md @@ -6,3 +6,15 @@ navigation.title: Overview When learning MassTransit, it is a good idea to understand messaging concepts and terminology. To ensure that you are on the right path when looking at a class or interface, review these concepts when working with MassTransit. +## The Basics + +- [Messages](/documentation/concepts/messages) +- [Consumers](/documentation/concepts/consumers) +- [Producers](/documentation/concepts/producers) +- [Exceptions](/documentation/concepts/exceptions) +- [Testing](/documentation/concepts/testing) + +## Intermediate + +- [Requests](/documentation/concepts/requests) +- [Routing Slips](/documentation/concepts/routing-slips) diff --git a/doc/content/3.documentation/1.concepts/1.messages.md b/doc/content/3.documentation/1.concepts/1.messages.md index 2d75f1c8328..99ff601cd04 100644 --- a/doc/content/3.documentation/1.concepts/1.messages.md +++ b/doc/content/3.documentation/1.concepts/1.messages.md @@ -54,9 +54,9 @@ namespace Company.Application.Contracts } ``` -When defining a message type using an interface, MassTransit will create a dynamic class implementing the interface for serialization, allowing the interface with get-only properties to be presented to the consumer. To create an interface message, use a [message initializer](/documentation/concepts/producers##message-initialization). +When defining a message type using an interface, MassTransit will create a dynamic class implementing the interface for serialization, allowing the interface with get-only properties to be presented to the consumer. To create an interface message, use a [message initializer](/documentation/concepts/producers#message-initialization). -### Classes +### Classes ```csharp namespace Company.Application.Contracts @@ -85,11 +85,12 @@ A common mistake when engineers are new to messaging is to create a base class f ## Message Attributes -| Attribute | Description | -|:----------------------------|:------------------------------------------------------------------------------| -| EntityName | The exchange or topic name | -| ExcludeFromTopology | Don't create an exchange or topic unless it is directly consumed or published | -| ExcludeFromImplementedTypes | Don't create a middleware filter for the message type | +| Attribute | Description | +|:---------------------------------------------------------------------------------------------------------|:------------------------------------------------------------------------------| +| [EntityName](/documentation/configuration/topology/message#entityname) | The exchange or topic name | +| [ExcludeFromTopology](/documentation/configuration/topology/message#excludefromtopology) | Don't create an exchange or topic unless it is directly consumed or published | +| [ExcludeFromImplementedTypes](/documentation/configuration/topology/message#excludefromimplementedtypes) | Don't create a middleware filter for the message type | +| [MessageUrn](/documentation/configuration/topology/message#messageurn) | The message urn | ## Message Names @@ -97,7 +98,7 @@ There are two main message types, _events_ and _commands_. When choosing a name ### Commands -A command tells _a_ service to do something, and typically a command should only be consumed by a single consumer. If you have a command, such as `SubmitOrder`, then you should have only one consumer that implements `IConsumer` or one saga state machine with the `Event` configured. By maintaining the one-to-one relationship of a command to a consumer, commands may by _published_ and they will be automatically routed to the consumer. +A command tells _a_ service to do something, and typically a command should only be consumed by a single consumer. If you have a command, such as `SubmitOrder`, then you should have only one consumer that implements `IConsumer` or one saga state machine with the `Event` configured. By maintaining the one-to-one relationship of a command to a consumer, commands may be _published_ and they will be automatically routed to the consumer. When using RabbitMQ, there is _no additional overhead_ using this approach. However, both Azure Service Bus and Amazon SQS have a more complicated routing structure and because of that structure, additional charges may be incurred since messages need to be forwarded from topics to queues. For low- to medium-volume message loads this isn't a major concern, but for larger high-volume loads it may be preferable to _[send](/documentation/concepts/producers#send)_ (using `Send`) commands directly to the queue to reduce latency and cost. @@ -211,7 +212,7 @@ When defining message contracts, what follows is general guidance based upon yea ::list{type="success"} - Use records, define properties as `public` and specify `{ get; init; }` accessors. Create messages using the constructor/object initializer or a [message initializer](producers#message-initialization). - - Use interfaces, specify only `{ get; }` accessors. Create messages using message initializers and use the Roslyn Analyzer to identify missing or incompatible properties. + - Use interfaces, specify only `{ get; }` accessors. Create messages using message initializers and use the Roslyn Analyzer to identify missing or incompatible properties. - Limit the use of inheritance, pay attention to polymorphic message routing. A message type containing a dozen interfaces is a bit annoying to untangle if you need to delve deep into message routing to troubleshoot an issue. - Class inheritance has the same guidance as interfaces, but with more caution. :: @@ -223,3 +224,44 @@ When defining message contracts, what follows is general guidance based upon yea - A big base class may cause pain down the road as changes are made, particularly when supporting multiple message versions. :: +### Message Inheritance + +::alert{type="info"} +Message design is not object-oriented design. +:: + +This concept comes up often enough that it warrants its own special section. By design, MassTransit treats your classes, records, and interfaces as a "contract". + +An example, let's say that you have a message that is defined by the dotnet class below + +```csharp +public record SubmitOrder +{ + public string Sku { get; init; } + public int Quantity { get; init; } +} +``` + +You want all of your messages to have a common set of properties so you try and do this. + +```csharp +public record CoreEvent +{ + public string User { get; init; } +} + +public record SubmitOrder : + CoreEvent +{ + public string Sku { get; init; } + public int Quantity { get; init; } +} +``` + +If you try and consume a `Batch` and expect to get a variety of types, one of which would be `SubmitOrder`. In OOP land, that makes all the sense in the world, but in MassTransit contract design it does not. The application has said that it cares about batches of `CoreEvent` so it will only get back the single property `User`. This is not a symptom of using System.Text.Json, this has been the standard behavior of MassTransit since day one, even when using Netwonsoft.Json. MassTransit will always respect the contract that has been designed. + +If you want to have a standard set of properties available, by all means use a base class, or bundle them up into a single property, our preference. If you want to subscribe to all implementations of class, then you will need to subscribe to all implementations of a class. + + + + diff --git a/doc/content/3.documentation/1.concepts/2.consumers.md b/doc/content/3.documentation/1.concepts/2.consumers.md index b2b6896a6bc..aac90104967 100644 --- a/doc/content/3.documentation/1.concepts/2.consumers.md +++ b/doc/content/3.documentation/1.concepts/2.consumers.md @@ -33,7 +33,7 @@ class SubmitOrderConsumer : } ``` -To add a consumer and automatically configure a receive endpoint for the consumer, call one of the [_AddConsumer_](/documentation/configuration/bus/consumers) methods and call _ConfigureEndpoints_ as shown below. +To add a consumer and automatically configure a receive endpoint for the consumer, call one of the [_AddConsumer_](/documentation/configuration/consumers) methods and call _ConfigureEndpoints_ as shown below. ```csharp services.AddMassTransit(x => diff --git a/doc/content/3.documentation/1.concepts/4.exceptions.md b/doc/content/3.documentation/1.concepts/4.exceptions.md index e1ac33bb8b7..c5484aae8cd 100644 --- a/doc/content/3.documentation/1.concepts/4.exceptions.md +++ b/doc/content/3.documentation/1.concepts/4.exceptions.md @@ -62,11 +62,14 @@ With this consumer, an `ADOException` can be thrown, say there is a deadlock or services.AddMassTransit(x => { x.AddConsumer(); - - x.UsingRabbitMq((context,cfg) => + + x.AddConfigureEndpointsCallback((context,name,cfg) => { cfg.UseMessageRetry(r => r.Immediate(5)); + }); + x.UsingRabbitMq((context,cfg) => + { cfg.ConfigureEndpoints(context); }); }); @@ -97,7 +100,7 @@ services.AddMassTransit(x => }); ``` -MassTransit retry filters execute in memory and maintain a _lock_ on the message. As such, they should only be used to handle short, transient error conditions. Setting a retry interval of an hour would fall into the category of _bad things_. To retry messages after longer waits, look at the next section on redelivering messages. +MassTransit retry filters execute in memory and maintain a _lock_ on the message. As such, they should only be used to handle short, transient error conditions. Setting a retry interval of an hour would fall into the category of _bad things_. To retry messages after longer waits, look at the next section on redelivering messages. For example, if a consumer with a concurrency limit of 5 and a retry interval of one hour consumes 5 messages that causes retries, the consumer will be effectively stalled for a whole hour as all the concurrent message slots are in use waiting for the retry interval. ## Retry Configuration @@ -113,14 +116,14 @@ Learn more about message retry in this video When configuring message retry, there are several retry policies available, including: -| Policy | Description | -| :-- | :-- | -| None | No retry -| Immediate | Retry immediately, up to the retry limit -| Interval | Retry after a fixed delay, up to the retry limit -| Intervals | Retry after a delay, for each interval specified -| Exponential | Retry after an exponentially increasing delay, up to the retry limit -| Incremental | Retry after a steadily increasing delay, up to the retry limit +| Policy | Description | +|:------------|:---------------------------------------------------------------------| +| None | No retry | +| Immediate | Retry immediately, up to the retry limit | +| Interval | Retry after a fixed delay, up to the retry limit | +| Intervals | Retry after a delay, for each interval specified | +| Exponential | Retry after an exponentially increasing delay, up to the retry limit | +| Incremental | Retry after a steadily increasing delay, up to the retry limit | Each policy has configuration settings which specifies the expected behavior. @@ -189,12 +192,15 @@ To use delayed redelivery, ensure the transport is properly configured. RabbitMQ services.AddMassTransit(x => { x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => + + x.AddConfigureEndpointsCallback((context,name,cfg) => { cfg.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); cfg.UseMessageRetry(r => r.Immediate(5)); + }); + x.UsingRabbitMq((context, cfg) => + { cfg.ConfigureEndpoints(context); }); }); @@ -203,7 +209,8 @@ services.AddMassTransit(x => Now, if the initial 5 immediate retries fail (the database is really, really down), the message will retry an additional three times after 5, 15, and 30 minutes. This could mean a total of 15 retry attempts (on top of the initial 4 attempts prior to the retry/redelivery filters taking control). ::alert{type="info"} -MassTransit also supports scheduled redelivery using the `UseScheduledRedelivery` configuration method. Scheduled redelivery requires the use of a message scheduler, which can be configured to use the message transport or Quartz.NET/Hangfire. The configuration is similar, just ensure the scheduler is properly configured. +MassTransit also supports scheduled redelivery using the `UseScheduledRedelivery` configuration method. Scheduled redelivery requires the use of a message scheduler, which can be configured to use the message transport or Quartz.NET/Hangfire. The configuration is similar, just ensure the scheduler is properly configured. +However, in most cases using `UseDelayedRedelivery` (as configured above) is preferred to avoid overloading the scheduler with delayed redeliveries that typically have short redelivery times, leaving the scheduler free to do things like actual scheduling of messages. :: ## Outbox @@ -216,13 +223,16 @@ To configure the outbox with redelivery and retry: services.AddMassTransit(x => { x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => + + x.AddConfigureEndpointsCallback((context,name,cfg) => { cfg.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); cfg.UseMessageRetry(r => r.Immediate(5)); - cfg.UseInMemoryOutbox(); + cfg.UseInMemoryOutbox(context); + }); + x.UsingRabbitMq((context, cfg) => + { cfg.ConfigureEndpoints(context); }); }); @@ -247,7 +257,7 @@ services.AddMassTransit(x => { c.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); c.UseMessageRetry(r => r.Immediate(5)); - c.UseInMemoryOutbox(); + c.UseInMemoryOutbox(context); }); }); }); @@ -279,7 +289,7 @@ If the message headers specify a `FaultAddress`, the fault is sent directly to t ### Consuming Faults -Developers may want to do something with faults, such as updating an operational dashboard. To observe faults separate of the consumer that caused the fault to be produced, a consumer can consume fault messages the same as any other message. +Developers may want to do something with faults, such as updating an operational dashboard or notifying a support team. To observe faults separate of the consumer that caused the fault to be produced, a consumer can consume fault messages the same as any other message. ```csharp public class DashboardFaultConsumer : @@ -302,6 +312,18 @@ Event(() => SubmitOrderFaulted, x => x public Event> SubmitOrderFaulted { get; private set; } ``` +### Managing Faults + +In any production system faults will happen and you should be prepared to manage these. Faults need to be inspected to identify why the messages failed and once the cause for the problem has been identified and resolved, the messages should be retried. + +You can use broker built-in tools like RabbitMQ Management UI, Azure portal Service Bus Explorer, Amazon AWS SQS web console, or Cogent Queue Explorer to inspect the Faults. Once the reason for the fault has been resolved, you can use the tool to extract the original message and send it back to the original consumer. In this manner, messages that fail Retries and Redelivery can still be successfully processed at a later stage. + +Also see: + +- [Retrying messages with RabbitMQ](/documentation/transports/rabbitmq#retrying-messages) +- [Retrying messages with Azure Service Bus](/documentation/transports/azure-service-bus#retrying-messages) +- [Retrying messages with Amazon SQS](/documentation/transports/amazon-sqs#retrying-messages) + ## Error Pipe By default, MassTransit will move faulted messages to the *_error* queue. This behavior can be customized for each receive endpoint. diff --git a/doc/content/3.documentation/1.concepts/5.requests.md b/doc/content/3.documentation/1.concepts/5.requests.md deleted file mode 100644 index f42caed782e..00000000000 --- a/doc/content/3.documentation/1.concepts/5.requests.md +++ /dev/null @@ -1,366 +0,0 @@ -# Requests - -Request/response is a commonly used message pattern where one service sends a request to another service, continuing after the response is received. In a distributed system, this can increase the latency of an application since the service may be hosted in another process, on another machine, or may even be a remote service in another network. While in many cases it is best to avoid request/response use in distributed applications, particularly when the request is a command, it is often necessary and preferred over more complex solutions. - -In MassTransit, developers use a _request client_ to send or publish requests and wait for a response. The request client is asynchronous, and supports use of the _await_ keyword since it returns a _Task_. - -## Message Contracts - -To use the request client, create two message contracts, one for the request and one for the response. - -```csharp -public record CheckOrderStatus -{ - public string OrderId { get; init; } -} - -public record OrderStatusResult -{ - public string OrderId { get; init; } - public DateTime Timestamp { get; init; } - public short StatusCode { get; init; } - public string StatusText { get; init; } -} -``` - -## Request Consumer - -Request messages can be handled by any consumer type, including consumers, sagas, and routing slips. In this case, the consumer below consumes the _CheckOrderStatus_ message and responds with the _OrderStatusResult_ message. - -```csharp -public class CheckOrderStatusConsumer : - IConsumer -{ - readonly IOrderRepository _orderRepository; - - public CheckOrderStatusConsumer(IOrderRepository orderRepository) - { - _orderRepository = orderRepository; - } - - public async Task Consume(ConsumeContext context) - { - var order = await _orderRepository.Get(context.Message.OrderId); - if (order == null) - throw new InvalidOperationException("Order not found"); - - await context.RespondAsync(new - { - OrderId = order.Id, - order.Timestamp, - order.StatusCode, - order.StatusText - }); - } -} -``` - -If the _OrderId_ is found in the repository, an _OrderStatusResult_ message will be sent to the response address included with the request. The waiting request client will handle the response and complete the returned _Task_ allowing the requesting application to continue. - -If the _OrderId_ was not found, the consumer throws an exception. MassTransit catches the exception, generates a `Fault` message, and sends it to the response address. The request client handles the fault message and throws a _RequestFaultException_ via the awaited _Task_ containing the exception detail. - -## Request Client - -To use the request client, add the request client as a dependency as shown in the example API controller below. - -```csharp -public class RequestController : - Controller -{ - IRequestClient _client; - - public RequestController(IRequestClient client) - { - _client = client; - } - - [HttpGet("{orderId}")] - public async Task Get(string orderId, CancellationToken cancellationToken) - { - var response = await _client.GetResponse(new { orderId }, cancellationToken); - - return Ok(response.Message); - } -} -``` - -The controller method will send the request and return the order status after the response has been received. - -If the _cancellationToken_ passed to _GetResponse_ is canceled, the request client will stop waiting for a response. However, the request message produced remains in the queue until it is consumed or the message time-to-live expires. By default, the message time-to-live is set to the request timeout (which defaults to 30 seconds). - -## Client Configuration - -A request client can be resolved using dependency injection for any valid message type, no configuration is required. By default, request messages are _published_ and should be consumed by only one consumer/receive endpoint connected to the message broker. Multiple consumers connected to the same receive endpoint are fine, requests will be load balanced across the connected consumers. - -To configure the request client for a message type, add the request client to the configuration explicitly. - -```csharp -services.AddMassTransit(x => -{ - // configure the consumer on a specific endpoint address - x.AddConsumer() - .Endpoint(e => e.Name = 'order-status'); - - // Sends the request to the specified address, instead of publishing it - x.AddRequestClient(new Uri("exchange:order-status")); - - x.UsingInMemory((context, cfg) => - { - cfg.ConfigureEndpoints(context); - })); -}); -``` - -::alert{type="success"} -The request client receives responses on the bus endpoint, which is a temporary queue created by MassTransit. The queue is only created after the bus has started just before the first request. MassTransit adds a hosted service to the service collection that starts and stops the bus. If the request client is timing out (and therefore throwing a _RequestTimeoutException_), make sure that the bus is being started (check the logs, etc.). -:: - -## Request Headers - -To create a request and add a header to the `SendContext`, one option is to add an execute filter to the request pipeline. - -```csharp -await client.GetResponse(new GetOrderStatus{ OrderId = orderId }, - x => x.UseExecute(context => context.Headers.Set("tenant-id", "some-value"))); -``` - -Another option is to use the _object values_ overload, which uses a message initializer, to specify the header value. Learn more about [message initializers](/documentation/concepts/producers#message-initializers) in the _Concepts_ section. - -```csharp -await client.GetResponse(new -{ - orderId, - __Header_Tenant_Id = "some-value" -}); -``` - - -## Multiple Response Types - -Another powerful feature with the request client is the ability support multiple (such as positive and negative) result types. For example, adding an `OrderNotFound` response type to the consumer as shown eliminates throwing an exception since a missing order isn't really a fault. - -```csharp -public class CheckOrderStatusConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - var order = await _orderRepository.Get(context.Message.OrderId); - if (order == null) - await context.RespondAsync(context.Message); - else - await context.RespondAsync(new - { - OrderId = order.Id, - order.Timestamp, - order.StatusCode, - order.StatusText - }); - } -} -``` - -The client can now wait for multiple response types (in this case, two) by using a little tuple magic. - -```csharp -var response = await client.GetResponse(new { OrderId = id}); - -if (response.Is(out Response responseA)) -{ - // do something with the order -} -else if (response.Is(out Response responseB)) -{ - // the order was not found -} -``` - -This cleans up the processing, an eliminates the need to catch a `RequestFaultException`. - -It's also possible to use some of the switch expressions via deconstruction, but this requires the response variable to be explicitly specified as `Response`. - -```csharp -Response response = await client.GetResponse(new { OrderId = id}); - -// Using a regular switch statement -switch (response) -{ - case (_, OrderStatusResult a) responseA: - // order found - break; - case (_, OrderNotFound b) responseB: - // order not found - break; -} - -// Or using a switch expression -var accepted = response switch -{ - (_, OrderStatusResult a) => true, - (_, OrderNotFound b) => false, - _ => throw new InvalidOperationException() -}; -``` - -## Accept Response Types - -The request client sets a message header, `MT-Request-AcceptType`, that contains the response types supported by the request client. This allows the request consumer to determine if the client can handle a response type, which can be useful as services evolve and new response types may be added to handle new conditions. For instance, if a consumer adds a new response type, such as `OrderAlreadyShipped`, if the response type isn't supported an exception may be thrown instead. - -To see this in code, check out the client code: - -```csharp -var response = await client.GetResponse(new CancelOrder()); - -if (response.Is(out Response canceled)) -{ - return Ok(); -} -else if (response.Is(out Response responseB)) -{ - return NotFound(); -} -``` - -The original consumer, prior to adding the new response type: - -```csharp -public async Task Consume(ConsumeContext context) -{ - var order = _repository.Load(context.Message.OrderId); - if(order == null) - { - await context.ResponseAsync(new { context.Message.OrderId }); - return; - } - - order.Cancel(); - - await context.RespondAsync(new { context.Message.OrderId }); -} -``` - -Now, the new consumer that checks if the order has already shipped: - -```csharp -public async Task Consume(ConsumeContext context) -{ - var order = _repository.Load(context.Message.OrderId); - if(order == null) - { - await context.ResponseAsync(new { context.Message.OrderId }); - return; - } - - if(order.HasShipped) - { - if (context.IsResponseAccepted()) - { - await context.RespondAsync(new { context.Message.OrderId, order.ShipDate }); - return; - } - else - throw new InvalidOperationException("The order has already shipped"); // to throw a RequestFaultException in the client - } - - order.Cancel(); - - await context.RespondAsync(new { context.Message.OrderId }); -} -``` - -This way, the consumer can check the request client response types and act accordingly. - -::alert{type="success"} -For backwards compatibility, if the new `MT-Request-AcceptType` header is not found, `IsResponseAccepted` will return true for all message types. -:: - -## Concurrent Requests - -If there were multiple requests to be performed, it is easy to wait on all results at the same time, benefiting from the concurrent operation. - -```csharp -public class RequestController : - Controller -{ - IRequestClient _clientA; - IRequestClient _clientB; - - public RequestController(IRequestClient clientA, IRequestClient clientB) - { - _clientA = clientA; - _clientB = clientB; - } - - public async Task Get() - { - var resultA = _clientA.GetResponse(new RequestA()); - var resultB = _clientB.GetResponse(new RequestB()); - - await Task.WhenAll(resultA, resultB); - - var a = await resultA; - var b = await resultB; - - var model = new Model(a.Message, b.Message); - - return View(model); - } -} -``` - -The power of concurrency, for the win! - -## Request Client Details - -> The internals are documented for understanding, but what follows is optional reading. The above container-based configuration handles all the details to ensure the property context is used. - -The request client is composed of two parts, a client factory, and a request client. The client factory is created from the bus, or a connected endpoint, and has the interface below (some overloads are omitted, but you get the idea). - -```csharp -public interface IClientFactory -{ - IRequestClient CreateRequestClient(ConsumeContext context, Uri destinationAddress, RequestTimeout timeout); - - IRequestClient CreateRequestClient(Uri destinationAddress, RequestTimeout timeout); - - RequestHandle CreateRequest(T request, Uri destinationAddress, CancellationToken cancellationToken, RequestTimeout timeout); - - RequestHandle CreateRequest(ConsumeContext context, T request, Uri destinationAddress, CancellationToken cancellationToken, RequestTimeout timeout); -} -``` - -As shown, the client factory can create a request client, or it can create a request directly. There are advantages to each approach, although it's typically best to create a request client and use it if possible. If a consumer is sending the request, a new client should be created for each message (and is handled automatically if you're using a dependency injection container and the container registration methods). - -To create a client factory, call `bus.CreateClientFactory` or `host.CreateClientFactory` -- after the bus has been started. - -The request client can be used to create requests (returning a `RequestHandle`, which must be disposed after the request completes) or it can be used directly to send a request and get a response (asynchronously, of course). - -> Using `Create` returns a request handle, which can be used to set headers and other attributes of the request before it is sent. - -```csharp -public interface IRequestClient - where TRequest : class -{ - RequestHandle Create(TRequest request, CancellationToken cancellationToken, RequestTimeout timeout); - - Task> GetResponse(TRequest request, CancellationToken cancellationToken, RequestTimeout timeout); -} -``` - -> For `RequestTimeout` three options are available, `None`, `Default`, and a factory with `RequestTimeout.After`. `None` would never be recommended since it would essentially wait forever for a response. There is always a relevant timeout, or you're using the wrong pattern. - -## Sending a Request - -To create a request client, and use it to make a standalone request (not from a consumer, API controller, etc.): - -```csharp -var serviceAddress = new Uri("rabbitmq://localhost/check-order-status"); -var client = bus.CreateRequestClient(serviceAddress); - -var response = await client.GetResponse(new { OrderId = id}); -``` - -The response type, `Response` includes the _MessageContext_ from when the response was received, providing access to the message properties (such as `response.ConversationId`) and headers (`response.Headers`). - - - diff --git a/doc/content/3.documentation/1.concepts/5.testing.md b/doc/content/3.documentation/1.concepts/5.testing.md new file mode 100644 index 00000000000..20cb9eb5b83 --- /dev/null +++ b/doc/content/3.documentation/1.concepts/5.testing.md @@ -0,0 +1,318 @@ +# Testing + +MassTransit is an asynchronous framework that enables the development of high-performance and flexible distributed applications. Because of MassTransit's asynchronous underpinning, unit testing consumers, sagas, and routing slip activities can be significantly more complex. To simplify the creation of unit and integration tests, MassTransit includes a [Test Harness](/documentation/configuration/test-harness) that simplifies test creation. + +## Test Harness Features + +- Simplifies configuration for a majority of unit test scenarios +- Provides an in-memory transport, saga repository, and message scheduler +- Exposes published, sent, and consumed messages +- Supports Web Application Factory for testing ASP.NET Applications + +## Test Harness Concepts + +As stated above, MassTransit is an asynchronous framework. In most cases, developers want to test that message consumption is successful, consumer behavior is as expected, and messages are published and/or sent. Because these actions are performed asynchronously, MassTransit's test harness exposes several asynchronous collections allowing test assertions verifying developer expectations. These asynchronous collections are backed by an over test timer and an inactivity timer, so it's important to use a test harness only once for a given scenario. Multiple test assertions, messages, and behaviors are normal in a given test, but unrelated scenarios should not share a single test harness. + +MassTransit's test harness is built around Microsoft's Dependency Injection and is configured using the `AddMassTransitTestHarness` extension method. An test example is shown below, that verifies a `SubmitOrderConsumer` consumes a request and responds to the requester. + +```csharp +[Test] +public async Task An_example_unit_test() +{ + await using var provider = new ServiceCollection() + .AddYourBusinessServices() // register all of your normal business services + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetRequiredService(); + + await harness.Start(); + + var client = harness.GetRequestClient(); + + var response = await client.GetResponse(new + { + OrderId = InVar.Id, + OrderNumber = "123" + }); + + Assert.IsTrue(await harness.Sent.Any()); + + Assert.IsTrue(await harness.Consumed.Any()); + + var consumerHarness = harness.GetConsumerHarness(); + + Assert.That(await consumerHarness.Consumed.Any()); + + // test side effects of the SubmitOrderConsumer here +} +``` + +In the example above, the `AddMassTransitTestHarness` method is used to configure MassTransit, the in-memory transport, and the test harness on the service collection using all the default settings. This simple method is the same as the configuration shown below. The default settings eliminate all the extra code, simplifying the test set up. + +```csharp +.AddMassTransitTestHarness(x => +{ + x.AddDelayedMessageScheduler(); + + x.AddConsumer(); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); +}) +``` + +### Transport Support + +MassTransit's test harness can be used with any supported transport, and can also be used write rider integration tests (there is no in-memory rider implementation for unit testing). For example, to create an integration test using RabbitMQ, the RabbitMQ transport can be configured as shown. + +```csharp +.AddMassTransitTestHarness(x => +{ + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); +}) +``` + +In this example, RabbitMQ is configured using the default settings (which specifies a broker running on `localhost` using the default username and password of `guest`). + + +### SetTestTimeouts + +In the example above, the `await harness.Consumed.Any()` method call waits for the `SubmitOrder` message to be consumed within the time specified by the `TestInactivityTimeout` property of the Test Harness. The default timeout is 30 seconds, and 50 minutes while debugging, configured by the [`TestHarnessOptions`](https://github.com/MassTransit/MassTransit/blob/develop/src/MassTransit/Configuration/DependencyInjection/TestHarnessOptions.cs#L9), and can be set: + +To set both the overall test timeout and the test inactivity timeout, call the `SetTestTimeouts` method: + +```csharp +cfg.SetTestTimeouts(TimeSpan.FromSeconds(60), TimeSpan.FromSeconds(5))); +``` + +Either timeout value can be set individually using the appropriately named parameter: + +```csharp +cfg.SetTestTimeouts(testTimeout: TimeSpan.FromSeconds(60)); + +cfg.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); +``` + +### Message Assertions + +To read the messages that were sent you can use the `Select` or `SelectAsync` methods: + +```csharp +var messageSent = await harness.Sent.SelectAsync() + .FirstOrDefault(); + +Assert.AreEqual(orderId, messageSent?.Context.Message.OrderId); +``` + +::callout{type="info"} +#summary +If you don't limit the number of messages to wait for, e.g. by using the `.FirstOrDefault()`, then the `Select` and `SelectAsync` methods **wait until the `TestTimeout`.** This means **50 minutes** by default while debugging, making the debugging experience unpleasant. +#content +For example, using Fluent Assertions the line below would be perfectly fine, but in this case it causes the test to block until the Test Timeout expires. + +```csharp +// Waits for the full TestTimeout +❌ harness.Sent.Select() + .Should().HaveCountGreaterThan(0); +``` +:: + + +## Web Application Factory + +MassTransit's test harness can be used with Microsoft's Web Application Factory, allowing unit and/or integration testing of ASP.NET applications. + +:sample{sample=web-application-factory} + +To configure MassTransit's test harness for use with the Web Application Factory, call `AddMassTransitTestHarness` in the set up as shown below. + +```csharp +await using var application = new WebApplicationFactory() + .WithWebHostBuilder(builder => + builder.ConfigureServices(services => + services.AddMassTransitTestHarness())); + +var testHarness = application.Services.GetTestHarness(); + +using var client = application.CreateClient(); + +var orderId = NewId.NextGuid(); + +var submitOrderResponse = await client.PostAsync("/Order", JsonContent.Create(new Order +{ + OrderId = orderId +})); + +var consumerTestHarness = testHarness.GetConsumerHarness(); + +Assert.That(await consumerTestHarness.Consumed.Any(x => x.Context.Message.OrderId == orderId), Is.True); +``` + +## Examples + +The following are examples of using the `TestHarness` to test various components. + +### Consumer + +To test a consumer using the MassTransit Test Harness: + +```csharp +[Test] +public async Task ASampleTest() +{ + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(cfg => + { + cfg.AddConsumer(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetRequiredService(); + + await harness.Start(); + + var client = harness.GetRequestClient(); + + await client.GetResponse(new + { + OrderId = InVar.Id, + OrderNumber = "123" + }); + + Assert.IsTrue(await harness.Sent.Any()); + + Assert.IsTrue(await harness.Consumed.Any()); + + var consumerHarness = harness.GetConsumerHarness(); + + Assert.That(await consumerHarness.Consumed.Any()); + + // test side effects of the SubmitOrderConsumer here +} +``` + +### Request / Response + +To test a Request using the MassTransit Test Harness: + +```csharp +[Test] +public async Task ASampleTest() +{ + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(cfg => + { + cfg.Handler(async cxt => + { + await cxt.RespondAsync(new OrderResponse("OK")); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetRequiredService(); + + await harness.Start(); + + var client = harness.GetRequestClient(); + + var response = await client.GetResponse(new SubmitOrder + { + OrderNumber = "123" + }); + + Assert.That(response.Message.Status, Is.EqualTo("OK")); +} +``` +[Short Addresses](https://masstransit.io/documentation/concepts/producers#short-addresses) + +### Saga State Machine + +To test a saga state machine using the MassTransit Test Harness: + +```csharp +[Test] +public async Task ASampleTest() +{ + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(cfg => + { + cfg.AddSagaStateMachine(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetRequiredService(); + + await harness.Start(); + + var sagaId = Guid.NewGuid(); + var orderNumber = "ORDER123"; + + await harness.Bus.Publish(new OrderSubmitted + { + CorrelationId = sagaId, + OrderNumber = orderNumber + }); + + Assert.That(await harness.Consumed.Any()); + + var sagaHarness = harness.GetSagaStateMachineHarness(); + + Assert.That(await sagaHarness.Consumed.Any()); + + Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == sagaId)); + + var instance = sagaHarness.Created.ContainsInState(sagaId, sagaHarness.StateMachine, sagaHarness.StateMachine.Submitted); + Assert.IsNotNull(instance, "Saga instance not found"); + Assert.That(instance.OrderNumber, Is.EqualTo(orderNumber)); + + Assert.IsTrue(await harness.Published.Any()); + + // test side effects of OrderState here +} +``` + +### Routing Slips + +To test a routing slip activity using the MassTransit Test Harness: + +```csharp +[Test] +public async Task ASampleTest() +{ + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(cfg => + { + cfg.AddActivity(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetRequiredService(); + + await harness.Start(); + + var addr = harness.GetExecuteActivityAddress(); + var builder = new RoutingSlipBuilder(NewId.NextGuid()); + builder.AddActivity("test", addr, new { + OrderNumber = "ORDER123" + }) + + await harness.Bus.Execute(builder.Build()) + + await harness.Published.Any(); + + // test side effects of MyActivity here +} +``` diff --git a/doc/content/3.documentation/1.concepts/6.requests.md b/doc/content/3.documentation/1.concepts/6.requests.md new file mode 100644 index 00000000000..743217dc66c --- /dev/null +++ b/doc/content/3.documentation/1.concepts/6.requests.md @@ -0,0 +1,393 @@ +# Requests + +Request/response is a commonly used message pattern where one service sends a request to another service, continuing after the response is received. In a distributed system, this can increase the latency of an application since the service may be hosted in another process, on another machine, or may even be a remote service in another network. While in many cases it is best to avoid request/response use in distributed applications, particularly when the request is a command, it is often necessary and preferred over more complex solutions. + +In MassTransit, developers use a _request client_ to send or publish requests and wait for a response. The request client is asynchronous, and supports use of the _await_ keyword since it returns a _Task_. + +## Message Contracts + +To use the request client, create two message contracts, one for the request and one for the response. + +```csharp +public record CheckOrderStatus +{ + public string OrderId { get; init; } +} + +public record OrderStatusResult +{ + public string OrderId { get; init; } + public DateTime Timestamp { get; init; } + public short StatusCode { get; init; } + public string StatusText { get; init; } +} +``` + +## Request Consumer + +Request messages can be handled by any consumer type, including consumers, sagas, and routing slips. In this case, the consumer below consumes the _CheckOrderStatus_ message and responds with the _OrderStatusResult_ message. + +```csharp +public class CheckOrderStatusConsumer : + IConsumer +{ + readonly IOrderRepository _orderRepository; + + public CheckOrderStatusConsumer(IOrderRepository orderRepository) + { + _orderRepository = orderRepository; + } + + public async Task Consume(ConsumeContext context) + { + var order = await _orderRepository.Get(context.Message.OrderId); + if (order == null) + throw new InvalidOperationException("Order not found"); + + await context.RespondAsync(new + { + OrderId = order.Id, + order.Timestamp, + order.StatusCode, + order.StatusText + }); + } +} +``` + +If the _OrderId_ is found in the repository, an _OrderStatusResult_ message will be sent to the response address included with the request. The waiting request client will handle the response and complete the returned _Task_ allowing the requesting application to continue. + +If the _OrderId_ was not found, the consumer throws an exception. MassTransit catches the exception, generates a `Fault` message, and sends it to the response address. The request client handles the fault message and throws a _RequestFaultException_ via the awaited _Task_ containing the exception detail. + +## Request Client + +To use the request client, add the request client as a dependency as shown in the example API controller below. + +```csharp +public class RequestController : + Controller +{ + IRequestClient _client; + + public RequestController(IRequestClient client) + { + _client = client; + } + + [HttpGet("{orderId}")] + public async Task Get(string orderId, CancellationToken cancellationToken) + { + var response = await _client.GetResponse(new { orderId }, cancellationToken); + + return Ok(response.Message); + } +} +``` + +The controller method will send the request and return the order status after the response has been received. + +If the _cancellationToken_ passed to _GetResponse_ is canceled, the request client will stop waiting for a response. However, the request message produced remains in the queue until it is consumed or the message time-to-live expires. By default, the message time-to-live is set to the request timeout (which defaults to 30 seconds). + +### Client Configuration + +A request client can be resolved using dependency injection for any valid message type, no configuration is required. By default, request messages are _published_ and should be consumed by only one consumer/receive endpoint connected to the message broker. Multiple consumers connected to the same receive endpoint are fine, requests will be load balanced across the connected consumers. + +To configure the request client for a message type, add the request client to the configuration explicitly. + +```csharp +services.AddMassTransit(x => +{ + // configure the consumer on a specific endpoint address + x.AddConsumer() + .Endpoint(e => e.Name = "order-status"); + + // Sends the request to the specified address, instead of publishing it + x.AddRequestClient(new Uri("exchange:order-status")); + + x.UsingInMemory((context, cfg) => + { + cfg.ConfigureEndpoints(context); + })); +}); +``` + +::alert{type="success"} +The request client receives responses on the bus endpoint, which is a temporary queue created by MassTransit. The queue is only created after the bus has started just before the first request. MassTransit adds a hosted service to the service collection that starts and stops the bus. If the request client is timing out (and therefore throwing a _RequestTimeoutException_), make sure that the bus is being started (check the logs, etc.). +:: + +### Request Headers + +To create a request and add a header to the `SendContext`, one option is to add an execute filter to the request pipeline. + +```csharp +await client.GetResponse(new GetOrderStatus{ OrderId = orderId }, + x => x.UseExecute(context => context.Headers.Set("tenant-id", "some-value"))); +``` + +Another option is to use the _object values_ overload, which uses a message initializer, to specify the header value. Learn more about [message initializers](/documentation/concepts/producers#message-initializers) in the _Concepts_ section. + +```csharp +await client.GetResponse(new +{ + orderId, + __Header_Tenant_Id = "some-value" +}); +``` + + +### Multiple Response Types + +Another powerful feature with the request client is the ability support multiple (such as positive and negative) result types. For example, adding an `OrderNotFound` response type to the consumer as shown eliminates throwing an exception since a missing order isn't really a fault. + +```csharp +public class CheckOrderStatusConsumer : + IConsumer +{ + public async Task Consume(ConsumeContext context) + { + var order = await _orderRepository.Get(context.Message.OrderId); + if (order == null) + await context.RespondAsync(context.Message); + else + await context.RespondAsync(new + { + OrderId = order.Id, + order.Timestamp, + order.StatusCode, + order.StatusText + }); + } +} +``` + +The client can now wait for multiple response types (in this case, two) by using a little tuple magic. + +```csharp +var response = await client.GetResponse(new { OrderId = id}); + +if (response.Is(out Response responseA)) +{ + // do something with the order +} +else if (response.Is(out Response responseB)) +{ + // the order was not found +} +``` + +This cleans up the processing, an eliminates the need to catch a `RequestFaultException`. + +It's also possible to use some of the switch expressions via deconstruction, but this requires the response variable to be explicitly specified as `Response`. + +```csharp +Response response = await client.GetResponse(new { OrderId = id}); + +// Using a regular switch statement +switch (response) +{ + case (_, OrderStatusResult a) responseA: + // order found + break; + case (_, OrderNotFound b) responseB: + // order not found + break; +} + +// Or using a switch expression +var accepted = response switch +{ + (_, OrderStatusResult a) => true, + (_, OrderNotFound b) => false, + _ => throw new InvalidOperationException() +}; +``` + +### Accept Response Types + +The request client sets a message header, `MT-Request-AcceptType`, that contains the response types supported by the request client. This allows the request consumer to determine if the client can handle a response type, which can be useful as services evolve and new response types may be added to handle new conditions. For instance, if a consumer adds a new response type, such as `OrderAlreadyShipped`, if the response type isn't supported an exception may be thrown instead. + +To see this in code, check out the client code: + +```csharp +var response = await client.GetResponse(new CancelOrder()); + +if (response.Is(out Response canceled)) +{ + return Ok(); +} +else if (response.Is(out Response responseB)) +{ + return NotFound(); +} +``` + +The original consumer, prior to adding the new response type: + +```csharp +public async Task Consume(ConsumeContext context) +{ + var order = _repository.Load(context.Message.OrderId); + if(order == null) + { + await context.ResponseAsync(new { context.Message.OrderId }); + return; + } + + order.Cancel(); + + await context.RespondAsync(new { context.Message.OrderId }); +} +``` + +Now, the new consumer that checks if the order has already shipped: + +```csharp +public async Task Consume(ConsumeContext context) +{ + var order = _repository.Load(context.Message.OrderId); + if(order == null) + { + await context.ResponseAsync(new { context.Message.OrderId }); + return; + } + + if(order.HasShipped) + { + if (context.IsResponseAccepted()) + { + await context.RespondAsync(new { context.Message.OrderId, order.ShipDate }); + return; + } + else + throw new InvalidOperationException("The order has already shipped"); // to throw a RequestFaultException in the client + } + + order.Cancel(); + + await context.RespondAsync(new { context.Message.OrderId }); +} +``` + +This way, the consumer can check the request client response types and act accordingly. + +::alert{type="success"} +For backwards compatibility, if the new `MT-Request-AcceptType` header is not found, `IsResponseAccepted` will return true for all message types. +:: + +### Concurrent Requests + +If there were multiple requests to be performed, it is easy to wait on all results at the same time, benefiting from the concurrent operation. + +```csharp +public class RequestController : + Controller +{ + IRequestClient _clientA; + IRequestClient _clientB; + + public RequestController(IRequestClient clientA, IRequestClient clientB) + { + _clientA = clientA; + _clientB = clientB; + } + + public async Task Get() + { + var resultA = _clientA.GetResponse(new RequestA()); + var resultB = _clientB.GetResponse(new RequestB()); + + await Task.WhenAll(resultA, resultB); + + var a = await resultA; + var b = await resultB; + + var model = new Model(a.Message, b.Message); + + return View(model); + } +} +``` + +The power of concurrency, for the win! + +### Request Handle + +Client factories or the request client can also be used to create a request instead of calling `GetResponse`. This is an uncommon scenario, but is available as an option and may make sense depending on the situation. If a request is created (which returns a `RequestHandle`), the request handle must be disposed after the request completes. + +> Using `Create` returns a request handle, which can be used to set headers and other attributes of the request before it is sent. + +```csharp +public interface IRequestClient + where TRequest : class +{ + RequestHandle Create(TRequest request, CancellationToken cancellationToken, RequestTimeout timeout); +} +``` + +> For `RequestTimeout` three options are available, `None`, `Default`, and a factory with `RequestTimeout.After`. `None` would never be recommended since it would essentially wait forever for a response. There is always a relevant timeout, or you're using the wrong pattern. + +## Request Client Factory + +> The internals are documented for understanding, but what follows is optional reading. The above container-based configuration handles all the details to ensure the proper context is used. + +The request client is composed of two parts, a client factory and a request client. There are two client factories, the scoped client factory, and the bus client factory. + +### IScopedClientFactory + +Using `IRequestClient` requires a container scope, and the request client for a request message type is resolved from container scope using a scoped client factory. As an alternative to specifying `IRequestClient` as a constructor dependency, the scoped client factory can be used instead of create a request client directly. This can be useful when the destination address may change based on context, such as a _TenantId_. + +```csharp +public interface IScopedClientFactory +{ + IRequestClient CreateRequestClient(RequestTimeout timeout = default) + where T : class; + + IRequestClient CreateRequestClient(Uri destinationAddress, RequestTimeout timeout = default) + where T : class; +} +``` + +An example showing how to use `IScopedClientFactory` is shown below. + +```csharp +[HttpGet] +public async Task HandleGet(string tenantId, int id, [FromServices] IScopedClientFactory clientFactory) +{ + var serviceAddress = new Uri($"exchange:check-order-status-{tenantId}"); + + var client = clientFactory.CreateRequestClient(serviceAddress); + + var response = await client.GetResponse(new { OrderId = id}); + + return Ok(); +} +``` + +### IClientFactory + +If there is no container scope available, and one cannot be created, the root client factory can be used instead. *Note that non-scoped interfaces are not compatible with scoped publish or send filters*. + +```csharp +public interface IClientFactory +{ + IRequestClient CreateRequestClient(ConsumeContext context, Uri destinationAddress, RequestTimeout timeout); + + IRequestClient CreateRequestClient(Uri destinationAddress, RequestTimeout timeout); +} +``` + +An example showing how to use `IClientFactory` is shown below. + +```csharp +public async Task WorkerMethod(IServiceProvider provider) +{ + var clientFactory = provider.GetRequiredService(); + + var serviceAddress = new Uri("exchange:check-order-status"); + + var client = clientFactory.CreateRequestClient(serviceAddress); + + var response = await client.GetResponse(new { OrderId = id}); +} +``` + diff --git a/doc/content/3.documentation/1.concepts/7.routing-slips.md b/doc/content/3.documentation/1.concepts/7.routing-slips.md new file mode 100644 index 00000000000..e25b4026bdc --- /dev/null +++ b/doc/content/3.documentation/1.concepts/7.routing-slips.md @@ -0,0 +1,422 @@ +# Routing Slips + +Developing applications with a distributed, message-based architecture adds complexity to handling transactions, especially when all steps must either succeed together or fail completely. In traditional applications using an ACID database, transactions are managed through SQL, where partial operations are rolled back if the transaction fails. However, this approach doesn’t scale well when the transaction spans multiple services or databases. In modern microservices architectures, the reliance on a single ACID database has become increasingly rare. + +MassTransit Routing Slips address this challenge by enabling distributed transactions with fault compensation, designed to scale across a network of services. It provides functionality previously handled by database transactions, but adapted for distributed systems. Routing Slips also integrate seamlessly with saga state machines, which add capabilities for transaction monitoring and recoverability. + +MassTransit implements the [Routing Slip pattern](https://www.enterpriseintegrationpatterns.com/patterns/messaging/RoutingTable.html), leveraging durable messaging transports and the advanced saga features of MassTransit. Routing slips simplify the coordination of distributed transactions. When combined with a saga state machine, routing slips create a robust, recoverable, and maintainable approach to message processing across multiple services. + +Beyond basic routing slip functionality, MassTransit also supports [compensations][1], allowing activities to store execution data so that reversible operations can be undone. Compensation can be achieved either through traditional rollbacks or by performing offsetting operations. For instance, an activity that reserves a seat for a customer could release that reservation if compensation is triggered. + +## Activities + +In a MassTransit routing slip, an *Activity* refers to a processing step that can be added to a routing slip. + +### Compensating + +To create an activity, create a class that implements the `IActivity` interface for activities that support compensation or `IExecuteActivity` for those that don't need compensation. + +```csharp +public class DownloadImageActivity : + IActivity +{ + Task Execute(ExecuteContext context); + Task Compensate(CompensateContext context); +} +``` + +The `IActivity` interface has two generic arguments. The first specifies the activity’s argument type and the second specifies the activity’s log type. In the example above, `DownloadImageArguments` is the argument type and `DownloadImageLog` is the log type. The type parameters may be an interface, class or record type. Where the type is a class or a record, the proper accessors should be specified (i.e. `{ get; set; }` or `{ get; init; }`). + +### Non-Compensating + +```csharp +public class DownloadImageActivity : + IExecuteActivity +{ + Task Execute(ExecuteContext context); +} +``` + +::alert{type="info"} +An *Execute Activity* is an activity that only executes and does not support compensation. As such, the declaration of a log type is not required. +:: + +## Implementation + +| Verb | Description | +|:-----------|:----------------------------------------------------------------------------------------------------| +| Execute | The primary action that the activity performs as part of the workflow. | +| Compensate | The action the activity must take to undo or reverse its effects if any subsequent activities fail. | + +### Execute + +Both `IActivity`and `IExecuteActivity` require you to implement the `Execute` method. _Execute_ is called while the routing slip is executing activities. + +When *Execute* is called, the `ExecuteContext` argument contains the activity arguments, the routing slip's *TrackingNumber*, and methods to complete or fault the activity. The actual routing slip message, as well as any details of the underlying infrastructure, are excluded to prevent coupling between the activity and the implementation. An example *Execute* method is shown below. + +```csharp +async Task Execute(ExecuteContext execution) +{ + DownloadImageArguments args = execution.Arguments; + string imageSavePath = Path.Combine(args.WorkPath, + execution.TrackingNumber.ToString()); + + await _httpClient.GetAndSave(args.ImageUri, imageSavePath); + + return execution.Completed(new {ImageSavePath = imageSavePath}); +} +``` + +## Execution Results + +After an activity finishes processing, it returns an *ExecutionResult* to the host. If the activity completes successfully, it can choose to store compensation data in an activity log, which is passed to the *Completed* method on the `ExecuteContext` argument. If no compensation data is needed, the activity log is optional. In addition to compensation data, the activity can also add or modify variables stored in the routing slip for use by subsequent activities. + +| Result | Description | +|:----------|:--------------------------------------------------------------------------------| +| Complete | Indicates the activity completed successfully. | +| Fault | Indicates the activity failed. | +| Terminate | Indicates the routing slip should stop, but the process is considered complete. | + +### Completing + +#### Complete + + +```csharp +async Task Execute(ExecuteContext execution) +{ + DownloadImageArguments args = execution.Arguments; + string imageSavePath = Path.Combine(args.WorkPath, + execution.TrackingNumber.ToString()); + + await _httpClient.GetAndSave(args.ImageUri, imageSavePath); + + // success with no compensation log + return execution.Completed(); +} +``` + +#### Complete - With Compensation Log + +```csharp +async Task Execute(ExecuteContext execution) +{ + DownloadImageArguments args = execution.Arguments; + string imageSavePath = Path.Combine(args.WorkPath, + execution.TrackingNumber.ToString()); + + await _httpClient.GetAndSave(args.ImageUri, imageSavePath); + + return execution.Completed(new {ImageSavePath = imageSavePath}); +} +``` + +In the example above, the activity specifies the *DownloadImageLog* interface and initializes the log using an anonymous object. The object is then passed to the *Completed* method for storage in the routing slip before sending the routing slip to the next activity. + +### Revise Itinerary + +An activity has complete control over the routing slip and can revise the itinerary to include additional activities. + +```csharp +async Task Execute(ExecuteContext execution) +{ + DownloadImageArguments args = execution.Arguments; + string imageSavePath = Path.Combine(args.WorkPath, + execution.TrackingNumber.ToString()); + + await _httpClient.GetAndSave(args.ImageUri, imageSavePath); + + return execution.ReviseItinerary(builder => + { + // add activity at the beginning of the current itinerary + builder.AddActivity("Deviation", new Uri($"exchange:{optionalAddress}")); + + // maintain the existing activities + builder.AddActivitiesFromSourceItinerary(); + + // add activity at the end of the current itinerary + builder.AddActivity("Deviation", new Uri($"exchange:{optionalAddress}")); + }); +} +``` + +### Faulting + +By default, if an activity throws an exception, it will be _faulted_ and a `RoutingSlipFaulted` event will be published (unless a subscription changes the rules). An activity can also return _Faulted_ rather than throwing an exception. + +#### Throwing an Exception + +```csharp +async Task Execute(ExecuteContext execution) +{ + DownloadImageArguments args = execution.Arguments; + string imageSavePath = Path.Combine(args.WorkPath, + execution.TrackingNumber.ToString()); + + await _httpClient.GetAndSave(args.ImageUri, imageSavePath); + + // will throw an exception + var result = 100 / 0; + + return execution.Completed(); +} +``` + +In the example above, the activity will throw an exception which will result in a `Fault` which will include data about the exception. + +#### Explicitly + +```csharp +async Task Execute(ExecuteContext execution) +{ + DownloadImageArguments args = execution.Arguments; + string imageSavePath = Path.Combine(args.WorkPath, + execution.TrackingNumber.ToString()); + + await _httpClient.GetAndSave(args.ImageUri, imageSavePath); + + return execution.Faulted(); +} +``` + +In the example above, the activity can look at the data and then explicitly return a `Faulted` result. + +### Terminating + +In some situations, it may make sense to terminate the routing slip without executing any of the subsequent activities in the itinerary. This might be due to a business rule, in which the routing slip shouldn't be faulted, but needs to end immediately. + +To terminate a routing slip, call _Terminate_ as shown. + +```csharp +async Task Execute(ExecuteContext execution) +{ + // regular termination + return execution.Terminate(); +} +``` + +An optional reason can also be specified. + +```csharp +async Task Execute(ExecuteContext execution) +{ + // terminate and include additional variables in the event + return execution.Terminate(new { Reason = "Not a good time, dude."}); +} +``` + +### Compensating + +Only `IActivity` requires you to implement the `Compensate` method. _Compensate_ is called when a subsequent activity has faulted so that the activity can undo or reverse its effects. + +When an activity fails, the *Compensate* method is called for previously executed activities in the routing slip that stored compensation data. If an activity does not store any compensation data, the *Compensate* method is never called. + +```csharp +return context.Completed(new LogModel { + SomeData = "abc" +}) +``` + +The compensation method for the example above is shown below. + +```csharp +Task Compensate(CompensateContext compensation) +{ + DownloadImageLog log = compensation.Log; + File.Delete(log.ImageSavePath); + + return compensation.Compensated(); +} +``` + +Using the activity log data, the activity compensates by removing the downloaded image from the work directory. Once the activity has successfully compensated for the previous execution, it returns a *CompensationResult* by calling the *Compensated* method. If the compensation cannot be completed (due to logic issues or exceptions) and this results in a failure, the *Failed* method should be used, optionally providing an *Exception*. + +## Building a Routing Slip + +A routing slip defines a sequence of processing steps, called *activities*, that are combined into a single itinerary. As each activity finishes, the routing slip is forwarded to the next activity in the itinerary. When all activities are completed, the routing slip is finalized, marking the transaction as complete. + +One key advantage of using a routing slip is its flexibility—activities can vary for each transaction. Depending on factors like payment methods, billing/shipping address, or customer preferences, the routing slip builder can dynamically add activities. This is in contrast to the more rigid, predefined behavior of a state machine or sequential workflow, which are statically defined through code, a DSL, or frameworks like Windows Workflow. + +### Routing Slip Structure + +A routing slip contains an itinerary, variables, and activity/compensation logs. It is defined by a message contract, which the underlying Courier components use to execute and compensate the transaction. The routing slip contract includes: + +- A unique tracking number for each routing slip +- An itinerary, which is an ordered list of activities +- An activity log, recording an ordered list of previously executed activities +- A compensation log, listing previously executed activities that can be compensated if the routing slip faults +- A collection of variables, which can be mapped to activity arguments +- A collection of subscriptions for notifying consumers of routing slip events +- A collection of exceptions that may have occurred during routing slip execution + +### Routing Slip Builder + +Instead of directly implementing the *RoutingSlip* message type, developers are encouraged to use a *RoutingSlipBuilder* to construct the routing slip. The *RoutingSlipBuilder* simplifies the process by providing methods to add activities (and their arguments), activity logs, and variables to the routing slip. For example, to create a routing slip with two activities and an additional variable, a developer might write: + + +```csharp +var builder = new RoutingSlipBuilder(NewId.NextGuid()); +builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage"), + new + { + ImageUri = new Uri("http://images.google.com/someImage.jpg") + }); +builder.AddActivity("FilterImage", new Uri("rabbitmq://localhost/execute_filterimage")); +builder.AddVariable("WorkPath", @"\dfs\work"); + +var routingSlip = builder.Build(); +``` + +Each activity requires a name for display purposes and a URI specifying the execution address. The execution address is where the routing slip should be sent to execute the activity. For each activity, arguments can be specified that are stored and presented to the activity via the activity arguments interface type specify by the first argument of the *IActivity* interface. The activities added to the routing slip are combined into an *Itinerary*, which is the list of activities to be executed, and stored in the routing slip. + +> Managing the inventory of available activities, as well as their names and execution addresses, is the responsibility of the application and is not part of the MassTransit Courier. Since activities are application specific, and the business logic to determine which activities to execute and in what order is part of the application domain, the details are left to the application developer. + +### Activity Arguments + +Each activity declares an activity argument type, which must be an interface. When the routing slip is received by an activity host, the argument type is used to read data from the routing slip and deliver it to the activity. + +The argument properties are mapped, by name, to the argument type from the routing slip using: + +- Explicitly declared arguments, added to the itinerary with the activity +- Implicitly mapped arguments, added as variables to the routing slip + +To specify an explicit activity argument, specify the argument value while adding the activity using the routing slip builder. + +```csharp +var builder = new RoutingSlipBuilder(NewId.NextGuid()); +builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage"), new + { + ImageUri = new Uri("http://images.google.com/someImage.jpg") + }); +``` + +To specify an implicit activity argument, add a variable to the routing slip with the same name/type as the activity argument. + +```csharp +var builder = new RoutingSlipBuilder(NewId.NextGuid()); +builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage")); +builder.AddVariable("ImageUri", "http://images.google.com/someImage.jpg"); +``` + +If an activity argument is not specified when the routing slip is created, it may be added by an activity that executes prior to the activity that requires the argument. For instance, if the _DownloadImage_ activity stored the image in a local cache, that address could be added and used by another activity to access the cached image. + +First, the routing slip would be built without the argument value. + +```csharp +var builder = new RoutingSlipBuilder(NewId.NextGuid()); +builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage")); +builder.AddActivity("ProcessImage", new Uri("rabbitmq://localhost/execute_processimage")); +builder.AddVariable("ImageUri", "http://images.google.com/someImage.jpg"); +``` + +Then, the first activity would add the variable to the routing slip on completion. + +```csharp +async Task Execute(ExecuteContext context) +{ + ... + return context.CompletedWithVariables(new { ImagePath = ...}); +} +``` + +The process image activity would then use that variable as an argument value. + +```csharp +async Task Execute(ExecuteContext context) +{ + var path = context.Arguments.ImagePath; +} +``` + +### Executing + +Once built, the routing slip is executed, which sends it to the first activity’s execute URI. To make it easy and to ensure that source information is included, an extension method on *IBus* is available, the usage of which is shown below. + +```csharp +await bus.Execute(routingSlip); +``` + +It should be pointed out that if the address for the first activity is invalid or cannot be reached, an exception will be thrown by the *Execute* method. + + +## Routing Slip Events + +During routing slip execution, events are published when the routing slip completes or faults. Every event message includes the *TrackingNumber* as well as a *Timestamp* (in UTC, of course) indicating when the event occurred: + + * RoutingSlipCompleted + * RoutingSlipFaulted + * RoutingSlipCompensationFailed + +Additional events are published for each activity, including: + + * RoutingSlipActivityCompleted + * RoutingSlipActivityFaulted + * RoutingSlipActivityCompensated + * RoutingSlipActivityCompensationFailed + +By observing these events, an application can monitor and track the state of a routing slip. To maintain the current state, an Automatonymous state machine could be created. To maintain history, events could be stored in a database and then queried using the *TrackingNumber* of the routing slip. + +### Subscriptions + +By default, routing slip events are published -- which means that any subscribed consumers will receive the events. While this is useful getting started, it can quickly get out of control as applications grow and multiple unrelated routing slips are used. To handle this, subscriptions were added (yes, added, because they weren't though of until we experienced this ourselves). + +Subscriptions are added to the routing slip at the time it is built using the `RoutingSlipBuilder`. + +```csharp +builder.AddSubscription(new Uri("rabbitmq://localhost/log-events"), + RoutingSlipEvents.All); +``` + +This subscription would send all routing slip events to the specified endpoint. If the application only wanted specified events, the events can be selected by specifying the enumeration values for those events. For example, to only get the `RoutingSlipCompleted` and `RoutingSlipFaulted` events, the following code would be used. + +```csharp +builder.AddSubscription(new Uri("rabbitmq://localhost/log-events"), + RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); +``` + +It is also possible to tweak the content of the events to cut down on message size. For instance, by default, the `RoutingSlipCompleted` event includes the variables from the routing slip. If the variables contained a large document, that document would be copied to the event. Eliminating the variables from the event would reduce the message size, thereby reducing the traffic on the message broker. To specify the contents of a routing slip event subscription, an additional argument is specified. + +```csharp +builder.AddSubscription(new Uri("rabbitmq://localhost/log-events"), + RoutingSlipEvents.Completed, RoutingSlipEventContents.None); +``` + +This would send the `RoutingSlipCompleted` event to the endpoint, without any of the variables be included (only the main properties of the event would be present). + +> Once a subscription is added to a routing slip, events are no longer published -- they are only sent to the addresses specified in the subscriptions. However, multiple subscriptions can be specified -- the endpoints just need to be known at the time the routing slip is built. + +### Custom + +It is also possible to specify a subscription with a custom event, a message that is created by the application developer. This makes it possible to create your own event types and publish them in response to routing slip events occurring. And this includes having the full context of a regular endpoint `Send` so that any headers or context settings can be applied. + +To create a custom event subscription, use the overload shown below. + +```csharp +// first, define the event type in your assembly +public record OrderProcessingCompleted +{ + public Guid TrackingNumber { get; init; } + public DateTime Timestamp { get; init; } + + public string OrderId { get; init; } + public string OrderApproval { get; init; } +} + +// then, add the subscription with the custom properties +builder.AddSubscription(new Uri("rabbitmq://localhost/order-events"), + RoutingSlipEvents.Completed, + x => x.Send(new + { + OrderId = "BFG-9000", + OrderApproval = "ComeGetSome" + })); +``` + +In the message contract above, there are four properties, but only two of them are specified. By default, the base `RoutingSlipCompleted` event is created, and then the content of that event is *merged* into the message created in the subscription. This ensures that the dynamic values, such as the `TrackingNumber` and the `Timestamp`, which are present in the default event, are available in the custom event. + +Custom events can also select with contents are merged with the custom event, using an additional method overload. + + + +[1]: https://learn.microsoft.com/en-us/azure/architecture/patterns/compensating-transaction +[2]: https://github.com/MassTransit/Automatonymous diff --git a/doc/content/3.documentation/1.concepts/mediator.md b/doc/content/3.documentation/1.concepts/mediator.md index e877473eb0a..eda6e15e8f8 100644 --- a/doc/content/3.documentation/1.concepts/mediator.md +++ b/doc/content/3.documentation/1.concepts/mediator.md @@ -35,7 +35,8 @@ _Publish_, on the other hand, does not require the message to be consumed and do ### Scoped Mediator -Main mediator interface `IMediator` is registered as a singleton but there is another scoped version of it `IScopedMediator`. This interface is registered as a part of current IoC scope (`HttpContext` or manually created) and can be used in order to share it for the entire pipeline. +Main mediator interface `IMediator` is registered as a singleton but there is another scoped version of it `IScopedMediator`. This interface is registered as a part of current IoC scope (`HttpContext` or manually created) and can be used in order to share the scope for the entire pipeline. +By default with `IMediator`, each consumer has its own scope. By using `IScopedMediator`, the scope is shared between several consumers. ::alert{type="success"} No additional configuration is required as long as Mediator is configured via `services.AddMediator()` diff --git a/doc/content/3.documentation/1.concepts/testing.md b/doc/content/3.documentation/1.concepts/testing.md deleted file mode 100644 index d301ffedafd..00000000000 --- a/doc/content/3.documentation/1.concepts/testing.md +++ /dev/null @@ -1,78 +0,0 @@ -# Testing - -MassTransit is a framework, and follows the Hollywood principle – don't call us, we'll call you. This inversion of control, combined with asynchronous execution, can complicate unit tests. To make it easy, MassTransit includes test harnesses to create unit tests that run entirely in-memory but behave close to an actual message broker. In fact, the included memory-based messaging fabric was inspired by RabbitMQ exchanges and queues. - -Since MassTransit is typically configured using `AddMassTransit`, the preferred testing approach is to use a `ServiceCollection` to configure the test combined with the test harness. - -## Consumer - -To test a consumer using container-based configuration: - -```csharp -await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(cfg => - { - cfg.AddConsumer(); - }) - .BuildServiceProvider(true); - -var harness = provider.GetRequiredService(); - -await harness.Start(); - -var client = harness.GetRequestClient(); - -await client.GetResponse(new -{ - OrderId = InVar.Id, - OrderNumber = "123" -}); - -Assert.IsTrue(await harness.Sent.Any()); - -Assert.IsTrue(await harness.Consumed.Any()); - -var consumerHarness = harness.GetConsumerHarness(); - -Assert.That(await consumerHarness.Consumed.Any()); -``` - -## Saga State Machine - -To test a saga state machine using container-based configuration: - -```csharp -await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(cfg => - { - cfg.AddSagaStateMachine(); - }) - .BuildServiceProvider(true); - -var harness = provider.GetRequiredService(); - -await harness.Start(); - -var sagaId = Guid.NewGuid(); -var orderNumber = "ORDER123"; - -await harness.Bus.Publish(new OrderSubmitted -{ - CorrelationId = sagaId, - OrderNumber = orderNumber -}); - -Assert.That(await harness.Consumed.Any()); - -var sagaHarness = harness.GetSagaStateMachineHarness(); - -Assert.That(await sagaHarness.Consumed.Any()); - -Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == sagaId)); - -var instance = sagaHarness.Created.ContainsInState(sagaId, sagaHarness.StateMachine, sagaHarness.StateMachine.Submitted); -Assert.IsNotNull(instance, "Saga instance not found"); -Assert.That(instance.OrderNumber, Is.EqualTo(orderNumber)); - -Assert.IsTrue(await harness.Published.Any()); -``` diff --git a/doc/content/3.documentation/2.configuration/0.index.md b/doc/content/3.documentation/2.configuration/0.index.md new file mode 100755 index 00000000000..bbd7e45b103 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/0.index.md @@ -0,0 +1,656 @@ +--- +navigation.title: Overview +--- + +# Configuration + +MassTransit is usable in most .NET application types. MassTransit is easily configured in ASP.NET Core or .NET Generic Host applications (using .NET 6 or later). + +To use MassTransit, add the _MassTransit_ package (from NuGet) and start with the _AddMassTransit_ method shown below. + +```csharp +using MassTransit; + +services.AddMassTransit(x => +{ + // A Transport + x.UsingRabbitMq((context, cfg) => + { + }); +}); +``` + +In this configuration, the following variables are used: + +| Variable | Type | Description | +|-----------|:----------------------------------|-------------------------------------------------------------------------------------------| +| `x` | `IBusRegistrationConfigurator` | Configure the bus instance (not transport specific) and the underlying service collection | +| `context` | `IBusRegistrationContext` | The configured bus context, also implements `IServiceProvider` | +| `cfg` | `IRabbitMqBusFactoryConfigurator` | Configure the bus specific to the transport (each transport has its own interface type | + +:::alert{type="info"} +The callback passed to the _UsingRabbitMq_ method is invoked after the service collection has been built. Any methods to configure the bus instance (using `x`) should be called outside of this callback. +::: + +Adding MassTransit, as shown above, will configure the service collection with required components, including: + + * Several interfaces (and their implementations, appropriate for the transport specified) + * `IBus` (singleton) + * `IBusControl` (singleton) + * `IReceiveEndpointConnector` (singleton) + * `ISendEndpointProvider` (scoped) + * `IPublishEndpoint` (scoped) + * `IRequestClient` (scoped) + * The bus endpoint with the default settings (not started by default) + * The _MassTransitHostedService_ + * Health checks for the bus (or buses) and receive endpoints + * Using `ILoggerFactory` for log output + +> To configure multiple bus instances in the same service collection, refer to the [MultiBus](/documentation/configuration/multibus) section. + +## Host Options + +MassTransit adds a hosted service so that the generic host can start and stop the bus (or buses, if multiple bus instances are configured). The host options can be configured via _MassTransitHostOptions_ using the _Options_ pattern as shown below. + +```csharp +services.AddOptions() + .Configure(options => + { + }); +``` + +| Option | Description | +|---------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| WaitUntilStarted | By default, MassTransit connects to the broker asynchronously. When set to _true_, the MassTransit Hosted Service will block startup until the broker connection has been established. | +| StartTimeout | By default, MassTransit waits infinitely until the broker connection is established. If specified, MassTransit will give up after the timeout has expired. | +| StopTimeout | MassTransit waits infinitely for the bus to stop, including any active message consumers. If specified, MassTransit will force the bus to stop after the timeout has expired. | +| ConsumerStopTimeout | If specified, the `ConsumeContext.CancellationToken` will be canceled after the specified timeout when the bus is stopping. This allows long-running consumers to observe the cancellation token and react accordingly. Must be <= the `StopTimeout` | + +::callout{type="info"} +#summary +The .NET Generic Host has its own internal shutdown timeout. +#content +To configure the Generic Host options so that the bus has sufficient time to stop, configure the host options as shown. + +```csharp +services.Configure( + options => options.ShutdownTimeout = TimeSpan.FromMinutes(1)); +``` +:: + +## Transport Options + +Each supported transport can be configured via a `.Host()` method or via the .NET [Options Pattern](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/options?view=aspnetcore-7.0). + +- Rabbit MQ: [RabbitMqTransportOptions](/documentation/configuration/transports/rabbitmq#transport-options) +- Azure Service Bus: [AzureServiceBusTransportOptions](documentation/configuration/transports/azure-service-bus#transport-options) +- Amazon SQS: [AmazonSqsTransportOptions](/documentation/configuration/transports/amazon-sqs#transport-options) + +## Consumer Registration + +To consume messages, one or more consumers must be added and receive endpoints configured for the added consumers. MassTransit connects each receive endpoint to a queue on the message broker. + +To add a consumer and automatically configure a receive endpoint for the consumer, call one of the [_AddConsumer_](/documentation/configuration/bus/consumers) methods and call [_ConfigureEndpoints_](documentation/configuration#configure-endpoints) as shown below. + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); +}); +``` + +:::alert{type="info"} +**_ConfigureEndpoints_** should be the last method called after all settings and middleware components have been configured. +::: + +MassTransit will automatically configure a receive endpoint for the _SubmitOrderConsumer_ using the name returned by the configured endpoint name formatter. When the bus is started, the receive endpoint will be started and messages will be delivered from the queue by the transport to an instance of the consumer. + +> All consumer types can be added, including consumers, sagas, saga state machines, and routing slip activities. If a job consumer is added, [additional configuration](/documentation/patterns/job-consumers) is required. + +::callout{type="info"} +#summary +Learn about the default conventions as well as how to tailor the naming style to meet your requirements in this short video: +#content +::div + :video-player{src="https://www.youtube.com/watch?v=bsUlQ93j2MY"} +:: +:: + +To exclude a consumer, saga, or routing slip activity from automatic configuration, use the _ExcludeFromConfigureEndpoints_ extension method when adding the consumer: + +```csharp +x.AddConsumer() + .ExcludeFromConfigureEndpoints() +``` + +Alternatively, the _ExcludeFromConfigureEndpoints_ attribute may be specified on the consumer. + +```csharp +[ExcludeFromConfigureEndpoints] +public class SubmitOrderConsumer : + IConsumer +{ +} +``` + +## Configure Endpoints + +As shown in the example above, using `ConfigureEndpoints` is the preferred approach to configure receive endpoints. By registering consumers, sagas, and routing slip activities along with their optional definitions, MassTransit is able to configure receive endpoints for all registered consumer types. Receive endpoint names are generated using an [endpoint name formatter](#endpoint-name-formatters) (unless otherwise specified in a definition), and each receive endpoint is configured. + +As receive endpoints are configured, one or more consumer types are configured on each receive endpoint. If multiple consumer types share the same endpoint name, those consumer types will be configured on the same receive endpoint. For each consumer type, its respective consumer, saga, or activity definition will be applied to the receive endpoint. + +::alert{type="warning"} +If multiple consumer types share the same receive endpoint, and more than one of those consumer types have a matching definition that specifies the same middleware component, **multiple** filters may be configured! This may lead to unpredictable results, so caution is advised when configuring multiple consumer types on the same receive endpoint. +:: + +#### Configure Endpoints Callback + +To apply receive endpoint settings or configure middleware for all receive endpoints configured by `ConfigureEndpoints`, a callback can be added. + +```csharp +x.AddConfigureEndpointsCallback((name, cfg) => +{ + cfg.UseMessageRetry(r => r.Immediate(2)); +}); +``` + +When `ConfigureEndpoints` is called, any registered callbacks will be called for every recieve endpoint endpoint. Each callback will only be called _once_ per receive endpoint. + +To conditionally apply transport-specific settings, the `cfg` parameter can be pattern-matched to the transport type as shown below. + +```csharp +x.AddConfigureEndpointsCallback((name, cfg) => +{ + if (cfg is IRabbitMqReceiveEndpointConfigurator rmq) + rmq.SetQuorumQueue(3); + + cfg.UseMessageRetry(r => r.Immediate(2)); +}); +``` + +## Endpoint Strategies + +Deciding how to configure receive endpoints in your application can be easy or hard, depending upon how much energy you want to spend being concerned with things that usually don't matter. However, there are nuances to the following approaches that should be considered. + +#### One Consumer for Each Queue + +Creates a queue for each registered consumer, saga, and routing slip activity. Separate queues are created for execute and compensate if compensation is supported by the activity. + +::alert{type="info"} +This is the preferred approach since it ensures that every consumer can be configured independently, including retries, delivery, and the outbox. It also ensures that messages for a consumer are not stuck behind other messages for other consumers sharing the same queue. +:: + +#### Multiple Consumers on a Single Queue + +Configuring multiple consumers, while fully supported by MassTransit, may make sense in certain circumstances, however, proceed with caution as there are limitations to this approach. + +The recommendation here is to configure multiple consumers on a single queue only when those consumers are closely related in terms of business function and each consumer consumes distinct message types. An example might be consumers that each create, update, or delete an entity when the dependencies of those operations are different – create and update may depend upon a validation component, while delete may not share that dependency. + +##### Consume Multiple Message Types + +In situations where it is preferable to consume multiple message types from a single queue, create a consumer that consumes multiple message types by adding more IConsumer interface implementations to the consumer class. + +```csharp +public class AddressConsumer : + IConsumer, + IConsumer +{ +} +``` + +Sagas follow this approach, creating a single queue for each saga and configuring the broker to route message types consumed by the saga that are published to topic/exchanges to the saga’s queue. + +#### All Consumers on a Single Queue + +This is never a good idea and is highly discouraged. While it is supported by MassTransit, it’s unlikely to be operationally sustainable. + +Routing slip activities must not be configured on a single queue as they will not work properly. + + + + +## Endpoint Name Formatters + +_ConfigureEndpoints_ uses an `IEndpointNameFormatter` to format the queue names for all supported consumer types. The default endpoint name formatter returns _PascalCase_ class names without the namespace. There are several built-in endpoint name formatters included. For the _SubmitOrderConsumer_, the receive endpoint names would be formatted as shown below. Note that class suffixes such as _Consumer_, _Saga_, and _Activity_ are trimmed from the endpoint name by default. + +| Format | Configuration | Name | +|:-----------|:------------------------------------|:---------------| +| Default | `SetDefaultEndpointNameFormatter` | `SubmitOrder` | +| Snake Case | `SetSnakeCaseEndpointNameFormatter` | `submit_order` | +| Kebab Case | `SetKebabCaseEndpointNameFormatter` | `submit-order` | + +The endpoint name formatters can also be customized by constructing a new instance and configuring MassTransit to use it. + +```csharp +x.SetEndpointNameFormatter(new KebabCaseEndpointNameFormatter(prefix: "Dev", includeNamespace: false)); +``` + +By specifying a prefix, the endpoint name would be `dev-submit-order`. This is useful when sharing a single broker with multiple developers (Amazon SQS is account-wide, for instance). + +::callout{type="info"} +#summary +When using MultiBus with different endpoint name formatters for each bus... +#content +Specify the endpoint name formatter when calling `ConfigureEndpoints` as shown. +```csharp +cfg.ConfigureEndpoints(context, new KebabCaseEndpointNameFormatter(prefix: "Mobile", includeNamespace: false)); +``` +:: + +## Receive Endpoints + +The previous examples use conventions to configure receive endpoints. Alternatively, receive endpoints can be explicitly configured. + +> When configuring endpoints manually, _ConfigureEndpoints_ should be excluded or be called **after** any explicitly configured receive endpoints. + +To explicitly configure endpoints, use the _ConfigureConsumer_ or _ConfigureConsumers_ method. + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ReceiveEndpoint("order-service", e => + { + e.ConfigureConsumer(context); + }); + }); +}); +``` + +Receive endpoints have transport-independent settings that can be configured. + +| Name | Description | Default | +|:-------------------------|:--------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------| +| PrefetchCount | Number of unacknowledged messages delivered by the broker | max(CPU Count x 2,16) | +| ConcurrentMessageLimit | Number of concurrent messages delivered to consumers | (none, uses PrefetchCount) | +| ConfigureConsumeTopology | Create exchanges/topics on the broker and bind them to the receive endpoint | true | +| ConfigureMessageTopology | Create exchanges/topics on the broker and bind them to the receive endpoint for a specific message type | true | +| PublishFaults | Publish `Fault` events when consumers fault | true | +| DefaultContentType | The default content type for received messages | See [serialization](configuration/integrations/serialization#serializers) | +| SerializerContentType | The default content type for sending/publishing messages | See [serialization](configuration/integrations/serialization#serializers) | + +> The _PrefetchCount_, _ConcurrentMessageLimit_, and serialization settings can be specified at the bus level and will be applied to all receive endpoints. + +In the following example, the _PrefetchCount_ is set to 32 and the _ConcurrentMessageLimit_ is set to 28. + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.PrefetchCount = 32; // applies to all receive endpoints + + cfg.ReceiveEndpoint("order-service", e => + { + e.ConcurrentMessageLimit = 28; // only applies to this endpoint + e.ConfigureConsumer(context); + }); + }); +}); +``` + +> When using _ConfigureConsumer_ with a consumer that has a definition, the _EndpointName_, _PrefetchCount_, and _Temporary_ properties of the consumer definition are not used. + +### Temporary Endpoints + +Some consumers only need to receive messages while connected, and any messages published while disconnected should be discarded. This can be achieved by using a TemporaryEndpointDefinition to configure the receive endpoint. + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.UsingInMemory((context, cfg) => + { + cfg.ReceiveEndpoint(new TemporaryEndpointDefinition(), e => + { + e.ConfigureConsumer(context); + }); + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +### Consumer Definition + +A consumer definition is used to configure the receive endpoint and pipeline behavior for the consumer. When scanning assemblies or namespaces for consumers, consumer definitions are also found and added to the container. The _SubmitOrderConsumer_ and matching definition are shown below. + +```csharp +class SubmitOrderConsumer : + IConsumer +{ + readonly ILogger _logger; + + public SubmitOrderConsumer(ILogger logger) + { + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + _logger.LogInformation("Order Submitted: {OrderId}", context.Message.OrderId); + + await context.Publish(new + { + context.Message.OrderId + }); + } +} + +class SubmitOrderConsumerDefinition : + ConsumerDefinition +{ + public SubmitOrderConsumerDefinition() + { + // override the default endpoint name + EndpointName = "order-service"; + + // limit the number of messages consumed concurrently + // this applies to the consumer only, not the endpoint + ConcurrentMessageLimit = 8; + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) + { + // configure message retry with millisecond intervals + endpointConfigurator.UseMessageRetry(r => r.Intervals(100,200,500,800,1000)); + + // use the outbox to prevent duplicate events from being published + endpointConfigurator.UseInMemoryOutbox(context); + } +} +``` + +### Endpoint Configuration + +To configure the endpoint for a consumer registration, or override the endpoint configuration in the definition, the `Endpoint` method can be added to the consumer registration. This will create an endpoint definition for the consumer, and register it in the container. This method is available on consumer and saga registrations, with separate execute and compensate endpoint methods for activities. + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer() + .Endpoint(e => + { + // override the default endpoint name + e.Name = "order-service-extreme"; + + // more options shown below + }); + + x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); +}); +``` + +The configurable settings for an endpoint include: + +| Property | Type | Description | +|:-------------------------------|:---------|:-------------------------------------------------------------------------------------------------------| +| `Name` | `string` | The receive endpoint (queue) name | +| `InstanceId` | `string` | If specified, should be unique for each bus instance To enable fan-out (instead of load balancing) | +| `Temporary` | `bool` | If true, the endpoint will be automatically removed after the bus has stopped. | +| `PrefetchCount` | `int` | Number of unacknowledged messages delivered by the broker | +| `ConcurrentMessageLimit` | `int?` | Number of concurrent messages delivered to consumers | +| `ConfigureConsumeTopology` | `bool` | Create exchanges/topics on the broker and bind them to the receive endpoint | +| `AddConfigureEndpointCallback` | delegate | (Added in v8.3.0) Adds a callback method that will be invoked when the receive endpoint is configured. | + +#### Endpoint Naming + +When the endpoint is configured after the _AddConsumer_ method, the configuration then overrides the endpoint configuration in the consumer definition. However, it cannot override the `EndpointName` if it is specified in the constructor. The order of precedence for endpoint naming is explained below. + +1. Specifying `EndpointName = "submit-order-extreme"` in the constructor which cannot be overridden + + ```csharp + x.AddConsumer() + + public SubmitOrderConsumerDefinition() + { + EndpointName = "submit-order-extreme"; + } + ``` + +2. Specifying `.Endpoint(x => x.Name = "submit-order-extreme")` in the consumer registration, chained to `AddConsumer` + + ```csharp + x.AddConsumer() + .Endpoint(x => x.Name = "submit-order-extreme"); + + public SubmitOrderConsumerDefinition() + { + Endpoint(x => x.Name = "not used"); + } + ``` + +3. Specifying `Endpoint(x => x.Name = "submit-order-extreme")` in the constructor, which creates an endpoint definition + + ```csharp + x.AddConsumer() + + public SubmitOrderConsumerDefinition() + { + Endpoint(x => x.Name = "submit-order-extreme"); + } + ``` + +4. Unspecified, the endpoint name formatter is used (in this case, the endpoint name is `SubmitOrder` using the default formatter) + + ```csharp + x.AddConsumer() + + public SubmitOrderConsumerDefinition() + { + } + ``` + +## Saga Registration + +To add a state machine saga, use the _AddSagaStateMachine_ methods. For a consumer saga, use the _AddSaga_ methods. + +::alert{type="success"} +State machine sagas should be added before class-based sagas, and the class-based saga methods should not be used to add state machine sagas. This may be simplified in the future, but for now, be aware of this registration requirement. +:: + +```csharp +services.AddMassTransit(r => +{ + // add a state machine saga, with the in-memory repository + r.AddSagaStateMachine() + .InMemoryRepository(); + + // add a consumer saga with the in-memory repository + r.AddSaga() + .InMemoryRepository(); + + // add a saga by type, without a repository. The repository should be registered + // in the container elsewhere + r.AddSaga(typeof(OrderSaga)); + + // add a state machine saga by type, including a saga definition for that saga + r.AddSagaStateMachine(typeof(OrderState), typeof(OrderStateDefinition)) + + // add all saga state machines by type + r.AddSagaStateMachines(Assembly.GetExecutingAssembly()); + + // add all sagas in the specified assembly + r.AddSagas(Assembly.GetExecutingAssembly()); + + // add sagas from the namespace containing the type + r.AddSagasFromNamespaceContaining(); + r.AddSagasFromNamespaceContaining(typeof(OrderSaga)); +}); +``` + +To add a saga registration and configure the consumer endpoint in the same expression, a definition can automatically be created. + +```csharp +services.AddMassTransit(r => +{ + r.AddSagaStateMachine() + .NHibernateRepository() + .Endpoint(e => + { + e.Name = "order-state"; + e.ConcurrentMessageLimit = 8; + }); +}); +``` + +Supported saga persistence storage engines are documented in the [saga documentation](/documentation/patterns/saga/) section. + + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.SetKebabCaseEndpointNameFormatter(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); +}); +``` + +And the consumer: + +```csharp +class ValueEnteredEventConsumer : + IConsumer +{ + ILogger _logger; + + public ValueEnteredEventConsumer(ILogger logger) + { + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + _logger.LogInformation("Value: {Value}", context.Message.Value); + } +} +``` + +An ASP.NET Core application can also configure receive endpoints. The consumer, along with the receive endpoint, is configured within the _AddMassTransit_ configuration. Separate registration of the consumer is not required (and discouraged), however, any consumer dependencies should be added to the container separately. Consumers are registered as scoped, and dependencies should be registered as scoped when possible, unless they are singletons. + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ReceiveEndpoint("event-listener", e => + { + e.ConfigureConsumer(context); + }); + }); +}); +``` + +```csharp +class EventConsumer : + IConsumer +{ + ILogger _logger; + + public EventConsumer(ILogger logger) + { + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + _logger.LogInformation("Value: {Value}", context.Message.Value); + } +} +``` + +## Health Checks + +The _AddMassTransit_ method adds an `IHealthCheck` to the service collection that you can use to monitor your health. The health check is added with the tags `ready` and `masstransit`. + + +To configure health checks, map the ready and live endpoints in your ASP.NET application. + +```csharp +app.MapHealthChecks("/health/ready", new HealthCheckOptions() +{ + Predicate = (check) => check.Tags.Contains("ready"), +}); + +app.MapHealthChecks("/health/live", new HealthCheckOptions()); +``` + +**Example Output** + +```json +{ + "status": "Healthy", + "totalDuration": "00:00:00.2134026", + "entries": { + "masstransit-bus": { + "data": { + "Endpoints": { + "rabbitmq://localhost/dev-local/SubmitOrder": { + "status": "Healthy", + "description": "ready" + } + } + }, + "description": "Ready", + "duration": "00:00:00.1853530", + "status": "Healthy", + "tags": [ + "ready", + "masstransit" + ] + } + } +} +``` + +- When everything works correctly, MassTransit will report `Healthy`. +- If any problems occur on application startup, MassTransit will report `Unhealthy`. This can cause an orcestrator to restart your application. +- If any problems occur while the application is working (for example, application loses connection to broker), MassTransit will report `Degraded`. + +### Health Check Options + +Health Checks can be further configured using _ConfigureHealthCheckOptions_: + +```csharp +builder.Services.AddMassTransit(bus => +{ + bus.ConfigureHealthCheckOptions(options => + { + options.Name = "masstransit"; + options.MinimalFailureStatus = HealthStatus.Unhealthy; + options.Tags.Add("health"); + }); + +} +``` + +| Setting | Description | Default value | +|:---------------------|:-------------------------------------------------------------------------------|:-----------------------| +| Name | Set the health check name, overrides the default bus type name. | Bus name. | +| MinimalFailureStatus | The minimal `HealthStatus` that will be reported when the health check fails. | `Unhealthy` | +| Tags | A list of tags that can be used to filter sets of health checks. | "ready", "masstransit" | + +By default MassTransit reports all three statuses depending on application state. +If `MinimalFailureStatus` is set to `Healthy`, MassTransit will log any issues, but the health check will always report `Healthy`. +If `MinimalFailureStatus` is set to `Degraded`, MassTransit will report `Degraded` if any issues occur, but never report `Unhealthy`. + +Tags inside options will override default tags. You will need to add `ready` and `masstransit` tags manually if you want to keep them. diff --git a/doc/content/3.documentation/2.configuration/1.consumers.md b/doc/content/3.documentation/2.configuration/1.consumers.md new file mode 100644 index 00000000000..fa10a3e8927 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/1.consumers.md @@ -0,0 +1,253 @@ +# Consumers + +To understand consumers and how to create one, refer to the [Consumers](/documentation/concepts/consumers) section. + +High-level concepts covered in this configuration section include: + + +| Concept | Description | +|---------------------|------------------------------------------------------------------------------------------------------------------------| +| Consumer | A class that consumes one or more messages types, one for each implementation of `IConsumer` | +| Batch Consumer | A class that consumes multiple messages in batches, by implementing `IConsumer>` | +| Job Consumer | A class that consumes a job message, specified by the `IJobConsumer` interface | +| Consumer Definition | A class, derived from `ConsumerDefinition` that configures settings and the consumer's receive endpoint | +| Receive Endpoint | Receives messages from a broker queue and delivers those messages to consumer types configured on the receive endpoint | + +Consumers can be added many ways allowing either a simple of fine-grained approach to registration. Consumers are added inside the `AddMassTransit` configuration, but before the transport. + +```csharp +using MassTransit; + +services.AddMassTransit(x => +{ + x.AddConsumer(); + + x.Using[Transport]((context, cfg) => + { + // transport, middleware, other configuration + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +## Adding Consumers + +Adds a single consumer, with all defaults + +```csharp +AddConsumer(); +AddConsumer(typeof(MyConsumer)); + +``` + +Adds a consumer, with a consumer definition. + +```csharp +AddConsumer(); +AddConsumer(typeof(MyConsumer), typeof(MyConsumerDefinition)); +``` + +Adds a consumer with a matching consumer definition and configures the consumer pipeline. + +```csharp +AddConsumer(cfg => +{ + cfg.ConcurrentMessageLimit = 8; +}); +``` + +Adds the specified consumers and consumer definitions. When consumer definitions are included they will be added with the matching consumer type. + +```csharp +void AddConsumers(params Type[] types); +``` + +Adds all consumers and consumer definitions in the specified an assembly or assemblies. + +```csharp +void AddConsumers(params Assembly[] assemblies); +``` + +Adds the consumers and any matching consumer definitions in the specified an assembly or assemblies that pass the filter. The filter is only called for consumer types. + +```csharp +void AddConsumers(Func filter, params Assembly[] assemblies); +``` + +### Batch Options + +If you want your consumer to process multiple messages at a time, you can configure a `Batch Consumer`. This +is a consumer that implements `IConsumer>`. + +```csharp +AddConsumer(cfg => +{ + cfg.Options(options => options + .SetMessageLimit(100) + .SetTimeLimit(s: 1) + .SetTimeLimitStart(BatchTimeLimitStart.FromLast) + .GroupBy(x => x.CustomerId) + .SetConcurrencyLimit(10)); +}); +``` + +| Property | Type | Default | Description | +|------------------|----------|------------|-------------| +| MessageLimit | int | 10 | Max number of messages in a batch | +| ConcurrencyLimit | int | 1 | number of concurrent batches | +| TimeLimit | TimeSpan | 1 sec | maximum time to wait before delivering a partial batch | +| TimeLimitStart | TimeSpan | From First | starting point | +| GroupKeyProvider | object? | null | the property to group by | + + +### Job Options + +If your consumer needs to work for an extended period of time, greater than a second, you may want to +register the consumer as a job consumer. You can read more about this feature in the [Job Consumer pattern](/documentation/patterns/job-consumers) section. + +```csharp +AddConsumer(cfg => +{ + cfg.Options>(options => options + .SetJobTimeout(TimeSpan.FromMinutes(15)) + .SetConcurrentJobLimit(10) + .SetRetry(r => r.Interval(5,30000))); +}); +``` + +| Property | Type | Default | Description | +|--------------------|--------------|-----------|--------------------------------------------------------------------------------| +| JobTimeout | TimeSpan | 5 minutes | Maximum time the job is allowed to run | +| ConcurrentJobLimit | int | 1 | Number of concurrent executing jobs | +| RetryPolicy | IRetryPolicy | None | How should failures be retried, if at all | +| JobTypeName | string | Job Type | Override the default job type name used in the JobTypeSaga table (display one) | + +#### Retry Policies + +- **None**: No retries +- **Immediate**: retry N times, with an optional exception filter +- **Intervals**: retry N times, with a pause between and an optional exception filter +- **Incremental**: retry N times, with an increasing pause between and an optional exception filter +- **Exponential**: retry N times, with an ever increasing pause between and an optional exception filter + +## Configuring Endpoints + +By default MassTransit requires no explicit configuration of endpoints, and can be created +automatically by calling `ConfigureEndpoints`. You can customize this behavior using `ConsumerDefinition` +or by specifying the endpoint configuration inline. + +```csharp +using MassTransit; +services.AddMassTransit(x => +{ + // Step 1: Add Consumers Here + + // Step 2: Select a Transport + x.Using[Transport]((context, cfg) => { + // Step 3: Configure the Transport + + // Step 4: Configure Endpoints + // All consumers registered in step 1, will get + // default endpoints created. + cfg.ConfigureEndpoints(context); + }); +}); +``` + +### Customized Endpoints + +To manually configure a consumer on a receive endpoint, use one of the following methods. You may want to do this +for the following reasons. + +- Group Consumers onto a specific queue, vs the default of one queue per consumer + +::alert{type="info"} +**Order Matters**: Manually configured receive endpoints should be configured **before** calling _ConfigureEndpoints_. +:: + +```csharp +cfg.ReceiveEndpoint("manually-configured", e => +{ + // configure endpoint-specific settings first + e.SomeEndpointSetting = someValue; + + // configure any required middleware components next + e.UseMessageRetry(r => r.Interval(5, 1000)); + + // configure the consumer last + e.ConfigureConsumer(context); +}); + +// configure any remaining consumers, sagas, etc. +cfg.ConfigureEndpoints(context); +``` + +Endpoint Configuration is Custom by Transport + +- [RabbitMQ](/documentation/configuration/transports/rabbitmq#endpoint-configuration) +- [Azure Service Bus](/documentation/configuration/transports/azure-service-bus#endpoint-configuration) +- [Amazon SQS](/documentation/configuration/transports/amazon-sqs#endpoint-configuration) + +### Consumer Configuration + +```csharp +ConfigureConsumer(context); +``` + +Configures the consumer on the receive endpoint. + +```csharp +ConfigureConsumer(context, consumer => +{ + // configure consumer-specific middleware +}); +``` + +Configures the consumer on the receive endpoint and applies the additional consumer configuration to the consumer pipeline. + +```csharp +ConfigureConsumers(context); +``` + +Configures all consumers that haven't been configured on the receive endpoint. + +## Consumer Definitions + +Inside of a consumer definition you can control all of the definitions about a consumer and its associated endpoint. + +```csharp +public class SubmitOrderConsumerDefinition : + ConsumerDefinition +{ + public SubmitOrderConsumerDefinition() + { + // override the default endpoint name, for whatever reason + EndpointName = "ha-submit-order"; + + // limit the number of messages consumed concurrently + // this applies to the consumer only, not the endpoint + ConcurrentMessageLimit = 4; + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, + IConsumerConfigurator consumerConfigurator) + { + endpointConfigurator.UseMessageRetry(r => r.Interval(5, 1000)); + endpointConfigurator.UseInMemoryOutbox(); + } +} +``` + +### Endpoint Options + +| Concept | Type | Description | +|------------------------|-------|------| +| EndpointDefinition | `IEndpointDefinition` | ?? | +| EndpointName | string | the name of the queue that will be generated | + +### Consumer Options + +| Concept | Type | Description | +|------------------------|-------|------| +| ConcurrentMessageLimit | int | the number of messages THIS consumer can process concurrently | diff --git a/doc/content/3.documentation/2.configuration/1.sagas/0.overview.md b/doc/content/3.documentation/2.configuration/1.sagas/0.overview.md new file mode 100644 index 00000000000..2f5bf3b97c8 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/1.sagas/0.overview.md @@ -0,0 +1,64 @@ +--- +navigation.title: Overview +--- + +# Sagas + +To understand sagas and how to create one, refer to the [Saga](/documentation/patterns/saga) section. + + +## Configuring Sagas + +Sagas are automatically configured when `ConfigureEndpoints` is called, which is highly recommended. The endpoint configuration can be mostly customized using either a saga definition or by specifying the endpoint configuration inline. + +To manually configure a saga on a receive endpoint, use one of the following methods. + +::alert{type="warning"} +Manually configured receive endpoints should be configured **before** calling _ConfigureEndpoints_. +:: + +```csharp +services.AddMassTransit(cfg => +{ + cfg.Using[Transport]((context, transport) => + { + transport.ReceiveEndpoint("manually-configured", e => + { + // configure endpoint-specific settings first + e.SomeEndpointSetting = someValue; + + // configure any required middleware components next + e.UseMessageRetry(r => r.Interval(5, 1000)); + + // configure the saga last + e.ConfigureSaga(context); + }); + + // configure any remaining consumers, sagas, etc. + transport.ConfigureEndpoints(context); + }); +}) +``` + +#### Configuration Methods + +```csharp +ConfigureSaga(context); +``` + +Configures the saga on the receive endpoint. + +```csharp +ConfigureSaga(context, saga => +{ + // configure saga-specific middleware +}); +``` + +Configures the saga on the receive endpoint and applies the additional saga configuration to the saga pipeline. + +```csharp +ConfigureSagas(context); +``` + +Configures all sagas that haven't been configured on the receive endpoint. diff --git a/doc/content/3.documentation/2.configuration/1.sagas/1.state.md b/doc/content/3.documentation/2.configuration/1.sagas/1.state.md new file mode 100644 index 00000000000..46b2ab652c4 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/1.sagas/1.state.md @@ -0,0 +1,181 @@ +--- +navigation.title: States +--- + +# State Machine States + +Saga state machines are _stateful_ consumers designed to retain the outcome of preceding events when a subsequent event is consumed. The current state is stored +within a saga state machine instance. A saga state machine instance can only be in one state at a given time. + +A newly created saga state machine instance starts in the internally defined `Initial` state. When a saga state machine has completed, an instance should +transition to the `Final` state (which is also already defined). + +## Initial State + +The `Initial` state is the starting point of all sagas. When an existing saga state machine instance cannot be found that correlates to an event behavior +defined for the _Initial_ state, a new instance is created. The defined behavior should initialize the newly created instance and transition to the next state. + +## Final State + +The `Final` state is the last (or terminal) state that a saga state machine instance should transition to when the instance has completed. When an instance +is in the _Final_ state no further events should be handled. + +#### SetCompletedWhenFinalized + +To remove the saga state machine instance from the repository when the instance is in the _Final_ state, specify `SetCompletedWhenFinalized()` in the saga +state machine. + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public State Submitted { get; private set; } = null!; + public State Accepted { get; private set; } = null!; + + public OrderStateMachine() + { + SetCompletedWhenFinalized(); + } +} +``` + +## Declaring States + +States are declared as _public_ properties on the saga state machine with the `State` property type. In the example below, two states are defined: +_Submitted_ and _Accepted_. + +:::alert{type="info"} +The `MassTransitStateMachine` base class automatically initializes _State_ properties in its constructor, so they don't need to be explicitly initialized. +::: + +```csharp +public class OrderState : + SagaStateMachineInstance +{ + public Guid CorrelationId { get; set; } + + /// + /// The saga state machine instance current state + /// + public string CurrentState { get; set; } +} + +public class OrderStateMachine : + MassTransitStateMachine +{ + public State Submitted { get; private set; } = null!; + public State Accepted { get; private set; } = null!; +} +``` + +## Instance State + +In the example above, the `CurrentState` property on the saga state machine instance is used to store the instance's current state. The saga state machine +must be configured to use that property. + +### String Instance State + +The `InstanceState` method is used to configure the property, in the example below the `string` property type is used. + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public State Submitted { get; private set; } = null!; + public State Accepted { get; private set; } = null!; + + public OrderStateMachine() + { + InstanceState(x => x.CurrentState); + } +} +``` + +### Integer Instance State + +In addition to using a `string`, and `int` can also be used to store the current state. An _integer_ can be more efficient to store in a database compared to +a verbose _string_ value. In the example below the `int` property type is used instead. + +```csharp +public class OrderState : + SagaStateMachineInstance +{ + public Guid CorrelationId { get; set; } + + public int CurrentState { get; set; } +} + +public class OrderStateMachine : + MassTransitStateMachine +{ + public State Submitted { get; private set; } = null!; + public State Accepted { get; private set; } = null!; + + public OrderStateMachine() + { + InstanceState(x => x.CurrentState, Submitted, Accepted); + } +} +``` + +## Transitioning States + +When configuring saga state machine behavior for an event, the last activity configured is usually a `TransitionTo` a state. By transitioning to another state, +the saga state machine adapts its behavior so that the next event consumed will pick up in the new state and execute the appropriate behavior for the event +in that state. + +For example, when an `OrderSubmitted` event is consumed by a new saga state machine instance, the saga state machine will transition to the `Submitted` state. + +```csharp +public record OrderSubmitted(Guid OrderId, string CustomerNumber); + +public class OrderStateMachine : + MassTransitStateMachine +{ + public Event OrderSubmitted { get; private set; } = null!; + + public State Submitted { get; private set; } = null!; + public State Accepted { get; private set; } = null!; + + public OrderStateMachine() + { + Initially( + // Event is consumed, new instance is created in Initial state + When(OrderSubmitted) + // copy some data from the event to the saga + .Then(context => context.Saga.CustomerNumber = context.Message.CustomerNumber) + // transition to the Submitted state + .TransitionTo(Submitted) + ); + } +} +``` + +### TransitionTo Anti-Pattern + +Saga state machine instances are persisted after _all_ state machine activities have completed. If there were additional activities after the `TransitionTo`, +such as a `Publish`, those activities execute before the instance is persisted. After all the activities have completed, the instance is persisted and the +message is acknowledged with the message broker. + +In the example below, the newly created saga state machine instance would _not_ be persisted because of the exception thrown by the activity following the first +`TransitionTo` call. + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public State Submitted { get; private set; } = null!; + public State Accepted { get; private set; } = null!; + + public OrderStateMachine() + { + Initially( + When(OnSubmit) + .Then(context => context.Saga.CustomerNumber = context.Message.CustomerNumber) + .TransitionTo(Submitted) + .Then(context => throw new InvalidOperationException()) + .TransitionTo(Accepted) + ); + } +} +``` diff --git a/doc/content/3.documentation/2.configuration/1.sagas/2.event.md b/doc/content/3.documentation/2.configuration/1.sagas/2.event.md new file mode 100644 index 00000000000..0132bdccb11 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/1.sagas/2.event.md @@ -0,0 +1,305 @@ +--- +navigation.title: Events +--- + +# State Machine Events + +An event is something that happened which may result in a state change. In a saga state machine, an event is used to correlate a message to a saga state +machine instance. Saga state machine events are used to add behavior to a saga state machine, such as adding or updating saga state machine instance data, +publishing or sending messages, and changing the instance's current state. + +## Declaring Events + +Events are declared as _public_ properties on the saga state machine with the `Event` property type, where `T` is a valid message type. + +In the example below, the _SubmitOrder_ message is configured as an event. The event configuration also specifies the message property used to correlate the +event to a saga state machine instance. In this case, the `Guid` property `OrderId` is used. + +```csharp +public record SubmitOrder(Guid OrderId); + +public class OrderStateMachine : + MassTransitStateMachine +{ + public Event SubmitOrder { get; private set; } = null!; + + public OrderStateMachine() + { + Event(() => SubmitOrder, + e => e.CorrelateById(x => x.Message.OrderId) + ); + } +} +``` + +### Event Conventions + +There are several conventions applied when configuring event correlation in a saga state machine. These conventions may reduce the amount of configuration +required for an event meeting a convention's criteria. + +#### CorrelatedBy<Guid> + +If an event message type implements the `CorrelatedBy` interface, the event will automatically be configured to correlate using the `CorrelationId` +property on that interface. + +#### Property Name + +If an event message has a `CorrelationId`, `CommandId`, or `EventId` property and that properties type is `Guid`, the event will automatically be configured +to correlate using the first property found (in that order). + +#### Global Topology + +It's also possible to configure the correlation property for a message type using `GlobalTopology`. This configures the message type globally so that it is +automatically available to any saga state machine. However, a saga state machine can override the global settings by explicitly configuring the event +correlation. + +```csharp +GlobalTopology.Send.UseCorrelationId(x => x.OrderId); +``` + +## Initiating Events + +The `Initial` state is the starting point of all sagas. When an existing saga state machine instance cannot be found that correlates to an event behavior +defined for the _Initial_ state, a new instance is created. + +Events handled in the _Initial_ state are _initiating events_ that result in a newly created saga state machine instance is an instance does not already +exist. + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public State Submitted { get; private set; } = null!; + + public Event SubmitOrder { get; private set; } = null!; + + public OrderStateMachine() + { + Initially( + When(SubmitOrder) + .Then(context => + { + context.Saga.CustomerNumber = context.Message.CustomerNumber; + }) + .TransitionTo(Submitted) + ); + } +} +``` + +## Handling Events + +Event can be handled in any _state_, and can be configured using `During` and specifying the states in which the event is accepted. In the example below, +the `AcceptOrder` event is handled in the `Submitted` state. + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public State Submitted { get; private set; } = null!; + public State Accepted { get; private set; } = null!; + + public Event AcceptOrder { get; private set; } = null!; + + public OrderStateMachine() + { + During(Submitted, + When(AcceptOrder) + .Then(context => + { + context.Saga.AcceptedAt = context.SentTime ?? DateTime.UtcNow; + }) + .TransitionTo(Accepted) + ); + } +} +``` + +Multiple states can be specified using `During` to avoid duplicating behavior configuration. In the updated example below, the `AcceptOrder` event is also +handled in the `Accepted` state, to add some idempotency to the saga state machine. + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public State Submitted { get; private set; } = null!; + public State Accepted { get; private set; } = null!; + + public Event AcceptOrder { get; private set; } = null!; + + public OrderStateMachine() + { + During(Submitted, Accepted, + When(AcceptOrder) + .Then(context => + { + context.Saga.AcceptedAt ??= context.SentTime ?? DateTime.UtcNow; + }) + .TransitionTo(Accepted) + ); + } +} +``` + +## Event Options + +Several additional properties can be configured on the event, including: + +| Property | Type | Default | Description | +|--------------------------|----------|---------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| ConfigureConsumeTopology | bool | true | When false, the event message type will not be configured on the broker. | +| InsertOnInitial | bool | false | If true and the event is handled in the _Initial_ state, the saga repository will attempt to insert a new saga instance before trying to load it. This option was introduced to deal with S-RANGE locks on SQL Server that would slow down inserts/updates due to querying for non-existent rows. | +| ReadOnly | bool | false | When true, the saga state machine instance will not be persisted when handling this event. | +| OnMissingInstance | delegate | null | Used to configure the behavior of an event when no matching instance is found. | + +## Read Only Events + +A saga state machine instance is the _source of truth_ for an instance. It's common to expose that state by handling an incoming _request_ event and responding +with the current state. To reduce saga repository resource usage, or in some cases to simply avoid updating the instance in the repository when nothing has +been updated, a read-only event can be configured. + +In the example below, an event handling the request is configured as read-only. + +```csharp +public record GetOrderState(Guid OrderId); +public record OrderState(Guid OrderId, string CurrentState); + +public class OrderStateMachine : + MassTransitStateMachine +{ + public Event OrderStateRequested { get; private set; } = null!; + + public OrderStateMachine() + { + Event(() => GetOrderState, e => + { + e.CorrelateById(x => x.Message.OrderId); + + e.ReadOnly = true; + }); + + DuringAny( + When(OrderStateRequested) + .RespondAsync(async context => new OrderState( + context.Saga.CorrelationId, + await Accessor.Get(context).Name)) + ); + } +} +``` + +> `Accessor` is a saga state machine property that can be used to get the current state from a saga state machine instance. + + +## On Missing Instance + +When a non-initiating event (an event without a behavior in the _Initial_ state) is received that does not match an existing saga state machine instance, the +message is ignored by default. This can lead to a misunderstanding that messages are being "lost." In many cases, this may be due to message order, +concurrency, or even timing. + +The missing instance behavior can be configured for an event using the `OnMissingInstance` method. In the example below the event is configured to +respond with `OrderNotFound` when a saga state machine instance matching the `OrderId` is not found. This ensures that the request doesn't time out +and receives a proper response. + +```csharp +public record RequestOrderCancellation(Guid OrderId); +public record OrderNotFound(Guid OrderId); + +public class OrderStateMachine : + MassTransitStateMachine +{ + public OrderStateMachine() + { + Event(() => OrderCancellationRequested, e => + { + e.CorrelateById(context => context.Message.OrderId); + + e.OnMissingInstance(m => + { + return m.ExecuteAsync(x => x.RespondAsync(new OrderNotFound(x.OrderId))); + }); + }); + } + + public Event OrderCancellationRequested { get; private set; } +} +``` + +Other missing instance options include `Discard`, `Fault`, and `Execute` (a synchronous version of _ExecuteAsync_). + +### Redeliver on Missing Instance + +Another option when a matching saga state machine instance is not found is to redeliver the message. Redelivery allows time for consumption of other events +which may create a matching instance, after which the redelivered message would be correlated to the matching instance. + +Redelivery can be configured as shown in the example below. The options are the same as configuring +[redelivery for exceptions](/documentation/concepts/exceptions#redelivery). + +```csharp +public record OrderAddressValidated(Guid OrderId); + +public class OrderStateMachine : + MassTransitStateMachine +{ + public OrderStateMachine() + { + Event(() => OrderAddressValidated, e => + { + e.CorrelateById(context => context.Message.OrderId); + + e.OnMissingInstance(m => m.Redeliver(r => + { + r.Interval(5, 1000); + r.OnRedeliveryLimitReached(n => n.Fault()); + })); + }); + } + + public Event OrderAddressValidated { get; private set; } +} +``` + +IF a matching saga state machine instance is not found, the message will be redelivered to the queue five times after which a fault (exception) will be +produced if no matching instance is found. + +## Advanced Options + +Like most things in MassTransit, the everyday use case of MassTransit should not need to use these options. But sometimes, you have to really dig in to make +things happen. + +### Setting the Saga Factory + +::alert{type="warning"} +The only time is when using `InsertOnInitial` and you have required properties that must be present or the insert will fail. Typically with SQL and not null +columns. +:: + +On events that are in the `Initial` state, a new instance of the saga will be created. You can use the `SetSagaFactory` to control how the saga is instantiated. + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public Event SubmitOrder { get; private set; } = null!; + + public OrderStateMachine() + { + Event( + () => SubmitOrder, + e => + { + e.CorrelateById(cxt => cxt.Message.OrderId) + e.SetSagaFactory(cxt => + { + // complex constructor logic + return new OrderState + { + CorrelationId = cxt.Message.OrderId + }; + }); + } + + ); + } +} +``` diff --git a/doc/content/3.documentation/2.configuration/1.sagas/3.requests.md b/doc/content/3.documentation/2.configuration/1.sagas/3.requests.md new file mode 100644 index 00000000000..0fd00ee6984 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/1.sagas/3.requests.md @@ -0,0 +1,227 @@ +--- +navigation.title: Requests +--- + +# State Machine Requests + +Request/response is easily the most commonly used integration pattern. A service sends a request to another service and continues after receiving the response. +Most of the time, waiting for the response is a _blocking_ operation – the requester waits for the response before it continues processing. In early days of +software development, blocking could limit overall system throughput. However, with modern _async_/_await_ solutions and the .NET Task Parallel Library (TPL), +the impact of waiting is mitigated. + +In event-based application, the combination of a _command_ followed by an _event_ usually refines down to the same request/response pattern. In many cases, +the event produced is _only_ interesting to the command's sender. + +Saga state machines support request/response, both as a requester and a responder. Unlike the [request client](/documentation/concepts/requests), however, +support for requests is asynchronous at the message-level, eliminating the overhead of _waiting_ for the response. After the request is produced, the saga +state machine instance is persisted. When the response is received, the instance is loaded and the response is consumed by the state machine. + + +## Declaring Requests + +Requests are declared as _public_ properties on the saga state machine with the `Request` property type where `TSaga` is the saga +state machine instance type and both `TRequest` and `TResponse` are valid message types. + +```csharp +public record ValidateOrder(Guid OrderId); +public record OrderValidated(Guid OrderId); + +public class OrderStateMachine : + MassTransitStateMachine +{ + public Request + ValidateOrder { get; private set; } = null!; + + public OrderStateMachine() + { + Request(() => ValidateOrder, o => + { + o.Timeout = TimeSpan.FromMinutes(30); + }); + } +} +``` + +In the example above, `ValidateOrder` is a request to an order validation service that responds with `OrderValidated`. One of three possible outcomes will +happen after the request is produced. + +| Event | Description | +|--------------------------------|--------------------------------------| +| `ValidateOrder.Completed` | The response was received | +| `ValidateOrder.TimeoutExpired` | The request timed out | +| `ValidateOrder.Faulted` | The order validation service faulted | + +The request also includes a `ValidateOrder.Pending` state that can optionally be used while the request is pending. + +### Request Configuration + +The request options can be configured using the configuration callback. In the example above, the `Timeout` option is set. The complete list of request options +includes: + +| Property | Type | Description | +|------------------|-------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `ServiceAddress` | `Uri?` | If specified, the endpoint address of the request service. If unspecified, the request is published. | +| `Timeout` | `TimeSpan` | The request timeout. If set to `TimeSpan.Zero`, the request never times out. This is useful for requests that are guaranteed to complete or fault and reduces the load on the message scheduler. | +| `TimeToLive` | `TimeSpan?` | The request message time-to-live, which is used by the transport to automatically delete the message after the time period elapses. If unspecified, and the `Timeout` is greater than `TimeSpan.Zero`, the timeout value is used. | + +### Response Types + +Requests usually have a single response, however, up to two additional responses are supported. Additional response types are specified as generic parameters +on the request property. + +In the example below, the request includes an additional response type `OrderNotValid`. + +```csharp +public record ValidateOrder(Guid OrderId); +public record OrderValidated(Guid OrderId); +public record OrderNotValid(Guid OrderId); + +public class OrderStateMachine : + MassTransitStateMachine +{ + public Request + ValidateOrder { get; private set; } = null!; + + public OrderStateMachine() + { + Request(() => ValidateOrder, o => + { + o.Timeout = TimeSpan.FromMinutes(30); + }); + } +} +``` + +### Response Events + +Additionally, each request event can be configured allowing complete control over how the response is correlated to the saga state machine instance. + +| Property | Description | +|----------------|-----------------------------------------| +| Completed | The first response event | +| Completed2 | The second response event, if specified | +| Completed3 | The third response event, if specified | +| Faulted | The `Fault` event | +| TimeoutExpired | The timeout event | + +This can be useful to configure how an event is configured on the message broker. For example, to remove the response type bindings from the message broker, +the events can be configured with `ConfigureConsumeTopology = false`. Since responses are always _sent_ to the `ResponseAddress` specified by the requester, +the bindings are not necessary and can be eliminated. + +```csharp +public record ValidateOrder(Guid OrderId); +public record OrderValidated(Guid OrderId); + +public class OrderStateMachine : + MassTransitStateMachine +{ + public Request + ValidateOrder { get; private set; } = null!; + + public OrderStateMachine() + { + Request(() => ValidateOrder, r => + { + r.Timeout = TimeSpan.FromMinutes(30); + + r.Completed = e => e.ConfigureConsumeTopology = false; + r.Faulted = e => e.ConfigureConsumeTopology = false; + r.TimeoutExpired = e => e.ConfigureConsumeTopology = false; + }); + } +} +``` + +## Sending Requests + +To send a request, add a `Request` activity to an event behavior as shown in the example below. + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public Request + ValidateOrder { get; private set; } = null!; + + public OrderStateMachine() + { + Initially( + When(OrderSubmitted) + .Request(ValidateOrder, + x => new ValidateOrder(x.Saga.CorrelationId)) + .TransitionTo(ValidateOrder.Pending) + ); + } +} +``` + +The request is published with the `RequestId` set the saga state machine instance `CorrelationId` (since no _RequestId_ property was specified) and the +`ResponseAddress` set to the receive endpoint address of the saga state machine. + +:::alert{type="info"} +In this example, the `ValidateOrder.Pending` state is used while the request is pending. However, any state defined in the saga state machine can be used. +::: + +### Handling Responses + +When the response is received, the `Completed` event is triggered. If the order validation service threw an exception, the `Faulted` event is triggered +instead. + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public Request + ValidateOrder { get; private set; } = null!; + + public OrderStateMachine() + { + During(ValidateOrder.Pending, + // Handle the valid response + When(ValidateOrder.Completed) + .TransitionTo(Completed), + + // Handle a validation fault + When(ValidateOrder.Faulted) + .TransitionTo(Failed) + ); + } +} +``` + +### Request Overrides + +There are many different `Request` method overrides that can be used depending on the features required. A few examples are shown below. + +#### Service Address + +Specify the service address for the request, optionally using the contents of the saga state machine instance or the event (via `context.Message`). +Useful when the instance stores data about which service should process the request. + +```csharp +.Request(ValidateOrder, serviceAddress, + context => new ValidateOrder(context.Saga.CorrelationId)) + +.Request(ValidateOrder, context => context.Saga.ServiceAddress, + context => new ValidateOrder(context.Saga.CorrelationId)) +``` + +#### Async Message Factory + +The request message can be created asynchronously, if a message initializer is used or when the request message needs data returned by an asynchronous method. + +```csharp +.Request(ValidateOrder, + async context => new ValidateOrder(context.Saga.CorrelationId)) + +.Request(ValidateOrder, context => context.Saga.ServiceAddress, + async context => new ValidateOrder(context.Saga.CorrelationId)) + +.Request(ValidateOrder, async context => +{ + await Task.Delay(1); // some async method + return new ValidateOrder(); +}); +``` + + diff --git a/doc/content/3.documentation/2.configuration/1.sagas/4.registration.md b/doc/content/3.documentation/2.configuration/1.sagas/4.registration.md new file mode 100644 index 00000000000..f4f207ad763 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/1.sagas/4.registration.md @@ -0,0 +1,46 @@ +--- +navigation.title: Registration +--- + +# Registering Sagas with MassTransit + +Sagas are added inside the `AddMassTransit` configuration using any of the following methods. + +```csharp +services.AddMassTransit(cfg => +{ + cfg.AddSaga(); + cfg.AddSaga(typeof(MySaga)); + + // Adds a saga with a matching saga definition + cfg.AddSaga(); + cfg.AddSaga(typeof(MySaga), typeof(MySagaDefinition)); + + // Adds a saga with a matching saga definition + // and configures the saga pipeline. + cfg.AddSaga(cfg => + { + cfg.ConcurrentMessageLimit = 8; + }); + + // Adds the specified sagas and saga definitions. + // When saga definitions are included they will + // be added with the matching saga type. + // AddSagas(params Type[] types); + cfg.AddSagas(typeof(MySaga), typeof(MyOtherSagaDefinition)); + + // Adds all sagas and saga definitions in the specified + // an assembly or assemblies. + // AddSagas(params Assembly[] assemblies); + cfg.AddSagas(typeof(Program).Assembly) + + // Adds the sagas and any matching saga definitions + // in the specified an assembly or assemblies that pass + // the filter. The filter is only called for saga types. + // AddSagas(Func filter, params Assembly[] assemblies); + cfg.AddSagas( + t => t.Name.StartsWith("S"), + typeof(Program).Assembly + ) +}); +``` diff --git a/doc/content/3.documentation/2.configuration/1.sagas/9.custom.md b/doc/content/3.documentation/2.configuration/1.sagas/9.custom.md new file mode 100644 index 00000000000..8775ccf8c07 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/1.sagas/9.custom.md @@ -0,0 +1,145 @@ +--- +navigation.title: Custom Activities +--- + +# Saga State Machine Activities + +There are scenarios when an event behavior may have dependencies that need to be managed at a scope level, such as a database connection, or the complexity is +best encapsulated in a separate class rather than being part of the state machine itself. Developers can create their own activities for state machine use, and +optionally create their own extension methods to add them to a behavior. + +> Key use cases include the need for dependency injection within the current scope or the desire to encapsulate complex logic into a single activity. + +## Calling a Custom Activity + +```csharp +public class OrderStateMachine : + MassTransitStateMachine +{ + public State Submitted { get; private set; } = null!; + + public Event OrderClosed { get; private set; } = null!; + + public OrderStateMachine() + { + // Tell the saga where to store the current state + InstanceState(x => x.CurrentState); + + Initially( + When(OrderClosed) + .Activity(x => x.OfType()) + .TransitionTo(Submitted) + ); + } +} +``` + +## Creating a Custom Activity + +To create an activity, create a class that implements `IStateMachineActivity` as shown. This class has full access to the dependency container, and all services will be resolved from the current scope. + +```csharp +public class OrderClosedActivity : + IStateMachineActivity +{ + readonly ISomeService _service; + + public OrderClosedActivity(ISomeService service) + { + _service = service; + } + + public async Task Execute( + BehaviorContext context, + IBehavior next) + { + await _service.OnOrderClosed(context.Saga.CorrelationId); + + // always call the next activity in the behavior + await next.Execute(context).ConfigureAwait(false); + } + + public Task Faulted( + BehaviorExceptionContext context, + IBehavior next + ) + where TException : Exception + { + // always call the next activity in the behavior + return next.Faulted(context); + } + + + public void Probe(ProbeContext context) + { + context.CreateScope("publish-order-closed"); + } + + public void Accept(StateMachineVisitor visitor) + { + visitor.Visit(this); + } +} +``` + +### Handling Any Event Type + +In the above example, the event type was known in advance. If an activity for any event type is needed, it can be created without specifying the event type. + +```csharp +public class OrderClosedActivity : + IStateMachineActivity +{ + readonly ISomeService _service; + + public OrderClosedActivity(ISomeService service) + { + _service = service; + } + + public async Task Execute(BehaviorContext context, IBehavior next) + { + await _service.OnOrderClosed(context.Saga.CorrelationId); + + // always call the next activity in the behavior + await next.Execute(context).ConfigureAwait(false); + } + + public async Task Execute(BehaviorContext context, IBehavior next) + { + await _service.OnOrderClosed(context.Saga.CorrelationId); + + // always call the next activity in the behavior + await next.Execute(context).ConfigureAwait(false); + } + + public Task Faulted(BehaviorExceptionContext context, IBehavior next) + where TException : Exception + { + + // always call the next activity in the behavior + return next.Faulted(context); + } + + public Task Faulted(BehaviorExceptionContext context, IBehavior next) + where TException : Exception + { + + // always call the next activity in the behavior + return next.Faulted(context); + } + + public void Probe(ProbeContext context) + { + context.CreateScope("publish-order-closed"); + } + + public void Accept(StateMachineVisitor visitor) + { + visitor.Visit(this); + } +} +``` + +[2]: https://github.com/MassTransit/Sample-ShoppingWeb + diff --git a/doc/content/3.documentation/2.configuration/1.sagas/_dir.yml b/doc/content/3.documentation/2.configuration/1.sagas/_dir.yml new file mode 100644 index 00000000000..f09256a27de --- /dev/null +++ b/doc/content/3.documentation/2.configuration/1.sagas/_dir.yml @@ -0,0 +1,4 @@ +title: Saga State Machines +description: | + Saga state machines are the secret sauce that make time travel possible. + diff --git a/doc/content/3.documentation/5.configuration/1.transports/10.kafka.md b/doc/content/3.documentation/2.configuration/2.transports/10.kafka.md similarity index 83% rename from doc/content/3.documentation/5.configuration/1.transports/10.kafka.md rename to doc/content/3.documentation/2.configuration/2.transports/10.kafka.md index 4e952c184c0..3e7cc9b6119 100755 --- a/doc/content/3.documentation/5.configuration/1.transports/10.kafka.md +++ b/doc/content/3.documentation/2.configuration/2.transports/10.kafka.md @@ -6,14 +6,11 @@ navigation.title: Kafka Kafka is supported as a [Rider](/documentation/concepts/riders), and supports consuming and producing messages from/to Kafka topics. The Confluent .NET client is used, and has been tested with the community edition (running in Docker). -### Topic Endpoints +:sample{sample=sample-kafka} -> Uses [MassTransit.RabbitMQ](https://nuget.org/packages/MassTransit.RabbitMQ/), [MassTransit.Kafka](https://nuget.org/packages/MassTransit.Kafka/), [MassTransit.Extensions.DependencyInjection](https://www.nuget.org/packages/MassTransit.Extensions.DependencyInjection/) +### Topic Endpoints -::alert{type="success"} -Note: the following examples are using the RabbitMQ Transport. You can also use InMemory Transport to achieve the same effect when developing. With that, there is no need to install MassTransit.RabbitMQ. -`x.UsingInMemory((context,config) => config.ConfigureEndpoints(context));` -:: +> Uses [MassTransit.Kafka](https://nuget.org/packages/MassTransit.Kafka/) To consume a Kafka topic, configure a Rider within the bus configuration as shown. @@ -32,7 +29,7 @@ public class Program services.AddMassTransit(x => { - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); + x.UsingInMemory(); x.AddRider(rider => { @@ -89,7 +86,7 @@ public class Program services.AddMassTransit(x => { - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); + x.UsingInMemory(); x.AddRider(rider => { @@ -133,11 +130,11 @@ The configuration includes through [Confluent](https://docs.confluent.io/kafka-c Rider implementation is taking full responsibility of Checkpointing, there is no ability to change it. Checkpointer can be configured on topic bases through next properties: -| Name | Description | Default | -|:-----------------------|:------------------------------------------------------|:-----| -| CheckpointInterval | Checkpoint frequency based on time | 1 min -| CheckpointMessageCount | Checkpoint every X messages | 5000 -| MessageLimit | Checkpointer buffer size without blocking consumption | 10000 +| Name | Description | Default | +|:-----------------------|:----------------------------------------------------|:--------| +| CheckpointInterval | Checkpoint frequency based on time | 1 min | +| CheckpointMessageCount | Checkpoint every X messages | 5000 | +| MessageLimit | Checkpoint buffer size without blocking consumption | 10000 | ::alert{type="info"} Please note, each topic partition has it's own checkpointer and configuration is applied to partition and not to entire topic. @@ -149,11 +146,11 @@ During graceful shutdown Checkpointer will try to "checkpoint" all already consu Riders are designed with performance in mind, handling each topic partition withing separate threadpool. As well, allowing to scale-up consumption within same partition by using Key, as long as keys are different they will be processed concurrently and all this **without** sacrificing ordering. | Name | Description | Default | -|:------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----| -| ConcurrentConsumerLimit | Number of Confluent Consumer instances withing same endpoint | 1 -| ConcurrentDeliveryLimit | Number of Messages delivered concurrently within same partition + key. Increasing this value will **break ordering**, helpful for topics where ordering is not required | 1 -| ConcurrentMessageLimit | Number of Messages processed concurrently witin different keys (preserving ordering). When keys are the same for entire partition `ConcurrentDeliveryLimit` will be used instead | 1 -| PrefetchCount | Number of Messages to prefetch from kafka topic into memory | 1000 +|:------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------| +| ConcurrentConsumerLimit | Number of Confluent Consumer instances withing same endpoint | 1 | +| ConcurrentDeliveryLimit | Number of Messages delivered concurrently within same partition + key. Increasing this value will **break ordering**, helpful for topics where ordering is not required | 1 | +| ConcurrentMessageLimit | Number of Messages processed concurrently witin different keys (preserving ordering). When keys are the same for entire partition `ConcurrentDeliveryLimit` will be used instead | 1 | +| PrefetchCount | Number of Messages to prefetch from kafka topic into memory | 1000 | ::alert{type="info"} `ConcurrentConsumerLimit` is very powerful setting as Confluent consumer is reading one partition at a time, this will allow creating multiple consumers to read from separate partitions. But having higher number of Consumers than Number of Total Partitions would result of having **idle** consumers @@ -185,7 +182,7 @@ public class Program services.AddMassTransit(x => { - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); + x.UsingInMemory(); x.AddRider(rider => { @@ -234,6 +231,77 @@ public class Program } ``` +#### Producer Provider + +With MassTransit v8.1, you can dynamically resolve a producer using `ITopicProducerProvider` (registered as `Scoped`). + +```csharp +namespace KafkaProducer; + +using System; +using System.Threading; +using System.Threading.Tasks; +using MassTransit; +using Microsoft.Extensions.DependencyInjection; + +public class Program +{ + public static async Task Main() + { + var services = new ServiceCollection(); + + services.AddMassTransit(x => + { + x.UsingInMemory(); + + x.AddRider(rider => + { + rider.UsingKafka((context, k) => { k.Host("localhost:9092"); }); + }); + }); + + var provider = services.BuildServiceProvider(); + + var busControl = provider.GetRequiredService(); + + await busControl.StartAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); + try + { + var producerProvider = provider.GetRequiredService(); + + do + { + string value = await Task.Run(() => + { + Console.WriteLine("Enter topic name (or quit to exit)"); + Console.Write("> "); + return Console.ReadLine(); + }); + + if ("quit".Equals(value, StringComparison.OrdinalIgnoreCase)) + break; + + var producer = producerProvider.GetProducer(new Uri($"topic:{value}")); + + await producer.Produce(new + { + TopicName = value + }); + } while (true); + } + finally + { + await busControl.StopAsync(); + } + } + + public record KafkaMessage + { + public string TopicName { get; init; } + } +} +``` + #### Tombstone message A record with the same key from the record we want to delete is produced to the same topic and partition with a null payload. These records are called tombstones. @@ -256,7 +324,7 @@ public class Program services.AddMassTransit(x => { - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); + x.UsingInMemory(); x.AddRider(rider => { diff --git a/doc/content/3.documentation/5.configuration/1.transports/11.azure-event-hub.md b/doc/content/3.documentation/2.configuration/2.transports/11.azure-event-hub.md similarity index 93% rename from doc/content/3.documentation/5.configuration/1.transports/11.azure-event-hub.md rename to doc/content/3.documentation/2.configuration/2.transports/11.azure-event-hub.md index 38943514473..29a55799dc1 100755 --- a/doc/content/3.documentation/5.configuration/1.transports/11.azure-event-hub.md +++ b/doc/content/3.documentation/2.configuration/2.transports/11.azure-event-hub.md @@ -6,7 +6,7 @@ navigation.title: Azure Event Hub Azure Event Hub is included as a [Rider](/documentation/concepts/riders), and supports consuming and producing messages from/to Azure event hubs. -> Uses [MassTransit.Azure.ServiceBus.Core](https://nuget.org/packages/MassTransit.Azure.ServiceBus.Core/), [MassTransit.EventHub](https://nuget.org/packages/MassTransit.EventHub/), [MassTransit.Extensions.DependencyInjection](https://www.nuget.org/packages/MassTransit.Extensions.DependencyInjection/) +> Uses [MassTransit.Azure.ServiceBus.Core](https://nuget.org/packages/MassTransit.Azure.ServiceBus.Core/), [MassTransit.EventHub](https://nuget.org/packages/MassTransit.EventHub/) To consume messages from an event hub, configure a Rider within the bus configuration as shown. @@ -76,11 +76,11 @@ The familiar _ReceiveEndpoint_ syntax is used to configure an event hub. The con Rider implementation is taking full responsibility of Checkpointing, there is no ability to change it. Checkpointer can be configured on topic bases through next properties: -| Name | Description | Default | -|:-----------------------|:------------------------------------------------------|:-----| -| CheckpointInterval | Checkpoint frequency based on time | 1 min -| CheckpointMessageCount | Checkpoint every X messages | 5000 -| MessageLimit | Checkpointer buffer size without blocking consumption | 10000 +| Name | Description | Default | +|:-----------------------|:----------------------------------------------------|:--------| +| CheckpointInterval | Checkpoint frequency based on time | 1 min | +| CheckpointMessageCount | Checkpoint every X messages | 5000 | +| MessageLimit | Checkpoint buffer size without blocking consumption | 10000 | ::alert{type="info"} Please note, each topic partition has it's own checkpointer and configuration is applied to partition and not to entire topic. @@ -92,11 +92,10 @@ During graceful shutdown Checkpointer will try to "checkpoint" all already consu Riders are designed with performance in mind, handling each topic partition withing separate threadpool. As well, allowing to scale-up consumption within same partition by using PartitionKey, as long as keys are different they will be processed concurrently and all this **without** sacrificing ordering. | Name | Description | Default | -|:------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----| -| ConcurrentDeliveryLimit | Number of Messages delivered concurrently within same partition + PartitionKey. Increasing this value will **break ordering**, helpful for topics where ordering is not required | 1 -| ConcurrentMessageLimit | Number of Messages processed concurrently witin different keys (preserving ordering). When keys are the same for entire partition `ConcurrentDeliveryLimit` will be used instead | 1 -| PrefetchCount | Number of Messages to prefetch from kafka topic into memory | 1000 - +|:------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------| +| ConcurrentDeliveryLimit | Number of Messages delivered concurrently within same partition + PartitionKey. Increasing this value will **break ordering**, helpful for topics where ordering is not required | 1 | +| ConcurrentMessageLimit | Number of Messages processed concurrently witin different keys (preserving ordering). When keys are the same for entire partition `ConcurrentDeliveryLimit` will be used instead | 1 | +| PrefetchCount | Number of Messages to prefetch from kafka topic into memory | 1000 | ### Producers diff --git a/doc/content/3.documentation/2.configuration/2.transports/2.rabbitmq.md b/doc/content/3.documentation/2.configuration/2.transports/2.rabbitmq.md new file mode 100755 index 00000000000..75b1d77be4c --- /dev/null +++ b/doc/content/3.documentation/2.configuration/2.transports/2.rabbitmq.md @@ -0,0 +1,273 @@ +--- +navigation.title: RabbitMQ +--- + +# RabbitMQ Configuration + +[![alt MassTransit on NuGet](https://img.shields.io/nuget/v/MassTransit.svg "MassTransit on NuGet")](https://nuget.org/packages/MassTransit.RabbitMQ/) + +With tens of thousands of users, RabbitMQ is one of the most popular open source message brokers. RabbitMQ is lightweight and easy to deploy on premises and in the cloud. RabbitMQ can be deployed in distributed and federated configurations to meet high-scale, high-availability requirements. + +MassTransit fully supports RabbitMQ, including many of the advanced features and capabilities. + +::alert{type="info"} +To get started with RabbitMQ, refer to the [configuration](/documentation/configuration) section which uses RabbitMQ in the examples. +:: + +## Minimal Example + +In the example below, which configures a receive endpoint, consumer, and message type, the bus is configured to use RabbitMQ. + +```csharp +namespace RabbitMqConsoleListener; + +using System.Threading.Tasks; +using MassTransit; +using Microsoft.Extensions.Hosting; + +public static class Program +{ + public static async Task Main(string[] args) + { + await Host.CreateDefaultBuilder(args) + .ConfigureServices(services => + { + services.AddMassTransit(x => + { + x.UsingRabbitMq((context, cfg) => + { + cfg.Host("localhost", "/", h => + { + h.Username("guest"); + h.Password("guest"); + }); + }); + }); + }) + .Build() + .RunAsync(); + } +} +``` + +## Broker Topology + +With RabbitMQ, which supports exchanges and queues, messages are _sent_ or _published_ to exchanges and RabbitMQ routes those messages through exchanges to the appropriate queues. + +When the bus is started, MassTransit will create exchanges and queues on the virtual host for the receive endpoint. MassTransit creates durable, _fanout_ exchanges by default, and queues are also durable by default. + +## Configuration + +The configuration includes: + +* The RabbitMQ host + - Host name: `localhost` + - Virtual host: `/` + - User name and password used to connect to the virtual host (credentials are virtual-host specific) +* The receive endpoint + - Queue name: `order-events-listener` + - Consumer: `OrderSubmittedEventConsumer` + - Message type: `OrderSystem.Events.OrderSubmitted` + +| Name | Description | +|:----------------------------------|:------------------------------------------------------------------------------------------------------------------| +| order-events-listener | Queue for the receive endpoint | +| order-events-listener | An exchange, bound to the queue, used to _send_ messages | +| OrderSystem.Events:OrderSubmitted | An exchange, named by the message-type, bound to the _order-events-listener_ exchange, used to _publish_ messages | + +When a message is sent, the endpoint address can be one of two values: + +`exchange:order-events-listener` + +Send the message to the _order-events-listener_ exchange. If the exchange does not exist, it will be created. _MassTransit translates topic: to exchange: when using RabbitMQ, so that topic: addresses can be resolved – since RabbitMQ is the only supported transport that doesn't have topics._ + +`queue:order-events-listener` + +Send the message to the _order-events-listener_ exchange. If the exchange or queue does not exist, they will be created and the exchange will be bound to the queue. + +With either address, RabbitMQ will route the message from the _order-events-listener_ exchange to the _order-events-listener_ queue. + +When a message is published, the message is sent to the _OrderSystem.Events:OrderSubmitted_ exchange. If the exchange does not exist, it will be created. RabbitMQ will route the message from the _OrderSystem.Events:OrderSubmitted_ exchange to the _order-events-listener_ exchange, and subsequently to the _order-events-listener_ queue. If other receive endpoints connected to the same virtual host include consumers that consume the _OrderSubmitted_ message, a copy of the message would be routed to each of those endpoints as well. + +::alert{type="danger"} +If a message is published before starting the bus, so that MassTransit can create the exchanges and queues, the exchange _OrderSystem.Events:OrderSubmitted_ will be created. However, until the bus has been started at least once, there won't be a queue bound to the exchange and any published messages will be lost. Once the bus has been started, the queue will remain bound to the exchange even when the bus is stopped. +:: + +Durable exchanges and queues remain configured on the virtual host, so even if the bus is stopped messages will continue to be routed to the queue. When the bus is restarted, queued messages will be consumed. + +## Transport Options + +All RabbitMQ transport options can be configured using the `.Host()` method. The most commonly used settings can be configured via transport options. + +```csharp +services.AddOptions() + .Configure(options => + { + // configure options manually, but usually bind them to a configuration section + }); +``` + +| Property | Type | Description | +|----------------|--------|---------------------| +| Host | string | Network host name | +| Port | ushort | Network port | +| ManagementPort | ushort | Management API port | +| VHost | string | Virtual host name | +| User | string | Username | +| Pass | string | Password | +| UseSsl | bool | True to use SSL/TLS | + +## Host Configuration + +MassTransit includes several RabbitMQ options that configure the behavior of the entire bus instance. + +| Property | Type | Description | +|----------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| PublisherConfirmation | bool | MassTransit will wait until RabbitMQ confirms messages when publishing or sending messages (default: true) | +| Heartbeat | TimeSpan | The heartbeat interval used by the RabbitMQ client to keep the connection alive | +| RequestedChannelMax | ushort | The maximum number of channels allowed on the connection | +| RequestedConnectionTimeout | TimeSpan | The connection timeout | +| ContinuationTimeout | TImeSpan | Sets the time the client will wait for the broker to response to RPC requests. Increase this value if you are experiencing timeouts from RabbitMQ due to a slow broker instance. | + +#### UseCluster + +MassTransit can connect to a cluster of RabbitMQ virtual hosts and treat them as a single virtual host. To configure a cluster, call the `UseCluster` methods, and add the cluster nodes, each of which becomes part of the virtual host identified by the host name. Each cluster node can specify either a `host` or a `host:port` combination. + +> While this exists, it's generally preferable to configure something like HAProxy in front of a RabbitMQ cluster, instead of using MassTransit's built-in cluster configuration. + +#### ConfigureBatchPublish + +MassTransit will briefly buffer messages before sending them to RabbitMQ, to increase message throughput. While use of the default values is recommended, the batch options can be configured. + +| Property | Type | Default | Description | +|:-------------|:---------|---------|---------------------------------------------------------| +| Enabled | bool | false | Enable or disable batch sends to RabbitMQ | +| MessageLimit | int | 100 | Limit the number of messages per batch | +| SizeLimit | int | 64K | A rough limit of the total message size | +| Timeout | TimeSpan | 1ms | The time to wait for additional messages before sending | + + +```csharp +x.UsingRabbitMq((context, cfg) => +{ + cfg.Host("localhost", h => + { + h.ConfigureBatchPublish(x => + { + x.Enabled = true; + x.Timeout = TimeSpan.FromMilliseconds(2); + }); + }); +}); +``` + +## Endpoint Configuration + +MassTransit includes several receive endpoint level configuration options that control receive endpoint behavior. + +| Property | Type | Description | +|----------------|--------|-------------------------------------------------------------------------------------------------------| +| PrefetchCount | ushort | The number of unacknowledged messages that can be processed concurrently (default based on CPU count) | +| PurgeOnStartup | bool | Removes all messages from the queue when the bus is started (default: false) | +| AutoDelete | bool | If true, the queue will be automatically deleted when the bus is stopped (default: false) | +| Durable | bool | If true, messages are persisted to disk before being acknowledged (default: true) | + +### Quorum Queues + +Quorum queues are a more reliable, replicated queue type supported by RabbitMQ. To specify the use of quorum queues, MassTransit can be configured at the individual receive endpoint level or globally using a configure endpoints callback. + +To configure a receive endpoint to use a quorum queue: + +```csharp +x.UsingRabbitMq((context, cfg) => +{ + cfg.ReceiveEndpoint("queue-name", e => + { + e.SetQuorumQueue(3); // replication factor of 3 + }); +}); +``` + +To configure all receive endpoints using a configure endpoints callback: + +```csharp +services.AddMassTransit(x => +{ + x.AddConfigureEndpointsCallback((name, cfg) => + { + if (cfg is IRabbitMqReceiveEndpointConfigurator rmq) + rmq.SetQuorumQueue(3); + }); +}); +``` + +## Reply To Request Client + +> New in MassTransit v8.3.0 + +RabbitMQ provides a default _ReplyTo_ address for every broker connection that can be used to send messages directly to the connection without the need to +create a temporary queue. MassTransit supports use of the _ReplyTo_ address for the request client. + +::alert{type="info"} +By default, MassTransit will create a non-durable, auto-delete queue for the bus and use that queue for responses sent to the request client. +:: + +To configure MassTransit to use the _ReplyTo_ address instead of the bus endpoint: + +```csharp +services.AddMassTransit(x => +{ + x.SetRabbitMqReplyToRequestClientFactory(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); +}); +``` + +This will replace the default request client factory with the RabbitMQ-specific _ReplyTo_ client factory, allowing responses to requests sent using the request +client to to delivered using the broker connection's _ReplyTo_ address. + +## Additional Examples + +### CloudAMQP + +MassTransit can be used with CloudAMQP, which is a great SaaS-based solution to host your RabbitMQ broker. To configure MassTransit, the host and virtual host must be specified, and _UseSsl_ must be configured. + +```csharp +services.AddMassTransit(x => +{ + x.UsingRabbitMq((context, cfg) => + { + cfg.Host("wombat.rmq.cloudamqp.com", 5671, "your_vhost", h => + { + h.Username("your_vhost"); + h.Password("your_password"); + + h.UseSsl(s => + { + s.Protocol = SslProtocols.Tls12; + }); + }); + }); +}); +``` + +### AmazonMQ - RabbitMQ + +AmazonMQ now includes [RabbitMQ support](https://us-east-2.console.aws.amazon.com/amazon-mq/home), which means the best message broker can now be used easily on AWS. To configure MassTransit, the AMQPS endpoint address can be used to configure the host as shown below. + +```csharp +services.AddMassTransit(x => +{ + x.UsingRabbitMq((context, cfg) => + { + cfg.Host(new Uri("amqps://b-12345678-1234-1234-1234-123456789012.mq.us-east-2.amazonaws.com:5671"), h => + { + h.Username("username"); + h.Password("password"); + }); + }); +}); +``` diff --git a/doc/content/3.documentation/5.configuration/1.transports/21.azure-functions.md b/doc/content/3.documentation/2.configuration/2.transports/21.azure-functions.md similarity index 89% rename from doc/content/3.documentation/5.configuration/1.transports/21.azure-functions.md rename to doc/content/3.documentation/2.configuration/2.transports/21.azure-functions.md index 1fc498d6dd2..83a3bfe2a82 100644 --- a/doc/content/3.documentation/5.configuration/1.transports/21.azure-functions.md +++ b/doc/content/3.documentation/2.configuration/2.transports/21.azure-functions.md @@ -2,11 +2,19 @@ navigation.title: Azure Functions --- -# Azure Functions Configuration +# Azure Functions Azure Functions is a consumption-based compute solution that only runs code when there is work to be done. MassTransit supports Azure Service Bus and Azure Event Hubs when running as an Azure Function. -> The [Sample Code](https://github.com/MassTransit/Sample-AzureFunction) is available for reference as well, which is based on the 8.0.0 version of MassTransit. +> The [Sample Code](https://github.com/MassTransit/Sample-AzureFunction) provides the best starting point to use MassTransit with Azure Functions and Azure Service Bus. + +::alert{type="info"} +Using MassTransit with Azure Functions works, but many of MassTransit's capabilities are either limited or unavailable due to the underlying Azure Functions runtime. +Azure has significantly better offerings for consumption-based, scale-to-zero service hosting. For instance, Azure Container Apps allows a complete Docker container +deployment model with full scale-to-zero support for both HTTP and Azure Service Bus queues and enables the complete MassTransit feature set. +:: + +## Azure Function Configuration The functions [host.json](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-service-bus-trigger?tabs=csharp) file needs to have messageHandlerOptions > autoComplete set to true. If this isn't set to true, MassTransit will _try_ to set it to true for you. This is so that the message is acknowledged by the Azure Functions runtime, which removes it from the queue once processing has completed successfully. diff --git a/doc/content/3.documentation/5.configuration/1.transports/22.aws-lambda.md b/doc/content/3.documentation/2.configuration/2.transports/22.aws-lambda.md similarity index 100% rename from doc/content/3.documentation/5.configuration/1.transports/22.aws-lambda.md rename to doc/content/3.documentation/2.configuration/2.transports/22.aws-lambda.md diff --git a/doc/content/3.documentation/2.configuration/2.transports/3.azure-service-bus.md b/doc/content/3.documentation/2.configuration/2.transports/3.azure-service-bus.md new file mode 100755 index 00000000000..65a5905d9f4 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/2.transports/3.azure-service-bus.md @@ -0,0 +1,220 @@ +--- +navigation.title: Azure Service Bus +--- + +# Azure Service Bus Configuration + +[![alt MassTransit on NuGet](https://img.shields.io/nuget/v/MassTransit.Azure.ServiceBus.Core.svg "MassTransit on NuGet")](https://nuget.org/packages/MassTransit.Azure.ServiceBus.Core/) + +[![alt MassTransit on NuGet](https://img.shields.io/nuget/dt/MassTransit.Azure.ServiceBus.Core.svg "MassTransit on NuGet")](https://nuget.org/packages/MassTransit.Azure.ServiceBus.Core/) + +MassTransit fully supports Azure Service Bus, including many of the advanced features and capabilities. + +::alert{type="warning"} +The Azure Service Bus transport only supports **Standard** and **Premium** tiers of the Microsoft Azure Service Bus service. Premium tier is recommended for production environments. See [Performance](#performance) section below. +:: + +## Minimal Example + +To configure Azure Service Bus, use the connection string (from the Azure portal) to configure the host as shown below. + +```csharp +namespace ServiceBusConsoleListener; + +using System.Threading.Tasks; +using MassTransit; +using Microsoft.Extensions.Hosting; + +public class Program +{ + public static async Task Main(string[] args) + { + await Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddMassTransit(x => + { + x.UsingAzureServiceBus((context, cfg) => + { + cfg.Host("connection-string"); + }); + }); + }) + .Build() + .RunAsync(); + } +} +``` + +## Broker Topology + +With Azure Service Bus (ASB), which supports topics and queues, messages are _sent_ or _published_ to topics and ASB routes those messages through topics to the appropriate queues. + +## Configuration + + +Azure Service Bus queues includes an extensive set a properties that can be configured. All of these are optional, MassTransit uses sensible defaults, but the control is there when needed. + +```csharp +services.AddMassTransit(x => +{ + x.UsingAzureServiceBus((context, cfg) => + { + cfg.Host("connection-string"); + + cfg.ReceiveEndpoint("input-queue", e => + { + // all of these are optional!! + + e.PrefetchCount = 100; + + // number of messages to deliver concurrently + e.ConcurrentMessageLimit = 100; + + // default, but shown for example + e.LockDuration = TimeSpan.FromMinutes(5); + + // lock will be renewed up to 30 minutes + e.MaxAutoRenewDuration = TimeSpan.FromMinutes(30); + }); + }); +}); +``` + +### Host Settings + +| Property | Type | Description | +|----------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| TokenCredential | | Use a specific token-based credential, such as a managed identity token, to access the namespace. You can use the [DefaultAzureCredential](https://docs.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet) to automatically apply any one of several credential types. | +| TransportType | | Change the transport type from the default (AMQP) to use WebSockets | + +For example, to configure the transport type to use AMQP over Web Sockets: + +```csharp +cfg.Host(connectionString, h => +{ + h.TransportType = ServiceBusTransportType.AmqpWebSockets; +}); + +``` + +### Receive Settings + +| Property | Type | Description | +|----------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| PrefetchCount | int | The number of unacknowledged messages that can be processed concurrently (default based on CPU count) | +| MaxConcurrentCalls | int | How many concurrent messages to dispatch (transport-throttled) | +| LockDuration | TimeSpan | How long to hold message locks (max is 5 minutes) | +| MaxAutoRenewDuration | TimeSpan | How long to renew message locks (maximum consumer duration) | +| MaxDeliveryCount | int | How many times the transport will redeliver the message on negative acknowledgment. This is different from retry, this is the transport redelivering the message to a receive endpoint before moving it to the dead letter queue. | +| RequiresSession | bool | If true, a message SessionId must be specified when sending messages to the queue (see [sessions](#using-service-bus-sessions)) | + + +### Transport Options + +All Azure Service Bus transport options can be configured using the `.Host()` method. The most commonly used settings can be configured via transport options. + +```csharp +services.AddOptions() + .Configure(options => + { + // configure options manually, but usually bind them to a configuration section + }); +``` + +| Property | Type | Description | +|------------------|--------|-----------------------| +| ConnectionString | string | The connection string | + +## Additional Examples + +### Using Azure Managed Identity + +The following example shows how to configure Azure Service Bus using an Azure Managed Identity: + +```csharp +services.AddMassTransit(x => +{ + x.UsingAzureServiceBus((context, cfg) => + { + cfg.Host(new Uri("sb://your-service-bus-namespace.servicebus.windows.net")); + }); +}); +``` + +During local development, in the case of Visual Studio, you can configure the account to use under Options -> Azure Service Authentication. Note that your Azure Active Directory user needs explicit access to the resource and have the 'Azure Service Bus Data Owner' role assigned. + +::alert{type="warning"} +To ensure that MassTransit has sufficient permissions to perform queue management as well as messaging operations. Your identity & managed identity will need to have the correct role assignments within Azure. + +Assigning the role **Azure Service Bus Data Owner** will provide sufficient permissions for Mass Transit to function on the namespace. +:: + +### Using Service Bus Dead-letter Queues + +MassTransit can be configured to use the built-in dead-letter queue instead moving messages to the *_skipped* or *_error* queues. Each can be configured independently. + +To use the built-in dead-letter queue for all skipped and faulted messages on all receive endpoints: + +```csharp +services.AddMassTransit(x => +{ + x.AddConfigureEndpointsCallback((_, cfg) => + { + if (cfg is IServiceBusReceiveEndpointConfigurator sb) + { + sb.ConfigureDeadLetterQueueDeadLetterTransport(); + sb.ConfigureDeadLetterQueueErrorTransport(); + } + }); + + x.UsingAzureServiceBus((context, cfg) => + { + cfg.Host(new Uri("sb://your-service-bus-namespace.servicebus.windows.net")); + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +### Using Service Bus Sessions + +#### Receive Settings +| Property | Type | Description | +|------------------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| RequiresSession | bool | Set to true | +| MaxConcurrentSessions | int | How many concurrent sessions to receive messages from (transport-throttled) | +| MaxConcurrentCallsPerSession | int | How many concurrent messages to dispatch from each session (transport-throttled) | +| SessionIdleTimeout | TimeSpan | How long to wait to receive a message before abandoning the session for another | + +#### Batch Consumer + +For batch-based in-order processing of sessions: + +```csharp +AddConsumer(cfg => +{ + cfg.SetServiceBusSessionBatchOptions(o => + o.SetMessageLimitPerSession(8) + .SetMaxConcurrentSessions(4) + .SetSessionIdleTimeout(TimeSpan.FromSeconds(30)) + .SetTimeLimit(TimeSpan.FromSeconds(5)) + ); +}); +``` +Batches will be grouped by [SessionId](/documentation/transports/azure-service-bus#sessionid) + +## Performance + +We **really** recommend that you use the Premium subscription levels for production workloads. We have performed our own testing using [MassTransit Benchmark](https://github.com/MassTransit/MassTransit-Benchmark) on a P4 instance. It is also critical that your application is in the same DC as the ASB instance. From a home test using a 1Gb fiber connection we could not get over 600/second. When running in the same DC as the ASB we were able to acheive 6k/second. This test was done with one instance writing to ASB and another instance reading from ASB, as adding consumption over the same AMQP connection killed throughput. + +## Security Settings + +Some security teams may initially balk at the necessity for your application to have the management role. Most teams value the developer productivity that comes with MassTransit managing the topics, queues, and subscriptions for them. In order to support this, MassTransit needs the `Manage` permissions to support this development model. + +When the Bus starts up, it will also confirm that the Topology is correct, and make any adjustments that are needed for ongoing work. To simply check the existence of queues and topics, Azure requires that the process have `Manage` permissions. Some teams find this frustrating, and we encourge them to reach out to their Azure representative to request a finer grained access model. + +When working with your Security and Compliance team, it can also be helpful to remind them that for most multi-service systems the MassTransit systems are running in an isolated Namespace. This namespace is usually only used by MassTransit services, and is not likely to interfere with anything other than this singular namespace. This creates a nicely defined "blast zone" that limits the scope of the impact. + +You can read more about the Azure Service Bus security roles here: +[Mapping of Operations to Rights Needed](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-sas#rights-required-for-service-bus-operations) diff --git a/doc/content/3.documentation/2.configuration/2.transports/4.amazon-sqs.md b/doc/content/3.documentation/2.configuration/2.transports/4.amazon-sqs.md new file mode 100755 index 00000000000..d440ba17240 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/2.transports/4.amazon-sqs.md @@ -0,0 +1,191 @@ +--- +navigation.title: Amazon SQS +--- + +# Amazon SQS Configuration + +[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.AmazonSQS.svg "NuGet")](https://nuget.org/packages/MassTransit.AmazonSQS/) + + +MassTransit combines Amazon SQS (Simple Queue Service) with SNS (Simple Notification Service) to provide both send and publish support. + +Configuring a receive endpoint will use the message topology to create and subscribe SNS topics to SQS queues so that published messages will be delivered to the receive endpoint queue. + +## Minimal Example + +In the example below, the Amazon SQS settings are configured. + +```csharp +namespace AmazonSqsConsoleListener; + +using System.Threading.Tasks; +using MassTransit; +using Microsoft.Extensions.Hosting; + +public class Program +{ + public static async Task Main(string[] args) + { + await Host.CreateDefaultBuilder(args) + .ConfigureServices((hostContext, services) => + { + services.AddMassTransit(x => + { + x.UsingAmazonSqs((context, cfg) => + { + cfg.Host("us-east-2", h => + { + h.AccessKey("your-iam-access-key"); + h.SecretKey("your-iam-secret-key"); + }); + }); + }); + }) + .Build() + .RunAsync(); + } +} +``` + +## Broker Topology + +With SQS/SNS, which supports topics and queues, messages are _sent_ or _published_ to SNS Topics and then routes those messages through subscriptions to the appropriate SQS Queues. + +When the bus is started, MassTransit will create SNS Topics and SQS Queues for the receive endpoint. + +## Configuration + +The configuration includes: + +* The Amazon SQS host + - Region name: `us-east-2` + - Access key and secret key used to access the resources + + + +## Transport Options + +All AWS SQS transport options can be configured using the .Host() method. The most commonly used settings can be configured via transport options. + +```csharp +services.AddOptions() + .Configure(options => + { + // configure options manually, but usually bind them to a configuration section + }); +``` + +| Property | Type | Description | +|----------------|--------|---------------------| +| Region | string | The AWS Region | +| Scope | string | Will be used as a prefix for queue/topic name | +| AccessKey | string | Access Key | +| SecretKey | string | Access Secret | + + +## Endpoint Configuration + +some endpoint love + +## Additional Examples + +Any topic can be subscribed to a receive endpoint, as shown below. The topic attributes can also be configured, in case the topic needs to be created. + +```csharp +services.AddMassTransit(x => +{ + x.UsingAmazonSqs((context, cfg) => + { + cfg.Host("us-east-2", h => + { + h.AccessKey("your-iam-access-key"); + h.SecretKey("your-iam-secret-key"); + }); + + cfg.ReceiveEndpoint("input-queue", e => + { + // disable the default topic binding + e.ConfigureConsumeTopology = false; + + e.Subscribe("event-topic", s => + { + // set topic attributes + s.TopicAttributes["DisplayName"] = "Public Event Topic"; + s.TopicSubscriptionAttributes["some-subscription-attribute"] = "some-attribute-value"; + s.TopicTags.Add("environment", "development"); + }); + }); + }); +}); +``` + +## Errata + +### Scoping + +Because there is only ever one "SQS/SNS" per AWS account it can be helpful to "Scope" your queues and topics. This will prefix all SQS queues and SNS topics with scope value. + +```csharp +services.AddMassTransit(x => +{ + x.UsingAmazonSqs((context, cfg) => + { + cfg.Host("us-east-2", h => + { + h.AccessKey("your-iam-access-key"); + h.SecretKey("your-iam-secret-key"); + + // specify a scope for all topics + h.Scope("dev", true); + }); + + // additionally include the queues + cfg.ConfigureEndpoints(context, new DefaultEndpointNameFormatter("dev-", false)); + }); +}); +``` + +### Example IAM Policy + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "SqsAccess", + "Effect": "Allow", + "Action": [ + "sqs:SetQueueAttributes", + "sqs:ReceiveMessage", + "sqs:CreateQueue", + "sqs:DeleteMessage", + "sqs:SendMessage", + "sqs:GetQueueUrl", + "sqs:GetQueueAttributes", + "sqs:ChangeMessageVisibility", + "sqs:PurgeQueue", + "sqs:DeleteQueue", + "sqs:TagQueue" + ], + "Resource": "arn:aws:sqs:*:YOUR_ACCOUNT_ID:*" + },{ + "Sid": "SnsAccess", + "Effect": "Allow", + "Action": [ + "sns:GetTopicAttributes", + "sns:CreateTopic", + "sns:Publish", + "sns:Subscribe" + ], + "Resource": "arn:aws:sns:*:YOUR_ACCOUNT_ID:*" + },{ + "Sid": "SnsListAccess", + "Effect": "Allow", + "Action": [ + "sns:ListTopics" + ], + "Resource": "*" + } + ] +} +``` diff --git a/doc/content/3.documentation/5.configuration/1.transports/5.activemq.md b/doc/content/3.documentation/2.configuration/2.transports/5.activemq.md similarity index 100% rename from doc/content/3.documentation/5.configuration/1.transports/5.activemq.md rename to doc/content/3.documentation/2.configuration/2.transports/5.activemq.md diff --git a/doc/content/3.documentation/2.configuration/2.transports/6.sql.md b/doc/content/3.documentation/2.configuration/2.transports/6.sql.md new file mode 100644 index 00000000000..7955f90677d --- /dev/null +++ b/doc/content/3.documentation/2.configuration/2.transports/6.sql.md @@ -0,0 +1,291 @@ +# SQL/DB + +The SQL database transport is implemented for both PostgreSQL and SQL Server. + +## Transport Functions + +MassTransit uses PostgreSQL functions or SQL Server stored procedures for all transport operations. These functions are built to manage the constraints of the +transport, such as message locking, redelivery, and subscription. + +For completeness, the functions are detailed below. + +### Create Queue + +```sql +create_queue(queue_name text, + auto_delete integer DEFAULT NULL) +``` + +Creates a queue with its associated *error* and *dead-letter* queues. The resulting three queues each have their own *type*. + +### Create Topic + +```sql +create_topic(topic_name text) +``` + +Creates a topic. + +### Create Topic Subscription + +```sql +create_topic_subscription(source_topic_name text, + destination_topic_name text, + type integer, + routing_key text DEFAULT '', + filter jsonb DEFAULT '{{}}') +``` + +Creates a subscription from one topic to another topic. + +### Create Queue Subscription + +```sql +create_queue_subscription(source_topic_name text, + destination_queue_name text, + type integer, + routing_key text DEFAULT '', + filter jsonb DEFAULT '{{}}') +``` + +Creates a subscription from a topic to a queue. + +### Purge Queue + +```sql +purge_queue(queue_name text) +``` + +Removes all messages from a queue, including messages in the *error* and *dead-letter* sub-queues. + +### Fetch Messages + +```sql +fetch_messages(queue_name text, + fetch_consumer_id uuid, + fetch_lock_id uuid, + lock_duration interval, + fetch_count integer DEFAULT 1) +``` + +Fetches messages from the specified queue. + +### Fetch Messages Partitioned + +```sql +fetch_messages_partitioned(queue_name text, + fetch_consumer_id uuid, + fetch_lock_id uuid, + lock_duration interval, + fetch_count integer DEFAULT 1, + concurrent_count integer DEFAULT 1, + ordered integer DEFAULT 0) +``` + +Fetches messages from the specified queue using the partitioned receive mode. + +### Delete Message + +```sql +delete_message(message_delivery_id bigint, + lock_id uuid) +``` + +Deletes a message that was previously fetched with the specified *lock_id*. + +### Renew Message Lock + +```sql +renew_message_lock(message_delivery_id bigint, + lock_id uuid, + duration interval) +``` + +Renews (extends) the lock on a message that was previously fetched with the specified *lock_id*. + +### Move Message + +```sql +move_message(message_delivery_id bigint, + lock_id uuid, + queue_name text, + queue_type integer, + headers jsonb) +``` + +Moves a message that was previously fetched with the specified *lock_id* to the destination queue, adding the headers to the message delivery. Typically used +by the receive endpoint to either dead-letter (skipped) or fault (error) a message. + +### Unlock Message + +```sql +unlock_message(message_delivery_id bigint, + lock_id uuid, + delay interval, + headers jsonb) +``` + +Unlocks a message that was previously fetched with the specified *lock_id*, adding the specified delay to the *enqueue_time* and any additional headers. +Typically used when delayed message redelivery is used, or when faults are thrown back to the transport. + +### Send Message + +```sql +send_message(entity_name text, + priority integer DEFAULT NULL, + transport_message_id uuid DEFAULT gen_random_uuid(), + body jsonb DEFAULT NULL, + binary_body bytea DEFAULT NULL, + content_type text DEFAULT NULL, + message_type text DEFAULT NULL, + message_id uuid DEFAULT NULL, + correlation_id uuid DEFAULT NULL, + conversation_id uuid DEFAULT NULL, + request_id uuid DEFAULT NULL, + initiator_id uuid DEFAULT NULL, + source_address text DEFAULT NULL, + destination_address text DEFAULT NULL, + response_address text DEFAULT NULL, + fault_address text DEFAULT NULL, + sent_time timestamptz DEFAULT NULL, + headers jsonb DEFAULT NULL, + host jsonb DEFAULT NULL, + partition_key text DEFAULT NULL, + routing_key text DEFAULT NULL, + delay interval DEFAULT INTERVAL '0 seconds', + scheduling_token_id uuid DEFAULT NULL, + max_delivery_count int DEFAULT 10) +``` + +Sends a message to the *entity_name* queue with all the specified properties. + +### Publish Message + +```sql +publish_message(entity_name text, + priority integer DEFAULT NULL, + transport_message_id uuid DEFAULT gen_random_uuid(), + body jsonb DEFAULT NULL, + binary_body bytea DEFAULT NULL, + content_type text DEFAULT NULL, + message_type text DEFAULT NULL, + message_id uuid DEFAULT NULL, + correlation_id uuid DEFAULT NULL, + conversation_id uuid DEFAULT NULL, + request_id uuid DEFAULT NULL, + initiator_id uuid DEFAULT NULL, + source_address text DEFAULT NULL, + destination_address text DEFAULT NULL, + response_address text DEFAULT NULL, + fault_address text DEFAULT NULL, + sent_time timestamptz DEFAULT NULL, + headers jsonb DEFAULT NULL, + host jsonb DEFAULT NULL, + partition_key text DEFAULT NULL, + routing_key text DEFAULT NULL, + delay interval DEFAULT INTERVAL '0 seconds', + scheduling_token_id uuid DEFAULT NULL, + max_delivery_count int DEFAULT 10) +``` + +Publishes a message to the *entity_name* topic with all the specified properties. Queues with matching subscriptions to the topic will each receive an instance +of the message. + +### Delete Scheduled Message + +```sql +delete_scheduled_message(token_id uuid) +``` + +Deletes a previously scheduled message using the *token_id*. + +### Touch Queue + +```sql +touch_queue(queue_name text) +``` + +When a receive endpoint doesn't receive any messages from a queue for a period of time, and that queue is an auto-delete queue, this function is called to +add metrics for the queue so that it isn't automatically deleted. + +### Process Metrics + +```sql +process_metrics(row_limit int DEFAULT 10000) +``` + +A background function used to process queue metrics, which includes messages consumed, faulted, and dead-lettered. Automatically called by idle receive +endpoints. Metrics can be viewed using the `queues` view. + +### Purge Topology + +```sql +purge_topology() +``` + +A background function used to purge any auto-delete queues that have reached their idle threshold. Automatically called by idle receive endpoints. + +## End-User Functions + +### Requeue Messages + +```sql +requeue_messages(queue_name text, + source_queue_type int, + target_queue_type int, + message_count int, + delay interval DEFAULT INTERVAL '0 seconds', + redelivery_count int DEFAULT 10) +``` + +Use this function to move messages from the *error* or *dead-letter* queue back to the main queue. Up to *message_count* messages are moved. An optional +_delay_ can be added to the current time to delay the message redelivery. An additional *redelivery_count* can also be added to the message's +*max_delivery_count* to ensure the message can be consumed if the delivery count previously exceeded the delivery count limit. + +### Requeue Message + +```sql +requeue_message(message_delivery_id bigint, + target_queue_type int, + delay interval DEFAULT INTERVAL '0 seconds', + redelivery_count int DEFAULT 10) +``` + +Use this function to move a message from the *error* or *dead-letter* queue back to the main queue. An optional +_delay_ can be added to the current time to delay the message redelivery. An additional *redelivery_count* can also be added to the message's +*max_delivery_count* to ensure the message can be consumed if the delivery count previously exceeded the delivery count limit. + +## Views + +### Queues + +Returns details about the queues and their metrics. + +### Subscriptions + +Returns details about all subscriptions and their settings. + +## Tables + +The tables used by the SQL transport include: + +### Queue + +### Topic + +### Topic Subscription + +#### Subscription Type + +| Type | Description | +|------|----------------------------| +| 1 | All | +| 2 | Routing Key | +| 3 | Pattern (uses Routing Key) | + + +### Queue Subscription + +### Message + +### Message Delivery diff --git a/doc/content/3.documentation/5.configuration/1.transports/_dir.yml b/doc/content/3.documentation/2.configuration/2.transports/_dir.yml similarity index 100% rename from doc/content/3.documentation/5.configuration/1.transports/_dir.yml rename to doc/content/3.documentation/2.configuration/2.transports/_dir.yml diff --git a/doc/content/3.documentation/2.configuration/3.middleware/0.index.md b/doc/content/3.documentation/2.configuration/3.middleware/0.index.md new file mode 100644 index 00000000000..ce47c9a2e92 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/3.middleware/0.index.md @@ -0,0 +1,350 @@ +--- +navigation.title: Overview +--- + +# Middleware + +MassTransit is built using a network of pipes and filters to dispatch messages. A pipe is composed of a series of filters, each of which is a key atom and are described below. + +Middleware components are configured using extension methods on any pipe configurator `IPipeConfigurator`, and the extension methods all begin with `Use` to separate them from other methods. + +To understand how middleware components are built, an understanding of filters and pipes is needed. + +## Filters + +A filter is a middleware component that performs a specific function, and should adhere to the single responsibility principal – do one thing, one thing only (and hopefully do it well). By sticking to this approach, developers are able to opt-in to each behavior without including unnecessary or unwatched functionality. + +There are many filters included with GreenPipes, and a whole lot more of them are included with MassTransit. In fact, the entire MassTransit message flow is built around pipes and filters. + +Developers can create their own filters. To create a filter, create a class that implements `IFilter`. + +```csharp +public interface IFilter + where T : class, PipeContext +{ + void Probe(ProbeContext context); + + Task Send(T context, IPipe next); +} +``` + +The _Probe_ method is used to interrogate the filter about its behavior. This should describe the filter in a way that a developer would understand its role when looking at a network graph. For example, a transaction filter may add the following to the context. + +```csharp +public void Probe(ProbeContext context) +{ + context.CreateFilterScope("transaction"); +} +``` + +The _Send_ method is used to send contexts through the pipe to each filter. _Context_ is the actual context, and _next_ is used to pass the context to the next filter in the pipe. Send returns a Task, and should always follow the .NET guidelines for asynchronous methods. + + + +### Creating Filters + +Middleware components are configured using extension methods, to make them easy to discover. + +::alert{type="info"} +To be consistent with MassTransit conventions, middleware configuration methods should start with `Use`. +:: + +An example middleware component that would log exceptions to the console is shown below. + +```csharp +x.UsingInMemory((context,cfg) => +{ + cfg.UseExceptionLogger(); + + cfg.ConfigureEndpoints(context); +}); +``` + +The extension method creates the pipe specification for the middleware component, which can be added to any pipe. For a component on the message consumption pipeline, use `ConsumeContext` instead of any `PipeContext`. + +```csharp +public static class ExampleMiddlewareConfiguratorExtensions +{ + public static void UseExceptionLogger(this IPipeConfigurator configurator) + where T : class, PipeContext + { + configurator.AddPipeSpecification(new ExceptionLoggerSpecification()); + } +} +``` + +The pipe specification is a class that adds the filter to the pipeline. Additional logic can be included, such as configuring optional settings, etc. using a closure syntax similar to the other configuration classes in MassTransit. + +```csharp +public class ExceptionLoggerSpecification : + IPipeSpecification + where T : class, PipeContext +{ + public IEnumerable Validate() + { + return Enumerable.Empty(); + } + + public void Apply(IPipeBuilder builder) + { + builder.AddFilter(new ExceptionLoggerFilter()); + } +} +``` + +Finally, the middleware component itself is a filter added to the pipeline. All filters have absolute and complete control of the execution context and flow of the message. Pipelines are entirely asynchronous, and expect that asynchronous operations will be performed. + +::alert{type="danger"} +Do not use legacy constructs such as .Wait, .Result, or .WaitAll() as these can cause blocking in the message pipeline. While they might work in same cases, you've been warned! +:: + +```csharp +public class ExceptionLoggerFilter : + IFilter + where T : class, PipeContext +{ + long _exceptionCount; + long _successCount; + long _attemptCount; + + public void Probe(ProbeContext context) + { + var scope = context.CreateFilterScope("exceptionLogger"); + scope.Add("attempted", _attemptCount); + scope.Add("succeeded", _successCount); + scope.Add("faulted", _exceptionCount); + } + + /// + /// Send is called for each context that is sent through the pipeline + /// + /// The context sent through the pipeline + /// The next filter in the pipe, must be called or the pipe ends here + public async Task Send(T context, IPipe next) + { + try + { + Interlocked.Increment(ref _attemptCount); + + // here the next filter in the pipe is called + await next.Send(context); + + Interlocked.Increment(ref _successCount); + } + catch (Exception ex) + { + Interlocked.Increment(ref _exceptionCount); + + await Console.Out.WriteLineAsync($"An exception occurred: {ex.Message}"); + + // propagate the exception up the call stack + throw; + } + } +} +``` + +The example filter above is stateful. If the filter was stateless, the same filter instance could be used by multiple pipes — worth considering if the filter has high memory requirements. + +### Message Type Filters + +In many cases, the message type is used by a filter. To create an instance of a generic filter that includes the message type, use the configuration observer. + +```csharp +public class MessageFilterConfigurationObserver : + ConfigurationObserver, + IMessageConfigurationObserver +{ + public MessageFilterConfigurationObserver(IConsumePipeConfigurator receiveEndpointConfigurator) + : base(receiveEndpointConfigurator) + { + Connect(this); + } + + public void MessageConfigured(IConsumePipeConfigurator configurator) + where TMessage : class + { + var specification = new MessageFilterPipeSpecification(); + + configurator.AddPipeSpecification(specification); + } +} +``` + +Then, in the specification, the appropriate filter can be created and added to the pipeline. + +```csharp +public class MessageFilterPipeSpecification : + IPipeSpecification> + where T : class +{ + public void Apply(IPipeBuilder> builder) + { + var filter = new MessageFilter(); + + builder.AddFilter(filter); + } + + public IEnumerable Validate() + { + yield break; + } +} +``` + +The filter could then include the message type as a generic type parameter. + +```csharp +public class MessageFilter : + IFilter> + where T : class +{ + public void Probe(ProbeContext context) + { + var scope = context.CreateFilterScope("messageFilter"); + } + + public async Task Send(ConsumeContext context, IPipe> next) + { + // do something + + await next.Send(context); + } +} +``` + +The extension method for the above is shown below (for completeness). + +```csharp +public static class MessageFilterConfigurationExtensions +{ + public static void UseMessageFilter(this IConsumePipeConfigurator configurator) + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + + var observer = new MessageFilterConfigurationObserver(configurator); + } +} +``` + + +## Pipes + +Filters are combined in sequence to form a pipe. A pipe configurator, along with a pipe builder, is used to configure and build a pipe. + +```csharp +public interface CustomContext : + PipeContext +{ + string SomeThing { get; } +} + +IPipe pipe = Pipe.New(x => +{ + x.UseFilter(new CustomFilter(...)); +}) +``` + +The `IPipe` interface is similar to `IFilter`, but a pipe hides the _next_ parameter as it is part of the pipe's structure. It is the pipe's responsibility to pass the +appropriate _next_ parameter to the individual filters in the pipe. + +```csharp +public interface IPipe + where T : class, PipeContext +{ + Task Send(T context); +} +``` + +Send can be called, passing a context instance as shown. + +```csharp +public class BaseCustomContext : + BasePipeContext, + CustomContext +{ + public string SomeThing { get; set; } +} + +await pipe.Send(new BaseCustomContext { SomeThing = "Hello" }); +``` + + +### PipeContext + +The _context_ type has a `PipeContext` constraint, which is another core atom in _GreenPipes_. A pipe context can include _payloads_, which are kept in a last-in, first-out (LIFO) collection. Payloads are identified by _type_, and can be retrieved, added, and updated using the `PipeContext` methods: + +```csharp +public interface PipeContext +{ + /// + /// Used to cancel the execution of the context + /// + CancellationToken CancellationToken { get; } + + /// + /// Checks if a payload is present in the context + /// + bool HasPayloadType(Type payloadType); + + /// + /// Retrieves a payload from the pipe context + /// + /// The payload type + /// The payload + /// + bool TryGetPayload(out T payload) + where T : class; + + /// + /// Returns an existing payload or creates the payload using the factory method provided + /// + /// The payload type + /// The payload factory is the payload is not present + /// The payload + T GetOrAddPayload(PayloadFactory payloadFactory) + where T : class; + + /// + /// Either adds a new payload, or updates an existing payload + /// + /// The payload factory called if the payload is not present + /// The payload factory called if the payload already exists + /// The payload type + /// + T AddOrUpdatePayload(PayloadFactory addFactory, UpdatePayloadFactory updateFactory) + where T : class; +``` + +The payload methods are also used to check if a pipe context is another type of context. For example, to see if the `SendContext` is a `RabbitMqSendContext`, the `TryGetPayload` method should be used instead of trying to pattern match or cast the _context_ parameter. + +```csharp +public async Task Send(SendContext context, IPipe next) +{ + if(context.TryGetPayload(out var rabbitMqSendContext)) + rabbitMqSendContext.Priority = 3; + + return next.Send(context); +} +``` + +::alert{type="warning"} +It is entirely the filter's responsibility to call _Send_ on the _next_ parameter. This gives the filter ultimately control over the context and behavior. It is how the retry filter is able to retry – by controlling the context flow. +:: + +User-defined payloads are easily added, so that subsequent filters can use them. The following example adds a payload. + +```csharp +public class SomePayload +{ + public int Value { get; set; } +} + +public async Task Send(SendContext context, IPipe next) +{ + var payload = context.GetOrAddPayload(() => new SomePayload{Value = 27}); + + return next.Send(context); +} +``` diff --git a/doc/content/3.documentation/2.configuration/3.middleware/1.filters.md b/doc/content/3.documentation/2.configuration/3.middleware/1.filters.md new file mode 100644 index 00000000000..43dbd2e1a2f --- /dev/null +++ b/doc/content/3.documentation/2.configuration/3.middleware/1.filters.md @@ -0,0 +1,162 @@ +--- +navigation.title: Filters +--- + +# Middleware Filters + + +## Kill Switch + +A Kill Switch is used to prevent failing consumers from moving all the messages from the input queue to the error queue. By monitoring message consumption and tracking message successes and failures, a Kill Switch stops the receive endpoint when a trip threshold has been reached. + +Typically, consumer exceptions are transient issues and suspending consumption until a later time when the transient issue may have been resolved. + +::alert{type="info"} +A Kill Switch is the messaging analog of a Circuit Breaker, and operates in a similar manner. However, instead of inducing failure to reduce pressure on a backing service, the kill switch stops consuming messages instead thereby reducing pressure on backing services. + +> Read Martin Fowler's description of the pattern [here](http://martinfowler.com/bliki/CircuitBreaker.html). +:: + +#### UseKillSwitch + +A Kill Switch can be configured on an individual receive endpoint or all receive endpoints on the bus. To configure a kill switch on all receive endpoints, add the _UseKillSwitch_ method as shown. + +```csharp +cfg.UseKillSwitch(options => options + .SetActivationThreshold(10) + .SetTripThreshold(0.15) + .SetRestartTimeout(m: 1)); +``` + +In the above example, the kill switch will activate after _10_ messages have been consumed. If the ratio of failures/attempts exceeds _15%_, the kill switch will trip and stop the receive endpoint. After _1_ minute, the receive endpoint will be restarted. Once restarted, if exceptions are still observed, the receive endpoint will be stopped again for _1_ minute. + +A kill switch may be configured on the bus or on individual receive endpoint(s). When configured on the bus, the kill switch is applied to all receive endpoints. + +#### Options + +| Option | Description | +|-----------------------|------------------------------------------------------------------------------------------------------------------| +| `TrackingPeriod` | The time window for tracking exceptions | +| `TripThreshold` | The percentage of failed messages that triggers the kill switch. Should be 0-100, but seriously like 5-10. | +| `ActivationThreshold` | The number of messages that must be consumed before the kill switch activates. | +| `RestartTimeout` | The wait time before restarting the receive endpoint | +| `ExceptionFilter` | By default, all exceptions are tracked. An exception filter can be configured to only track specific exceptions. | + + + +## Circuit Breaker + +A circuit breaker is used to protect resources (remote, local, or otherwise) from being overloaded when +in a failure state. For example, a remote web site may be unavailable and calling that web site in a +message consumer takes 30-60 seconds to time out. By continuing to call the failing service, the service +may be unable to recover. A circuit breaker detects the repeated failures and trips, preventing further +calls to the service and giving it time to recover. Once the reset interval expires, calls are slowly allowed +back to the service. If it is still failing, the breaker remains open, and the timeout interval resets. +Once the service returns to healthy, calls flow normally as the breaker closes. + +Read Martin Fowler's description of the pattern [here](http://martinfowler.com/bliki/CircuitBreaker.html). + +#### UseCircuitBreaker + +To add the circuit breaker to a receive endpoint: + +```csharp +cfg.UseCircuitBreaker(cb => +{ + cb.TrackingPeriod = TimeSpan.FromMinutes(1); + cb.TripThreshold = 15; + cb.ActiveThreshold = 10; + cb.ResetInterval = TimeSpan.FromMinutes(5); +}); +``` + +#### Options + +There are four options that can be adjusted on a circuit breaker. + +| Option | Description | +|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `TrackingPeriod` | The time window for tracking exceptions | +| `TripThreshold` | This is a percentage, and is based on the ratio of successful to failed attempts. When set to 15, if the ratio exceeds 15%, the circuit breaker opens and remains open until the `ResetInterval` expires. | +| `ActiveThreshold` | that must reach the circuit breaker in a tracking period before the circuit breaker can trip. If set to 10, the trip threshold is not evaluated until at least 10 messages have been received. | +| `ResetInterval` | The period of time between the circuit breaker trip and the first attempt to close the circuit breaker. Messages that reach the circuit breaker during the open period will immediately fail with the same exception that tripped the circuit breaker. | + +## Rate Limiter + +A rate limiter is used to restrict the number of messages processed within a time period. The reason may be +that an API or service only accepts a certain number of calls per minute, and will delay any subsequent attempts +until the rate limiting period has expired. + +::alert{type="warning"} +The rate limiter will delay message delivery until the rate limit expires, so it is best to avoid large time windows +and keep the rate limits sane. Something like 1000 over 10 minutes is a bad idea, versus 100 over a minute. Try to +adjust the values and see what works for you. +:: + +#### UsePartitioner + +To limit concurrent message consumption by partition key on a single bus instance, the partitioner filter can be used. For each message type, a partition key provider must be specified. + +To configure the partition key filter, a good example is the job service state machine: + +```csharp +var partition = new Partitioner(16, new Murmur3UnsafeHashGenerator()); + +e.UsePartitioner(partition, p => p.Message.JobId); +e.UsePartitioner(partition, p => p.Message.JobId); + +e.UsePartitioner(partition, p => p.Message.JobId); +e.UsePartitioner>(partition, p => p.Message.Message.JobId); + +e.UsePartitioner(partition, p => p.Message.JobId); +e.UsePartitioner>(partition, p => p.Message.Message.JobId); + +// ... +``` + +::alert{type="warning"} +This filter does not partition across load balanced consumer instances. If load-balanced, partitioned, in-order message consumption is needed, consider using the [SQL Transport](/documentation/transports/sql). +:: + + +#### UseRateLimit + +To add a rate limiter to a receive endpoint: + +```csharp +cfg.ReceiveEndpoint("customer_update_queue", e => +{ + e.UseRateLimit(1000, TimeSpan.FromSeconds(5)); + // other configuration +}); +``` + +The two arguments supported by the rate limiter include: + +#### RateLimit + The number of calls allowed in the time period. + +#### Interval + The time interval before the message count is reset to zero. + + +## Concurrency Limit + +::alert{type="danger"} +The concurrency limit filter has been deprecated for most scenarios, developers should instead specify a `ConcurrentMessageLimit` at the bus, endpoint, or consumer level to limit the number of messages processed concurrently. +:: + +The concurrency limit filter supports any pipe context (any type that implements `PipeContext`, which includes most `*Context` types in MassTransit. For this reason alone the filter still exists in MassTransit despite being deprecated in concurrent message limit scenarios. + +#### UseConcurrencyLimit + +To use the concurrency limit filter: + +```csharp +cfg.ReceiveEndpoint("submit-order", e => +{ + e.UseConcurrencyLimit(4); + + e.ConfigureConsumer(context); +}); +``` diff --git a/doc/content/3.documentation/2.configuration/3.middleware/2.scoped.md b/doc/content/3.documentation/2.configuration/3.middleware/2.scoped.md new file mode 100644 index 00000000000..b486987b463 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/3.middleware/2.scoped.md @@ -0,0 +1,322 @@ +--- +navigation.title: Scoped Filters +--- + +# Scoped Middleware Filters + +## Scoped Filters + +Most of the built-in filters are created and added to the pipeline during configuration. This approach is typically sufficient, however, there are scenarios where the filter needs access to other components at runtime. + +Using a scoped filter allows a new filter instance to be resolved from the container for each message. If a current scope is not available, a new scope will be created using the root container. + +### Filter Classes + +Scoped filters can be either an open generic class implementing one of the supported filter contexts or a concrete class implementing a filter context for one more valid message type(s). + +For example, a scoped open generic consume filter would be defined as shown below. + +```csharp +public class TFilter : + IFilter> +``` + +A concrete consume filter can also be defined. + +```csharp +public class MyMessageConsumeFilter : + IFilter> +``` + +### Supported Filter Contexts + +Scope filters are added using one of the following methods, which are specific to the filter context type. + +| Type | Usage | +|------------------------------|-----------------------------------------------------------| +| `ConsumeContext` | `UseConsumeFilter(typeof(TFilter<>), context)` | +| `SendContext` | `UseSendFilter(typeof(TFilter<>), context)` | +| `PublishContext` | `UsePublishFilter(typeof(TFilter<>), context)` | +| `ExecuteContext` | `UseExecuteActivityFilter(typeof(TFilter<>), context)` | +| `CompensateContext` | `UseCompensateActivityFilter(typeof(TFilter<>), context)` | + +More information could be found inside [Middleware](/documentation/configuration/middleware) section + +### UseConsumeFilter + +To create a `ConsumeContext` filter and add it to the receive endpoint: + +```csharp +public class MyConsumeFilter : + IFilter> + where T : class +{ + public MyConsumeFilter(IMyDependency dependency) { } + + public async Task Send(ConsumeContext context, IPipe> next) + { + await next.Send(context); + } + + public void Probe(ProbeContext context) { } +} + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + + services.AddMassTransit(x => + { + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ReceiveEndpoint("input-queue", e => + { + e.UseConsumeFilter(typeof(MyConsumeFilter<>), context); + + e.ConfigureConsumer(); + }); + }); + }); + } +} +``` + +To configure a scoped filter for a specific message type (or types) and configure it on _all_ receive endpoints: + +```csharp +public class MyMessageConsumeFilter : + IFilter>, + IFilter> + where T : class +{ + public MyConsumeFilter(IMyDependency dependency) { } + + public async Task Send(ConsumeContext context, IPipe> next) + { + await next.Send(context); + } + + public async Task Send(ConsumeContext context, IPipe> next) + { + await next.Send(context); + } + + public void Probe(ProbeContext context) { } +} + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + + services.AddMassTransit(x => + { + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseConsumeFilter(context); + + cfg.ConfigureEndpoints(context); + }); + }); + } +} +``` + +To use an open generic filter but only configure the filter for specific message types: + +```csharp +public class MyCommandFilter : + IFilter> + where T : class, ICommand +{ + public MyCommandFilter(IMyDependency dependency) { } + + public async Task Send(ConsumeContext context, IPipe> next) + { + await next.Send(context); + } + + public void Probe(ProbeContext context) { } +} + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + + services.AddMassTransit(x => + { + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + // Specify a conditional expression to only + // add the filter for certain message types + cfg.UseConsumeFilter(typeof(MyCommandFilter<>), context, + x => x.Include(type => type.HasInterface())); + + cfg.ConfigureEndpoints(context); + }); + }); + } +} +``` + +### UseSendFilter + +To create a `SendContext` filter and add it to the send pipeline: + +```csharp +public class MySendFilter : + IFilter> + where T : class +{ + public MySendFilter(IMyDependency dependency) { } + + public async Task Send(SendContext context, IPipe> next) + { + await next.Send(context); + } + + public void Probe(ProbeContext context) { } +} + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + + services.AddMassTransit(x => + { + x.UsingRabbitMq((context, cfg) => + { + cfg.UseSendFilter(typeof(MySendFilter<>), context); + }); + }); + } +} +``` + +### UsePublishFilter + +```csharp +public class MyPublishFilter : + IFilter> + where T : class +{ + public MyPublishFilter(IMyDependency dependency) { } + + public async Task Send(PublishContext context, IPipe> next) + { + await next.Send(context); + } + + public void Probe(ProbeContext context) { } +} + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + + services.AddMassTransit(x => + { + x.UsingRabbitMq((context, cfg) => + { + cfg.UsePublishFilter(typeof(MyPublishFilter<>), context); + }); + }); + } +} +``` + +### Combining Consume And Send/Publish Filters + +A common use case with scoped filters is transferring data between the consumer. This data may be extracted from headers, or could include context or authorization information that needs to be passed from a consumed message context to sent or published messages. In these situations, there _may_ be some special requirements to ensure everything works as expected. + +The following example has both consume and send filters, and utilize a shared dependency to communicate data to outbound messages. + +```csharp +public class MyConsumeFilter : + IFilter> + where T : class +{ + public MyConsumeFilter(MyDependency dependency) { } + + public async Task Send(ConsumeContext context, IPipe> next) { } + + public void Probe(ProbeContext context) { } +} + +public class MySendFilter : + IFilter> + where T : class +{ + public MySendFilter(MyDependency dependency) { } + + public async Task Send(SendContext context, IPipe> next) { } + + public void Probe(ProbeContext context) { } +} + +public class MyDependency +{ + public string SomeValue { get; set; } +} + +public class Startup +{ + public void ConfigureServices(IServiceCollection services) + { + services.AddScoped(); + + services.AddMassTransit(x => + { + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseSendFilter(typeof(MySendFilter<>), context); + + cfg.ReceiveEndpoint("input-queue", e => + { + e.UseConsumeFilter(typeof(MyConsumeFilter<>), context); + e.ConfigureConsumer(context); + }); + }); + }); + } +} +``` + +::alert{type="warning"} +When using the InMemoryOutbox with scoped publish or send filters, `UseMessageScope` (for MSDI) or `UseMessageLifetimeScope` (for Autofac) must be configured _before_ the InMemoryOutbox. If `UseMessageRetry` is used, it must come _before_ either `UseMessageScope` or `UseMessageLifetimeScope`. +:: + +Because the InMemoryOutbox delays publishing and sending messages until after the consumer or saga completes, the created container scope will have been disposed. The `UseMessageScope` or `UseMessageLifetimeScope` filters create the scope before the InMemoryOutbox, which is then used by the consumer or saga and any scoped filters (consume, publish, or send). + +The updated receive endpoint configuration using the InMemoryOutbox is shown below. + +```csharp +cfg.ReceiveEndpoint("input-queue", e => +{ + e.UseMessageRetry(r => r.Intervals(100, 500, 1000, 2000)); + e.UseMessageScope(context); + e.UseInMemoryOutbox(); + + e.UseConsumeFilter(typeof(MyConsumeFilter<>), context); + e.ConfigureConsumer(context); +}); +``` + + diff --git a/doc/content/3.documentation/2.configuration/3.middleware/3.outbox.md b/doc/content/3.documentation/2.configuration/3.middleware/3.outbox.md new file mode 100644 index 00000000000..4e638119ab4 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/3.middleware/3.outbox.md @@ -0,0 +1,167 @@ +--- +navigation.title: Transactional Outbox +--- + +# Transactional Outbox Configuration + +The Transaction Outbox is explained in the [patterns](/documentation/patterns/transactional-outbox) section. This section covers how to configure the transactional outbox using any of the supported databases. + + +## Bus Outbox Options + +The bus outbox has its own configuration settings, which are common across all supported databases. + +| Setting | Description | +|--------------------------|------------------------------------------------------------------------------------------------------| +| MessageDeliveryLimit | The number of messages to deliver at a time from the outbox to the broker | +| MessageDeliveryTimeout | Transport Send timeout when delivering messages to the transport | +| DisableDeliveryService() | Disable the outbox message delivery service, removing the hosted service from the service collection | + + +## Entity Framework Outbox + +The Transactional Outbox for Entity Framework Core uses three tables in the `DbContext` to store messages that are subsequently delivered to the message broker. + +| Table | Description | +|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------| +| InboxState | Tracks received messages by `MessageId` for each endpoint | +| OutboxMessage | Stores messages published or sent using `ConsumeContext`, `IPublishEndpoint`, and `ISendEndpointProvider` | +| OutboxState | Tracks delivery of outbox messages by the delivery service (similar to _InboxState_ but for message sent outside of a consumer via the bus outbox) | + +### Configuration + +> The code below is based upon the [sample application](https://github.com/MassTransit/Sample-Outbox) + +The outbox components are included in the `MassTransit.EntityFrameworkCore` NuGet packages. The code below configures both the bus outbox and the consumer outbox using the default settings. In this case, PostgreSQL is the database engine. + +```csharp +x.AddEntityFrameworkOutbox(o => +{ + // configure which database lock provider to use (Postgres, SqlServer, or MySql) + o.UsePostgres(); + + // enable the bus outbox + o.UseBusOutbox(); +}); +``` + +To configure the _DbContext_ with the appropriate tables, use the extension methods shown below: + +```csharp +public class RegistrationDbContext : + DbContext +{ + public RegistrationDbContext(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + base.OnModelCreating(modelBuilder); + + modelBuilder.AddInboxStateEntity(); + modelBuilder.AddOutboxMessageEntity(); + modelBuilder.AddOutboxStateEntity(); + } +} +``` + +To configure the outbox on a receive endpoint, configure the receive endpoint as shown below. The configuration below uses a `SagaDefinition` to configure the receive endpoint, which is added to MassTransit along with the saga state machine. + +```csharp +public class RegistrationStateDefinition : + SagaDefinition +{ + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, + ISagaConfigurator consumerConfigurator, IRegistrationContext context) + { + endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 500, 1000, 1000, 1000, 1000, 1000)); + + endpointConfigurator.UseEntityFrameworkOutbox(context); + } +} +``` + +The definition is added with the saga state machine: + +```csharp +x.AddSagaStateMachine() + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.UsePostgres(); + }); +``` + +The Entity Framework outbox adds a hosted service which removes delivered _InboxState_ entries after the _DuplicateDetectionWindow_ has elapsed. The Bus Outbox includes an additional hosted service that delivers the outbox messages to the broker. + +The outbox can also be added to all consumers using a configure endpoints callback: + +```csharp +x.AddConfigureEndpointsCallback((context, name, cfg) => +{ + cfg.UseEntityFrameworkOutbox(context); +}); +``` + +### Configuration Options + +The available outbox settings are listed below. + +| Setting | Description | +|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| DuplicateDetectionWindow | The amount of time a message remains in the inbox for duplicate detection (based on MessageId) | +| IsolationLevel | The transaction isolation level to use (Serializable by default) | +| LockStatementProvider | The lock statement provider, needed to execute pessimistic locks. Is set via `UsePostgres`, `UseSqlServer` (the default), or `UseMySql` | +| QueryDelay | The delay between queries once messages are no longer available. When a query returns messages, subsequent queries are performed until no messages are returned after which the QueryDelay is used. | +| QueryMessageLimit | The maximum number of messages to query from the database at a time | +| QueryTimeout | The database query timeout | + +## MongoDB Outbox + +### Configuration + +> The code below is based upon the [sample application](https://github.com/MassTransit/Sample-Outbox/tree/mongodb) + +The outbox components are included in the `MassTransit.MongoDb` NuGet packages. The code below configures both the bus outbox and the consumer outbox using the default settings. + +```csharp +x.AddMongoDbOutbox(o => +{ + o.QueryDelay = TimeSpan.FromSeconds(1); + + o.ClientFactory(provider => provider.GetRequiredService()); + o.DatabaseFactory(provider => provider.GetRequiredService()); + + o.DuplicateDetectionWindow = TimeSpan.FromSeconds(30); + + o.UseBusOutbox(); +}); +``` + +To configure the transactional outbox for a specific consumer, use a consumer definition: + +```csharp +public class ValidateRegistrationConsumerDefinition : + ConsumerDefinition +{ + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) + { + endpointConfigurator.UseMessageRetry(r => r.Intervals(10, 50, 100, 1000, 1000, 1000, 1000, 1000)); + + endpointConfigurator.UseMongoDbOutbox(context); + } +} +``` + +To configure the transactional outbox for all configured receive endpoints, use a configure endpoints callback: + +```csharp +x.AddConfigureEndpointsCallback((context, name, cfg) => +{ + cfg.UseMongoDbOutbox(context); +}); +``` + diff --git a/doc/content/3.documentation/5.configuration/3.middleware/_dir.yml b/doc/content/3.documentation/2.configuration/3.middleware/_dir.yml similarity index 100% rename from doc/content/3.documentation/5.configuration/3.middleware/_dir.yml rename to doc/content/3.documentation/2.configuration/3.middleware/_dir.yml diff --git a/doc/content/3.documentation/2.configuration/3.middleware/transactions.md b/doc/content/3.documentation/2.configuration/3.middleware/transactions.md new file mode 100644 index 00000000000..fe6ecccfe7f --- /dev/null +++ b/doc/content/3.documentation/2.configuration/3.middleware/transactions.md @@ -0,0 +1,344 @@ +--- +navigation.title: Transaction +--- + +# Transaction Filter + +::alert{type="warning"} +Transactions, and using a shared transaction, is an advanced concept. Every scenario is different, so this is more of a guideline than a rule. +:: + +The message pipeline in MassTransit is asynchronous, leveraging the Task Parallel Library (TPL) extensively to maximum thread utilization. This means that receiving an individual message may involve several threads over the life cycle of the consumer. To prevent strange things from happening, developers should avoid using any *static* or *thread static* variables as these are one of the main causes of errors in asynchronous programming. + +The .NET `System.Transactions` namespace is a static hound, with many applications following the model of using a transaction scope to wrap a transactional operation. + +```csharp +public class Repository +{ + public void Save(Entity entity) + { + using(var scope = new TransactionScope()) + { + SaveEntity(entity); + + scope.Complete(); + } + } +} +``` + +In this example, the creation of a `TransactionScope` actually sets a static variable, `Transaction.Current`, to the created or ambient transaction. That word *ambient* should be a big clue — it's using a static variable (in this case, it's actually a thread static, but anyway). + +It turns out that the above example is simple, and works, because there are no asynchronous methods. But that also means that the method blocks the thread while the database performs work (which takes an eternity in CPU time). Most databases support asynchronous operations (including Entity Framework), so it is logical to assume that using those methods would increase thread utilization. + +It is also often requested that a set of operations be managed as a *unit of work*. A single transaction is shared across multiple operations that are committed as a single unit. If the commit fails, everything is undone and the message is faulted (or retried, if the retry middleware is used). + +## Usage + +MassTransit includes transaction middleware to share a single committable transaction across any number consumers and any dependencies used by the those consumers. To use the middleware, it must be added to the bus or receive endpoint. + +```csharp +Bus.Factory.CreateUsingRabbitMq(cfg => +{ + cfg.ReceiveEndpoint("event_queue", e => + { + e.UseTransaction(x => + { + Timeout = TimeSpan.FromSeconds(90); + IsolationLevel = IsolationLevel.ReadCommitted; + }); + + e.Consumer(); + }) +}); +``` + +For each message, a new `CommittableTransaction` is created. This transaction can be passed to classes that support transactional operations, such as `DbContext`, `SqlCommand`, and `SqlConnection`. It can also be used to create any `TransactionScope` that may be required to support a synchronous operation. + +To use the transaction directly in a consumer, the transaction can be pulled from the `ConsumeContext`. + +```csharp +public class TransactionalConsumer : + IConsumer +{ + readonly SqlConnection _connection; // ctor injected + + public async Task Consume(ConsumeContext context) + { + var transactionContext = context.GetPayload(); + + _connection.EnlistTransaction(transactionContext.Transaction); + + using (SqlCommand command = new SqlCommand(sql, _connection)) + { + using (var reader = await command.ExecuteReaderAsync()) + { + } + } + + // the connection lifetime should be managed by a container + // or perhaps another more specific middleware component. + } +} +``` + +The connection (and by use of the connection, the command) are enlisted in the transaction. Once the method completes, and control is returned to the transaction middleware, if no exceptions are thrown the transaction is committed (which should complete the database operation). If an exception is thrown, the transaction is rolled back. + +While not shown here, a class that provides the connection, and enlists the connection upon creation, should be added to the container to ensure that the transaction is not enlisted twice (not sure that's a bad thing though, it should be ignored). Also, as long as only a single connection string is enlisted, the DTC should not get involved. Using the same transaction across multiple connection strings is a bad thing, as it will make the DTC come into play which slows the world down significantly. + +## Unit of Work (Buffer) + +Sometimes you just have to integrate with Database first systems, but still want some of the perks that message buses have to offer. A good example is an API with your typical HTTP Requests. You want to manipulate your DB, commit, and then upon success, release the messages to the broker. This is NOT a distributed transaction. There's still a risk that you could have the DB up and the broker down, causing the messages to never be sent to the broker. So you've been warned! + +There are two options to provide this buffer: + +- Transactional Enlistment Bus +- Transactional Bus + +## Transactional Enlistment Bus + +Transports don't typically support transactions, so sending messages during a transaction only to encounter an exception resulting in a transaction rollback may lead to messages that were sent without the transaction being committed. + +::alert{type="info"} +MassTransit has an in-memory outbox to deal with this problem, which can be used within a message consumer. It leverages the durable nature of a message transport to ensure that messages are ultimately sent. There is an extensive article and [video](https://youtu.be/P41IsVAc1nI) explaining the outbox behavior. This approach is preferred to performing transactional database writes outside of a consumer. +:: + +However, sometimes you are coming from the database first and can't get around it. For those situations, MassTransit has a _very simple_ transactional bus which enlists in the current transaction and defers outgoing messages until the transaction is being committed. There is still no rollback, once the messages are delivered to the broker, there is no pulling them back. + + +```csharp +services.AddMassTransit(x => +{ + x.UsingRabbitMq((context, cfg) => + { + }); + + x.AddTransactionalEnlistmentBus(); +}); +``` + +That is all that's needed. Now here's an example usage within an MVC Action. It's also important to use `TransactionScopeAsyncFlowOption.Enabled` as shown below. + +```csharp +public class MyController : ControllerBase +{ + private readonly IPublishEndpoint _publishEndpoint; + private readonly MyDbContext _dbContext; + + public ValuesController(IPublishEndpoint publishEndpoint, MyDbContext dbContext) + { + _publishEndpoint = publishEndpoint; + _dbContext = dbContext; + } + + [HttpPost] + public async Task Post([FromBody] string value) + { + using(var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + _dbContext.Posts.Add(new Post{...}); + await _dbContext.SaveChangesAsync(); + + await _publishEndpoint.Publish(new PostCreated{...}); + + transaction.Complete(); + } + + return Ok(); + } +} +``` + +Here's an example from within a Console App, with no Container: + +```csharp +public class Program +{ + public static async Task Main() + { + var bus = Bus.Factory.CreateUsingRabbitMq(sbc => + { + sbc.Host("rabbitmq://localhost"); + }); + + await bus.StartAsync(); // This is important! + + var transactionalBus = new TransactionalEnlistmentBus(bus); + + while(/*some condition*/) + { + using(var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) + { + // Do whatever business logic you need. + + await transactionalBus.Publish(new ReportQueued{...}); + await transactionalBus.Send(new CalculateReport{...}); + + // Maybe other business logic + + transaction.Complete(); + } + } + + Console.WriteLine("Press any key to exit"); + await Task.Run(() => Console.ReadKey()); + + await bus.StopAsync(); + } +} +``` + +## Transactional Bus + +Here we are again, another option for holding onto the messages and releasing them as close to the database transaction Commit as possible. We made this alternative because using TransactionScope from the previous section, could [in certain cases](https://github.com/MassTransit/MassTransit/issues/2075) still cause a 2 phase commit escalation (not to mention that TransactionScope doesn't truely have async support, so we make [concessions by calling TaskUtil.Await](https://github.com/MassTransit/MassTransit/blob/develop/src/MassTransit/Transactions/TransactionalBusEnlistment.cs#L83)). So to offer an alternative to these drawbacks, MassTransit provides an Outbox Bus. + +::alert{type="danger"} +Never use the TransactionalBus or TransactionalEnlistmentBus when writing consumers. These tools are very specific and should be used only in the scenarios described. +:: + +The examples will show it's usage in an ASP.NET MVC application, which is where we most commonly use Scoped lifetime for our DbContext and therefore we want the same for our TransactionalBus. You could possibly use it in some console applications, but ones WITHOUT a MT Consumer. Once you have consumers you will ALWAYS use `ConsumeContext` to interact with the bus, and never the `IBus`. + +First Register the outbox bus. + +```csharp +services.AddMassTransit(x => +{ + x.UsingRabbitMq((context, cfg) => + { + }); + + x.AddTransactionalBus(); +}); +``` + +Then use within your controller. + +```csharp +public class MyController : ControllerBase +{ + private readonly ITransactionalBus _transactionalBus; + private readonly MyDbContext _dbContext; + + public ValuesController(ITransactionalBus transactionalBus, MyDbContext dbContext) + { + _transactionalBus = transactionalBus; + _dbContext = dbContext; + } + + [HttpPost] + public async Task Post([FromBody] string value) + { + using(var transaction = await _dbContext.Database.BeginTransactionAsync()) + { + try + { + _dbContext.Posts.Add(new Post{...}); + await _dbContext.SaveChangesAsync(); + + await _transactionalBus.Publish(new PostCreated{...}); + + await transaction.CommitAsync(); + await _transactionalBus.Release(); // Immediately after CommitAsync + } + catch (Exception) + { + transaction.Rollback(); + } + + } + + return Ok(); + } +} +``` + +One option to remove some of the boilerplate of opening a transaction each Action that writes to the DB is to make a Filter. You can then include all of the boilerplate code to begin the transaction, and release the outbox. + +```csharp +public class DbContextTransactionFilter : TypeFilterAttribute +{ + public DbContextTransactionFilter() + : base(typeof(DbContextTransactionFilterImpl)) + { + } + + // This will be scoped per http request + private class DbContextTransactionFilterImpl : IAsyncActionFilter + { + private readonly MyDbContext _db; + private readonly ILogger _logger; + private readonly ITransactionalBus _transactionalBus; + + public DbContextTransactionFilterImpl( + MyDbContext db, + ILogger logger, + ITransactionalBus transactionalBus) + { + _db = db; + _logger = logger; + _transactionalBus = transactionalBus; + } + + public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) + { + using var transaction = await _db.Database.BeginTransactionAsync(); + + try + { + var actionExecuted = await next(); + if (actionExecuted.Exception != null && !actionExecuted.ExceptionHandled) + { + await transaction.RollbackAsync(); + } + else + { + await transaction.CommitAsync(); + await _transactionalBus.Release(); // Immediately after CommitAsync + } + } + catch (Exception) + { + try + { + await transaction.RollbackAsync(); + } + catch (Exception e) + { + // Swallow failed rollback + _logger.LogWarning(e, "Tried to rollback transaction but failed, swallow exception."); + } + + throw; + } + } + } +} +``` + +Now your Controller Action will look like: + +```csharp +public class MyController : ControllerBase +{ + private readonly ITransactionalBus _transactionalBus; + private readonly MyDbContext _dbContext; + + public ValuesController(ITransactionalBus transactionalBus, MyDbContext dbContext) + { + _transactionalBus = transactionalBus; + _dbContext = dbContext; + } + + [HttpPost] + [DbContextTransactionFilter] + public async Task Post([FromBody] string value) + { + _dbContext.Posts.Add(new Post{...}); + await _dbContext.SaveChangesAsync(); + + await _transactionalBus.Publish(new PostCreated{...}); + + return Ok(); + } +} +``` diff --git a/doc/content/3.documentation/5.configuration/2.persistence/_dir.yml b/doc/content/3.documentation/2.configuration/4.persistence/_dir.yml similarity index 100% rename from doc/content/3.documentation/5.configuration/2.persistence/_dir.yml rename to doc/content/3.documentation/2.configuration/4.persistence/_dir.yml diff --git a/doc/content/3.documentation/5.configuration/2.persistence/azure-cosmos.md b/doc/content/3.documentation/2.configuration/4.persistence/azure-cosmos.md similarity index 100% rename from doc/content/3.documentation/5.configuration/2.persistence/azure-cosmos.md rename to doc/content/3.documentation/2.configuration/4.persistence/azure-cosmos.md diff --git a/doc/content/3.documentation/5.configuration/2.persistence/azure-service-bus.md b/doc/content/3.documentation/2.configuration/4.persistence/azure-service-bus.md similarity index 100% rename from doc/content/3.documentation/5.configuration/2.persistence/azure-service-bus.md rename to doc/content/3.documentation/2.configuration/4.persistence/azure-service-bus.md diff --git a/doc/content/3.documentation/2.configuration/4.persistence/azure-table.md b/doc/content/3.documentation/2.configuration/4.persistence/azure-table.md new file mode 100755 index 00000000000..e9eb81c7547 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/4.persistence/azure-table.md @@ -0,0 +1,59 @@ +# Azure Table Storage + +[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.Azure.Cosmos.Table.svg "NuGet")](https://nuget.org/packages/MassTransit.Azure.Cosmos.Table/) + +Azure Tables are exposed in two ways in Azure - via Storage accounts & via the premium offering within Cosmos DB APIs. This persistence supports both implementations and behind the curtains uses the Azure.Data.Tables library for communication. + +::alert{type="success"} +Azure Tables currently only supports Optimistic Concurrency. Mass Transit manages the ETag property in Payload Context and uses this property for state machine updates. Concurrency errors can be spotted in logs via standard "Precondition Failed" errors from Table Storage. +:: + +::alert{type="warning"} +Be sure to set DateTime properties as nullable when updated later in the saga. Failure to do this can result in 400 bad requests from Table Storage. +:: + +```csharp +public class OrderState : + SagaStateMachineInstance +{ + public Guid CorrelationId { get; set; } + public string CurrentState { get; set; } + + public DateTime? OrderDate { get; set; } +} +``` + +## Container Integration + +To configure a Table as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. + +```csharp +TableClient cloudTable; +container.AddMassTransit(cfg => +{ + cfg.AddSagaStateMachine() + .AzureTableRepository(endpointUri, key, r => + { + cfg.ConnectionFactory(() => cloudTable); + }); +}); +``` + +The container extension will register the saga repository in the container. + +To configure the saga repository with a specific key formatter, use the code shown below with _KeyFormatter_ configuration extension. + +```csharp +TableClient cloudTable; +container.AddMassTransit(cfg => +{ + cfg.AddSagaStateMachine() + .AzureTableRepository(endpointUri, key, r => + { + cfg.ConnectionFactory(() => cloudTable); + cfg.KeyFormatter(() => new ConstRowSagaKeyFormatter(typeof(OrderState).Name))) + }); +}); +``` + +Unlike the default `ConstPartitionSagaKeyFormatter`, `ConstRowSagaKeyFormatter` in this example uses `PartitionKey` to store the correlationId which may benefit from [scale-out capability of Tables](https://docs.microsoft.com/en-us/rest/api/storageservices/designing-a-scalable-partitioning-strategy-for-azure-table-storage#scalability). diff --git a/doc/content/3.documentation/5.configuration/2.persistence/dapper.md b/doc/content/3.documentation/2.configuration/4.persistence/dapper.md similarity index 100% rename from doc/content/3.documentation/5.configuration/2.persistence/dapper.md rename to doc/content/3.documentation/2.configuration/4.persistence/dapper.md diff --git a/doc/content/3.documentation/5.configuration/2.persistence/dynamodb.md b/doc/content/3.documentation/2.configuration/4.persistence/dynamodb.md similarity index 100% rename from doc/content/3.documentation/5.configuration/2.persistence/dynamodb.md rename to doc/content/3.documentation/2.configuration/4.persistence/dynamodb.md diff --git a/doc/content/3.documentation/2.configuration/4.persistence/entity-framework.md b/doc/content/3.documentation/2.configuration/4.persistence/entity-framework.md new file mode 100755 index 00000000000..8969fdb7e64 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/4.persistence/entity-framework.md @@ -0,0 +1,237 @@ +# Entity Framework + +[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.EntityFrameworkCore.svg "NuGet")](https://nuget.org/packages/MassTransit.EntityFrameworkCore/) + +An example saga instance is shown below, which is orchestrated using an Automatonymous state machine. The _CorrelationId_ will be the primary key, and +_CurrentState_ will be used to store the current state of the saga instance. + +```csharp +public class OrderState : + SagaStateMachineInstance +{ + public Guid CorrelationId { get; set; } + public string CurrentState { get; set; } + + public DateTime? OrderDate { get; set; } + + // If using Optimistic concurrency, this property is required + public byte[] RowVersion { get; set; } +} +``` + +The instance properties are configured using a _SagaClassMap_. + +::alert{type="warning"} +The `SagaClassMap` has a default mapping for the `CorrelationId` as the primary key. If you create your own mapping, you must follow the same convention, or at +least make it a Clustered Index + Unique, otherwise you will likely experience deadlock exceptions and/or performance issues in high throughput scenarios. +:: + +```csharp +public class OrderStateMap : + SagaClassMap +{ + protected override void Configure(EntityTypeBuilder entity, ModelBuilder model) + { + entity.Property(x => x.CurrentState).HasMaxLength(64); + entity.Property(x => x.OrderDate); + + // If using Optimistic concurrency, otherwise remove this property + entity.Property(x => x.RowVersion).IsRowVersion(); + } +} +``` + +Include the instance map in a _DbContext_ class that will be used by the saga repository. + +```csharp +public class OrderStateDbContext : + SagaDbContext +{ + public OrderStateDbContext(DbContextOptions options) + : base(options) + { + } + + protected override IEnumerable Configurations + { + get { yield return new OrderStateMap(); } + } +} +``` + +## Configuration + +Once the class map and associated _DbContext_ class have been created, the saga repository can be configured with the saga registration, which is done using the +configuration method passed to _AddMassTransit_. The following example shows how the repository is configured for using Microsoft Dependency Injection +Extensions, which are used by default with Entity Framework Core. + +```csharp +services.AddMassTransit(cfg => +{ + cfg.AddSagaStateMachine() + .EntityFrameworkRepository(r => + { + r.ConcurrencyMode = ConcurrencyMode.Pessimistic; // or use Optimistic, which requires RowVersion + + r.AddDbContext((provider,builder) => + { + builder.UseSqlServer(connectionString, m => + { + m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); + m.MigrationsHistoryTable($"__{nameof(OrderStateDbContext)}"); + }); + }); + }); +}); +``` + +### PostgreSQL + +By default, MassTransit uses Microsoft SQL Server locking statements to handle concurrency. It is important, however, if using PostgreSQL, MySQL or Sqlite that +you specify this as part of the setup of the DbContextOptionsBuilder options. + +The following shows an example for PostgreSQL + +```csharp +services.AddMassTransit(cfg => +{ + cfg.AddSagaStateMachine() + .EntityFrameworkRepository(r => + { + r.ConcurrencyMode = ConcurrencyMode.Optimistic; // or use Pessimistic, which does not require RowVersion + + r.AddDbContext((provider,builder) => + { + builder.UseNpgsql(connectionString, m => + { + m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); + m.MigrationsHistoryTable($"__{nameof(OrderStateDbContext)}"); + }); + }); + + //This line is added to enable PostgreSQL features + r.UsePostgres(); + }); +}); +``` + +Further, in PostgreSQL, the RowVersion column [is a hidden system column](https://www.postgresql.org/docs/current/ddl-system-columns.html) which already exists +on every table, called ```xmin``` with a type ```xid```. For this reason, we do not need to create a new RowVersion column when using "Optimistic" mode. +Instead, we simply bind our RowVersion property to a ```uint``` type, and apply the correct mappings in our ```OrderStateMap``` class. + +The example below shows the original OrderState model, using a PostgreSQL RowVersion + +```csharp +public class OrderState : + SagaStateMachineInstance +{ + public Guid CorrelationId { get; set; } + public string CurrentState { get; set; } + + public DateTime? OrderDate { get; set; } + + // If using Optimistic concurrency, this property is required + public uint RowVersion { get; set; } +} +``` + +The state mapping must also be modified to use the ```xmin``` column of PostgreSQL + +```csharp +public class OrderStateMap : + SagaClassMap +{ + protected override void Configure(EntityTypeBuilder entity, ModelBuilder model) + { + entity.Property(x => x.CurrentState).HasMaxLength(64); + entity.Property(x => x.OrderDate); + + // If using Optimistic concurrency, otherwise remove this property + entity.Property(x => x.RowVersion) + .HasColumnName("xmin") + .HasColumnType("xid") + .IsRowVersion() + } +} +``` + +### Job Saga + +A single `DbContext` can be registered in the container which can then be used to configure sagas that are mapped by the `DbContext`. For +example, [Job Consumers](/documentation/patterns/job-consumers) needs three saga repositories, and the Entity Framework Core package includes the +`JobServiceSagaDbContext` which can be configured using the `AddSagaRepository` method as shown below. + +```csharp +services.AddDbContext(builder => + builder.UseNpgsql(Configuration.GetConnectionString("JobService"), m => + { + m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); + m.MigrationsHistoryTable($"__{nameof(JobServiceSagaDbContext)}"); + })); + +services.AddMassTransit(x => +{ + x.AddSagaRepository() + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.UsePostgres(); + }); + x.AddSagaRepository() + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.UsePostgres(); + }); + x.AddSagaRepository() + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.UsePostgres(); + }); + + // other configuration, such as consumers, etc. +}); +``` + +The above code using the standard Entity Framework configuration extensions to add the _DbContext_ to the container, using PostgreSQL. Because the job service +state machine receive endpoints are configured by _ConfigureJobServiceEndpoints_, the saga repositories must be configured separately. The _AddSagaRepository_ +method is used to register a repository for a saga that has already been added, and uses the same extension methods as the _AddSaga_ and _AddSagaStateMachine_ +methods. + +Once configured, the job service sagas can be configured as shown below. + +```csharp +x.AddJobSagaStateMachines() + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.UsePostgres(); + }); +``` + +:sample{sample="job-consumer"} + +The sample above is a working example of this configuration style. + +## Multiple DbContexts + +Multiple `DbContext` can be registered in the container which can then be used to configure sagas that are mapped by the `DbContext` and injected into other +components. Calling the `AddDbContext` extension method will register a scoped `DbContext` by default. For simple scenarios where there is a single `DbContext` +this will work. However, in scenarios where there is at least one other `DbContext` the dotnet command that generates Entity Framework migrations will not work. +To resolve this issue, you'll need to perform the following steps: + +1. Make sure that all `DbContext` has a constructor that takes `DbContextOptions` instead of `DbContextOptions`. + +2. Run the Entity Framework Core command to create your migrations as shown below. + + ```bash + dotnet ef migrations add InitialCreate -c JobServiceSagaDbContext + ``` + +3. Run the Entity Framework Core command to sync with the database as shown below. + + ```bash + dotnet ef database update -c JobServiceSagaDbContext + ``` + diff --git a/doc/content/3.documentation/2.configuration/4.persistence/marten.md b/doc/content/3.documentation/2.configuration/4.persistence/marten.md new file mode 100755 index 00000000000..61e17085e02 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/4.persistence/marten.md @@ -0,0 +1,138 @@ +# Marten + +[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.Marten.svg "NuGet")](https://nuget.org/packages/MassTransit.Marten/) + +[Marten][2] is an open source library that provides provides .NET developers with the ability to easily use the proven PostgreSQL database engine and its fantastic [JSON support][1] as a fully fledged document database. To use Marten and PostgreSQL as saga persistence, you need to install `MassTransit.Marten` NuGet package and add some code. + +> MassTransit will automatically configure the _CorrelationId_ property so that Marten will use that property as the primary key. No attribute is necessary. + +```csharp +public class OrderState : + SagaStateMachineInstance +{ + public Guid CorrelationId { get; set; } + public string CurrentState { get; set; } + + public DateTime? OrderDate { get; set; } +} +``` + +## Configuration + +To configure Marten and use it as a saga repository with MassTransit, use the `MartenRepository` method when adding the saga. + +```csharp +services.AddMarten(options => +{ + const string connectionString = "host=localhost;port=5432;database=orders;username=web;password=webpw;"; + + options.Connection(connectionString); +}); + +services.AddMassTransit(x => +{ + x.AddSagaStateMachine() + .MartenRepository(); +}); +``` + +### Repository Provider + +When adding sagas using any of the `AddSagas` or `AddSagaStateMachines` methods, the Marten saga repository provider can be used to automatically configure Marten as the saga repository. + +```csharp +services.AddMarten(options => +{ + const string connectionString = "host=localhost;port=5432;database=orders;username=web;password=webpw;"; + + options.Connection(connectionString); +}); + +services.AddMassTransit(x => +{ + x.SetMartenSagaRepositoryProvider(); + + var entryAssembly = System.Reflection.Assembly.GetEntryAssembly(); + + x.AddSagaStateMachines(entryAssembly); +}); +``` + +To configure the saga repository for a specific saga type, use the `AddSagaRepository` method and specify the appropriate saga repository. + +```csharp +services.AddMassTransit(x => +{ + x.SetMartenSagaRepositoryProvider(); + + var entryAssembly = System.Reflection.Assembly.GetEntryAssembly(); + + x.AddSagaStateMachines(entryAssembly); + + x.AddSagaRepository() + .MartenRepository(); +}); +``` + +## Optimistic Concurrency + +To use Marten's built-in optimistic concurrency, which uses an *eTag*-like version metadata field, an additional schema configuration may be specified. + +> This does **not** require any additional fields in the saga class. + +```csharp +services.AddMassTransit(x => +{ + x.AddSagaStateMachine() + .MartenRepository(r => r.UseOptimisticConcurrency(true)); +}); +``` + +Alternatively, you can add the `UseOptimisticConcurrency` attribute to the saga class. + +```csharp +[UseOptimisticConcurrency] +public class OrderState : + SagaStateMachineInstance +{ + public Guid CorrelationId { get; set; } + // ... +} +``` + +## Index Creation + +Marten can create indices for properties, which greatly increases query performance. If your saga is correlating events using other saga properties, index creation is recommended. For example, if an _OrderNumber_ property was added to the _OrderState_ class, it could be indexed by configuring it in the repository. + +```csharp +public class OrderState : + SagaStateMachineInstance +{ + public Guid CorrelationId { get; set; } + public string CurrentState { get; set; } + + public string OrderNumber { get; set; } + + public DateTime? OrderDate { get; set; } +} +``` + +```csharp +services.AddMarten(options => +{ + const string connectionString = "host=localhost;port=5432;database=orders;username=web;password=webpw;"; + + options.Connection(connectionString); +}); + +services.AddMassTransit(x => +{ + x.AddSagaStateMachine() + .MartenRepository(r => r.Index(x => x.OrderNumber)); +}); +``` + +Details on how Marten creates indices is available in the [Indexing](https://martendb.io/documents/indexing/) documentation. + +[1]: https://www.postgresql.org/docs/9.5/static/functions-json.html +[2]: http://jasperfx.github.io/marten/ diff --git a/doc/content/3.documentation/5.configuration/2.persistence/mongodb.md b/doc/content/3.documentation/2.configuration/4.persistence/mongodb.md similarity index 100% rename from doc/content/3.documentation/5.configuration/2.persistence/mongodb.md rename to doc/content/3.documentation/2.configuration/4.persistence/mongodb.md diff --git a/doc/content/3.documentation/5.configuration/2.persistence/nhibernate.md b/doc/content/3.documentation/2.configuration/4.persistence/nhibernate.md similarity index 100% rename from doc/content/3.documentation/5.configuration/2.persistence/nhibernate.md rename to doc/content/3.documentation/2.configuration/4.persistence/nhibernate.md diff --git a/doc/content/3.documentation/2.configuration/4.persistence/redis.md b/doc/content/3.documentation/2.configuration/4.persistence/redis.md new file mode 100755 index 00000000000..89c87c2a026 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/4.persistence/redis.md @@ -0,0 +1,79 @@ +# Redis + +[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.Redis.svg "NuGet")](https://nuget.org/packages/MassTransit.Redis/) + +Redis is a very popular key-value store, which is known for being very fast. To support Redis, MassTransit uses the `StackExchange.Redis` library. + +::alert{type="warning"} +Redis only supports event correlation by _CorrelationId_, it does not support queries. If a saga uses expression-based correlation, a _NotImplementedByDesignException_ will be thrown. +:: + +::alert{type="info"} +The Redis package also supports the recent fork of Redis called [Valkey](https://github.com/valkey-io/valkey) +:: + +Storing a saga in Redis requires an additional interface, _ISagaVersion_, which has a _Version_ property used for optimistic concurrency. An example saga is shown below. + +```csharp +public class OrderState : + SagaStateMachineInstance, + ISagaVersion +{ + public Guid CorrelationId { get; set; } + public string CurrentState { get; set; } + + public DateTime? OrderDate { get; set; } + + public int Version { get; set; } +} +``` + +## Configuration + +To configure Redis as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. This will configure Redis to connect to the local Redis instance on the default port using Optimistic concurrency. This will also store the _ConnectionMultiplexer_ in the container as a single instance, which will be disposed by the container. + +```csharp +services.AddMassTransit(x => +{ + const string configurationString = "127.0.0.1"; + + x.AddSagaStateMachine() + .RedisRepository(configurationString); +}); +``` + +The example below includes all the configuration options, in cases where additional settings are required. + +```csharp +services.AddMassTransit(x => +{ + const string configurationString = "127.0.0.1"; + + x.AddSagaStateMachine() + .RedisRepository(r => + { + r.DatabaseConfiguration(configurationString); + + // Default is Optimistic + r.ConcurrencyMode = ConcurrencyMode.Pessimistic; + + // Optional, prefix each saga instance key with the string specified + // resulting dev:c6cfd285-80b2-4c12-bcd3-56a00d994736 + r.KeyPrefix = "dev"; + + // Optional, to customize the lock key + r.LockSuffix = "-lockage"; + + // Optional, the default is 30 seconds + r.LockTimeout = TimeSpan.FromSeconds(90); + });; +}); +``` + +## Concurrency + +Redis supports both Optimistic (default) and Pessimistic concurrency. + +In optimistic mode, the saga instance is not locked before reading, which can ultimately lead to a write conflict if the instance was updated by another message. The _Version_ property is used to compare that the update would not overwrite a previous update. It is recommended that a retry policy is configured (using `UseMessageRetry`, see the [exceptions](/documentation/concepts/exceptions#retry) documentation). + +Pessimistic concurrency uses the Redis lock mechanism. During the message processing, the repository will lock the saga instance before reading it, so that any concurrent attempts to lock the same instance will wait until the current message has completed or the lock timeout expires. diff --git a/doc/content/3.documentation/2.configuration/5.scheduling.md b/doc/content/3.documentation/2.configuration/5.scheduling.md new file mode 100644 index 00000000000..d13a97cc450 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/5.scheduling.md @@ -0,0 +1,358 @@ +--- +navigation.title: Scheduling +--- + +# Scheduling Configuration + +Time is important, particularly in distributed applications. Sophisticated systems need to schedule things, and MassTransit has extensive scheduling support. + +MassTransit supports two different methods of message scheduling: + +1. Scheduler-based, using either Quartz.NET or Hangfire, where the scheduler runs in a service and schedules messages using a queue. +2. Transport-based, using the transports built-in message scheduling/delay capabilities. In some cases, such as RabbitMQ, this requires an additional plug-in to be installed and configured. + +> Recurring schedules are only supported by Quartz.NET or Hangfire. + +## Configuration + +Depending upon the scheduling method used, the bus must be configured to use the appropriate scheduler. + +::code-group + + ::code-block{label="Quartz/Hangfire" quartz} + ```csharp + services.AddMassTransit(x => + { + Uri schedulerEndpoint = new Uri("queue:scheduler"); + + x.AddMessageScheduler(schedulerEndpoint); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseMessageScheduler(schedulerEndpoint); + + cfg.ConfigureEndpoints(context); + }); + }); + ``` + :: + + ::code-block{label="RabbitMQ" rabbitmq} + ```csharp + services.AddMassTransit(x => + { + x.AddDelayedMessageScheduler(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }); + ``` + :: + + ::code-block{label="Azure Service Bus" azuresb} + ```csharp + services.AddMassTransit(x => + { + x.AddServiceBusMessageScheduler(); + + x.UsingAzureServiceBus((context, cfg) => + { + cfg.UseServiceBusMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }); + ``` + :: + + ::code-block{label="Amazon SQS" sqs} + ```csharp + services.AddMassTransit(x => + { + x.AddDelayedMessageScheduler(); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }); + ``` + :: + + ::code-block{label="SQL" sql} + ```csharp + services.AddMassTransit(x => + { + x.AddSqlMessageScheduler(); + + x.UsingPostgres((context, cfg) => + { + cfg.UseSqlMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }); + ``` + :: + + ::code-block{label="ActiveMQ" activemq} + ```csharp + services.AddMassTransit(x => + { + x.AddDelayedMessageScheduler(); + + x.UsingActiveMq((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }); + ``` + :: + +:: + +::callout{type="info"} +#summary +RabbitMQ + +#content +When using RabbitMQ, MassTransit uses the Delayed Exchange plug-in to schedule messages. + +The plug-in can be downloaded from [GitHub][1]. A [Docker Image](https://hub.docker.com/r/masstransit/rabbitmq) with RabbitMQ ready to run, including the delayed exchange plug-in is also available. +:: + +::callout{type="info"} +#summary +Azure Service Bus + +#content +Azure Service Bus supports message cancellation, unlike the other transports. +:: + +::callout{type="info"} +#summary +Amazon SQS + +#content +Scheduled messages cannot be canceled when using the Amazon SQS message scheduler +:: + +::callout{type="info"} +#summary +SQL Transport + +#content +The SQL transport supports scheduling and canceling scheduled messages, so be sure to explicitly configured the SQL message scheduler. While the delayed +message scheduler can also be used, the SQL message scheduler is a better choice. +:: + +## Usage + +To use the message scheduler (outside of a consumer), resolve _IMessageScheduler_ from the container. + +### Consumer + +To schedule messages from a consumer, use any of the _ConsumeContext_ extension methods, such as _ScheduleSend_, to schedule messages. + +```csharp +services.AddMassTransit(x => +{ + Uri schedulerEndpoint = new Uri("queue:scheduler"); + + x.AddMessageScheduler(schedulerEndpoint); + + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseMessageScheduler(schedulerEndpoint); + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +```csharp +public class ScheduleNotificationConsumer : + IConsumer +{ + public async Task Consume(ConsumeContext context) + { + Uri notificationService = new Uri("queue:notification-service"); + + await context.ScheduleSend(notificationService, + context.Message.DeliveryTime, new() + { + EmailAddress = context.Message.EmailAddress, + Body = context.Message.Body + }); + } +} +``` + +```csharp +public record ScheduleNotification +{ + public DateTime DeliveryTime { get; init; } + public string EmailAddress { get; init; } + public string Body { get; init; } +} +``` + +```csharp +public record SendNotification +{ + public string EmailAddress { get; init; } + public string Body { get; init; } +} +``` + +The message scheduler, specified during bus configuration, will be used to schedule the message. + +### Scope + +To schedule messages from a bus, use _IMessageScheduler_ from the container (or create a new one using the bus and appropriate scheduler). + +```csharp +services.AddMassTransit(x => +{ + Uri schedulerEndpoint = new Uri("queue:scheduler"); + + x.AddMessageScheduler(schedulerEndpoint); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseMessageScheduler(schedulerEndpoint); + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +```csharp +await using var scope = provider.CreateAsyncScope(); + +var scheduler = scope.ServiceProvider.GetRequiredService(); + +await scheduler.SchedulePublish( + DateTime.UtcNow + TimeSpan.FromSeconds(30), new() + { + EmailAddress = "frank@nul.org", + Body = "Thank you for signing up for our awesome newsletter!" + }); +``` + +```csharp +public record SendNotification +{ + public string EmailAddress { get; init; } + public string Body { get; init; } +} +``` + +### Recurring Messages + +Using Quartz.NET or Hangfire, you can schedule a message to be sent or published periodically. This functionality requires some knowledge of cron expressions. + +A recurring message should have a unique _ScheduleId_ along with an optional _ScheduleGroup_. + +```csharp +public class PollExternalSystemSchedule : + DefaultRecurringSchedule +{ + public PollExternalSystemSchedule() + { + ScheduleId = "PollExternalSystem"; + CronExpression = "0 0/1 * 1/1 * ? *"; // this means every minute + } +} + +public record PollExternalSystem; +``` + +To schedule a recurring message, using the `IRecurringMessageScheduler` interface, which can be resolved from the container (_IServiceProvider_). This +interface is scoped, so it must be called from a valid container scope. + +If using in a consumer, add _IRecurringMessageScheduler_ as a constructor dependency. + +::alert{type="info"} +If using from a hosted service, you will need to create a scope using `IServiceScopeFactory` (injected via the constructor) and calling `CreateAsyncScope`. +:: + +```csharp +var scheduler = scope.ServiceProvider.GetService(); + +var message = await scheduler.ScheduleRecurringSend( + InputQueueAddress, new PollExternalSystemSchedule(), new PollExternalSystem()); +``` + +When you stop your service or just have any other need to tell Quartz service to stop sending you +these recurring messages, you can use the return value of `ScheduleRecurringSend` to cancel the recurring schedule. + +```csharp +await scheduler.CancelScheduledRecurringMessage("PollExternalSystem", null); +``` + +## Quartz.NET + +To host Quartz.NET with MassTransit, configure Quartz and MassTransit as shown below. + +```csharp +services.AddQuartz(q => +{ + q.UseMicrosoftDependencyInjectionJobFactory(); +}); +``` + +```csharp +services.AddMassTransit(x => +{ + x.AddPublishMessageScheduler(); + + x.AddQuartzConsumers(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UsePublishMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +## Hangfire + +```csharp +services.AddHangfire(h => +{ + h.UseRecommendedSerializerSettings(); + h.UseMemoryStorage(); +}); +``` + +```csharp +services.AddMassTransit(x => +{ + x.AddPublishMessageScheduler(); + + x.AddHangfireConsumers(); + + x.UsingInMemory((context, cfg) => + { + cfg.UsePublishMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); +}) +``` + + +[1]: https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/ diff --git a/doc/content/3.documentation/5.configuration/5.topology/0.index.md b/doc/content/3.documentation/2.configuration/6.topology/0.index.md similarity index 100% rename from doc/content/3.documentation/5.configuration/5.topology/0.index.md rename to doc/content/3.documentation/2.configuration/6.topology/0.index.md diff --git a/doc/content/3.documentation/5.configuration/5.topology/1.message.md b/doc/content/3.documentation/2.configuration/6.topology/1.message.md similarity index 87% rename from doc/content/3.documentation/5.configuration/5.topology/1.message.md rename to doc/content/3.documentation/2.configuration/6.topology/1.message.md index b42c5f649e1..c6136ac2616 100644 --- a/doc/content/3.documentation/5.configuration/5.topology/1.message.md +++ b/doc/content/3.documentation/2.configuration/6.topology/1.message.md @@ -123,11 +123,28 @@ public record ReformatHardDrive : } ``` -To avoid using the property, the publish topology can be configured along with the bus: +As an alternative to using the `ExcludeFromTopology` attribute, configure the publish topology during bus configuration. ```csharp -...UsingRabbitMq((context,cfg) => +x.UsingRabbitMq((context,cfg) => { cfg.Publish(p => p.Exclude = true); }); + +``` + +### ExcludeFromImplementedTypes + +_ExcludeFromImplementedTypes_ is an optional attribute that may be specified on a base message type to prevent scope filters being created for the message type. + +```csharp +[ExcludeFromImplementedTypes] +public interface ICommand +{ +} + +public record ReformatHardDrive : + ICommand +{ +} ``` diff --git a/doc/content/3.documentation/5.configuration/5.topology/_dir.yaml b/doc/content/3.documentation/2.configuration/6.topology/_dir.yaml similarity index 100% rename from doc/content/3.documentation/5.configuration/5.topology/_dir.yaml rename to doc/content/3.documentation/2.configuration/6.topology/_dir.yaml diff --git a/doc/content/3.documentation/5.configuration/5.topology/conventions.md b/doc/content/3.documentation/2.configuration/6.topology/conventions.md similarity index 100% rename from doc/content/3.documentation/5.configuration/5.topology/conventions.md rename to doc/content/3.documentation/2.configuration/6.topology/conventions.md diff --git a/doc/content/3.documentation/5.configuration/5.topology/deploy.md b/doc/content/3.documentation/2.configuration/6.topology/deploy.md similarity index 100% rename from doc/content/3.documentation/5.configuration/5.topology/deploy.md rename to doc/content/3.documentation/2.configuration/6.topology/deploy.md diff --git a/doc/content/3.documentation/2.configuration/7.routing-slips/0.overview.md b/doc/content/3.documentation/2.configuration/7.routing-slips/0.overview.md new file mode 100644 index 00000000000..96c676c2076 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/7.routing-slips/0.overview.md @@ -0,0 +1,26 @@ +--- +navigation.title: Overview +--- + +# Routing Slips + +To understand routing slips and how to create one, refer to the [Routing Slip](/documentation/concepts/routing-slips) section. + +## Configuring Routing Slips + +```csharp +services.AddMassTransit(cfg => +{ + // Execute Only Activities + cfg.AddExecuteActivity(); + + // Activities that have an execute and a compensation + cfg.AddActivity(); + + cfg.Using[Transport]((context, transport) => + { + // Register the Activties with the Transport + transport.ConfigureEndpoints(context); + }); +}) +``` diff --git a/doc/content/3.documentation/2.configuration/7.routing-slips/1.registration.md b/doc/content/3.documentation/2.configuration/7.routing-slips/1.registration.md new file mode 100644 index 00000000000..78d9b33d06a --- /dev/null +++ b/doc/content/3.documentation/2.configuration/7.routing-slips/1.registration.md @@ -0,0 +1,41 @@ +--- +navigation.title: Registration +--- + +# Registration Detail + +Activities are added inside the `AddMassTransit` configuration using any of the following methods. + +```csharp +services.AddMassTransit(cfg => +{ + cfg.AddActivity(); + cfg.AddActivity(typeof(MyActivityDefinition)); + cfg.AddActivity(typeof(MyActivityDefinition)); + + // Execution Only Activities + cfg.AddExecuteActivity(); + cfg.AddExecuteActivity(typeof(MyExecuteActivityDefinition)); +}); +``` + +## Execution Configuration + + +| Setting | Description | +|-------------------------|--------------| +| ConcurrentMessage Limit | the number of concurrent messages this activity will process at once | +| Arguments | add middleware | +| Activity Arguments | add middleware | +| Routing Slip | add middleware | + +These can be set either inline using the `AddActivity` methods, or by implementing the `IActivityDefinition` interface. + +## Compensate Configuration + +| Setting | Description | +|-------------------------|--------------| +| ConcurrentMessage Limit | the number of concurrent messages this activity will process at once | +| Log | add middleware | +| Activity Log | add middleware | +| Routing Slip | add middleware | diff --git a/doc/content/3.documentation/2.configuration/7.routing-slips/2.routing_slip.md b/doc/content/3.documentation/2.configuration/7.routing-slips/2.routing_slip.md new file mode 100644 index 00000000000..0af4b8e4e6b --- /dev/null +++ b/doc/content/3.documentation/2.configuration/7.routing-slips/2.routing_slip.md @@ -0,0 +1,81 @@ +--- +navigation.title: Itinerary +--- + +# Building an Itinerary + +On top of creating the various activities, you will also need to build out an itinerary via the `RoutingSlipBuilder`. + +| Property | Notes | +|--|--| +| Tracking Number | A unique identifier to track this specific route | + +## Adding an Activity + +```csharp +var builder = new RoutingSlipBuilder(NewId.NextGuid()); +builder.AddActivity("Name", new Uri("address"), new { + // args +}); +var slip = builder.Build(); +``` + +| Parameter | Notes | +|--|--| +| name | This is a human readable label for the activity | +| address | The address of the endpoint where the activity is listening. [Short Addresses](/documentation/concepts/producers#short-addresses) are also supported. | +| args | values to be bound to the Args type defined in this activity, not shared | + + + + +## Adding a Variable + +```csharp +var builder = new RoutingSlipBuilder(NewId.NextGuid()); +builder.AddVariable("Key", "Value") +var slip = builder.Build(); +``` + +These values are shared across activities. This is a great way to pass data from one activity to the next. + +| Parameter | Notes | +|--|--| +| key | The key to retrieve it from later | +| value | The value | + +## Binding Data to Args + +When you build a subscription, the `Arg` instance will be created by binding to data in both the variables and the args payload. Any property on your arg class will first get data from the Variables section, and then overridden by the Arg payload. + +### Example + +```csharp +public record MyCustomArgs(string Name, int Quantity, string Sku); +``` + + +| Property | Variable Value | Argument Value | Resolved Value | +|--|--|--|--| +| Name | "Bob" | undefined | "Bob" | +| Quantity | undefined | 123 | 123 | +| Sku | "abc" | "DEF" | "DEF" | + +## Adding a Subscription + +By default routing slip events are published. This can cause issues in mature systems. It is recommended +to add a subscription which will configure the events to be sent to the configured endpoint. + +```csharp +var builder = new RoutingSlipBuilder(NewId.NextGuid()); +builder.AddSubscription(new Uri("address"), RoutingSlipEvents.All) +var slip = builder.Build(); +``` + +| Parameter | Notes | +|--|--| +| address | Where should routing slip events be sent | +| events | `RoutingSlipEvents` is a flag enum for selecting desired events | + + + diff --git a/doc/content/3.documentation/2.configuration/7.routing-slips/_dir.yml b/doc/content/3.documentation/2.configuration/7.routing-slips/_dir.yml new file mode 100644 index 00000000000..1f8f1996a25 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/7.routing-slips/_dir.yml @@ -0,0 +1,4 @@ +title: Routing Slips +description: | + hey how cool is this +# icon: icon-park-outline:checklist diff --git a/doc/content/3.documentation/5.configuration/_dir.yml b/doc/content/3.documentation/2.configuration/_dir.yml similarity index 100% rename from doc/content/3.documentation/5.configuration/_dir.yml rename to doc/content/3.documentation/2.configuration/_dir.yml diff --git a/doc/content/3.documentation/5.configuration/integrations/1.signalr.md b/doc/content/3.documentation/2.configuration/integrations/1.signalr.md similarity index 100% rename from doc/content/3.documentation/5.configuration/integrations/1.signalr.md rename to doc/content/3.documentation/2.configuration/integrations/1.signalr.md diff --git a/doc/content/3.documentation/5.configuration/integrations/_dir.yaml b/doc/content/3.documentation/2.configuration/integrations/_dir.yaml similarity index 100% rename from doc/content/3.documentation/5.configuration/integrations/_dir.yaml rename to doc/content/3.documentation/2.configuration/integrations/_dir.yaml diff --git a/doc/content/3.documentation/2.configuration/integrations/external-systems.md b/doc/content/3.documentation/2.configuration/integrations/external-systems.md new file mode 100644 index 00000000000..06ad4a24fd5 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/integrations/external-systems.md @@ -0,0 +1,48 @@ +# Integrating with other systems + +Often people want to consume messages off of their broker that are coming from other +non-MassTransit systems. The below video reviews how to do this. + +:video-player{src="https://www.youtube.com/watch?v=xOxSLNeN5CU"} + +## Key Points + +::alert{type="info"} +You must specifically configure the receive endpoint +:: + +Exclude this endpoint from the topology mapping. Since this endpoint is defined +by a different system, we don't need MassTransit's help here. + +```csharp +endpointConfigurator.ConfigureConsumeTopology = false; +``` + +Add the `RawJsonSerializer` to the list of serializers supported. The serializer +specifically looks for `application/json` versus the standard content-type used +by MassTransit which is `application/vnd.masstransit+json`. + +```csharp +endpointConfigurator.UseRawJsonDeserializer(); +``` + +If you want to have MT bind the endpoint to the correct topic you can do the following: + +```csharp +if(endpointConfigurator is IRabbitMqReceiveEndpointConfigurator rabbit) +{ + rabbit.Bind("your-target-topic"); +} +``` + +## If the existing system isn't setting the content type + +If the messages doesn't have a content-type set, you can tell MassTransit +what the default content type should be. Since, as the app developer, you know +the content is `application/json` you can set that manually. Then when a +message comes in without a header, MT will select the correct one. + +```csharp +endpointConfigurator.DefaultContentType = new ContentType("application/json"); +endpointConfigurator.UseRawJsonDeserializer(); +``` diff --git a/doc/content/3.documentation/2.configuration/integrations/logging.md b/doc/content/3.documentation/2.configuration/integrations/logging.md new file mode 100644 index 00000000000..7ba824202ff --- /dev/null +++ b/doc/content/3.documentation/2.configuration/integrations/logging.md @@ -0,0 +1,59 @@ +# Logging + +The MassTransit framework has fully adopted the [`Microsoft.Extensions.Logging`](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0) framework. +So, it will use whatever logging configuration is already in your container. + +## Configuration + +By integrating with `Microsoft.Extensions.Logging` the basic configuration is no configuration. :tada: +When you run a project using the HostBuilder features of .Net you will get a basic logging experience right +out of the box. + +## Serilog + +At MassTransit, we are big fans of [Serilog](https://serilog.net/) and use this default configuration as a starting point in +most projects. + +```sh +dotnet add package Serilog.Extensions.Hosting +dotnet add package Serilog +dotnet add package Serilog.Sinks.Console +``` + +```csharp +public static IHostBuilder CreateHostBuilder(string[] args) +{ + return Host.CreateDefaultBuilder(args) + .UseSerilog((host, log) => + { + if (host.HostingEnvironment.IsProduction()) + log.MinimumLevel.Information(); + else + log.MinimumLevel.Debug(); + + log.MinimumLevel.Override("Microsoft", LogEventLevel.Warning); + log.MinimumLevel.Override("Quartz", LogEventLevel.Information); + log.WriteTo.Console(); + }) + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + }); +} +``` + +## Other Loggers + +For applications that are not using MassTransit's container-based configuration (`AddMassTransit`) or for those with non-standard log configurations, it's possible to explicitly configure MassTransit so that it uses a provided `ILoggerFactory`. + + +```csharp +ILoggerFactory loggerFactory = ; + +var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => +{ + LogContext.ConfigureCurrentLogContext(loggerFactory); +}); +``` + +This _must_ be specified within the bus configuration so that the provided `ILoggerFactory` is used. diff --git a/doc/content/3.documentation/5.configuration/integrations/nsb.md b/doc/content/3.documentation/2.configuration/integrations/nsb.md similarity index 92% rename from doc/content/3.documentation/5.configuration/integrations/nsb.md rename to doc/content/3.documentation/2.configuration/integrations/nsb.md index a98bc83b1ca..6fff07d8cb6 100644 --- a/doc/content/3.documentation/5.configuration/integrations/nsb.md +++ b/doc/content/3.documentation/2.configuration/integrations/nsb.md @@ -9,7 +9,7 @@ ### Interoperability -- [MassTransit.Interop.NServiceBus](https://nuget.org/packages/MassTransit.Interop.NServiceBus/) +This sample shows how to use the [MassTransit.Interop.NServiceBus](https://nuget.org/packages/MassTransit.Interop.NServiceBus/) package to send messages to an NServiceBus system. To see how to consume MassTransit messages by an NServiceBus system without using the interop package, see the [NServiceBus MassTransit Ingest Behavior docs](https://docs.particular.net/samples/pipeline/masstransit-messages/#nservicebus-subscriber-masstransit-ingest-behavior). ::alert{type="danger"} This package was built using a black box, clean room approach based on observed message formats within the message broker. As such, there may be edge cases and situations which are not handled by this package. Extensive testing is recommended to ensure all message properties are being properly interpreted. diff --git a/doc/content/3.documentation/5.configuration/integrations/roslyn-analyzer.md b/doc/content/3.documentation/2.configuration/integrations/roslyn-analyzer.md similarity index 100% rename from doc/content/3.documentation/5.configuration/integrations/roslyn-analyzer.md rename to doc/content/3.documentation/2.configuration/integrations/roslyn-analyzer.md diff --git a/doc/content/3.documentation/2.configuration/multibus.md b/doc/content/3.documentation/2.configuration/multibus.md new file mode 100644 index 00000000000..e204355ad74 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/multibus.md @@ -0,0 +1,205 @@ +# MultiBus + +_pronounced mool-tee-buss_ + +MassTransit is designed so that most applications only need a single bus, and that is the recommended approach. Using a single bus, with however many receive endpoints are needed, minimizes complexity and ensures efficient broker resource utilization. Consistent with this guidance, container configuration using the `AddMassTransit` method registers the appropriate types so that they are available to other components, as well as consumers, sagas, and activities. + +However, with broader use of cloud-based platforms comes a greater variety of messaging transports, not to mention HTTP as a transfer protocol. As application sophistication increases, connecting to multiple message transports and/or brokers is becoming more common. Therefore, rather than force developers to create their own solutions, MassTransit has the ability to configure additional bus instances within specific dependency injection containers. + +> And by specific, right now it is very specific: Microsoft.Extensions.DependencyInjection. Though technically, any container that supports `IServiceCollection` for configuration _might_ work. + +## Standard Configuration + +To review, the configuration for a single bus is shown below. + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer(); + x.AddRequestClient(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); +}); +``` + +This configures the container so that there is a bus, using RabbitMQ, with a single consumer `SubmitOrderConsumer`, using automatic endpoint configuration. The MassTransit hosted service, which configures the bus health checks and starts/stop the bus via `IHostedService`, is also added to the container. + +When a consumer, a saga, or an activity is consuming a message the _ConsumeContext_ is available in the container scope. When the consumer is created using the container, the consumer and any dependencies are created within that scope. If a dependency includes _ISendEndpointProvider_, _IPublishEndpoint_, or even _ConsumeContext_ (should not be the first choice, but totally okay) on the constructor, all three of those interfaces result in the same reference which is great because it ensures that messages sent and/or published by the consumer or its dependencies includes the proper correlation identifiers and monitoring activity headers. + +## MultiBus Configuration + +To support multiple bus instances in a single container, the interface behaviors described above had to be considered carefully. There are expectations as to how these interfaces behave, and it was important to ensure consistent behavior whether an application has one, two, or a dozen bus instances (please, not a dozen – think of the children). A way to differentiate between different bus instances ensuring that sent or published messages end up on the right queues or topics is needed. The ability to configure each bus instance separately, yet leverage the power of a single shared container is also a must. + +To configure additional bus instances, create a new interface that includes _IBus_. Then, using that interface, configure the additional bus using the `AddMassTransit` method, which is included in the **_MassTransit.MultiBus_** namespace. + +```csharp +public interface ISecondBus : + IBus +{ +} +``` + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer(); + x.AddRequestClient(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); +}); + +services.AddMassTransit(x => +{ + x.AddConsumer(); + x.AddRequestClient(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.Host("remote-host"); + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +This configures the container so that there is an additional bus, using RabbitMQ, with a single consumer _AllocateInventoryConsumer_, using automatic endpoint configuration. Only a single hosted service is required that will start all bus instances so there is no need to add it twice. + +Notable differences in the new method: + +- The generic argument, _ISecondBus_, is the type that will be added to the container instead of _IBus_. This ensures that access to the additional bus is directly available without confusion. + +## Using your MultiBus + +For consumers or dependencies that need to send or publish messages to a different bus instance, a dependency on that specific bus interface (such as _IBus_, or _ISecondBus_) would be added. + +::alert{type="warning"} +Some things do not work across bus instances. As stated above, calling Send or Publish on an IBus (or other bus instance interface) starts a new conversation. Middleware components such as the _InMemoryOutbox_ currently do not buffer messages across bus instances. +:: + +### Controller + +```csharp +[ApiController] +[Route("/inventory")] +public class InventoryController : ControllerBase +{ + readonly Bind _publishEndpoint; + + public InventoryController(Bind publishEndpoint) + { + _publishEndpoint = publishEndpoint; + } + + [HttpPost] + public async Task Post() + { + // .. do stuff + } +} +``` + +### Razor Page + +```csharp +public class InventoryPage : PageModel +{ + public void OnPost([FromServices] Bind publishEndpoint) + { + await publishEndpoint.Value.Publish(new + { + SomeData = { } + }) + } +} +``` + +## Advanced Bus Types + +In the example above, which should be the most common of this hopefully uncommon use, the _ISecondBus_ interface is all that is required. MassTransit creates a dynamic class to delegate the `IBus` methods to the bus instance. However, it is possible to specify a class that implements _ISecondBus_ instead. + +To specify a class, as well as take advantage of the container to bring additional properties along with it, take a look at the following types and configuration. + +```csharp +public interface IThirdBus : + IBus +{ +} + +class ThirdBus : + BusInstance, + IThirdBus +{ + public ThirdBus(IBusControl busControl, ISomeService someService) + : base(busControl) + { + SomeService = someService; + } + + public ISomeService SomeService { get; } +} + +public interface ISomeService +{ +} +``` + +```csharp +services.AddMassTransit(x => +{ + x.UsingRabbitMq((context, cfg) => + { + cfg.Host("third-host"); + }); +}); +``` + +This would add a third bus instance, the same as the second, but using the instance class specified. The class is resolved from the container and given `IBusControl`, which must be passed to the base class ensuring that it is properly configured. + + + + +## Container Registration Details + +To support a first class experience with `Microsoft.Extensions.DependencyInjection` MassTransit registers common interfaces +for MultiBus instances using a `Bind` that allows you to specify the owner of the type you are interested in. This +allows you to access various pieces of MassTransit outside of a Consumer. Below are two tables that list out the various items +you might be interested in. + +### First Bus + +There are several interfaces added to the container using this configuration: + +| Interface | Lifestyle | Notes | +|:------------------------------|:----------|:-----------------------------------------------------------------------| +| `IBusControl` | Singleton | Used to start/stop the bus (not typically used) | +| `IBus` | Singleton | Publish/Send on this bus, starting a new conversation | +| `ISendEndpointProvider` | Scoped | Send messages from consumer dependencies, ASP.NET Controllers | +| `IPublishEndpoint` | Scoped | Publish messages from consumer dependencies, ASP.NET Controllers | +| `IClientFactory` | Singleton | Used to create request clients (singleton, or within scoped consumers) | +| `IRequestClient` | Scoped | Used to send requests | +| `ConsumeContext` | Scoped | Available in any message scope, such as a consumer, saga, or activity | + +### Multibus + +The registered interfaces are slightly different for additional bus instances. + +| Interface | Lifestyle | Notes | +|:------------------------------|:----------|:------------------------------------------------------------------------| +| `IBusControl` | N/A | Not registered, but automatically started/stopped by the hosted service | +| ~~`IBus`~~ | N/A | Not registered, the new bus interface is registered instead | +| `ISecondBus` | Singleton | Publish/Send on this bus, starting a new conversation | +| `ISendEndpointProvider` | Scoped | Send messages from consumer dependencies only | +| `IPublishEndpoint` | Scoped | Publish messages from consumer dependencies only | +| `IClientFactory` | N/A | Registered as an instance-specific client factory | +| `IRequestClient` | Scoped | Created using the specific bus instance | +| `ConsumeContext` | Scoped | Available in any message scope, such as a consumer, saga, or activity | +| `Bind` | Scoped | Send messages from controllers or outside of a consumer context | +| `Bind` | Scoped | Publish messages from controllers or outside of a consumer context | +| `Bind` | Scoped | Registered as an instance-specific client factory | +| `Bind>` | Scoped | Created using the bound bus instance | diff --git a/doc/content/3.documentation/2.configuration/observability.md b/doc/content/3.documentation/2.configuration/observability.md new file mode 100644 index 00000000000..41ec35d3b03 --- /dev/null +++ b/doc/content/3.documentation/2.configuration/observability.md @@ -0,0 +1,420 @@ +# Observability + +## Monitoring + +### Open Telemetry + +OpenTelemetry is an open-source standard for distributed tracing, which allows you to collect and analyze data about the performance of your systems. MassTransit can be configured to use OpenTelemetry to instrument message handling, so that you can collect telemetry data about messages as they flow through your system. + +By using OpenTelemetry with MassTransit, you can gain insights into the performance of your systems, which can help you to identify and troubleshoot issues, and to improve the overall performance of your application. + +There is a good set of examples [opentelemetry-dotnet](https://github.com/open-telemetry/opentelemetry-dotnet/tree/main/examples) how it can be used for different cases + +#### Tracing +##### ASP.NET Core application +This example is using following packages: +- `OpenTelemetry.Extensions.Hosting` +- `OpenTelemetry.Exporter.Console` + +```csharp +var builder = WebApplication.CreateBuilder(args); + +void ConfigureResource(ResourceBuilder r) +{ + r.AddService("Service Name", + serviceVersion: "Version", + serviceInstanceId: Environment.MachineName); +} + +builder.Services.AddOpenTelemetry() + .ConfigureResource(ConfigureResource) + .WithTracing(b => b + .AddSource(DiagnosticHeaders.DefaultListenerName) // MassTransit ActivitySource + .AddConsoleExporter() // Any OTEL suportable exporter can be used here + ); +``` + +##### Console application +This example is using following packages: +- `OpenTelemetry` +- `OpenTelemetry.Exporter.Console` + +```csharp +void ConfigureResource(ResourceBuilder r) +{ + r.AddService("Service Name", + serviceVersion: "Version", + serviceInstanceId: Environment.MachineName); +} + +Sdk.CreateTracerProviderBuilder() + .ConfigureResource(ConfigureResource) + .AddSource(DiagnosticHeaders.DefaultListenerName) // MassTransit ActivitySource + .AddConsoleExporter() // Any OTEL suportable exporter can be used here + .Build() +``` + +That's it you application will start exporting MassTransit related traces within your application + +#### Metrics +##### ASP.NET Core application +This example is using following packages: +- `OpenTelemetry.Extensions.Hosting` +- `OpenTelemetry.Exporter.Console` + +```csharp +var builder = WebApplication.CreateBuilder(args); + +void ConfigureResource(ResourceBuilder r) +{ + r.AddService("Service Name", + serviceVersion: "Version", + serviceInstanceId: Environment.MachineName); +} + +builder.Services.AddOpenTelemetry() + .ConfigureResource(ConfigureResource) + .WithMetrics(b => b + .AddMeter(InstrumentationOptions.MeterName) // MassTransit Meter + .AddConsoleExporter() // Any OTEL suportable exporter can be used here + ); +``` + +##### Console application +This example is using following packages: +- `OpenTelemetry` +- `OpenTelemetry.Exporter.Console` + +```csharp +void ConfigureResource(ResourceBuilder r) +{ + r.AddService("Service Name", + serviceVersion: "Version", + serviceInstanceId: Environment.MachineName); +} + +Sdk.CreateTracerProviderBuilder() + .ConfigureResource(ConfigureResource) + .AddMeter(InstrumentationOptions.MeterName) // MassTransit Meter + .AddConsoleExporter() // Any OTEL suportable exporter can be used here + .Build() +``` + +The OpenTelemetry metrics captured by MassTransit: + +`Counters` + +| Name | Description | +|:---------------------------------------------|:--------------------------------------------| +| messaging.masstransit.receive | Number of messages received | +| messaging.masstransit.receive.errors | Number of messages receive faults | +| messaging.masstransit.consume | Number of messages consumed | +| messaging.masstransit.consume.errors | Number of message consume faults | +| messaging.masstransit.saga | Number of messages processed by saga | +| messaging.masstransit.saga.errors | Number of message faults by saga | +| messaging.masstransit.consume.retries | Number of message consume retries | +| messaging.masstransit.handler | Number of messages handled | +| messaging.masstransit.handler.errors | Number of message handler faults | +| messaging.masstransit.outbox.delivery | Number of messages delivered by outbox | +| messaging.masstransit.outbox.delivery.errors | Number of message delivery faults by outbox | +| messaging.masstransit.send | Number of messages sent | +| messaging.masstransit.send.errors | Number of message send faults | +| messaging.masstransit.outbox.send | Number of messages sent to outbox | +| messaging.masstransit.outbox.send.errors | Number of message send faults to outbox | +| messaging.masstransit.execute | Number of activities executed | +| messaging.masstransit.execute.errors | Number of activity execution faults | +| messaging.masstransit.compensate | Number of activities compensated | +| messaging.masstransit.compensate.errors | Number of activity compensation failures | + +`Gauges` + +| Name | Description | +|:----------------------------------------|:--------------------------------------------------------| +| messaging.masstransit.receive.active | Number of messages being received | +| messaging.masstransit.consume.active | Number of consumers in progress | +| messaging.masstransit.execute.active | Number of activity executions in progress | +| messaging.masstransit.compensate.active | Number of activity compensations in progress | +| messaging.masstransit.handler.active | Number of handlers in progress | +| messaging.masstransit.saga.active | Number of sagas in progress | + +`Histograms` + +| Name | Description | +|:------------------------------------------|:-----------------------------------------------------------------------------------| +| messaging.masstransit.receive.duration | Elapsed time spent receiving a message, in millis | +| messaging.masstransit.consume.duration | Elapsed time spent consuming a message, in millis | +| messaging.masstransit.saga.duration | Elapsed time spent saga processing a message, in millis | +| messaging.masstransit.handler.duration | Elapsed time spent handler processing a message, in millis | +| messaging.masstransit.delivery.durations | Elapsed time between when the message was sent and when it was consumed, in millis | +| messaging.masstransit.execute.duration | Elapsed time spent executing an activity, in millis | +| messaging.masstransit.compensate.duration | Elapsed time spent compensating an activity, in millis | + + +`Labels` + +| Name | Description | +|:-------------------------------------|:----------------------------------------------------| +| messaging.masstransit.service | The service name specified at bus configuration | +| messaging.masstransit.destination | The endpoint address | +| messaging.masstransit.message_type | The message type for the metric | +| messaging.masstransit.consumer_type | The consumer, saga, or activity type for the metric | +| messaging.masstransit.activity_type | The activity name | +| messaging.masstransit.argument_type | The activity execute argument type | +| messaging.masstransit.log_type | The activity compensate log type | +| messaging.masstransit.exception_type | The exception type for a fault metric | +| messaging.masstransit.bus | The bus instance | +| messaging.masstransit.endpoint | The receive endpoint | + +Metric names and labels can be configured with `Options`: + +```csharp +services.Configure(options => +{ + // Configure +}); +``` + +### Application Insights +Azure Monitor has direct integration with Open Telemetry: +- [OpenTelemetry overview](https://learn.microsoft.com/en-us/azure/azure-monitor/app/opentelemetry-overview) +- [Enable Azure Monitor OpenTelemetry for .NET](https://learn.microsoft.com/en-us/azure/azure-monitor/app/opentelemetry-enable?tabs=net) + +##### ASP.NET Core application +This example is using following packages: +- `OpenTelemetry.Extensions.Hosting` +- `Azure.Monitor.OpenTelemetry.Exporter` + +```csharp +var builder = WebApplication.CreateBuilder(args); + +void ConfigureResource(ResourceBuilder r) +{ + r.AddService("Service Name", + serviceVersion: "Version", + serviceInstanceId: Environment.MachineName); +} + +builder.Services.AddOpenTelemetry() + .ConfigureResource(ConfigureResource) + .WithTracing(b => b + .AddSource(DiagnosticHeaders.DefaultListenerName) // MassTransit ActivitySource + .AddAzureMonitorTraceExporter( + { + o.ConnectionString = ""; + })) + .WithMetrics(b => b + .AddMeter(InstrumentationOptions.MeterName) // MassTransit Meter + .AddAzureMonitorMetricExporter(o => + { + o.ConnectionString = ""; + })); +``` + +##### Console application +This example is using following packages: +- `OpenTelemetry` +- `Azure.Monitor.OpenTelemetry.Exporter` + +```csharp +void ConfigureResource(ResourceBuilder r) +{ + r.AddService("Service Name", + serviceVersion: "Version", + serviceInstanceId: Environment.MachineName); +} + +Sdk.CreateTracerProviderBuilder() + .ConfigureResource(ConfigureResource) + .AddSource(DiagnosticHeaders.DefaultListenerName) // MassTransit ActivitySource + .AddAzureMonitorTraceExporter( + { + o.ConnectionString = ""; + }) + .Build(); + +Sdk.CreateTracerProviderBuilder() + .ConfigureResource(ConfigureResource) + .AddMeter(InstrumentationOptions.MeterName) // MassTransit Meter + .AddAzureMonitorMetricExporter(o => + { + o.ConnectionString = ""; + }) + .Build() +``` +You can also refer to the sample: [Sample-ApplicationInsights](https://github.com/MassTransit/Sample-ApplicationInsights) + +### Prometheus + +::alert{type="info"} +The direct integration to Prometheus has been deprecated. Use the Open Telemetry integration instead. +:: + +Open Telemetry is more preferable choice of integration + +#### Open Telemetry integration + +This example is using following packages: +- `OpenTelemetry.Extensions.Hosting` +- `OpenTelemetry.Exporter.Prometheus` +- `OpenTelemetry.Exporter.Prometheus.AspNetCore` + +```csharp +void ConfigureResource(ResourceBuilder r) +{ + r.AddService("Service Name", + serviceVersion: "Version", + serviceInstanceId: Environment.MachineName); +} + +builder.Services.AddOpenTelemetry() + .ConfigureResource(ConfigureResource) + .WithMetrics(b => b + .AddMeter(InstrumentationOptions.MeterName) // MassTransit Meter + .AddPrometheusExporter() + ); + +var app = builder.Build(); + +app.UseOpenTelemetryPrometheusScrapingEndpoint(); // Map prometheus metrics endpoint +``` +In case you want to migrate from direct integration to using Open Telemetry, and use previous metric names, just configure them through `Options`: +```csharp +builder.Services.Configure(options => +{ + ReceiveTotal = "mt.receive.total"; + // Configure other names by using similar approach +}); +``` + +## Lifetime Observers + +MassTransit supports several message observers allowing received, consumed, sent, and published messages to be monitored. There is a bus observer as well, so that the bus life cycle can be monitored. + +::alert{type="warning"} +Observers should not be used to modify or intercept messages. To intercept messages to add headers or modify message content, create a new or use an existing middleware component. +:: + +### Bus + +To observe bus life cycle events, create a class which implements `IBusObserver`. To configure a bus observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. + +```csharp +services.AddBusObserver(); +``` + +```csharp +services.AddBusObserver(provider => new BusObserver()); +``` + +### Receive Endpoint + +To configure a receive endpoint observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. + +```csharp +services.AddReceiveEndpointObserver(); +``` + +```csharp +services.AddReceiveEndpointObserver(provider => new ReceiveEndpointObserver()); +``` + +## Pipeline Observers + +### Receive + +To observe messages as they are received by the transport, create a class that implements the `IReceiveObserver` interface, and connect it to the bus as shown below. + +To configure a receive observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. When a container is not being used, the `ConnectReceiveObserver` bus method can be used instead. + +```csharp +services.AddReceiveObserver(); +``` + +```csharp +services.AddReceiveObserver(provider => new ReceiveObserver()); +``` + +### Consume + +If the `ReceiveContext` isn't fascinating enough for you, perhaps the actual consumption of messages might float your boat. A consume observer implements the `IConsumeObserver` interface, as shown below. + +To configure a consume observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. When a container is not being used, the `ConnectConsumeObserver` bus method can be used instead. + +```csharp +services.AddConsumeObserver(); +``` + +```csharp +services.AddConsumeObserver(provider => new ConsumeObserver()); +``` + +#### Consume Message + +Okay, so it's obvious that if you've read this far you want a more specific observer, one that only is called when a specific message type is consumed. We have you covered there too, as shown below. + +To connect the observer, use the `ConnectConsumeMessageObserver` method before starting the bus. + +> The `ConsumeMessageObserver` interface may be deprecated at some point, it's sort of a legacy observer that isn't recommended. + +### Send + +Okay, so, incoming messages are not your thing. We get it, you're all about what goes out. It's cool. It's better to send than to receive. Or is that give? Anyway, a send observer is also available. + +To configure a send observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer +creation. When a container is not being used, the `ConnectSendObserver` bus method can be used instead. + +```csharp +services.AddSendObserver(); +``` + +```csharp +services.AddSendObserver(provider => new SendObserver()); +``` + +### Publish + +In addition to send, publish is also observable. Because the semantics matter, absolutely. Using the MessageId to link them up as it's unique for each message. Remember that Publish and Send are two distinct operations so if you want to observe all messages that are leaving your service, you have to connect both Publish and Send observers. + +To configure a public observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer +creation. When a container is not being used, the `ConnectPublishObserver` bus method can be used instead. + +```csharp +services.AddPublishObserver(); +``` + +```csharp +services.AddPublishObserver(provider => new PublishObserver()); +``` + +## State Machine Observers + +### Event + +To observe events consumed by a saga state machine, use an `IEventObserver` where `T` is the saga instance type. + +To configure an event observer, add it to the container using one of the methods shown below. The factory method version allows customization of the +observer creation. + +```csharp +services.AddEventObserver>(); +``` + +```csharp +services.AddEventObserver(provider => new EventObserver()); +``` + +### State + +To observe state changes that happen in a saga state machine, use an `IStateObserver` where `T` is the saga instance type. + +To configure a state observer, add it to the container using one of the methods shown below. The factory method version allows customization of the +observer creation. + +```csharp +services.AddStateObserver>(); +``` + +```csharp +services.AddStateObserver(provider => new StateObserver()); +``` + diff --git a/doc/content/3.documentation/2.configuration/obsolete.md b/doc/content/3.documentation/2.configuration/obsolete.md new file mode 100644 index 00000000000..d3147a19c7f --- /dev/null +++ b/doc/content/3.documentation/2.configuration/obsolete.md @@ -0,0 +1,59 @@ +--- +navigation.title: Obsolete +--- + +# Obsolete Methods + +MassTransit retains a lot of older methods to avoid breaking builds when upgrading to new versions. While these methods should still work as they did previously, +they don't represent the current usage guidelines. + +> Since obsolete method warnings can be ignored, they have been added to many methods that are completely usable but not recommended. + +## UseRetry + +The `UseRetry` middleware method was replaced by `UseMessageRetry`. The only scenario where the previous method should be used is in specific message +pipelines where retry within the same consumer or saga context is preferred. + +## Consumer Definitions + +The _ConfigureConsumer_, _ConfigureSaga_, _ConfigureExecuteActivity_, and _ConfigureCompensateActivity_ methods in consumer, saga, and activity definitions +have a new overload that adds the `IRegistrationContext` parameter. + +> The `IRegistrationContext` interface implements `IServiceProvider` as well, so there is no need to inject it into the constructor. + +The `IRegistrationContext` argument should be passed to methods that accept it, or that may have previously accepted `IServiceProvider`, to ensure the correct +context is used for the bus and to ensure message scope is properly handled. + +For example, the outbox now requires `IRegistrationContext` as an argument. + +```csharp +protected override void ConfigureSaga(IReceiveEndpointConfigurator configurator, + ISagaConfigurator sagaConfigurator, + IRegistrationContext context) +{ + configurator.UseMessageRetry(r => r.Intervals(100, 1000, 2000, 5000)); + + // use the new overload, not the obsolete one + configurator.UseInMemoryOutbox(context); +} +``` + +## AddMassTransitInMemoryTestHarness + +The original test harness has been deprecated, use `AddMassTransitTestHarness` instead. The current test harness is covered in the +[Testing](/documentation/concepts/testing) documentation. Additional methods including `AddConsumerTestHarness`, `AddSagaTestHarness`, +and `AddSagaStateMachineTestHarness` are not used with the new test harness. + +## AddBus + +Transports have their own `UsingXxx` methods, such as `UsingRabbitMq`. To ensure the container and all supporting bus components are properly configured, +the `UsingXxx` methods should be used. + +## Instance, Data (Saga State Machines) + +The _Automatonymous_ properties `Instance` and `Data` have been superseded by `Saga` and `Message` to be consistent with MassTransit terminology. + +## Job Service Configuration + +The job service configuration was drastically simplified in v8.1 - eliminating the need to configure the service instance. +See the [updated documentation](/documentation/patterns/job-consumers) for details. diff --git a/doc/content/3.documentation/2.configuration/serialization.md b/doc/content/3.documentation/2.configuration/serialization.md new file mode 100644 index 00000000000..53b10242f6a --- /dev/null +++ b/doc/content/3.documentation/2.configuration/serialization.md @@ -0,0 +1,241 @@ +--- +navigation.title: Serialization +--- + +# Message Serialization + +MassTransit uses _types_ for [messages](/documentation/concepts/messages). Serializers are used convert those types into their respective format (such as JSON, XML, BSON, etc.), which is sent to the message broker. A corresponding deserializer is then used to convert the serialized format back into a type. + +By default, MassTransit uses _System.Text.Json_ to serialize and deserialize messages using JSON. + +## Supported Serializers + +MassTransit include support for several commonly used serialization packages. + +### System.Text.Json + +_System.Text.Json_ is the default message serializer/deserializer. + +| Content Type | Format | Configuration Method | +|:-------------------------------------|:----------------------|:----------------------------------| +| **application/vnd.masstransit+json** | **JSON (w/envelope)** | `UseJsonSerializer` **(default)** | +| application/json | JSON | `UseRawJsonSerializer` | + +#### Customize the Serializer Support + +```csharp +services.AddMassTransit(cfg => +{ + cfg.Using[Broker](broker => + { + broker.ConfigureJsonSerializerOptions(options => + { + // customize the JsonSerializerOptions here + return options; + }); + }) + +}) +``` + +### Newtonsoft + +The [MassTransit.Newtonsoft](https://nuget.org/packages/MassTransit.Newtonsoft) package adds the serialization formats listed below. + +> In MassTransit versions before v8, Newtonsoft was the default message serializer/deserializer. + +| Content Type | Format | Configuration Method | +|:-----------------------------------|:--------------------|:---------------------------------| +| application/vnd.masstransit+json | JSON (w/envelope) | `UseNewtonsoftJsonSerializer` | +| application/json | JSON | `UseNewtonsoftRawJsonSerializer` | +| application/vnd.masstransit+bson | BSON (w/envelope) | `UseBsonSerializer` | +| application/vnd.masstransit+xml | XML (w/envelope) | `UseXmlSerializer` | +| application/xml | XML | `UseRawXmlSerializer` | +| application/vnd.masstransit+aes | Binary (w/envelope) | `UseEncryptedSerializer` | +| application/vnd.masstransit.v2+aes | Binary (w/envelope) | `UseEncryptedSerializerV2` | + +### MessagePack + +The [MassTransit.MessagePack](https://nuget.org/packages/MassTransit.MessagePack) package adds the serialization formats listed below. + +| Content Type | Format | Configuration Method | +|:------------------------------------|:-------------------------|:---------------------------| +| application/vnd.masstransit+msgpack | MessagePack (w/envelope) | `UseMessagePackSerializer` | + + +## Message Types + +MassTransit stores the supported .NET types for a message as an array of URNs, which include the namespace and name of the message type. All interfaces and +superclasses of the message type are included. The namespace and name are formatted as shown below. + +`urn:message:Namespace:TypeName` + +A few examples of valid message types: + +``` +urn:message:MyProject.Messages:UpdateAccount +urn:message:MyProject.Messages.Events:AccountUpdated +urn:message:MyProject:ChangeAccount +urn:message:MyProject.AccountService:MyService+AccountUpdatedEvent +``` + +> The last one is a nested class, as indicated by the '+' symbol. + + +### MessageUrn Attribute + +_MessageUrn_ is an optional attribute that may be specified on a message type to provide a custom Urn that will be used when the message is published or consumed. The generated Urn will be prefixed with `urn:messages:` by default, however a full Urn may be provided by specifying `useDefaultPrefix: false` in the attribute declaration. + +```csharp +[MessageUrn("publish-command")] +public record PublishCommand +{ + // Will generate a urn of: urn:messages:publish-command +} +``` + +```csharp +[MessageUrn("scheme:publish-command", useDefaultPrefix: false)] +public record PublishCommand +{ + // Will generate a urn of: scheme:publish-command +} +``` + +## Message Envelope + +To interoperate with other languages and platforms, message structure is important. MassTransit encapsulates messages in an envelope before they are serialized. An example JSON message envelope is shown below. + +```json +{ + "messageId": "181c0000-6393-3630-36a4-08daf4e7c6da", + "requestId": "ef375b18-69ee-4a9e-b5ec-44ee1177a27e", + "correlationId": null, + "conversationId": null, + "initiatorId": null, + "sourceAddress": "rabbitmq://localhost/source", + "destinationAddress": "rabbitmq://localhost/destination", + "responseAddress": "rabbitmq://localhost/response", + "faultAddress": "rabbitmq://localhost/fault", + "messageType": [ + "urn:message:Company.Project:SubmitOrder" + ], + "message": { + "orderId": "181c0000-6393-3630-36a4-08daf4e7c6da", + "timestamp": "2023-01-12T21:55:53.714Z" + }, + "expirationTime": null, + "sentTime": "2023-01-12T21:55:53.715882Z", + "headers": { + "Application-Header": "SomeValue" + }, + "host": { + "machineName": "MyComputer", + "processName": "dotnet", + "processId": 427, + "assembly": "TestProject", + "assemblyVersion": "2.11.1.93", + "frameworkVersion": "6.0.7", + "massTransitVersion": "8.0.10.0", + "operatingSystemVersion": "Unix 12.6.2" + } +} +``` + +| Property | Type | Notes | Set | +|:-------------------|:--------:|:------------|:---:| +| messageId | Guid | Recommended | Y | +| correlationId | Guid | Optional | | +| requestId | Guid | Situational | R | +| initiatorId | Guid | Optional | | +| conversationId | Guid | Optional | Y | +| sourceAddress | Uri | Optional | Y | +| destinationAddress | Uri | Optional | Y | +| responseAddress | Uri | Situational | R | +| faultAddress | Uri | Optional | | +| expirationTime | ISO-8601 | Situational | S | +| sentTime | ISO-8601 | Optional | Y | +| messageType | Urn\[\] | Required | Y | + +> Set indicates whether the property is automatically set by MassTransit when producing messages. _Yes_, _Requests_ only, or _Situational_. + +## Raw JSON + +Consuming messages from other systems where messages may not be produced by MassTransit, raw JSON is commonly used. + +When using a serializer that doesn't wrap the message in an envelope (_application/json_), the above message would be reduced to the raw JSON shown below. + +```json +{ + "orderId": "181c0000-6393-3630-36a4-08daf4e7c6da", + "timestamp": "2023-01-12T21:55:53.714Z" +} +``` + +::callout{type="info"} +#summary +Learn more about using raw JSON messages in this video. + +#content +::div + :video-player{src="https://www.youtube.com/watch?v=xOxSLNeN5CU"} +:: +:: + +### Options + +MassTransit provides several options when dealing with raw JSON messages. The options can be specified on the _UseRawJsonSerializer_ method._RawSerializerOptions_ includes the following flags: + +| Option | Value | Default | Notes | +|:--------------------|:-----:|:-------:|:-------------------------------------------------------------| +| AnyMessageType | 1 | Y | Messages will match any consumed message type | +| AddTransportHeaders | 2 | Y | MassTransit will add the above headers to outbound messages | +| CopyHeaders | 4 | N | Received message headers will be copied to outbound messages | + +In cases where MassTransit is used and raw JSON messages are preferred, the non-default options are recommended. + +```csharp +cfg.UseRawJsonSerializer(RawSerializerOptions.AddTransportHeaders | RawSerializerOptions.CopyHeaders); +``` + +### Headers + +When the _RawSerializerOptions.AddTransportHeaders_ option is specified, the following transport headers will be set (if not empty or default). + +| Header Name | Type | Notes | +|:---------------------|:--------:|:----------------------------------------------------------| +| MessageId | Guid | | +| CorrelationId | Guid | | +| RequestId | Guid | | +| MT-InitiatorId | Guid | | +| ConversationId | Guid | | +| MT-Source-Address | Uri | | +| MT-Response-Address | Uri | | +| MT-Fault-Address | Uri | | +| MT-MessageType | Urn\[\] | Multiple message types separated by ; | +| MT-Host-Info | string | JSON serialized host info | +| MT-OriginalMessageId | Guid | For redelivered messages with a newly generated MessageId | + +### Configuration + +::alert{type="info"} +Serialization customizations for using raw JSON is generally recommended on individual receive endpoints only, vs being globally configured at the bus level. +:: + +To configure a receive endpoint so that it can _receive_ raw JSON messages, specify the default content type and add the deserializer as shown below. When a raw JSON message is received, it will be delivered to every consumer configured on the receive endpoint. + +```csharp +endpointConfigurator.DefaultContentType = new ContentType("application/json"); +endpointConfigurator.UseRawJsonDeserializer(); +``` + +If messages produced by consumers on the receive endpoint should also be in the raw JSON format, `UseRawJsonSerializer()` may be used instead. + +Setting the default content type tells MassTransit to use the raw JSON deserializer for messages that do not have a recognized `Content-Type` header. + +To prevent MassTransit from creating exchanges or topics for the message types consumed on the endpoint, disable consume topology configuration. + +```csharp +endpointConfigurator.ConfigureConsumeTopology = false; +``` + diff --git a/doc/content/3.documentation/2.configuration/test-harness.md b/doc/content/3.documentation/2.configuration/test-harness.md new file mode 100644 index 00000000000..cd360075c6f --- /dev/null +++ b/doc/content/3.documentation/2.configuration/test-harness.md @@ -0,0 +1,17 @@ +# Test Harness + +The MassTransit [Test Harness](/documentation/concepts/testing) is a framework for extending your existing IOC registrations with a testable bus instance. + +## Lifecycle Controlling methods + +- `await harness.Start()` - start the timers, and capture of messages + +## Communication Methods + +- `harness.Bus.Publish(new MessageToPublish())` + +## Instance Methods + +- `Sent`: What messages were sent +- `Consumed`: What messages were consumed +- `Published`: What messages were published diff --git a/doc/content/3.documentation/2.transports/2.rabbitmq.md b/doc/content/3.documentation/2.transports/2.rabbitmq.md deleted file mode 100644 index cac76674730..00000000000 --- a/doc/content/3.documentation/2.transports/2.rabbitmq.md +++ /dev/null @@ -1,267 +0,0 @@ ---- -navigation.title: RabbitMQ ---- - -# RabbitMQ Transport - -RabbitMQ is an open-source message broker software that implements the Advanced Message Queuing Protocol (AMQP). It is written in the Erlang programming language and is built on the Open Telecom Platform framework for clustering and failover. - -RabbitMQ can be used to decouple and distribute systems by sending messages between them. It supports a variety of messaging patterns, including point-to-point, publish/subscribe, and request/response. - -RabbitMQ provides features such as routing, reliable delivery, and message persistence. It also has a built-in management interface that allows for monitoring and management of the broker, queues, and connections. Additionally, it supports various plugins, such as the RabbitMQ Management Plugin, that provide additional functionality. - -## Topology - -The send and publish topologies are extended to support RabbitMQ features, and make it possible to configure how exchanged are created. - -### Exchanges - -In RabbitMQ, an exchange is a component that receives messages from producers and routes them to one or more queues based on a set of rules called bindings. Exchanges are used to decouple the producer of a message from the consumer, by allowing messages to be sent to multiple queues and/or consumers. - -There are several types of exchanges in RabbitMQ, each with its own routing algorithm: - -Direct exchanges route messages to queues based on an exact match of the routing key. -Fanout exchanges route messages to all bound queues. -Topic exchanges route messages to queues based on a pattern match of the routing key. -Headers exchanges route messages to queues based on the headers of the message. -When a message is published to an exchange, the exchange applies the routing algorithm based on the routing key and the bindings to determine which queues the message should be sent to. The message is then sent to each of the queues that it matches. - -Exchanges allow for more complex routing and message distribution strategies, as they allow to route messages based on different criteria, such as routing key, headers, or patterns. - -When a message is published, MassTransit sends it to an exchange that is named based upon the message type. Using topology, the exchange name, as well as the exchange properties can be configured to support a custom behavior. - -To configure the properties used when an exchange is created, the publish topology can be configured during bus creation: - -```csharp -cfg.Publish(x => -{ - x.Durable = false; // default: true - x.AutoDelete = true; // default: false - x.ExchangeType = "fanout"; // default, allows any valid exchange type -}); - -cfg.Publish(x => -{ - x.Exclude = true; // do not create an exchange for this type -}); -``` - -### Exchange Binding - -To bind an exchange to a receive endpoint: - -```csharp -cfg.ReceiveEndpoint("input-queue", e => -{ - e.Bind("exchange-name"); - e.Bind(); -}) -``` - -The above will create two exchange bindings, one between the `exchange-name` exchange and the `input-queue` exchange and a second between the exchange name matching the `MessageType` and the same `input-queue` exchange. - -The properties of the exchange binding may also be configured: - -```csharp -cfg.ReceiveEndpoint("input-queue", e => -{ - e.Bind("exchange-name", x => - { - x.Durable = false; - x.AutoDelete = true; - x.ExchangeType = "direct"; - x.RoutingKey = "8675309"; - }); -}) -``` - -The above will create an exchange binding between the `exchange-name` and the `input-queue` exchange, using the configured properties. - -### RoutingKey - -The routing key on published/sent messages can be configured by convention, allowing the same method to be used for messages which implement a common interface type. If no common type is shared, each message type may be configured individually using various conventional selectors. Alternatively, developers may create their own convention to fit their needs. - -When configuring a bus, the send topology can be used to specify a routing key formatter for a particular message type. - -```csharp -public record SubmitOrder -{ - public string CustomerType { get; init; } - public Guid TransactionId { get; init; } - // ... -} -``` - -```csharp -cfg.Send(x => -{ - // use customerType for the routing key - x.UseRoutingKeyFormatter(context => context.Message.CustomerType); - - // multiple conventions can be set, in this case also CorrelationId - x.UseCorrelationId(context => context.Message.TransactionId); -}); - -//Keeping in mind that the default exchange config for your published type will be the full typename of your message -//we explicitly specify which exchange the message will be published to. So it lines up with the exchange we are binding our -//consumers too. -cfg.Message(x => x.SetEntityName("submitorder")); - -//Also if your publishing your message: because publishing a message will, by default, send it to a fanout queue. -//We specify that we are sending it to a direct queue instead. In order for the routingkeys to take effect. -cfg.Publish(x => x.ExchangeType = ExchangeType.Direct); -``` - -The consumer could then be created: - -```csharp -public class OrderConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - - } -} -``` - -And then connected to a receive endpoint: - -```csharp -cfg.ReceiveEndpoint("priority-orders", x => -{ - x.ConfigureConsumeTopology = false; - - x.Consumer(); - - x.Bind("submitorder", s => - { - s.RoutingKey = "PRIORITY"; - s.ExchangeType = ExchangeType.Direct; - }); -}); - -cfg.ReceiveEndpoint("regular-orders", x => -{ - x.ConfigureConsumeTopology = false; - - x.Consumer(); - - x.Bind("submitorder", s => - { - s.RoutingKey = "REGULAR"; - s.ExchangeType = ExchangeType.Direct; - }); -}); -``` - -This would split the messages sent to the exchange, by routing key, to the proper endpoint, using the CustomerType property. - -## Endpoint Address - -A RabbitMQ endpoint address supports the following query string parameters: - -| Parameter | Type | Description | Implies | -|------------|--------|----------------------------|------------------------------------| -| temporary | bool | Temporary endpoint | durable = false, autodelete = true | -| durable | bool | Save messages to disk | | -| autodelete | bool | Delete when bus is stopped | | -| bind | bool | Bind exchange to queue | | -| queue | string | Bind to queue name | bind = true | - -## Broker Topology - -In this example topology, two commands and events are used. - -First, the event contracts that are supported by an endpoint that receives files from a customer. - -```csharp -public interface FileReceived -{ - Guid FileId { get; } - DateTime Timestamp { get; } - Uri Location { get; } -} - -public interface CustomerDataReceived -{ - DateTime Timestamp { get; } - string CustomerId { get; } - string SourceAddress { get; } - Uri Location { get; } -} -``` - -Second, the command contract for processing a file that was received. - -```csharp -public interface ProcessFile -{ - Guid FileId { get; } - Uri Location { get; } -} -``` - -The above contracts are used by the consumers to receive messages. From a publishing or sending perspective, two classes are created by the event producer and the command sender which implement these interfaces. - -```csharp -public record FileReceivedEvent : - FileReceived, - CustomerDataReceived -{ - public Guid FileId { get; init; } - public DateTime Timestamp { get; init; } - public Uri Location { get; init; } - public string CustomerId { get; init; } - public string SourceAddress { get; init; } -} -``` - -And the command class. - -```csharp -public record ProcessFileCommand : - ProcessFile -{ - public Guid FileId { get; init; } - public Uri Location { get; init; } -} -``` - -The consumers for these message contracts are as below. - -```csharp -class FileReceivedConsumer : - IConsumer -{ -} - -class CustomerAuditConsumer : - IConsumer -{ -} - -class ProcessFileConsumer : - IConsumer -{ -} -``` - -### Publish - -The exchanges and queues configures for the event example are as shown below. - -> MassTransit publishes messages to the message type exchange, and copies are routed to all the subscribers by RabbitMQ. This approach was [based on an article][2] on how to maximize routing performance in RabbitMQ. - -[2]: http://spring.io/blog/2011/04/01/routing-topologies-for-performance-and-scalability-with-rabbitmq/ - -![rabbitmq-publish-topology](/rabbitmq-publish-topology.png) - -### Send - -The exchanges and queues for the send example are shown. - -![rabbitmq-send-topology](/rabbitmq-send-topology.png) - -> Note that the broker topology can now be configured using the [topology](/documentation/configuration/topology) API. - diff --git a/doc/content/3.documentation/2.transports/3.azure-service-bus.md b/doc/content/3.documentation/2.transports/3.azure-service-bus.md deleted file mode 100644 index 6ef5e2e918e..00000000000 --- a/doc/content/3.documentation/2.transports/3.azure-service-bus.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -navigation.title: Azure Service Bus ---- - -# Azure Service Bus Transport - -Azure Service Bus is a messaging service from Microsoft Azure that allows for communication between decoupled systems. It offers a reliable and secure platform for asynchronous transfer of data and state. It supports a variety of messaging patterns, including queuing, publish/subscribe, and request/response. - -With Service Bus, you can create messaging entities such as queues, topics, and subscriptions. Queues provide one-to-one messaging, where each message is consumed by a single receiver. Topics and subscriptions provide one-to-many messaging, where a message is delivered to multiple subscribers. - -Service Bus also provides advanced features such as partitioning and auto-scaling, which allow for high availability and scalability. Additionally, it offers a dead letter queue, which is a special queue that stores undelivered or expired messages. - -## Topology - -The send and publish topologies are extended to support the Azure Service Bus features, and make it possible to configure how topics are created. - -### Topics - -An Azure Service Bus Topic is a messaging entity that allows for one-to-many messaging, where a message is delivered to multiple subscribers. Topics are built on top of Azure Service Bus Queues and provide additional functionality for publish/subscribe messaging patterns. - -When a message is sent to a topic, it is automatically broadcast to all subscribers that have a subscription to that topic. Subscriptions are used to filter messages that are delivered to the subscribers. Subscribers can create multiple subscriptions to a topic, each with its own filter, to receive only the messages that are of interest to them. - -Topics also provide a feature called Session-based messaging, which allows for guaranteed ordering of messages, and the ability to send and receive messages in a stateful manner. - -Topics provide a robust and scalable messaging infrastructure for building distributed systems, where multiple services or systems can subscribe to a topic and receive messages that are relevant to them. Topics also support advanced features such as partitioning and auto-scaling, which allow for high availability and scalability. - -To specify properties used when a topic is created, the publish topology can be configured during bus creation: - -```csharp -cfg.Publish(x => -{ - x.EnablePartitioning = true; -}); -``` - -### PartitionKey - -When publishing messages to an Azure Service Bus topic, you can use the PartitionKey property to specify a value that will be used to partition the messages across multiple topic partitions. This can be useful in situations where you want to ensure that related messages are always delivered to the same partition, and thus will be guaranteed to be processed in the order they were sent. - -By setting a PartitionKey, all messages with the same key will be sent to the same partition, and thus will be received by consumers in the order they were sent. This is particularly useful when building distributed systems that require strict ordering of messages, such as event sourcing or stream processing. - -Another use case for the PartitionKey is when you have a large number of messages and want to distribute them evenly across multiple partitions for better performance, this way the messages are load balanced across all the partitions. - -It's important to note that when you use a PartitionKey, it's important to choose a key that will result in an even distribution of messages across partitions, to avoid overloading a single partition. - -The PartitionKey on published/sent messages can be configured by convention, allowing the same method to be used for messages which implement a common interface type. If no common type is shared, each message type may be configured individually using various conventional selectors. Alternatively, developers may create their own convention to fit their needs. - -When configuring a bus, the send topology can be used to specify a routing key formatter for a particular message type. - -```csharp -public record SubmitOrder -{ - public string CustomerId { get; init; } - public Guid TransactionId { get; init; } -} -``` - -```csharp -cfg.Send(x => -{ - x.UsePartitionKeyFormatter(context => context.Message.CustomerId); -}); -``` - -### SessionId - -When publishing messages to an Azure Service Bus Topic, you can use the SessionId property to specify a value that will be used to group messages together in a session. This can be useful in situations where you want to ensure that related messages are always delivered together, and thus will be guaranteed to be processed in the order they were sent. - -A session is a logical container for messages, and all messages within a session have a guaranteed order of delivery. This means that messages with the same SessionId will be delivered in the order they were sent, regardless of the order they were received by the topic. - -A common use case for sessions is when you have a set of related messages that need to be processed together. For example, if you are sending a series of commands to control a device, you would want to ensure that the commands are delivered in the order they were sent and that all related commands are delivered together. - -Another use case for sessions is when you have a large number of messages and want to ensure that each consumer processes the messages in a specific order. - -It's important to note that when you use sessions, the consumers must be able to process the messages in the order they were sent, otherwise messages might get stuck in the session and cause delays. - -The SessionId on published/sent messages can be configured by convention, allowing the same method to be used for messages which implement a common interface type. If no common type is shared, each message type may be configured individually using various conventional selectors. Alternatively, developers may create their own convention to fit their needs. - -When configuring a bus, the send topology can be used to specify a routing key formatter for a particular message type. - -```csharp -public record UpdateUserStatus -{ - public Guid UserId { get; init; } - public string Status { get; init; } -} -``` - -```csharp -cfg.Send(x => -{ - x.UseSessionIdFormatter(context => context.Message.UserId); -}); -``` - -## Subscriptions - -In Azure, topics and topic subscriptions provide a mechanism for one-to-many communication (versus queues that are designed for one-to-one). A topic subscription acts as a virtual queue. To subscribe to a topic subscription directly the `SubscriptionEndpoint` should be used: - -```csharp -cfg.SubscriptionEndpoint("subscription-name", e => -{ - e.ConfigureConsumer(provider); -}) -``` - -Note that a topic subscription's messages can be forwarded to a receive endpoint (an Azure Service Bus queue), in the following way. Behind the scenes MassTransit is setting up [Service Bus Auto-forwarding](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-auto-forwarding) between a topic subscription and a queue. - -```csharp -cfg.ReceiveEndpoint("input-queue", e => -{ - e.Subscribe("topic-name"); - e.Subscribe(); -}) -``` - -The properties of the topic subscription may also be configured: - -```csharp -cfg.ReceiveEndpoint("input-queue", e => -{ - e.Subscribe("topic-name", x => - { - x.AutoDeleteOnIdle = TimeSpan.FromMinutes(60); - }); -}) -``` - -## Broker Topology - -The topics, queues, and subscriptions configured on Azure Service Bus are shown below. - -![azure-topology](/azure-topology.png) - diff --git a/doc/content/3.documentation/2.transports/4.amazon-sqs.md b/doc/content/3.documentation/2.transports/4.amazon-sqs.md deleted file mode 100644 index e3723dfacfe..00000000000 --- a/doc/content/3.documentation/2.transports/4.amazon-sqs.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -navigation.title: Amazon SQS -title: Amazon SQS Transport ---- - -# Amazon SQS - -Amazon Simple Queue Service (SQS) is a fully managed message queuing service that enables you to decouple and scale microservices, distributed systems, and serverless applications. SQS eliminates the complexity and overhead associated with managing and operating message oriented middleware, and empowers developers to focus on differentiating work. - -With SQS, you can send, store, and receive messages between software components at any volume, without losing messages or requiring other services to be always available. SQS makes it simple and cost-effective to decouple and coordinate the components of a cloud application. - -SQS offers two types of queues, Standard and FIFO (First-In-First-Out). Standard queues offer best-effort ordering, which ensures that messages are generally delivered in the order in which they are sent. FIFO queues guarantee that messages are processed exactly once, in the order that they are sent, and they are designed to prevent duplicates. - - -## Amazon SNS - -Amazon Simple Notification Service (SNS) is a fully managed messaging service that enables you to send messages to multiple subscribers or endpoints. SNS supports multiple protocols including HTTP, HTTPS, email, and Lambda, and it can be used to send push notifications to mobile devices, or to process messages asynchronously using AWS Lambda. - -SNS allows you to send a message to a "topic" which is a logical access point and communication channel. Subscribers can then subscribe to that topic to receive the messages. - -SNS also provides a feature called fan-out delivery, which enables messages to be delivered to multiple subscribers in parallel, this allows SNS to handle high-throughput and burst traffic, and can improve the overall performance of your application. - -MassTransit uses SNS to route published messages to SQS queues. diff --git a/doc/content/3.documentation/2.transports/grpc.md b/doc/content/3.documentation/2.transports/grpc.md deleted file mode 100644 index 285c63fdb0c..00000000000 --- a/doc/content/3.documentation/2.transports/grpc.md +++ /dev/null @@ -1,79 +0,0 @@ -# gRPC - -> [MassTransit.Grpc](https://www.nuget.org/packages/MassTransit.Grpc) - -A new gRPC transport, designed to be a peer-to-peer distributed non-durable message transport, is now included. It's entirely in-memory, has zero dependencies, and allows multiple service instances to exchange messages across a shared message fabric. - -::alert{type="warning"} -New, shiny, and very much in the early availability stage. There may be edge cases, so proceed with caution. The gRPC transport will be supported, and issues resolved as they're reported. -:: - -[Introduction Video (YouTube)](https://youtu.be/ChtpCM3N5a8) - -## Broker Topology - -The gRPC is modeled after RabbitMQ, and supports many of the same features. It uses exchanges and queues, and follows the same topology structure as the RabbitMQ transport. - -Fanout, Direct, and Topic exchanges are supported, along with routing key support. - -And, it, is fast. Using the server GC, message throughput is pretty impressive. - -On a single node (essentially in-memory, but serialized via protocol buffers): - -``` -Send: 253,774 msg/s -Consume: 172,996 msg/s -``` - -Across two nodes, load balanced via competing consumer: -``` -Send: 232,597 msg/s -Consume: 36,331 msg/s -``` - -> Consume rate is slower because the messages are evenly split across the local and remote node. - -## Examples - -### Minimal - -```csharp -x.UsingGrpc((context, cfg) => -{ - cfg.Host(h => - { - h.Host = "127.0.0.1"; - h.Port = 19796; - }); - - cfg.ConfigureEndpoints(context); -}); -``` - -To configure the host using a complete address, such as `http://localhost:19796`, a `Uri` can be specified. The following configures a standalone instance, no servers are specified. Incoming connections are of course accepted. - -```csharp -cfg.Host(new Uri("http://localhost:19796")); -``` - -### Multiple Nodes - -To configure a host that connects to other bus instances, use the _AddServer_ method in the host. In this example, the _host_ and _port_ are configured separately. The bus will not start until the server connections are established. - -```csharp -x.UsingGrpc((context, cfg) => -{ - cfg.Host(h => - { - h.Host = "127.0.0.1"; - h.Port = 19796; - - h.AddServer(new Uri("http://127.0.0.1:19797")); - h.AddServer(new Uri("http://127.0.0.1:19798")); - }); - - cfg.ConfigureEndpoints(context); -}); -``` - -Check out [the discussion thread](https://github.com/MassTransit/MassTransit/discussions/2455) for more information. diff --git a/doc/content/3.documentation/3.patterns/10.in-memory-outbox.md b/doc/content/3.documentation/3.patterns/10.in-memory-outbox.md new file mode 100644 index 00000000000..e792fad8aff --- /dev/null +++ b/doc/content/3.documentation/3.patterns/10.in-memory-outbox.md @@ -0,0 +1,111 @@ +# In-Memory Outbox + +This post details the _In-Memory Outbox_, including what it does, how to configure it, and how it ensures eventual consistency in the presence of database or message transport failures. + +MassTransit implements messaging patterns, many of which are designed to ease the transition from a tightly-coupled, database-centric application to a set of services that are highly available, reliable, and eventually consistent. Some of these patterns are obvious, but some of them require a little more explanation to truly understand how they are best utilized. + +### Commands + +Commands are used to do things, like update a database record. Updating a database record usually includes publishing events to notify services that a change in state has occurred. + +In a transactional mindset, updating the database and publishing the event is expected to be performed as a single atomic operation. In distributed systems, performing a distributed transaction between the database and the message broker is unrealistic. + +### Sagas + +In MassTransit, sagas are message handlers that maintain state. An initial message creates a saga _instance_ and subsequent messages may correlate to the same instance. Between messages, the saga instance _state_ is persisted using a database. While consuming a message, a saga may send commands and/or publish events. + +The message flow for a saga includes: + +1. On message receipt, an existing saga instance is loaded from the database. If a matching instance does not exist, a new instance is created. +2. The message is delivered to the saga instance. +3. Once the message is handled, the saga instance is saved or updated in the database. + +Step 2 is where _the magic happens_. The state can be changed, messages can be sent and published, anything. + +So, what's the problem? A few things. + +#### Failures + +An obvious problem is a database failure saving the saga instance. If messages were already sent or published, and the instance was not saved, other services would receive those messages yet the database has not been updated. + +A race condition is another concern, since the events may be consumed before the database update is complete. Yes, message brokers are _fast_, and many times messages are already being consumed long before (in computer time) the database update is started. + +##### So, retry? + +Retrying operations is a key trait of a resilient system. Transient failures happen, even more so in distributed systems, so it makes sense to retry failures in the presence of failures. Of course, not all failures are transient. For instance, trying to take out the trash when it has already been taken out isn't possible (well, until tomorrow). + +> In this example, designing idempotent services such that duplicate commands do not result in duplicate operations would be the best solution. But that's another topic worth studying. + +If retrying the database failure isn't enough, it may make sense to retry the entire message processing sequence – starting at step 1. In this case, the saga instance is discarded, and the message is retried from the beginning. The saga instance is loaded (or created), the message is delivered, and the instance is saved. This is repeated until it is successful or until the retry policy expires and the message is moved to the *_error* queue. + +> Because it's bad. Study _poison message handling_. + +A new retry-related issue is duplicate messages. Messages may be sent or published multiple times – once for each attempt. This can create non-deterministic behavior in services that consume those messages. Therefore, a method to delay messages from being sent until the saga instance is saved is needed. + +### The Outbox + +The outbox holds messages and delivers them after the _transactional_ portion of the message processing has completed. With a saga, the messages are delivered after the saga instance is saved successfully. This ensures that the database is updated before any consumers can start processing any of the produced messages. + +#### The In-Memory Outbox + +The In-Memory Outbox, a feature included with MassTransit, holds published and sent messages in memory until the message is processed successfully (such as the saga being saved to the database). Once the received message has been processed, the message is delivered to the broker and the received message is acknowledged as successful. + +> MassTransit consumes messages in _acknowledgement_ mode. The broker locks the message and the message is invisible to other consumers until it is either acknowledged (ack'd) by the consumer or negatively-acknowledged (n'ack'd) explictly by the consumer or implicitly due to a service or network failure. + +The full configuration is in the [documentation](/documentation/concepts/exceptions#redelivery), a simple example is shown below. + +```cs +cfg.ReceiveEndpoint("r-trashy-saga", e => +{ + e.UseInMemoryOutbox(); + + e.StateMachineSaga(machine, repository); +}); +``` + +> In the example above, retry and redelivery was left out on purpose. The broker will redeliver the message if the process crashes or the network splits. For production services, retry filters should be added to handle transient database errors and ignore failures caused by business constraint violations. + +#### But what if the message doesn't send? + +This question comes up, and it is a fair question. If the broker goes down, the outbox would be unable to deliver the messages. If the process crashes, the messages in the outbox would be lost. Both of these failures can happen, though it is rare. And if computer science has one rule, it is that the rare will always happen. In production. On a Friday afternoon. + +#### Time to Take out the Trash + +Imagine you're twelve, sitting on the sofa, playing video games with your friends. Suddenly, from the other room, you hear your mom call out, "Take out the trash!" Of course, you're in the middle of a battle, and while you've explained many times that you can't pause a multiplayer game, mom just doesn't get it. So you do what any 12-year-old does, you ignore her. The trash remains right where it is, in the kitchen. + +After a while, the lack of a door opening and closing, the still present smell of burnt popcorn from the kitchen, and your mom calls out again, "Take out the trash." At this point, you're dead, in spectator mode, and decide to comply – you take out the trash. Then you slide back onto the sofa and get ready for round two. + +More time passes, the squad is ready and you're about to get on the bus. Your mom, however, didn't hear from you and shouts once more, "I said take out the trash." "Mom, I already took it out" you reply, realizing after that you forgot to mute your mic. The jests and jokes commence as you thank the bus driver and head out. + +#### The Story + +This real-world example includes both failures scenarios that are brought up when considering the in-memory outbox. + +First, it didn't happen. The database may have been unavailable or the service crashed deserializing the message. Either way, it failed. And the message? It's still on the broker. It will be redelivered. _Mom will keep telling you to take out the trash until you take it out._ + +Second, it happened but the messages were not delivered. _You didn't tell her you took it out._ In this case, the message will also be retried. But in this case, this rare case, this is where the previously mentioned term _idempotence_ comes back onto the field. + +When the message is attempted a third time (and face it, the third time is dangerously close to getting a chancla to the head), the database was already updated. The invoice is already approved, _in the database_. The messages weren't sent, however, so other services may not know that the invoice was approved. In this case, for the service to be idempotent, it should assume that: + +1. The message delivery failed because it is being delivered – again. +2. Since the invoice is approved, and this is the approve invoice command, something must have failed after the database was updated. +3. The only thing after the database update is the outbox delivering messages. + +> Study Occam's Razer (okay, yeah, I'm a fan of Razer gaming gear so I'm leaving it spelled that way) + +The correct thing to do at this point is to use the state in the database, along with any information that is contained in the message, to produce the same commands and events that were produced in the previous attempt. Those messages will be delivered by the outbox, and the message will be acknowledged. + +#### !! Victory !! + +That's it, an easy-to-use, reliable solution to perform atomic operations that update a database and send/publish messages, and it works for any database updates that are sent as commands (delivered by durable message queues). + +And a big thank you to Jimmy Bogard, who's tweet prompted me to write this article! + + + +##### Other Reading + +[Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html) +[NServiceBus Outbox](https://docs.particular.net/nservicebus/outbox/) + + diff --git a/doc/content/3.documentation/3.patterns/11.transactional-outbox.md b/doc/content/3.documentation/3.patterns/11.transactional-outbox.md new file mode 100644 index 00000000000..7bf6c480649 --- /dev/null +++ b/doc/content/3.documentation/3.patterns/11.transactional-outbox.md @@ -0,0 +1,54 @@ +# Transactional Outbox + +It is common that a service may need to combine database writes with publishing events and/or sending commands. And in this scenario, it is usually desirable to do this atomically in a transaction. However, message brokers typically do not participate in transactions. Even if a message broker did support transactions, it would require two-phase commit (2PC) which should be avoided whenever possible. + +> While MassTransit has long provided an [in-memory outbox](/documentation/patterns/in-memory-outbox), there has often been criticism that it isn't a _real_ outbox. And while I have proven that it works, is reliable, and is extremely fast (broker message delivery speed), it does require care to ensure operations are idempotent and when an idempotent operation is detected events are republished. The in-memory outbox also does not function as an _inbox_, so exactly-once message delivery is not supported. + +The Transactional Outbox has two main components: + +- The **Bus Outbox** works within a container scope (such as the scope created for an ASP.NET Controller) and adds published and sent messages to the specified `DbContext`. Once the changes are saved, the messages are available to the delivery service which delivers them to the broker. + +- The **Consumer Outbox** is a combination of an _inbox_ and an _outbox_. The _inbox_ is used to keep track of received messages to guarantee exactly-once consumer behavior. The _outbox_ is used to store published and sent messages until the consumer completes successfully. Once completed, the stored messages are delivered to the broker after which the received message is acknowledged. The Consumer Outbox works with all consumer types, including Consumers, Sagas, and Routing Slip Activities. + +Either of these components can be used independently or both at the same time. + +### Bus Outbox Behavior + +Normally when messages are published or sent they are delivered directly to the message broker: + +![Delivery to Broker](/write-to-broker.png "Delivery to Broker") + +When the bus outbox is configured, the scoped interfaces are replaced with versions that write to the outbox. Since `ISendEndpointProvider` and `IPublishEndpoint` are registered as scoped in the container, they are able to share the same scope as the `DbContext` used by the application. + +![Delivery to Outbox](/write-to-outbox.png "Delivery to Outbox") + +Once the changes are saved in the `DbContext` (typically by the application calling `SaveChangesAsync`), the messages will be written to the database as part of the transaction and will be available to the delivery service. + +The delivery service queries the `OutboxMessage` table for messages published or sent via the Bus Outbox, and attempts to deliver any messages found to the message broker. + +![Delivery to Broker](/outbox-to-broker.png "Delivery to Broker") + +The delivery service uses the _OutboxState_ table to ensure that messages are delivered to the broker in the order they were published/sent. The _OutboxState_ table is also used to lock messages so that multiple instances of the delivery service can coexist without conflict. + +### Consumer Outbox Behavior + +Normally, when messages are published or sent by a consumer or one of its dependencies they are delivered directly to the message broker: + +![Regular Consumer Behavior](/consumer-regular.png "Regular Consumer Behavior") + +When the outbox is configured, the behavior changes. As a message is received, the _inbox_ is used to lock the message by `MessageId`. + +![Consumer Inbox](/consumer-inbox.png "Consumer Inbox") + +When the consumer publishes or sends a message, instead of being delivered to the broker it is stored in the _OutboxMessage_ table. + +![Inbox to Outbox](/inbox-outbox.png "Inbox to Outbox") + +Once the consumer completes and the messages are saved to the outbox, those messages are delivered to the message broker in the order they were produced. + +![Deliver Outbox to Broker](/inbox-outbox-broker.png "Deliver Outbox to Broker") + +If there are issues delivering the messages to the broker, message retry will continue to attempt message delivery. + +For details on configuring the transactional outbox, refer to the [configuration](/documentation/configuration/middleware/outbox) section. + diff --git a/doc/content/3.documentation/3.patterns/newid.md b/doc/content/3.documentation/3.patterns/12.newid.md similarity index 100% rename from doc/content/3.documentation/3.patterns/newid.md rename to doc/content/3.documentation/3.patterns/12.newid.md diff --git a/doc/content/3.documentation/3.patterns/13.job-consumers.md b/doc/content/3.documentation/3.patterns/13.job-consumers.md new file mode 100644 index 00000000000..373994dcd4a --- /dev/null +++ b/doc/content/3.documentation/3.patterns/13.job-consumers.md @@ -0,0 +1,662 @@ +# Job Consumers + +In MassTransit, when a message is delivered from the broker to a consumer, it gets locked by the broker until the consumer completes processing. This lock +ensures that the message won’t be delivered to other consumers, even on different bus instances reading from the same queue (the competing consumer pattern). +Once the consumer completes, the message is acknowledged and removed from the queue. However, if the connection to the broker is lost, the message may be +unlocked and redelivered to another consumer. This works well for most cases where the processing time is short. + +But what happens when you need to process a message that takes a long time? In scenarios where a task might take several minutes, hours, or even longer, a +standard consumer may not be the best fit. This is where Job Consumers come in. + +## What are Job Consumers? + +A Job Consumer in MassTransit is a specialized type of consumer designed to handle long-running tasks, often referred to as jobs. Unlike traditional consumers +that lock and process messages quickly, job consumers are built to execute tasks that may take an extended time to complete. They provide additional +functionality to manage long-running tasks, including handling retries, concurrency, and ensuring job completion even in the face of system interruptions. + +Job consumers are implemented using the `IJobConsumer` interface, where `TJob` represents the message type that defines the job. This makes them ideal for +scenarios where jobs could take minutes or even hours, such as video processing, large data transformations, or background tasks that don’t fit within the +typical message lock period provided by brokers like RabbitMQ, Azure Service Bus, or Amazon SQS. + +One critical difference is that job consumers require a saga repository to store and manage the job state. They decouple the task of consuming the message from +the message broker, allowing more flexibility for handling tasks asynchronously and managing retries without relying solely on broker mechanisms. + +#### Why Should You Use a Job Consumer? + +The key question to ask is: How long does the task take to complete? + +- If your task takes less than 5 minutes, a standard consumer is usually sufficient. Brokers like RabbitMQ, Azure, or SQS can hold a message lock for around 5 + minutes, which is often long enough for most tasks. +- If your task exceeds 5 minutes, that’s when you should consider using a job consumer. When tasks exceed the broker’s lock time, you risk message reprocessing + or failures due to lock timeouts. Job consumers are specifically designed to handle these scenarios without worrying about broker timeouts. + +That said, it’s important to recognize that job consumers introduce some computational overhead due to the additional bookkeeping required to manage job state, +retries, and concurrency. Make sure the benefits outweigh the extra complexity before adopting job consumers for your long-running tasks. + +:sample{sample="job-consumer"} + +## Implementation + +To use job consumers, you'll need to create a consumer that implements the `IJobConsumer` interface. + +```csharp +public interface IJobConsumer : + IConsumer + where TJob : class +{ + Task Run(JobContext context); +} +``` + +```csharp +public class ConvertVideoJobConsumer : + IJobConsumer +{ + public async Task Run(JobContext context) + { + await Task.Delay(30000, context.CancellationToken); + } +} +``` + +### Job Context + +In a job consumer, `JobContext` is the job consumer version of `ConsumeContext`. Since job consumers do not run while a message is in flight or locked, +a separate context is used. In addition to the standard message context properties, the job context also includes the following properties. + +| Property | Type | Description | +|----------------------|--------------|-------------------------------------------------------------------------------------------| +| `JobId` | `Guid` | The job's identifier assigned when the job was submitted | +| `AttemptId` | `Guid` | Uniquely identifies this job attempt | +| `RetryAttempt` | `int` | If greater than zero, the retry attempt of the job | +| `LastProgressValue` | `long?` | If a previous job attempt updated the progress, the last updated value stored for the job | +| `LastProgressLimit` | `long?` | If a previous job attempt updated the progress, the last updated limit stored for the job | +| `ElapsedTime` | `TimeSpan` | How long the current job attempt has been running | +| `JobProperties` | `Dictionary` | The properties added when the job was submitted | +| `JobTypeProperties` | `Dictionary` | The properties configured by the `JobOptions` | +| `InstanceProperties` | `Dictionary` | The properties configured by the `JobOptions` on a specific job consumer instance | + + +### Job Cancellation + +When a job is canceled, the `CancellationToken` on `JobContext` is canceled. Job consumers should check for cancellation using +_IsCancellationRequested_ and when it is safe to cancel call: + +`context.CancellationToken.ThrowIfCancellationRequested()` + +This ensured the job is properly reported as canceled to the job saga state machines. + +::alert{type="info"} +When the bus is stopped and there are job consumers configured on the bus, any running jobs are canceled. Canceled jobs will be restarted by the next available +job consumer bus instance (added in MassTransit v8.3.0). +:: + +### Job Progress + +> New in MassTransit v8.3.0 + +Job consumers can track progress and that progress is saved by the job saga. If a job is canceled or faults, the most recently saved progress is included +in the `JobContext` passed to the job consumer if the job is retried. + +To save progress, call `SetJobProgress` as shown below. + +```csharp +public class ConvertVideoJobConsumer : + IJobConsumer +{ + public async Task Run(JobContext context) + { + // some aspects of the content being process + long length = File.Length; + + await context.SetJobProgress(0, length); + + for (int index = 1; index <= length; index++) + { + // do something + + context.SetJobProgress(index, length); + } + } +} +``` + +### Job State + +> New in MassTransit v8.3.0 + +Job consumers can save state in the job saga. In the event that a job is canceled or faults, when the job is retried the previously saved state will be +included in the `JobContext` passed to the job consumer. + +To save the job state when a job is canceled: + +```csharp +public class ConvertVideoJobConsumer : + IJobConsumer +{ + public async Task Run(JobContext context) + { + // some aspects of the content being process + long length = File.Length; + + int index = 1; + try + { + await context.SetJobProgress(0, length); + + for (; index <= length; index++) + { + context.CancellationToken.ThrowIfCancellationRequested(); + + // do something + + context.SetJobProgress(index, length); + } + } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + await context.SaveJobState(new ConsumerState { LastIndex = index }); + throw; + } + } + + class ConsumerState + { + public long LastIndex { get; set; } + } +} +``` + +When the job is started, the consumer can check if a previously saved job state exists, and use it to continue where processing left off. + +```csharp +public class ConvertVideoJobConsumer : + IJobConsumer +{ + public async Task Run(JobContext context) + { + // some aspects of the content being process + long length = File.Length; + + int index = context.TryGetJobState(out ConsumerState? state) + ? state.LastIndex + 1 + : 1; + + // elided, see above + } +} +``` + +The job state type (in this case, `ConsumerState`) is only relevant to the job consumer and is stored as a serialized dictionary in the job saga. + +## Configuration + +The example below configures a job consumer that is automatically configured by `ConfigureEndpoints`. + +```csharp +services.AddMassTransit(x => +{ + x.AddConsumer(cfg => + { + cfg.Options>(options => options + .SetJobTimeout(TimeSpan.FromMinutes(15)) + .SetConcurrentJobLimit(10)); + }); + + x.AddDelayedMessageScheduler(); + + x.SetKebabCaseEndpointNameFormatter(); + + // in this case, just use the in-memory saga repository, + // but an actual database should be used + x.SetInMemorySagaRepositoryProvider(); + + x.AddJobSagaStateMachines(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +::alert{type="info"} +The old syntax of creating a service instance and manually configuring job consumers is completely deprecated and will no longer be supported. +:: + +In this example, the job timeout as well as the number of concurrent jobs allowed is configured via `JobOptions` when configuring the consumer. The job +options can also be specified using a consumer definition in the same way. + +### Job Options + +There are several job options that can be configured, including: + +| Option | Type | Description | +|------------------------------|:---------------|------------------------------------------------------------------------------------------------------------------------------| +| `ConcurrentJobLimit` | `int` | The number of concurrent jobs allowed to run on a given instance | +| `JobTimeout` | `TimeSpan` | How long a job consumer is allowed to run before the job is canceled (via the CancellationToken) | +| `JobCancellationTimeout` | `TimeSpan` | How long after a job consumer is canceled to wait before considering the job canceled regardless of whether it has completed | +| `JobTypeName` | `string` | Override the default job type name (optional, not really recommended) | +| `RetryPolicy` | `IRetryPolicy` | The retry policy applied when a job faults | +| `ProgressBuffer.TimeLimit` | `TimeSpan` | How often any progress updates should be reported to the job saga | +| `ProgressBuffer.UpdateLimit` | `TimeSpan` | How many progress updates should be reported before updating the job saga | +| `JobTypeProperties` | `Dictionary` | Properties associated with the job type (should be the same on every job consumer bus instance) | +| `InstanceProperties` | `Dictionary` | Properties associated with the currently running job consumer bus instance | + +### Job Saga Options + +When adding the job saga state machines, the `JobSagaOptions` can also be configured. + +```csharp +x.AddJobSagaStateMachines(options => +{ + options.FinalizeCompleted = true; + options.ConcurrentMessageLimit = 32; + options.HeartbeatTimeout = TimeSpan.FromMinutes(5); + options.SlotWaitTime = TimeSpan.FromSeconds(30); + options.SuspectJobRetryCount = 2; + options.SuspectJobRetryDelay = TimeSpan.FromMinutes(1); +}); +``` + +| Option | Type | Description | +|--------------------------|:-----------|--------------------------------------------------------------------------------------------------------------------------| +| `FinalizeCompleted` | `bool` | Automatically remove completed job sagas | +| `ConcurrentMessageLimit` | `int` | Specifies the concurrency of the job service sagas (not the actual jobs) | +| `HeartbeatInterval` | `TimeSpan` | Period of time after which a job consumer bus instance is removed from the active list if no heartbeat has been received | +| `SlotWaitTime` | `TimeSpan` | How long a job waits for an available job slot between attempts | +| `SuspectJobRetryCount` | `int` | How many times to retry a job that becomes suspect (not faulted, but the job consumer bus instance stops responding) | +| `SuspectJobRetryDelay` | `TimeSpan` | How long a job should wait retrying when the job became suspect and `SuspectJobRetryCount` is > 0 | + +## Job Commands + +### Submit Job + +To submit a job, call the `SubmitJob` extension method on an `IPublishEndpoint` as shown below. This is a fire-and-forget method, no response is sent. + +```csharp +[HttpPut("{path}")] +public async Task FireAndForgetSubmitJob(string path, [FromServices] IPublishEndpoint publishEndpoint) +{ + var jobId = await publishEndpoint.SubmitJob(new ConvertVideo + { + Path = path + }); + + return Ok(new + { + jobId, + path + }); +} +``` + +To wait for a response indicating the job submission was successful (not really necessary, but commonly used), use the request client, `IRequestClient`, +submit a job using the `SubmitJob` extension method as shown below. The _RequestId_ generated by the request client will be used as the _JobId_. + +```csharp +[HttpPost("{path}")] +public async Task SubmitJob(string path, [FromServices] IRequestClient client) +{ + var jobId = await client.SubmitJob(new ConvertVideo + { + Path = path + }); + + return Ok(new + { + jobId, + path + }); +} +``` + +Additionally, a `jobId` can be specified if the `IRequestClient>` interface is used instead. + +```csharp +[HttpPost("{path}")] +public async Task SubmitJob(string path, [FromServices] IRequestClient> client) +{ + var jobId = NewId.NextGuid(); + + await client.SubmitJob(jobId, new ConvertVideo + { + Path = path + }); + + return Ok(new + { + jobId, + path + }); +} +``` + +To submit a job including job properties (such as a _tenantId_ or other property value typically reflecting some cross-cutting concern or environmental +setting such a data center location, country, etc.), use the overload as shown. + +```csharp +[HttpPost("{path}")] +public async Task SubmitJob(string path, [FromServices] IRequestClient> client) +{ + var jobId = NewId.NextGuid(); + + await client.SubmitJob(jobId, new ConvertVideo + { + Path = path + }, x => x.Set("TenantId", _tenantId)); + + return Ok(new + { + jobId, + path + }); +} +``` + +### Cancel Job + +To cancel a submitted job, call the `CancelJob` extension method on an `IPublishEndpoint` as shown. + +```csharp +[HttpPut("{jobId}")] +public async Task CancelJob(Guid jobId, [FromServices] IPublishEndpoint publishEndpoint) +{ + var jobId = await publishEndpoint.CancelJob(jobId); + + return Ok(); +} +``` + +### Retry Job + +To retry a faulted or canceled job, call the `RetryJob` extension method on an `IPublishEndpoint` as shown. + +```csharp +[HttpPut("{jobId}")] +public async Task RetryJob(Guid jobId, [FromServices] IPublishEndpoint publishEndpoint) +{ + var jobId = await publishEndpoint.RetryJob(jobId); + + return Ok(); +} +``` + +### Finalize Job + +When a job is canceled or faults, the job is not removed from the saga repository. To remove a job in the Canceled or Faulted state, use the `FinalizeJob` +method as shown. + +```csharp +[HttpPut("{jobId}")] +public async Task FinalizeJob(Guid jobId, [FromServices] IPublishEndpoint publishEndpoint) +{ + var jobId = await publishEndpoint.FinalizeJob(jobId); + + return Ok(); +} +``` + + +### Schedule Job + +> New in MassTransit v8.3.0 + +By default, submitted jobs will run as soon as possible. Jobs can also be scheduled by specifying a start time, using the `ScheduleJob` method. + +```csharp +[HttpPost("{path}")] +public async Task SubmitJob(string path, [FromServices] IRequestClient> client) +{ + await client.ScheduleJob(DateTimeOffset.Now.AddMinutes(15), new ConvertVideo + { + Path = path + }); + + return Ok(new + { + jobId, + path + }); +} +``` + +## Recurring Jobs + +> New in MassTransit v8.3.0 + +MassTransit supports recurring jobs, which are useful when a consumer needs to run on a predefined schedule. Recurring jobs use regular job consumers and are +scheduled using the transport's built-in message scheduling and the job saga state machines. + +::alert{type="info"} +Recurring jobs are entirely built into MassTransit, and require no additional application infrastructure. This means Quartz.NET or Hangfire are NOT required. +:: + +Recurring jobs are configuring using a cron expression. Cron expressions are a well known way to define a schedule and can be built using any of the +[online cron expression builders](https://crontab.cronhub.io/). Cron expressions can be very expressive. For instance, `0 0,15,30,45 * * * 1,3,5` would +mean _at 0, 15, 30, and 45 minutes past the hour, only on Monday, Wednesday, and Friday_. + +### Schedule Recurring Job + +To schedule a recurring job, use the `AddOrUpdateRecurringJob` method as shown below. + +```csharp +public async Task ConfigureRecurringJobs(IPublishEndpoint endpoint) +{ + await endpoint.AddOrUpdateRecurringJob("RoutineMaintenance", + new RoutineMaintenanceCommand(), "0 0,15,30,45 * * * 1,3,5"); +} + +public record RoutingMaintenanceCommand; +``` + +A simple expression builder is also available, which can be used to generate a cron expression. + +```csharp +public async Task ConfigureRecurringJobs(IPublishEndpoint endpoint) +{ + await endpoint.AddOrUpdateRecurringJob("RoutineMaintenance", + new RoutineMaintenanceCommand(), x => x.Every(minutes: 15)); +} +``` + +Recurring jobs can also be confined to a period of time, using start and end dates. Specifying a start date will apply the cron expression to run at the +first opportunity after the specified start date. Specifying an end date will ensure the job is not run after that date. + +```csharp +public async Task ConfigureRecurringJobs(IPublishEndpoint endpoint) +{ + await endpoint.AddOrUpdateRecurringJob("RoutineMaintenance", + new RoutineMaintenanceCommand(), x => + { + x.Start = new DateTimeOffset(2024, 1, 1, 0, 0, 0, TimeSpan.Zero); + x.End = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.Zero); + x.Every(minutes: 30); + }); +} +``` + +Calling `AddOrUpdateRecurringJob` can be used to update the job message or the schedule. If the schedule is changed, the next run will be rescheduled using the +newly specified cron expression. + +### Run Recurring Job + +To force a recurring job to run immediately, use the `RunRecurringJob` method. + +```csharp +public async Task RunJobNow(IPublishEndpoint publishEndpoint) +{ + await publishEndpoint.RunRecurringJob("RoutineMaintenance"); +} +``` + +On the job has completed, the job next job run will be scheduled using the previously supplied cron expression. + +## Job Saga Endpoints + +The job saga state machines are configured on their own endpoints, using the configured endpoint name formatter. These endpoints are required on _at +least one_ bus instance. Additionally, it is not necessary to configure them on _every_ bus instance. In the example above, the job saga state machines +endpoints +are configured. A standard __ConfigureEndpoints__ call will suffice to host the job consumers without the job saga state machines. + +### Job Saga Repository + +A persistent saga repository should be used with job consumers, and should be configured when adding the job saga state machines. In the example below, +Entity Framework Core is configured, along with the Postgres lock statement provider (required when using PostgreSQL). + +```csharp +x.AddJobSagaStateMachines() + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.UsePostgres(); + }); +``` + +For a more detailed example of configuring the job saga state machine endpoints, including persistent storage, see the sample mentioned in the box above. + +## Job Saga State Machines + +Job consumers use three saga state machines to orchestrate jobs and keep track of available job consumer bus instances. + +| Variable | Description | +|----------------|-------------------------------------------------------------------------------------------| +| JobSaga | Orchestrates each job, including scheduling, retry, and failure handling | +| JobAttemptSaga | Orchestrates each job attempt, communicating directly with the job consumer bus instances | +| JobTypeSaga | Keep track of available job instances and allocates job slots to waiting jobs | + +### Job Saga States + +A job can be in one of the following states: + +1. Initial +2. Final +3. Submitted +4. Waiting to Start +5. Waiting for Slot +6. Started +7. Completed +8. Faulted +9. Canceled +10. Starting Job Attempt +11. Allocating Job Slot +12. Waiting to Retry +13. Cancellation Pending + +### Job Attempt Saga States + +A job attempt can be in one of the following states: + +1. Initial +2. Final +3. Starting +4. Running +5. Faulted +6. Checking Status +7. Suspect + +### Job Type Saga States + +A job type has only two valid states + +1. Initial +2. Final +3. Active +4. Idle + +## Job Distribution Strategy + +> New in MassTransit v8.3.0 + +To support more complex job consumer scenarios, MassTransit enables the use of a custom job distribution strategy. This strategy is employed by the job type +saga to decide which job consumer bus instance should handle a particular job. By configuring `JobProperties` and `InstanceProperties` within `JobOptions`, you +can control how jobs are assigned to specific consumer instances. For example, you might allocate jobs from premium customers to consumer instances running on +premium hardware, ensuring that resource-intensive jobs are handled by more capable instances. + +To use a custom strategy, create a class that implements `IJobDistributionStrategy`. + +```csharp +public class MachineTypeJobDistributionStrategy : + IJobDistributionStrategy +{ + public Task IsJobSlotAvailable(ConsumeContext context, JobTypeInfo jobTypeInfo) + { + object? strategy = null; + jobTypeInfo.Properties?.TryGetValue("DistributionStrategy", out strategy); + + return strategy switch + { + "MachineType" => MachineType(context, jobTypeInfo), + _ => DefaultJobDistributionStrategy.Instance.IsJobSlotAvailable(context, jobTypeInfo) + }; + } + + Task MachineType(ConsumeContext context, JobTypeInfo jobTypeInfo) + { + var customerType = context.GetHeader("CustomerType"); + + var machineType = customerType switch + { + "Premium" => "S-Class", + _ => "E-Class" + }; + + var instances = from i in jobTypeInfo.Instances + join a in jobTypeInfo.ActiveJobs on i.Key equals a.InstanceAddress into ai + where (ai.Count() < jobTypeInfo.ConcurrentJobLimit + && string.IsNullOrEmpty(dataCenter)) + || (i.Value.Properties.TryGetValue("MachineType", out var mt) && mt is string mtext && mtext == machineType) + orderby ai.Count(), i.Value.Used + select new + { + Instance = i.Value, + InstanceAddress = i.Key, + InstanceCount = ai.Count() + }; + + var firstInstance = instances.FirstOrDefault(); + if (firstInstance == null) + return Task.FromResult(null); + + return Task.FromResult(new ActiveJob + { + JobId = context.Message.JobId, + InstanceAddress = firstInstance.InstanceAddress + }); + } +} +``` + +Then register the strategy using the `TryAddJobDistributionStrategy` method: + +```csharp +services.TryAddJobDistributionStrategy(); +``` + +The strategy must be registered where the job saga state machines are registered and is not required on the job consumer bus instances. + +The job distribution strategy is resolved from the container as a _scoped_ service and any class dependencies will be resolved using the current scope +of the job type saga state machine. This allows dependencies to be injected, including the current `DbContext` if using Entity Framework Core. + +### Job Strategy Options + +To support the use of job distribution strategies, new properties were added to `JobOptions`. Following the example above, the `MachineType` property +should be added at startup. + +```csharp +x.AddConsumer(c => + c.Options>(options => options + .SetRetry(r => r.Interval(3, TimeSpan.FromSeconds(30))) + .SetJobTimeout(TimeSpan.FromMinutes(10)) + .SetConcurrentJobLimit(10) + .SetJobTypeProperties(p => p.Set("DistributionStrategy", "MachineType")) + .SetInstanceProperties(p => p.Set("MachineType", "S-Class"))); +``` + +The properties should be set using environmental information, such as machine type, data center location, or whatever makes sense for the desired strategy. +`JobProperties` apply to the job type and `InstanceProperties` apply to the job consumer bus instance (the bus instance containing the job consumer). + + + diff --git a/doc/content/3.documentation/3.patterns/6.saga/0.index.md b/doc/content/3.documentation/3.patterns/6.saga/0.index.md index 4d67af0ba29..6a9b2302b09 100755 --- a/doc/content/3.documentation/3.patterns/6.saga/0.index.md +++ b/doc/content/3.documentation/3.patterns/6.saga/0.index.md @@ -48,7 +48,7 @@ public class OrderStateDefinition : ## Guidance -To address some common questions related to sagas, retries, Outbox, and concurrency, this [page](guidance) has been compiled. +To address some common questions related to sagas, retries, Outbox, and concurrency, this [page](/documentation/patterns/saga/guidance) has been compiled. [1]: http://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf diff --git a/doc/content/3.documentation/3.patterns/6.saga/2.state-machine.md b/doc/content/3.documentation/3.patterns/6.saga/2.state-machine.md index 4a8c4e808b6..2a57745953e 100755 --- a/doc/content/3.documentation/3.patterns/6.saga/2.state-machine.md +++ b/doc/content/3.documentation/3.patterns/6.saga/2.state-machine.md @@ -10,15 +10,18 @@ toc: true ## Introduction -Automatonymous is a state machine library for .NET and provides a C# syntax to define a state machine, including states, events, and behaviors. MassTransit includes Automatonymous, and adds instance storage, event correlation, message binding, request and response support, and scheduling. +Automatonymous is a state machine library for .NET and provides a C# syntax to define a state machine, including states, events, and behaviors. MassTransit +includes Automatonymous, and adds instance storage, event correlation, message binding, request and response support, and scheduling. ::alert{type="info"} -Automatonymous is no longer a separate NuGet package and has been assimilated by _MassTransit_. In previous versions, an additional package reference was required. If _Automatonymous_ is referenced, that reference must be removed as it is no longer compatible. +Automatonymous is no longer a separate NuGet package and has been assimilated by _MassTransit_. In previous versions, an additional package reference was +required. If _Automatonymous_ is referenced, that reference must be removed as it is no longer compatible. :: ### State Machine -A state machine defines the states, events, and behavior of a finite state machine. Implemented as a class, which is derived from `MassTransitStateMachine`, a state machine is created once, and then used to apply event triggered behavior to state machine _instances_. +A state machine defines the states, events, and behavior of a finite state machine. Implemented as a class, which is derived from `MassTransitStateMachine`, +a state machine is created once, and then used to apply event triggered behavior to state machine _instances_. ```csharp public class OrderStateMachine : @@ -29,7 +32,9 @@ public class OrderStateMachine : ### Instance -An instance contains the data for a state machine _instance_. A new instance is created for every consumed _initial_ event where an existing instance with the same _CorrelationId_ was not found. A saga repository is used to persist instances. Instances are classes, and must implement the `SagaStateMachineInstance` interface. +An instance contains the data for a state machine _instance_. A new instance is created for every consumed _initial_ event where an existing instance with the +same _CorrelationId_ was not found. A saga repository is used to persist instances. Instances are classes, and must implement the `SagaStateMachineInstance` +interface. ```csharp public class OrderState : @@ -83,7 +88,9 @@ This results in the following values: 0 - None, 1 - Initial, 2 - Final, 3 - Subm ### State -States represent previously consumed events resulting in an instance being in a current _state_. An instance can only be in one state at a given time. A new instance defaults to the _Initial_ state, which is automatically defined. The _Final_ state is also defined for all state machines and is used to signify the instance has reached the final state. +States represent previously consumed events resulting in an instance being in a current _state_. An instance can only be in one state at a given time. A new +instance defaults to the _Initial_ state, which is automatically defined. The _Final_ state is also defined for all state machines and is used to signify the +instance has reached the final state. In the example, two states are declared. States are automatically initialized by the _MassTransitStateMachine_ base class constructor. @@ -98,7 +105,8 @@ public class OrderStateMachine : ### Event -An event is something that happened which may result in a state change. An event can add or update instance data, as well as changing an instance's current state. The `Event` is generic, where `T` must be a valid message type. +An event is something that happened which may result in a state change. An event can add or update instance data, as well as changing an instance's current +state. The `Event` is generic, where `T` must be a valid message type. In the example below, the _SubmitOrder_ message is declared as an event including how to correlate the event to an instance. @@ -124,9 +132,11 @@ public class OrderStateMachine : ### Behavior -Behavior is what happens when an _event_ occurs during a _state_. +Behavior is what happens when an _event_ occurs during a _state_. -Below, the _Initially_ block is used to define the behavior of the _SubmitOrder_ event during the _Initial_ state. When a _SubmitOrder_ message is consumed and an instance with a _CorrelationId_ matching the _OrderId_ is not found, a new instance will be created in the _Initial_ state. The _TransitionTo_ activity transitions the instance to the _Submitted_ state, after which the instance is persisted using the saga repository. +Below, the _Initially_ block is used to define the behavior of the _SubmitOrder_ event during the _Initial_ state. When a _SubmitOrder_ message is consumed and +an instance with a _CorrelationId_ matching the _OrderId_ is not found, a new instance will be created in the _Initial_ state. The _TransitionTo_ activity +transitions the instance to the _Submitted_ state, after which the instance is persisted using the saga repository. ```csharp public class OrderStateMachine : @@ -169,8 +179,9 @@ public class OrderStateMachine : Message brokers typically do not guarantee message order. Therefore, it is important to consider out-of-order messages in state machine design. -In the example above, receiving a _SubmitOrder_ message after an _OrderAccepted_ event could cause the _SubmitOrder_ message to end up in the *_error* queue. If the _OrderAccepted_ event is received first, it would be discarded since it isn't accepted in the _Initial_ state. Below is an updated state machine that handles both of these scenarios. - +In the example above, receiving a _SubmitOrder_ message after an _OrderAccepted_ event could cause the _SubmitOrder_ message to end up in the *_error* queue. If +the _OrderAccepted_ event is received first, it would be discarded since it isn't accepted in the _Initial_ state. Below is an updated state machine that +handles both of these scenarios. ```csharp public class OrderStateMachine : @@ -194,7 +205,8 @@ public class OrderStateMachine : } ``` -In the updated example, receiving a _SubmitOrder_ message while in an _Accepted_ state ignores the event. However, data in the event may be useful. In that case, adding behavior to copy the data to the instance could be added. Below, data from the event is captured in both scenarios. +In the updated example, receiving a _SubmitOrder_ message while in an _Accepted_ state ignores the event. However, data in the event may be useful. In that +case, adding behavior to copy the data to the instance could be added. Below, data from the event is captured in both scenarios. ```csharp public interface SubmitOrder @@ -248,13 +260,15 @@ services.AddMassTransit(x => }); ``` -The example above uses the in-memory saga repository, but any saga repository could be used. The [persistence](/documentation/configuration#persistence) section includes details on the supported saga repositories. +The example above uses the in-memory saga repository, but any saga repository could be used. The [persistence](/documentation/configuration#persistence) section +includes details on the supported saga repositories. To test the state machine, see the [testing](/documentation/concepts/testing#saga-state-machine) section. ## Event -As shown above, an event is a message that can be consumed by the state machine. Events can specify any valid message type, and each event may be configured. There are several event configuration methods available. +As shown above, an event is a message that can be consumed by the state machine. Events can specify any valid message type, and each event may be configured. +There are several event configuration methods available. The built-in `CorrelatedBy` interface can be used in a message contract to specify the event `CorrelationId`. @@ -274,9 +288,11 @@ public class OrderStateMachine : } ``` -While the event is declared explicitly above, it is not required. The default convention will automatically configure events that have a `CorrelatedBy` interface. +While the event is declared explicitly above, it is not required. The default convention will automatically configure events that have a `CorrelatedBy` +interface. -While convenient, some consider the interface an intrusion of infrastructure to the message contract. MassTransit also supports a declarative approach to specifying the `CorrelationId` for events. By configuring the global message topology, it is possible to specify a message property to use for correlation. +While convenient, some consider the interface an intrusion of infrastructure to the message contract. MassTransit also supports a declarative approach to +specifying the `CorrelationId` for events. By configuring the global message topology, it is possible to specify a message property to use for correlation. ```csharp public interface SubmitOrder @@ -324,13 +340,16 @@ public class OrderStateMachine : } ``` -Since `OrderId` is a `Guid`, it can be used for event correlation. When `SubmitOrder` is accepted in the _Initial_ state, and because the _OrderId_ is a _Guid_, the `CorrelationId` on the new instance is automatically assigned the _OrderId_ value. +Since `OrderId` is a `Guid`, it can be used for event correlation. When `SubmitOrder` is accepted in the _Initial_ state, and because the _OrderId_ is a _Guid_, +the `CorrelationId` on the new instance is automatically assigned the _OrderId_ value. -Events can also be correlated using a query expression, which is required when events are not correlated to the instance's _CorrelationId_ property. Queries are more expensive, and may match multiple instances, which should be considered when designing state machines and events. +Events can also be correlated using a query expression, which is required when events are not correlated to the instance's _CorrelationId_ property. Queries are +more expensive, and may match multiple instances, which should be considered when designing state machines and events. -> Whenever possible, try to correlation using the CorrelationId. If a query is required, it may be necessary to create an index on the property so that database queries are optimized. +> Whenever possible, try to correlation using the CorrelationId. If a query is required, it may be necessary to create an index on the property so that database +> queries are optimized. -To correlate events using another type, additional configuration is required. +To correlate events using another type, additional configuration is required. ```csharp public interface ExternalOrderSubmitted @@ -374,10 +393,16 @@ public class OrderStateMachine : } ``` -When the event doesn't have a _Guid_ that uniquely correlates to an instance, the `.SelectId` expression must be configured. In the above example, [NewId](https://www.nuget.org/packages/NewId) is used to generate a sequential identifier which will be assigned to the instance _CorrelationId_. Any property on the event can be used to initialize the _CorrelationId_. +When the event doesn't have a _Guid_ that uniquely correlates to an instance, the `.SelectId` expression must be configured. In the above +example, [NewId](https://www.nuget.org/packages/NewId) is used to generate a sequential identifier which will be assigned to the instance _CorrelationId_. Any +property on the event can be used to initialize the _CorrelationId_. ::alert{type="warning"} -Initial events that do not correlate on CorrelationId, and use `SelectId` to generate a _CorrelationId_ should use a unique constraint on the instance property (_OrderNumber_ in this example) to avoid duplicate instances. If two events correlate to the same property value at the same time, only one of the two will be able to store the instance, the other will fail (and, if retry is configured, which it should be when using a saga) and retry at which time the event will be dispatched based upon the current instance state (which is likely no longer Initial). Failure to apply a unique constraint (on _OrderNumber_ in this example) will result in duplicates. +Initial events that do not correlate on CorrelationId, and use `SelectId` to generate a _CorrelationId_ should use a unique constraint on the instance +property (_OrderNumber_ in this example) to avoid duplicate instances. If two events correlate to the same property value at the same time, only one of the two +will be able to store the instance, the other will fail (and, if retry is configured, which it should be when using a saga) and retry at which time the event +will be dispatched based upon the current instance state (which is likely no longer Initial). Failure to apply a unique constraint (on _OrderNumber_ in this +example) will result in duplicates. :: The message headers are also available, for example, instead of always generating a new identifier, the _CorrelationId_ header could be used if present. @@ -387,12 +412,14 @@ The message headers are also available, for example, instead of always generatin ``` ::alert{type="info"} -Event correlation is critical, and should be consistently applied to all events on a state machine. Consider how events received in different orders may affect subsequent event correlations. +Event correlation is critical, and should be consistently applied to all events on a state machine. Consider how events received in different orders may affect +subsequent event correlations. :: ### Ignore Event -It may be necessary to ignore an event in a given state, either to avoid fault generation, or to prevent messages from being moved to the *_skipped* queue. To ignore an event in a state, use the `Ignore` method. +It may be necessary to ignore an event in a given state, either to avoid fault generation, or to prevent messages from being moved to the *_skipped* queue. To +ignore an event in a state, use the `Ignore` method. ```csharp public class OrderStateMachine : @@ -418,7 +445,8 @@ public class OrderStateMachine : ### Composite Event -A composite event is configured by specifying one or more events that must be consumed, after which the composite event will be raised. A composite event uses an instance property to keep track of the required events, which is specified during configuration. +A composite event is configured by specifying one or more events that must be consumed, after which the composite event will be raised. A composite event uses +an instance property to keep track of the required events, which is specified during configuration. To define a composite event, the required events must first be configured along with any event behaviors, after which the composite event can be configured. @@ -461,7 +489,8 @@ public class OrderStateMachine : Once the _SubmitOrder_ and _OrderAccepted_ events have been consumed, the _OrderReady_ event will be triggered. ::alert{type="warning"} -The order of events being declared can impact the order in which they execute. Therefore, it is best to declare composite events at the end of the state machine declaration, after all other events and behaviors are declared. That way, the composite events will be raised _after_ the dependent event behaviors. +The order of events being declared can impact the order in which they execute. Therefore, it is best to declare composite events at the end of the state machine +declaration, after all other events and behaviors are declared. That way, the composite events will be raised _after_ the dependent event behaviors. :: ### Missing Instance @@ -500,11 +529,14 @@ public class OrderStateMachine : ``` -In this example, when a cancel order request is consumed without a matching instance, a response will be sent that the order was not found. Instead of generating a `Fault`, the response is more explicit. Other missing instance options include `Discard`, `Fault`, and `Execute` (a synchronous version of _ExecuteAsync_). +In this example, when a cancel order request is consumed without a matching instance, a response will be sent that the order was not found. Instead of +generating a `Fault`, the response is more explicit. Other missing instance options include `Discard`, `Fault`, and `Execute` (a synchronous version of +_ExecuteAsync_). ### Initial Insert -To increase new instance performance, configuring an event to directly insert into a saga repository may reduce lock contention. To configure an event to insert, it should be in the _Initially_ block, as well as have a saga factory specified. +To increase new instance performance, configuring an event to directly insert into a saga repository may reduce lock contention. To configure an event to +insert, it should be in the _Initially_ block, as well as have a saga factory specified. ```csharp public interface SubmitOrder @@ -537,7 +569,10 @@ public class OrderStateMachine : } ``` -When using _InsertOnInitial_, it is critical that the saga repository is able to detect duplicate keys (in this case, _CorrelationId_ - which is initialized using _OrderId_). In this case, having a clustered primary key on _CorrelationId_ would prevent duplicate instances from being inserted. If an event is correlated using a different property, make sure that the database enforces a unique constraint on the instance property and the saga factory initializes the instance property with the event property value. +When using _InsertOnInitial_, it is critical that the saga repository is able to detect duplicate keys (in this case, _CorrelationId_ - which is initialized +using _OrderId_). In this case, having a clustered primary key on _CorrelationId_ would prevent duplicate instances from being inserted. If an event is +correlated using a different property, make sure that the database enforces a unique constraint on the instance property and the saga factory initializes the +instance property with the event property value. ```csharp public interface ExternalOrderSubmitted @@ -572,11 +607,13 @@ public class OrderStateMachine : } ``` -The database would use a unique constraint on the _OrderNumber_ to prevent duplicates, which the saga repository would detect as an existing instance, which would then be loaded to consume the event. +The database would use a unique constraint on the _OrderNumber_ to prevent duplicates, which the saga repository would detect as an existing instance, which +would then be loaded to consume the event. ### Completed Instance -By default, instances are not removed from the saga repository. To configure completed instance removal, specify the method used to determine if an instance has completed. +By default, instances are not removed from the saga repository. To configure completed instance removal, specify the method used to determine if an instance has +completed. ```csharp public interface OrderCompleted @@ -602,7 +639,8 @@ public class OrderStateMachine : } ``` -When the instance consumes the _OrderCompleted_ event, the instance is finalized (which transitions the instance to the _Final_ state). The `SetCompletedWhenFinalized` method defines an instance in the _Final_ state as completed – which is then used by the saga repository to remove the instance. +When the instance consumes the _OrderCompleted_ event, the instance is finalized (which transitions the instance to the _Final_ state). +The `SetCompletedWhenFinalized` method defines an instance in the _Final_ state as completed – which is then used by the saga repository to remove the instance. To use a different completed expression, such as one that checks if the instance is in a _Completed_ state, use the `SetCompleted` method as shown below. @@ -638,7 +676,8 @@ public class OrderStateMachine : ## Activities -State machine behaviors are defined as a sequence of activities which are executed in response to an event. In addition to the activities included with Automatonymous, MassTransit includes activities to send, publish, and schedule messages, as well as initiate and respond to requests. +State machine behaviors are defined as a sequence of activities which are executed in response to an event. In addition to the activities included with +Automatonymous, MassTransit includes activities to send, publish, and schedule messages, as well as initiate and respond to requests. ### Publish @@ -752,7 +791,9 @@ public class OrderStateMachine : ### Respond -A state machine can respond to requests by configuring the request message type as an event, and using the `Respond` method. When configuring a request event, configuring a missing instance method is recommended, to provide a better response experience (either through a different response type, or a response that indicates an instance was not found). +A state machine can respond to requests by configuring the request message type as an event, and using the `Respond` method. When configuring a request event, +configuring a missing instance method is recommended, to provide a better response experience (either through a different response type, or a response that +indicates an instance was not found). ```csharp public interface RequestOrderCancellation @@ -796,7 +837,8 @@ public class OrderStateMachine : } ``` -There are scenarios where it is required to _wait_ for the response from the state machine. In these scenarios the information that is required to respond to the original request should be stored. +There are scenarios where it is required to _wait_ for the response from the state machine. In these scenarios the information that is required to respond to +the original request should be stored. ```csharp public record CreateOrder(Guid CorrelationId) : CorrelatedBy; @@ -885,7 +927,8 @@ public class OrderStateMachine : MassTransitStateMachine ### Schedule ::alert{type="info"} -The bus must be configured to include a message scheduler to use the scheduling activities. See the [scheduling](/documentation/configuration/scheduling/) section to learn how to set up a message scheduler. +The bus must be configured to include a message scheduler to use the scheduling activities. See the [scheduling](/documentation/configuration/scheduling/) +section to learn how to set up a message scheduler. :: A state machine can schedule events, which uses the message scheduler to schedule a message for delivery to the instance. First, the schedule must be declared. @@ -922,7 +965,9 @@ public class OrderStateMachine : } ``` -The configuration specifies the _Delay_, which can be overridden by the schedule activity, and the correlation expression for the _Received_ event. The state machine can consume the _Received_ event as shown. The _OrderCompletionTimeoutTokenId_ is a `Guid?` instance property used to keep track of the scheduled message _tokenId_ which can later be used to unschedule the event. +The configuration specifies the _Delay_, which can be overridden by the schedule activity, and the correlation expression for the _Received_ event. The state +machine can consume the _Received_ event as shown. The _OrderCompletionTimeoutTokenId_ is a `Guid?` instance property used to keep track of the scheduled +message _tokenId_ which can later be used to unschedule the event. ```csharp public interface OrderCompleted @@ -961,7 +1006,8 @@ public class OrderStateMachine : } ``` -As stated above, the _delay_ can be overridden by the _Schedule_ activity. Both instance and message (_context.Data_) content can be used to calculate the delay. +As stated above, the _delay_ can be overridden by the _Schedule_ activity. Both instance and message (_context.Data_) content can be used to calculate the +delay. ```csharp public interface OrderAccepted @@ -1011,19 +1057,28 @@ public class OrderStateMachine : ### Request -A request can be sent by a state machine using the _Request_ method, which specifies the request type and the response type. Additional request settings may be specified, including the _ServiceAddress_ and the _Timeout_. +A request can be sent by a state machine using the _Request_ method, which specifies the request type and the response type. Additional request settings may be +specified, including the _ServiceAddress_ and the _Timeout_. -If the _ServiceAddress_ is specified, it should be the endpoint address of the service that will respond to the request. If not specified, the request will be published. +If the _ServiceAddress_ is specified, it should be the endpoint address of the service that will respond to the request. If not specified, the request will be +published. -The default _Timeout_ is thirty seconds but any value greater than or equal to `TimeSpan.Zero` can be specified. When a request is sent with a timeout greater than zero, a _TimeoutExpired_ message is scheduled. Specifying `TimeSpan.Zero` will not schedule a timeout message and the request will never time out. +The default _Timeout_ is thirty seconds but any value greater than or equal to `TimeSpan.Zero` can be specified. When a request is sent with a timeout greater +than zero, a _TimeoutExpired_ message is scheduled. Specifying `TimeSpan.Zero` will not schedule a timeout message and the request will never time out. ::alert{type="info"} -When a _Timeout_ greater than `Timespan.Zero` is configured, a message scheduler must be configured. See the [scheduling](/documentation/configuration/scheduling) section for details on configuring a message scheduler. +When a _Timeout_ greater than `Timespan.Zero` is configured, a message scheduler must be configured. See +the [scheduling](/documentation/configuration/scheduling) section for details on configuring a message scheduler. :: -When defining a `Request`, an instance property _should_ be specified to store the _RequestId_ which is used to correlate responses to the state machine instance. While the request is pending, the _RequestId_ is stored in the property. When the request has completed the property is cleared. If the request times out or faults, the _RequestId_ is retained to allow for later correlation if requests are ultimately completed (such as moving requests from the *_error* queue back into the service queue). +When defining a `Request`, an instance property _should_ be specified to store the _RequestId_ which is used to correlate responses to the state machine +instance. While the request is pending, the _RequestId_ is stored in the property. When the request has completed the property is cleared. If the request times +out or faults, the _RequestId_ is retained to allow for later correlation if requests are ultimately completed (such as moving requests from the *_error* queue +back into the service queue). -A recent enhancement making this property optional, instead using the instance's `CorrelationId` for the request message `RequestId`. This can simplify response correlation, and also avoids the need of a supplemental index on the saga repository. However, reusing the `CorrelationId` for the request might cause issues in highly complex systems. So consider this when choosing which method to use. +A recent enhancement making this property optional, instead using the instance's `CorrelationId` for the request message `RequestId`. This can simplify response +correlation, and also avoids the need of a supplemental index on the saga repository. However, reusing the `CorrelationId` for the request might cause issues in +highly complex systems. So consider this when choosing which method to use. #### Configuration @@ -1098,170 +1153,24 @@ public class OrderStateMachine : } ``` -The _Request_ includes three events: _Completed_, _Faulted_, and _TimeoutExpired_. These events can be consumed during any state, however, the _Request_ includes a _Pending_ state which can be used to avoid declaring a separate pending state. +The _Request_ includes three events: _Completed_, _Faulted_, and _TimeoutExpired_. These events can be consumed during any state, however, the _Request_ +includes a _Pending_ state which can be used to avoid declaring a separate pending state. ::alert{type="info"} -The request timeout is scheduled using the message scheduler, and the scheduled message is canceled when a response or fault is received. Not all message schedulers support cancellation, so it may be necessary to _Ignore_ the `TimeoutExpired` event in subsequent states. +The request timeout is scheduled using the message scheduler, and the scheduled message is canceled when a response or fault is received. Not all message +schedulers support cancellation, so it may be necessary to _Ignore_ the `TimeoutExpired` event in subsequent states. :: -### Custom - -There are scenarios when an event behavior may have dependencies that need to be managed at a scope level, such as a database connection, or the complexity is best encapsulated in a separate class rather than being part of the state machine itself. Developers can create their own activities for state machine use, and optionally create their own extension methods to add them to a behavior. - -To create an activity, create a class that implements `IStateMachineActivity` as shown. - -```csharp -public class PublishOrderSubmittedActivity : - IStateMachineActivity -{ - readonly ISomeService _service; - - public PublishOrderSubmittedActivity(ISomeService service) - { - _service = service; - } - - public void Probe(ProbeContext context) - { - context.CreateScope("publish-order-submitted"); - } - - public void Accept(StateMachineVisitor visitor) - { - visitor.Visit(this); - } - - public async Task Execute(BehaviorContext context, IBehavior next) - { - await _service.OnOrderSubmitted(context.Saga.CorrelationId); - - // always call the next activity in the behavior - await next.Execute(context).ConfigureAwait(false); - } - - public Task Faulted(BehaviorExceptionContext context, - IBehavior next) - where TException : Exception - { - return next.Faulted(context); - } -} -``` - -For _ISomeService_, implement the interface in a class that uses _IPublishEndpoint_ to publish the event as shown. - -```csharp -public class SomeService : - ISomeService -{ - IPublishEndpoint _publishEndpoint; - - public SomeService(IPublishEndpoint publishEndpoint) - { - _publishEndpoint = publishEndpoint; - } - - public async Task OnOrderSubmitted(Guid orderId) - { - await _publishEndpoint.Publish(new { OrderId = orderId }); - } -} -``` - -Once created, configure the activity in a state machine as shown. - -```csharp -public interface OrderSubmitted -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .Activity(x => x.OfType()) - .TransitionTo(Submitted)); - } -} -``` - -When the `SubmitOrder` event is consumed, the state machine will resolve the activity from the container, and call the `Execute` method. The activity will be scoped, so any dependencies will be resolved within the message `ConsumeContext`. +#### Missing Instance -In the above example, the event type was known in advance. If an activity for any event type is needed, it can be created without specifying the event type. +If the saga instance has been finalized before the response, fault, or timeout have been received, it is possible to configure a missing instance handler, +similar to a regular event. ```csharp -public class PublishOrderSubmittedActivity : - IStateMachineActivity +Request(() => ProcessOrder, x => x.ProcessOrderRequestId, r => { - readonly ISomeService _service; - - public PublishOrderSubmittedActivity(ISomeService service) - { - _service = service; - } - - public void Probe(ProbeContext context) - { - context.CreateScope("publish-order-submitted"); - } - - public void Accept(StateMachineVisitor visitor) - { - visitor.Visit(this); - } - - public async Task Execute(BehaviorContext context, IBehavior next) - { - await _service.OnOrderSubmitted(context.Saga.CorrelationId); - - await next.Execute(context).ConfigureAwait(false); - } - - public async Task Execute(BehaviorContext context, IBehavior next) - { - await _service.OnOrderSubmitted(context.Saga.CorrelationId); - - await next.Execute(context).ConfigureAwait(false); - } - - public Task Faulted(BehaviorExceptionContext context, IBehavior next) - where TException : Exception - { - return next.Faulted(context); - } - - public Task Faulted(BehaviorExceptionContext context, IBehavior next) - where TException : Exception - { - return next.Faulted(context); - } -} -``` - -To register an instance activity, use the following syntax. - -```csharp -public interface OrderSubmitted -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .Activity(x => x.OfInstanceType()) - .TransitionTo(Submitted)); - } -} + r.Completed = m => m.OnMissingInstance(i => i.Discard()); + r.Faulted = m => m.OnMissingInstance(i => i.Discard()); + r.TimeoutExpired = m => m.OnMissingInstance(i => i.Discard()); +}); ``` - -[2]: https://github.com/MassTransit/Sample-ShoppingWeb - diff --git a/doc/content/3.documentation/3.patterns/6.saga/guidance.md b/doc/content/3.documentation/3.patterns/6.saga/guidance.md index fd0cc5a8668..3634b4ca8c2 100644 --- a/doc/content/3.documentation/3.patterns/6.saga/guidance.md +++ b/doc/content/3.documentation/3.patterns/6.saga/guidance.md @@ -21,7 +21,7 @@ To configure the receive endpoint directly: ```csharp services.AddMassTransit(x => { - x.AddStateMachineSaga() + x.AddSagaStateMachine() .MongoDbRepository(r => { r.Connection = "mongodb://127.0.0.1"; @@ -41,7 +41,7 @@ services.AddMassTransit(x => e.ConfigureSaga(context, s => { - var partition = endpointConfigurator.CreatePartitioner(ConcurrencyLimit); + var partition = s.CreatePartitioner(ConcurrencyLimit); s.Message(x => x.UsePartitioner(partition, m => m.Message.OrderId)); s.Message(x => x.UsePartitioner(partition, m => m.Message.OrderId)); diff --git a/doc/content/3.documentation/3.patterns/7.routing-slip/0.index.md b/doc/content/3.documentation/3.patterns/7.routing-slip/0.index.md deleted file mode 100755 index 1e141e30081..00000000000 --- a/doc/content/3.documentation/3.patterns/7.routing-slip/0.index.md +++ /dev/null @@ -1,290 +0,0 @@ ---- -title: Overview -description: Distributed transactions using MassTransit Routing Slip -toc: true ---- - -# Routing Slip - -Developing applications using a distributed, message-based architecture significantly increases the complexity of performing transactions, where an end-to-end set of steps must be completed entirely, or not at all. In an application using an ACID database, this is typically done using SQL transactions, where partial operations are rolled back if the transaction cannot be completed. However, this doesn't scale when the steps being to include dependencies outside of a single database. And in the distributed, *microservices* based architectures, the use of a single ACID database is shrinking to completely non-existent. - -MassTransit Courier is a mechanism for creating and executing distributed transactions with fault compensation that can be used to meet the requirements previously within the domain of database transactions, but built to scale across a large system of distributed services. Courier also works well with MassTransit sagas, which add transaction monitoring and recoverability. - -MassTransit implements the routing slip pattern. Leveraging a durable messaging transport and the advanced saga features of MassTransit, routing slips provide a powerful set of components to simplify the use of routing slips in distributed applications. Combining the routing slip pattern with a saga state machine results in a reliable, recoverable, and supportable approach for coordinating and monitoring message processing across multiple services. - -In addition to the basic routing slip pattern, MassTransit also supports [compensations][1] which allow activities to store execution data so that reversible operations can be undone, using either a traditional rollback mechanism or by applying an offsetting operation. For example, an activity that holds a seat for a patron could release the held seat when compensated. - -## Activities - -In MassTransit Courier, an *Activity* refers to a processing step that can be added to a routing slip. - -To create an activity, create a class that implements the *IActivity* interface. - -```csharp -public class DownloadImageActivity : - IActivity -{ - Task Execute(ExecuteContext context); - Task Compensate(CompensateContext context); -} -``` - -The *IActivity* interface is generic with two arguments. The first parameter specifies the activity’s argument type and the second parameter specifies the activity’s log type. In the example shown above, *DownloadImageArguments* is the argument type and *DownloadImageLog* is the log type. Both parameters may be interface, class or record types. Where the type is a class or a record, the proper accessors should be specified (i.e. `{ get; set; }` or `{ get; init; }`). - -#### Execute Activities - -An *Execute Activity* is an activity that only executes and does not support compensation. As such, the declaration of a log type is not required. - -```csharp -public class ValidateImageActivity : - IExecuteActivity -{ - Task Execute(ExecuteContext context); -} -``` - -### Implementing - -An activity must implement two interface methods, *Execute* and *Compensate*. The *Execute* method is called while the routing slip is executing activities and the *Compensate* method is called when a routing slip faults and needs to be compensated. - -When the *Execute* method is called, an *execution* argument is passed containing the activity arguments, the routing slip *TrackingNumber*, and methods to mark the activity as completed or faulted. The actual routing slip message, as well as any details of the underlying infrastructure, are excluded from the *execution* argument to prevent coupling between the activity and the implementation. An example *Execute* method is shown below. - -```csharp -async Task Execute(ExecuteContext execution) -{ - DownloadImageArguments args = execution.Arguments; - string imageSavePath = Path.Combine(args.WorkPath, - execution.TrackingNumber.ToString()); - - await _httpClient.GetAndSave(args.ImageUri, imageSavePath); - - return execution.Completed(new {ImageSavePath = imageSavePath}); -} -``` - -### Completing - -Once activity processing is complete, the activity returns an *ExecutionResult* to the host. If the activity executes successfully, the activity can elect to store compensation data in an activity log which is passed to the *Completed* method on the *execution* argument. If the activity chooses not to store any compensation data, the activity log argument is not required. In addition to compensation data, the activity can add or modify variables stored in the routing slip for use by subsequent activities. - -> In the example above, the activity specifies the *DownloadImageLog* interface and initializes the log using an anonymous object. The object is then passed to the *Completed* method for storage in the routing slip before sending the routing slip to the next activity. - -### Terminating - -In some situations, it may make sense to terminate the routing slip without executing any of the subsequent activities in the itinerary. This might be due to a business rule, in which the routing slip shouldn't be faulted, but needs to end immediately. - -To terminate a routing slip, call _Terminate_ as shown. - -```csharp -// regular termination -return execution.Terminate(); - -// terminate and include additional variables in the event -return execution.Terminate(new { Reason = "Not a good time, dude."}); -``` - -### Faulting - -By default, if an activity throws an exception, it will be _faulted_ and a `RoutingSlipFaulted` event will be published (unless a subscription changes the rules). An activity can also return _Faulted_ rather than throwing an exception. - -### Compensating - -When an activity fails, the *Compensate* method is called for previously executed activities in the routing slip that stored compensation data. If an activity does not store any compensation data, the *Compensate* method is never called. The compensation method for the example above is shown below. - -```csharp -Task Compensate(CompensateContext compensation) -{ - DownloadImageLog log = compensation.Log; - File.Delete(log.ImageSavePath); - - return compensation.Compensated(); -} -``` - -Using the activity log data, the activity compensates by removing the downloaded image from the work directory. Once the activity has compensated the previous execution, it returns a *CompensationResult* by calling the *Compensated* method. If the compensating actions could not be performed (either via logic or an exception) and the inability to compensate results in a failure state, the *Failed* method can be used instead, optionally specifying an *Exception*. - -## Using a Routing Slip - -A routing slip specifies a sequence of processing steps called *activities* that are combined into a single transaction. As each activity completes, the routing slip is forwarded to the next activity in the itinerary. When all activities have completed, the routing slip is completed and the transaction is complete. - -A key advantage to using a routing slip is it allows the activities to vary for each transaction. Depending upon the requirements for each transaction, which may differ based on things like payment methods, billing or shipping address, or customer preference ratings, the routing slip builder can selectively add activities to the routing slip. This dynamic behavior is in contrast to a more explicit behavior defined by a state machine or sequential workflow that is statically defined (either through the use of code, a DSL, or something like Windows Workflow). - -### Building - -A routing slip contains an itinerary, variables, and activity/compensation logs. It is defined by a message contract, which is used by the underlying Courier components to execute and compensate the transaction. The routing slip contract includes: - -- A tracking number, which should be unique for each routing slip -- An itinerary, which is an ordered list of activities -- An activity log, containing an ordered list of previously executed activities -- A compensation log, containing an order list of previous executed activities which may be compensated if the routing slip faults -- A collection of variables, which can be mapped to activity arguments -- A collection of subscriptions, which can be added to notify consumers of routing slip events -- A collection of exceptions which may have occurred during routing slip execution - -Developers are discouraged from directly implementing the *RoutingSlip* message type and should instead use a *RoutingSlipBuilder* to create a routing slip. The *RoutingSlipBuilder* encapsulates the creation of the routing slip and includes methods to add activities (and their arguments), activity logs, and variables to the routing slip. For example, to create a routing slip with two activities and an additional variable, a developer would write: - -```csharp -var builder = new RoutingSlipBuilder(NewId.NextGuid()); -builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage"), - new - { - ImageUri = new Uri("http://images.google.com/someImage.jpg") - }); -builder.AddActivity("FilterImage", new Uri("rabbitmq://localhost/execute_filterimage")); -builder.AddVariable("WorkPath", @"\dfs\work"); - -var routingSlip = builder.Build(); -``` - -Each activity requires a name for display purposes and a URI specifying the execution address. The execution address is where the routing slip should be sent to execute the activity. For each activity, arguments can be specified that are stored and presented to the activity via the activity arguments interface type specify by the first argument of the *IActivity* interface. The activities added to the routing slip are combined into an *Itinerary*, which is the list of activities to be executed, and stored in the routing slip. - -> Managing the inventory of available activities, as well as their names and execution addresses, is the responsibility of the application and is not part of the MassTransit Courier. Since activities are application specific, and the business logic to determine which activities to execute and in what order is part of the application domain, the details are left to the application developer. - -### Activity Arguments - -Each activity declares an activity argument type, which must be an interface. When the routing slip is received by an activity host, the argument type is used to read data from the routing slip and deliver it to the activity. - -The argument properties are mapped, by name, to the argument type from the routing slip using: - -- Explicitly declared arguments, added to the itinerary with the activity -- Implicitly mapped arguments, added as variables to the routing slip - -To specify an explicit activity argument, specify the argument value while adding the activity using the routing slip builder. - -```csharp -var builder = new RoutingSlipBuilder(NewId.NextGuid()); -builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage"), new - { - ImageUri = new Uri("http://images.google.com/someImage.jpg") - }); -``` - -To specify an implicit activity argument, add a variable to the routing slip with the same name/type as the activity argument. - -```csharp -var builder = new RoutingSlipBuilder(NewId.NextGuid()); -builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage")); -builder.AddVariable("ImageUri", "http://images.google.com/someImage.jpg"); -``` - -If an activity argument is not specified when the routing slip is created, it may be added by an activity that executes prior to the activity that requires the argument. For instance, if the _DownloadImage_ activity stored the image in a local cache, that address could be added and used by another activity to access the cached image. - -First, the routing slip would be built without the argument value. - -```csharp -var builder = new RoutingSlipBuilder(NewId.NextGuid()); -builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage")); -builder.AddActivity("ProcessImage", new Uri("rabbitmq://localhost/execute_processimage")); -builder.AddVariable("ImageUri", "http://images.google.com/someImage.jpg"); -``` - -Then, the first activity would add the variable to the routing slip on completion. - -```csharp -async Task Execute(ExecuteContext context) -{ - ... - return context.CompletedWithVariables(new { ImagePath = ...}); -} -``` - -The process image activity would then use that variable as an argument value. - -```csharp -async Task Execute(ExecuteContext context) -{ - var path = context.Arguments.ImagePath; -} -``` - -### Executing - -Once built, the routing slip is executed, which sends it to the first activity’s execute URI. To make it easy and to ensure that source information is included, an extension method on *IBus* is available, the usage of which is shown below. - -```csharp -await bus.Execute(routingSlip); -``` - -It should be pointed out that if the address for the first activity is invalid or cannot be reached, an exception will be thrown by the *Execute* method. - - -## Routing Slip Events - -During routing slip execution, events are published when the routing slip completes or faults. Every event message includes the *TrackingNumber* as well as a *Timestamp* (in UTC, of course) indicating when the event occurred: - - * RoutingSlipCompleted - * RoutingSlipFaulted - * RoutingSlipCompensationFailed - -Additional events are published for each activity, including: - - * RoutingSlipActivityCompleted - * RoutingSlipActivityFaulted - * RoutingSlipActivityCompensated - * RoutingSlipActivityCompensationFailed - -By observing these events, an application can monitor and track the state of a routing slip. To maintain the current state, an Automatonymous state machine could be created. To maintain history, events could be stored in a database and then queried using the *TrackingNumber* of the routing slip. - -### Subscriptions - -By default, routing slip events are published -- which means that any subscribed consumers will receive the events. While this is useful getting started, it can quickly get out of control as applications grow and multiple unrelated routing slips are used. To handle this, subscriptions were added (yes, added, because they weren't though of until we experienced this ourselves). - -Subscriptions are added to the routing slip at the time it is built using the `RoutingSlipBuilder`. - -```csharp -builder.AddSubscription(new Uri("rabbitmq://localhost/log-events"), - RoutingSlipEvents.All); -``` - -This subscription would send all routing slip events to the specified endpoint. If the application only wanted specified events, the events can be selected by specifying the enumeration values for those events. For example, to only get the `RoutingSlipCompleted` and `RoutingSlipFaulted` events, the following code would be used. - -```csharp -builder.AddSubscription(new Uri("rabbitmq://localhost/log-events"), - RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); -``` - -It is also possible to tweak the content of the events to cut down on message size. For instance, by default, the `RoutingSlipCompleted` event includes the variables from the routing slip. If the variables contained a large document, that document would be copied to the event. Eliminating the variables from the event would reduce the message size, thereby reducing the traffic on the message broker. To specify the contents of a routing slip event subscription, an additional argument is specified. - -```csharp -builder.AddSubscription(new Uri("rabbitmq://localhost/log-events"), - RoutingSlipEvents.Completed, RoutingSlipEventContents.None); -``` - -This would send the `RoutingSlipCompleted` event to the endpoint, without any of the variables be included (only the main properties of the event would be present). - -> Once a subscription is added to a routing slip, events are no longer published -- they are only sent to the addresses specified in the subscriptions. However, multiple subscriptions can be specified -- the endpoints just need to be known at the time the routing slip is built. - -### Custom - -It is also possible to specify a subscription with a custom event, a message that is created by the application developer. This makes it possible to create your own event types and publish them in response to routing slip events occurring. And this includes having the full context of a regular endpoint `Send` so that any headers or context settings can be applied. - -To create a custom event subscription, use the overload shown below. - -```csharp -// first, define the event type in your assembly -public record OrderProcessingCompleted -{ - public Guid TrackingNumber { get; init; } - public DateTime Timestamp { get; init; } - - public string OrderId { get; init; } - public string OrderApproval { get; init; } -} - -// then, add the subscription with the custom properties -builder.AddSubscription(new Uri("rabbitmq://localhost/order-events"), - RoutingSlipEvents.Completed, - x => x.Send(new - { - OrderId = "BFG-9000", - OrderApproval = "ComeGetSome" - })); -``` - -In the message contract above, there are four properties, but only two of them are specified. By default, the base `RoutingSlipCompleted` event is created, and then the content of that event is *merged* into the message created in the subscription. This ensures that the dynamic values, such as the `TrackingNumber` and the `Timestamp`, which are present in the default event, are available in the custom event. - -Custom events can also select with contents are merged with the custom event, using an additional method overload. - - - -[1]: http://en.wikipedia.org/wiki/Compensation_%28engineering%29 -[2]: https://github.com/MassTransit/Automatonymous diff --git a/doc/content/3.documentation/3.patterns/7.routing-slip/1.configuration.md b/doc/content/3.documentation/3.patterns/7.routing-slip/1.configuration.md deleted file mode 100755 index 3884349df21..00000000000 --- a/doc/content/3.documentation/3.patterns/7.routing-slip/1.configuration.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -title: Configuration -description: Customizing routing slip configuration -toc: true ---- - -# Blah lah diff --git a/doc/content/3.documentation/3.patterns/7.routing-slip/2.request-proxy.md b/doc/content/3.documentation/3.patterns/7.routing-slip/2.request-proxy.md deleted file mode 100755 index 7274eb10c6d..00000000000 --- a/doc/content/3.documentation/3.patterns/7.routing-slip/2.request-proxy.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -title: Request Proxy -description: Respond to a request using a routing slip -toc: true ---- - -# Routing Slip Request Proxy - -The request proxy works like this: - - -
-graph TD - tp(Receive Transport)-->| ReceiveContext |rp(Receive Pipeline) - rp-->ds(Deserializer) - ds-->| ConsumeContext |ccm(Middleware) - ccm-->mtf(Message Type Filter) - mtf-->| ConsumeContext A |ccam(Middleware) - mtf-->| ConsumeContext B |ccbm(Middleware) - ccbm-->cf(Consumer Factory) - cf-->cs(Service Scope) - cs-->| ConsumerConsumeContext B |cccm(Middleware) - cccm-->cccf(Consumer Message Filter) - cccf-->| ConsumeContext B |cccc(Consumer Consume) -
-
diff --git a/doc/content/3.documentation/3.patterns/7.routing-slip/_dir.yml b/doc/content/3.documentation/3.patterns/7.routing-slip/_dir.yml deleted file mode 100644 index 531cdea951e..00000000000 --- a/doc/content/3.documentation/3.patterns/7.routing-slip/_dir.yml +++ /dev/null @@ -1,2 +0,0 @@ -title: Routing Slip -icon: icon-park-outline:checklist diff --git a/doc/content/3.documentation/3.patterns/7.routing-slips/_dir.yml b/doc/content/3.documentation/3.patterns/7.routing-slips/_dir.yml new file mode 100644 index 00000000000..de837930a34 --- /dev/null +++ b/doc/content/3.documentation/3.patterns/7.routing-slips/_dir.yml @@ -0,0 +1 @@ +title: Routing Slips diff --git a/doc/content/3.documentation/3.patterns/7.routing-slips/monitor-via-saga.md b/doc/content/3.documentation/3.patterns/7.routing-slips/monitor-via-saga.md new file mode 100644 index 00000000000..1fec32a394a --- /dev/null +++ b/doc/content/3.documentation/3.patterns/7.routing-slips/monitor-via-saga.md @@ -0,0 +1,50 @@ +--- +title: Monitor via Saga +--- + +# How to Monitor a Routing Slip with a Saga + +Because Routing Slips carry their state on the wire, it's not possible to query the state of a currently executing routing slip. Use this pattern when your solution requires tracking the progress of a Routing Slip that's in-flight. + +## Create a Normal Routing Slip + +```csharp +var builder = new RoutingSlipBuilder(NewId.NextGuid()); + +// add your activities as normal +builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage"), + new + { + ImageUri = new Uri("http://images.google.com/someImage.jpg") + }); +builder.AddActivity("FilterImage", new Uri("rabbitmq://localhost/execute_filterimage")); +builder.AddVariable("WorkPath", @"\dfs\work"); + +var routingSlip = builder.Build(); +``` + +## Build the Saga + +```csharp +public class MonitorRoutingSlip : + MassTransitStateMachine +{ + public MonitorRoutingSlip() + { + InstanceState(x => x.CurrentState); + } +} +``` + +## Add Subscription to Routing Slip + +```csharp +var builder = new RoutingSlipBuilder(NewId.NextGuid()); + +// ... add activities and variables as normal + +// ⭐️ KEY ITEM +builder.AddSubscription(new Uri(""), RoutingSlipEvents.All); + +var routingSlip = builder.Build(); +``` diff --git a/doc/content/3.documentation/3.patterns/8.claim-check.md b/doc/content/3.documentation/3.patterns/8.claim-check.md new file mode 100644 index 00000000000..f0284808823 --- /dev/null +++ b/doc/content/3.documentation/3.patterns/8.claim-check.md @@ -0,0 +1,211 @@ +# Claim Check + +> Implemented as Message Data + +Message brokers are built to be fast, and when it comes to messages, _size matters_. In this case, however, bigger is not better — large messages negatively impact broker performance. + +MassTransit offers a built-in solution which stores large data (either a string or a byte array) in a separate repository and replaces it with a reference to the stored data (yes, it's a URI, shocking I know) in the message body. When the message is consumed, the reference is replaced with the original data which is loaded from the repository. + +Message data is an implementation of the [Claim Check](https://www.enterpriseintegrationpatterns.com/patterns/messaging/StoreInLibrary.html) pattern. + +::alert{type="success"} +The message producer and consumer must both have access to the message data repository. +:: + +## Usage + +To use message data, add a `MessageData` property to a message. Properties can be anywhere in the message, nested within message properties, or in collections such as arrays, lists, or dictionaries. The generic argument `T` must be `string`, `byte[]`, or `Stream`. + +```csharp +public interface IndexDocumentContent +{ + Guid DocumentId { get; } + MessageData Document { get; } +} +``` + +The bus must be configured to use message data, specifying the message data repository. + +```csharp +IMessageDataRepository messageDataRepository = new InMemoryMessageDataRepository(); + +var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => +{ + cfg.UseMessageData(messageDataRepository); + + cfg.ReceiveEndpoint("document-service", e => + { + e.Consumer(); + }); +}); + +``` + +::alert{type="info"} +Previous versions of MassTransit required a generic type to be specified on the `UseMessageData` method. Individual receive endpoints could also be configured separately. The previous methods are deprecated and now a single bus configuration applies to all receive endpoints. +:: + +Configuring the message data middleware (via `UseMessageData`) adds a transform to replace any deserialized message data reference with an object that loads the message data asynchronously. By using middleware, the consumer doesn't need to use the message data repository. The consumer can simply use the property value to access the message data (asynchronously, of course). If the message data was not loaded, an exception will be thrown. The `HasValue` property is `true` if message data is present. + +```csharp +public class IndexDocumentConsumer : + IConsumer + +public async Task Consume(ConsumeContext context) +{ + byte[] document = await context.Message.Document.Value; +} +``` + +To initialize a message contract with one or more `MessageData` properties, the `byte[]`, `string`, or `Stream` value can be specified and the data will be stored to the repository by the initializer. If the message has the _TimeToLive_ header property specified, that same value will be used for the message data in the repository. + +```csharp +Guid documentId = NewId.NextGuid(); +byte[] document = new byte[100000]; // get byte array, or a big string + +await endpoint.Send(new +{ + DocumentId = documentId, + Document = document +}); +``` + +If using a message class, or not using a message initializer, the data must be stored to the repository explicitly. + +```csharp +class IndexDocumentContentMessage : + IndexDocumentContent +{ + public Guid DocumentId { get; set; } + public MessageData Document { get; set; } +} + +Guid documentId = NewId.NextGuid(); +byte[] document = new byte[100000]; // get byte array, or a big string + +await endpoint.Send(new IndexDocumentContentMessage +{ + DocumentId = documentId, + Document = await repository.PutBytes(document, TimeSpan.FromDays(1)) +}); +``` + +The message data is stored, and the reference added to the outbound message. + +::alert{type="info"} +In the event of message retries in consumer memory a reference to the stream is held. +On the first attempt the message stream is read then you may need to rewind the stream to make it available to read from again on retries + +```csharp + if (msg.Payload.HasValue) +{ + Stream s = await msg.Payload.Value; + + using (StreamReader sr = new StreamReader(s, leaveOpen: true)) + { + messageBody = await sr.ReadToEndAsync(); + + // In the case of retries, the Stream has likely already been read on previous attempts. + // The consumer doesn't put a message back on the queue if it is being retried and is held in memory by the consumer for retries + // so you will need to 'rewind' the stream to the beginning so it is ready to be read again on subsequent attempts + s.Seek(0, SeekOrigin.Begin); + } +} +``` + +Note that in this example the StreamReader argument `leaveOpen` is set to true to avoid disposing of the stream. +This means that you may need to manually disponse of the stream to avoid memory leaks when the message has been successful or faulted +:: + +## Configuration + +There are several configuration settings available to adjust message data behavior. + +### Time To Live + +By default, there is no default message data time-to-live. To specify a default time-to-live, set the default as shown. + +```csharp +MessageDataDefaults.TimeToLive = TimeSpan.FromDays(2); +``` + +This settings simply specifies the default value when calling the repository, it is up to the repository to apply any time-to-live values to the actual message data. + +If the `SendContext` has specified a time-to-live value, that value is applied to the message data automatically (when using message initializers). To add extra time, perhaps to account for system latency or differences in time, extra time can be added. + +```csharp +MessageDataDefaults.ExtraTimeToLive = TimeSpan.FromMinutes(5); +``` + +### Inline Threshold + +Newly added is the ability to specify a threshold for message data so that smaller values are included in the actual message body. This eliminates the need to read the data from storage, which increases performance. The message data can also be configured to _not_ write that data to the repository if it is under the threshold. By default (for now), data is written to the repository to support services that have not yet upgraded to the latest MassTransit. + +> If you know your systems are upgraded, you can change the default so that data sizes under the threshold are not written to the repository. + +To configure the threshold, and to optionally turn off storage of data sizes under the threshold: + +```csharp +// the default value is 4096 +MessageDataDefaults.Threshold = 8192; + +// to disable writing to the repository for sizes under the threshold +// defaults to true, which may change to false in a future release +MessageDataDefaults.AlwaysWriteToRepository = false; +``` + +## Repositories + +MassTransit includes several message data repositories. + +| Name | Description | +|:----------------------------------|:---------------------------------------------------------------------------------------------| +| InMemoryMessageDataRepository | Entirely in memory, meant for unit testing | +| FileSystemMessageDataRepository | Writes message data to the file system, which may be a network drive or other shared storage | +| MongoDbMessageDataRepository | Stores message data using MongoDB's GridFS | +| AzureStorageMessageDataRepository | Stores message data using Azure Blob Storage | +| EncryptedMessageDataRepository | Adds encryption to any other message data repository | + +### File System + +To configure the file system message data repository: + +```csharp +IMessageDataRepository CreateRepository(string path) +{ + var dataDirectory = new DirectoryInfo(path); + + return new FileSystemMessageDataRepository(dataDirectory); +} +``` + +### MongoDB + +> [MassTransit.MongoDb](https://www.nuget.org/packages/MassTransit.MongoDb/) + +To configure the MongoDB GridFS message data repository, follow the example shown below. + +```csharp +IMessageDataRepository CreateRepository(string connectionString, string databaseName) +{ + return new MongoDbMessageDataRepository(connectionString, databaseName); +} +``` + +### Azure Storage + +> [MassTransit.Azure.Storage](https://www.nuget.org/packages/MassTransit.Azure.Storage/) + +An Azure Cloud Storage account can be used to store message data. To configure Azure storage, first create the BlobServiceClient object using your connection string, and then use the extension method to create the repository as shown below. You can replace `message-data` with the desired container name. + +```csharp +var client = new BlobServiceClient(""); +_repository = client.CreateMessageDataRepository("message-data"); +``` + +Previous to version 7.1.8 of MassTransit this was done creating a CloudStorageAccount object from your connection string the following way. + +```csharp +var account = CloudStorageAccount.Parse(""); +_repository = account.CreateMessageDataRepository("message-data"); +``` diff --git a/doc/content/3.documentation/3.patterns/durable-futures.md b/doc/content/3.documentation/3.patterns/9.durable-futures.md similarity index 100% rename from doc/content/3.documentation/3.patterns/durable-futures.md rename to doc/content/3.documentation/3.patterns/9.durable-futures.md diff --git a/doc/content/3.documentation/3.patterns/_2.request-proxy.md b/doc/content/3.documentation/3.patterns/_2.request-proxy.md new file mode 100755 index 00000000000..944c0bbdd89 --- /dev/null +++ b/doc/content/3.documentation/3.patterns/_2.request-proxy.md @@ -0,0 +1,8 @@ +--- +title: Request Proxy +description: Respond to a request using a routing slip +toc: true +--- + +# Routing Slip Request Proxy + diff --git a/doc/content/3.documentation/3.patterns/claim-check.md b/doc/content/3.documentation/3.patterns/claim-check.md deleted file mode 100644 index 3e22128b1fe..00000000000 --- a/doc/content/3.documentation/3.patterns/claim-check.md +++ /dev/null @@ -1,211 +0,0 @@ -# Claim Check - -> Implemented as Message Data - -Message brokers are built to be fast, and when it comes to messages, _size matters_. In this case, however, bigger is not better — large messages negatively impact broker performance. - -MassTransit offers a built-in solution which stores large data (either a string or a byte array) in a separate repository and replaces it with a reference to the stored data (yes, it's a URI, shocking I know) in the message body. When the message is consumed, the reference is replaced with the original data which is loaded from the repository. - -Message data is an implementation of the [Claim Check](https://www.enterpriseintegrationpatterns.com/patterns/messaging/StoreInLibrary.html) pattern. - -::alert{type="success"} -The message producer and consumer must both have access to the message data repository. -:: - -## Usage - -To use message data, add a `MessageData` property to a message. Properties can be anywhere in the message, nested within message properties, or in collections such as arrays, lists, or dictionaries. The generic argument `T` must be `string`, `byte[]`, or `Stream`. - -```csharp -public interface IndexDocumentContent -{ - Guid DocumentId { get; } - MessageData Document { get; } -} -``` - -The bus must be configured to use message data, specifying the message data repository. - -```csharp -IMessageDataRepository messageDataRepository = new InMemoryMessageDataRepository(); - -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.UseMessageData(messageDataRepository); - - cfg.ReceiveEndpoint("document-service", e => - { - e.Consumer(); - }); -}); - -``` - -::alert{type="info"} -Previous versions of MassTransit required a generic type to be specified on the `UseMessageData` method. Individual receive endpoints could also be configured separately. The previous methods are deprecated and now a single bus configuration applies to all receive endpoints. -:: - -Configuring the message data middleware (via `UseMessageData`) adds a transform to replace any deserialized message data reference with an object that loads the message data asynchronously. By using middleware, the consumer doesn't need to use the message data repository. The consumer can simply use the property value to access the message data (asynchronously, of course). If the message data was not loaded, an exception will be thrown. The `HasValue` property is `true` if message data is present. - -```csharp -public class IndexDocumentConsumer : - IConsumer - -public async Task Consume(ConsumeContext context) -{ - byte[] document = await context.Message.Document.Value; -} -``` - -To initialize a message contract with one or more `MessageData` properties, the `byte[]`, `string`, or `Stream` value can be specified and the data will be stored to the repository by the initializer. If the message has the _TimeToLive_ header property specified, that same value will be used for the message data in the repository. - -```csharp -Guid documentId = NewId.NextGuid(); -byte[] document = new byte[100000]; // get byte array, or a big string - -await endpoint.Send(new -{ - DocumentId = documentId, - Document = document -}); -``` - -If using a message class, or not using a message initializer, the data must be stored to the repository explicitly. - -```csharp -class IndexDocumentContentMessage : - IndexDocumentContent -{ - public Guid DocumentId { get; set; } - public MessageData Document { get; set; } -} - -Guid documentId = NewId.NextGuid(); -byte[] document = new byte[100000]; // get byte array, or a big string - -await endpoint.Send(new IndexDocumentContentMessage -{ - DocumentId = documentId, - Document = await repository.PutBytes(document, TimeSpan.FromDays(1)) -}); -``` - -The message data is stored, and the reference added to the outbound message. - -::alert{type="info"} -In the event of message retries in consumer memory a reference to the stream is held. -On the first attempt the message stream is read then you may need to rewind the stream to make it available to read from again on retries - -```csharp - if (msg.Payload.HasValue) -{ - Stream s = await msg.Payload.Value; - - using (StreamReader sr = new StreamReader(s, leaveOpen: true)) - { - messageBody = await sr.ReadToEndAsync(); - - // In the case of retries, the Stream has likely already been read on previous attempts. - // The consumer doesn't put a message back on the queue if it is being retried and is held in memory by the consumer for retries - // so you will need to 'rewind' the stream to the beginning so it is ready to be read again on subsequent attempts - s.Seek(0, SeekOrigin.Begin); - } -} -``` - -Note that in this example the StreamReader argument `leaveOpen` is set to true to avoid disposing of the stream. -This means that you may need to manually disponse of the stream to avoid memory leaks when the message has been successful or faulted -:: - -## Configuration - -There are several configuration settings available to adjust message data behavior. - -### Time To Live - -By default, there is no default message data time-to-live. To specify a default time-to-live, set the default as shown. - -```csharp -MessageDataDefaults.TimeToLive = TimeSpan.FromDays(2); -``` - -This settings simply specifies the default value when calling the repository, it is up to the repository to apply any time-to-live values to the actual message data. - -If the `SendContext` has specified a time-to-live value, that value is applied to the message data automatically (when using message initializers). To add extra time, perhaps to account for system latency or differences in time, extra time can be added. - -```csharp -MessageDataDefaults.ExtraTimeToLive = TimeSpan.FromMinutes(5); -``` - -### Inline Threshold - -Newly added is the ability to specify a threshold for message data so that smaller values are included in the actual message body. This eliminates the need to read the data from storage, which increases performance. The message data can also be configured to _not_ write that data to the repository if it is under the threshold. By default (for now), data is written to the repository to support services that have not yet upgraded to the latest MassTransit. - -> If you know your systems are upgraded, you can change the default so that data sizes under the threshold are not written to the repository. - -To configure the threshold, and to optionally turn off storage of data sizes under the threshold: - -```csharp -// the default value is 4096 -MessageDataDefaults.Threshold = 8192; - -// to disable writing to the repository for sizes under the threshold -// defaults to false, which may change to true in a future release -MessageDataDefaults.AlwaysWriteToRepository = false; -``` - -## Repositories - -MassTransit includes several message data repositories. - -| Name | Description | -|:----------------------------------|:---------------------------------------------------------------------------------------------| -| InMemoryMessageDataRepository | Entirely in memory, meant for unit testing | -| FileSystemMessageDataRepository | Writes message data to the file system, which may be a network drive or other shared storage | -| MongoDbMessageDataRepository | Stores message data using MongoDB's GridFS | -| AzureStorageMessageDataRepository | Stores message data using Azure Blob Storage | -| EncryptedMessageDataRepository | Adds encryption to any other message data repository | - -### File System - -To configure the file system message data repository: - -```csharp -IMessageDataRepository CreateRepository(string path) -{ - var dataDirectory = new DirectoryInfo(path); - - return new FileSystemMessageDataRepository(dataDirectory); -} -``` - -### MongoDB - -> [MassTransit.MongoDb](https://www.nuget.org/packages/MassTransit.MongoDb/) - -To configure the MongoDB GridFS message data repository, follow the example shown below. - -```csharp -IMessageDataRepository CreateRepository(string connectionString, string databaseName) -{ - return new MongoDbMessageDataRepository(connectionString, databaseName); -} -``` - -### Azure Storage - -> [MassTransit.Azure.Storage](https://www.nuget.org/packages/MassTransit.Azure.Storage/) - -An Azure Cloud Storage account can be used to store message data. To configure Azure storage, first create the BlobServiceClient object using your connection string, and then use the extension method to create the repository as shown below. You can replace `message-data` with the desired container name. - -```csharp -var client = new BlobServiceClient(""); -_repository = client.CreateMessageDataRepository("message-data"); -``` - -Previous to version 7.1.8 of MassTransit this was done creating a CloudStorageAccount object from your connection string the following way. - -```csharp -var account = CloudStorageAccount.Parse(""); -_repository = account.CreateMessageDataRepository("message-data"); -``` diff --git a/doc/content/3.documentation/3.patterns/in-memory-outbox.md b/doc/content/3.documentation/3.patterns/in-memory-outbox.md deleted file mode 100644 index 8d76c94af6a..00000000000 --- a/doc/content/3.documentation/3.patterns/in-memory-outbox.md +++ /dev/null @@ -1,111 +0,0 @@ -# In-Memory Outbox - -This post details the _In-Memory Outbox_, including what it does, how to configure it, and how it ensures eventual consistency in the presence of database or message transport failures. - -MassTransit implements messaging patterns, many of which are designed to ease the transition from a tightly-coupled, database-centric application to a set of services that are highly available, reliable, and eventually consistent. Some of these patterns are obvious, but some of them require a little more explanation to truly understand how they are best utilized. - -### Commands - -Commands are used to do things, like update a database record. Updating a database record usually includes publishing events to notify services that a change in state has occurred. - -In a transactional mindset, updating the database and publishing the event is expected to be performed as a single atomic operation. In distributed systems, performing a distributed transaction between the database and the message broker is unrealistic. - -### Sagas - -In MassTransit, sagas are message handlers that maintain state. An initial message creates a saga _instance_ and subsequent messages may correlate to the same instance. Between messages, the saga instance _state_ is persisted using a database. While consuming a message, a saga may send commands and/or publish events. - -The message flow for a saga includes: - -1. On message receipt, an existing saga instance is loaded from the database. If a matching instance does not exist, a new instance is created. -2. The message is delivered to the saga instance. -3. Once the message is handled, the saga instance is saved or updated in the database. - -Step 2 is where _the magic happens_. The state can be changed, messages can be sent and published, anything. - -So, what's the problem? A few things. - -#### Failures - -An obvious problem is a database failure saving the saga instance. If messages were already sent or published, and the instance was not saved, other services would receive those messages yet the database has not been updated. - -A race condition is another concern, since the events may be consumed before the database update is complete. Yes, message brokers are _fast_, and many times messages are already being consumed long before (in computer time) the database update is started. - -##### So, retry? - -Retrying operations is a key trait of a resilient system. Transient failures happen, even more so in distributed systems, so it makes sense to retry failures in the presence of failures. Of course, not all failures are transient. For instance, trying to take out the trash when it has already been taken out isn't possible (well, until tomorrow). - -> In this example, designing idempotent services such that duplicate commands do not result in duplicate operations would be the best solution. But that's another topic worth studying. - -If retrying the database failure isn't enough, it may make sense to retry the entire message processing sequence – starting at step 1. In this case, the saga instance is discarded, and the message is retried from the beginning. The saga instance is loaded (or created), the message is delivered, and the instance is saved. This is repeated until it is successful or until the retry policy expires and the message is moved to the *_error* queue. - -> Because it's bad. Study _poison message handling_. - -A new retry-related issue is duplicate messages. Messages may be sent or published multiple times – once for each attempt. This can create non-deterministic behavior in services that consume those messages. Therefore, a method to delay messages from being sent until the saga instance is saved is needed. - -### The Outbox - -The outbox holds messages and delivers them after the _transactional_ portion of the message processing has completed. With a saga, the messages are delivered after the saga instance is saved successfully. This ensures that the database is updated before any consumers can start processing any of the produced messages. - -#### The In-Memory Outbox - -The In-Memory Outbox, a feature included with MassTransit, holds published and sent messages in memory until the message is processed successfully (such as the saga being saved to the database). Once the received message has been processed, the message is delivered to the broker and the received message is acknowledged as successful. - -> MassTransit consumes messages in _acknowledgement_ mode. The broker locks the message and the message is invisible to other consumers until it is either acknowledged (ack'd) by the consumer or negatively-acknowledged (n'ack'd) explictly by the consumer or implicitly due to a service or network failure. - -The full configuration is in the [documentation](/usage/exceptions.md#redelivery), a simple example is shown below. - -```cs -cfg.ReceiveEndpoint("r-trashy-saga", e => -{ - e.UseInMemoryOutbox(); - - e.StateMachineSaga(machine, repository); -}); -``` - -> In the example above, retry and redelivery was left out on purpose. The broker will redeliver the message if the process crashes or the network splits. For production services, retry filters should be added to handle transient database errors and ignore failures caused by business constraint violations. - -#### But what if the message doesn't send? - -This question comes up, and it is a fair question. If the broker goes down, the outbox would be unable to deliver the messages. If the process crashes, the messages in the outbox would be lost. Both of these failures can happen, though it is rare. And if computer science has one rule, it is that the rare will always happen. In production. On a Friday afternoon. - -#### Time to Take out the Trash - -Imagine you're twelve, sitting on the sofa, playing video games with your friends. Suddenly, from the other room, you hear your mom call out, "Take out the trash!" Of course, you're in the middle of a battle, and while you've explained many times that you can't pause a multiplayer game, mom just doesn't get it. So you do what any 12-year-old does, you ignore her. The trash remains right where it is, in the kitchen. - -After a while, the lack of a door opening and closing, the still present smell of burnt popcorn from the kitchen, and your mom calls out again, "Take out the trash." At this point, you're dead, in spectator mode, and decide to comply – you take out the trash. Then you slide back onto the sofa and get ready for round two. - -More time passes, the squad is ready and you're about to get on the bus. Your mom, however, didn't hear from you and shouts once more, "I said take out the trash." "Mom, I already took it out" you reply, realizing after that you forgot to mute your mic. The jests and jokes commence as you thank the bus driver and head out. - -#### The Story - -This real-world example includes both failures scenarios that are brought up when considering the in-memory outbox. - -First, it didn't happen. The database may have been unavailable or the service crashed deserializing the message. Either way, it failed. And the message? It's still on the broker. It will be redelivered. _Mom will keep telling you to take out the trash until you take it out._ - -Second, it happened but the messages were not delivered. _You didn't tell her you took it out._ In this case, the message will also be retried. But in this case, this rare case, this is where the previously mentioned term _idempotence_ comes back onto the field. - -When the message is attempted a third time (and face it, the third time is dangerously close to getting a chancla to the head), the database was already updated. The invoice is already approved, _in the database_. The messages weren't sent, however, so other services may not know that the invoice was approved. In this case, for the service to be idempotent, it should assume that: - -1. The message delivery failed because it is being delivered – again. -2. Since the invoice is approved, and this is the approve invoice command, something must have failed after the database was updated. -3. The only thing after the database update is the outbox delivering messages. - -> Study Occam's Razer (okay, yeah, I'm a fan of Razer gaming gear so I'm leaving it spelled that way) - -The correct thing to do at this point is to use the state in the database, along with any information that is contained in the message, to produce the same commands and events that were produced in the previous attempt. Those messages will be delivered by the outbox, and the message will be acknowledged. - -#### !! Victory !! - -That's it, an easy-to-use, reliable solution to perform atomic operations that update a database and send/publish messages, and it works for any database updates that are sent as commands (delivered by durable message queues). - -And a big thank you to Jimmy Bogard, who's tweet prompted me to write this article! - - - -##### Other Reading - -[Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html) -[NServiceBus Outbox](https://docs.particular.net/nservicebus/outbox/) - - diff --git a/doc/content/3.documentation/3.patterns/job-consumers.md b/doc/content/3.documentation/3.patterns/job-consumers.md deleted file mode 100644 index ad41eb4e0c1..00000000000 --- a/doc/content/3.documentation/3.patterns/job-consumers.md +++ /dev/null @@ -1,124 +0,0 @@ -# Job Consumers - -::div - :video-player{src="https://www.youtube.com/watch?v=nHrbw5cfNVo"} -:: - -When a message is delivered from the message broker to a consumer instance, the message is _locked_ by the broker. Once the consumer completes, MassTransit will acknowledge the message on the broker, removing it from the queue. While the message is locked, it will not be delivered to another consumer – on any bus instance reading from the same queue (competing consumer pattern). However, if the broker connection is lost the message will be unlocked and redelivered to a new consumer instance. The lock timeout is usually long enough for most message consumers, and this rarely is an issue in practice for consumers that complete quickly. - -However, there are plenty of use cases where consumers may run for a longer duration, from minutes to even hours. In these situations, a job consumer _may_ be used to decouple the consumer from the broker. A job consumer is a specialized consumer designed to execute _jobs_, defined by implementing the `IJobConsumer` interface where `T` is the job message type. Job consumers may be used for long-running tasks, such as converting a video file, but can really be used for any task. Job consumers have additional requirements, such as a database to store the job messages, manage concurrency and retry, and report job completion or failure. - -:sample{sample="job-consumer"} - -To use job consumers, a _service instance_ must be configured (see below). - -## IJobConsumer - -A job consumer implements the `IJobConsumer` interface, shown below. - -```csharp -public interface IJobConsumer : - IConsumer - where TJob : class -{ - Task Run(JobContext context); -} -``` - -## Configuration - -The example below configures a job consumer on a receive endpoint named using an _IEndpointNameFormatter_ passing the consumer type. - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(cfg => - { - cfg.Options>(options => options - .SetJobTimeout(TimeSpan.FromMinutes(15)) - .SetConcurrentJobLimit(10)); - }); - - x.SetKebabCaseEndpointNameFormatter(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ServiceInstance(instance => - { - instance.ConfigureJobServiceEndpoints(); - - instance.ConfigureEndpoints(context); - }); - }); -}); -``` - -In this example, the job timeout as well as the number of concurrent jobs allowed is specified using `JobOptions` when configuring the consumer. The job options can also be specified using a consumer definition in the same way. - -## Submitting a job - -To submit jobs to the job consumer, you can use the request client as shown to request. In this example, the _RequestId_ will be used as the _JobId_. - -```csharp -[HttpPost("{path}")] -public async Task SubmitJob(string path, [FromServices] IRequestClient client) -{ - _logger.LogInformation("Sending job: {Path}", path); - - Response response = await client.GetResponse(new - { - path - }); - - return Ok(new - { - response.Message.JobId, - Path = path - }); -} -``` - -```csharp -[HttpPut("{path}")] -public async Task FireAndForgetSubmitJob(string path, [FromServices] IPublishEndpoint publishEndpoint) -{ - _logger.LogInformation("Sending job: {Path}", path); - - var jobId = NewId.NextGuid(); - - await publishEndpoint.Publish>(new - { - JobId = jobId, - Job = new - { - path - } - }); - - return Ok(new - { - jobId, - Path = path - }); -} -``` - -## Job Service Endpoints - -The job service saga state machines are configured on their own endpoints, using the configured endpoint name formatter. These endpoints are required on _at least one_ bus instance. Additionally, it is not necessary to configure them on _every_ bus instance. In the example above, the job service endpoint are configured. Another method, _ConfigureJobService_, is used to configure the job service without configuring the saga state machine endpoints. In situations where there are many bus instances with job consumers, it is suggested that only one or two instances host the job service endpoints to avoid concurrency issues with the sagas repositories – particularly when optimistic locking is used. - -To configure a service instance without the job service endpoints, replace _ConfigureJobServiceEndpoints_ with _ConfigureJobService_. - -```csharp -x.UsingRabbitMq((context, cfg) => -{ - cfg.ServiceInstance(instance => - { - instance.ConfigureJobService(); - - instance.ConfigureEndpoints(context); - }); -}); -``` - -For a more detailed example of configuring the job service endpoints, including persistent storage, see the sample mentioned in the box above. diff --git a/doc/content/3.documentation/3.patterns/transactional-outbox.md b/doc/content/3.documentation/3.patterns/transactional-outbox.md deleted file mode 100644 index e03515ae18f..00000000000 --- a/doc/content/3.documentation/3.patterns/transactional-outbox.md +++ /dev/null @@ -1,54 +0,0 @@ -# Transactional Outbox - -It is common that a service may need to combine database writes with publishing events and/or sending commands. And in this scenario, it is usually desirable to do this atomically in a transaction. However, message brokers typically do not participate in transactions. Even if a message broker did support transactions, it would require two-phase commit (2PC) which should be avoided whenever possible. - -> While MassTransit has long provided an [in-memory outbox](/documentation/patterns/in-memory-outbox), there has often been criticism that it isn't a _real_ outbox. And while I have proven that it works, is reliable, and is extremely fast (broker message delivery speed), it does require care to ensure operations are idempotent and when an idempotent operation is detected events are republished. The in-memory outbox also does not function as an _inbox_, so exactly-once message delivery is not supported. - -The Transactional Outbox has two main components: - -- The **Bus Outbox** works within a container scope (such as the scope created for an ASP.NET Controller) and adds published and sent messages to the specified `DbContext`. Once the changes are saved, the messages are available to the delivery service which delivers them to the broker. - -- The **Consumer Outbox** is a combination of an _inbox_ and an _outbox_. The _inbox_ is used to keep track of received messages to guarantee exactly-once consumer behavior. The _outbox_ is used to store published and sent messages until the consumer completes successfully. Once completed, the stored messages are delivered to the broker after which the received message is acknowledged. The Consumer Outbox works with all consumer types, including Consumers, Sagas, and Courier Actvities. - -Either of these components can be used independently or both at the same time. - -### Bus Outbox Behavior - -Normally when messages are published or sent they are delivered directly to the message broker: - -![Delivery to Broker](/write-to-broker.png "Delivery to Broker") - -When the bus outbox is configured, the scoped interfaces are replaced with versions that write to the outbox. Since `ISendEndpointProvider` and `IPublishEndpoint` are registered as scoped in the container, they are able to share the same scope as the `DbContext` used by the application. - -![Delivery to Outbox](/write-to-outbox.png "Delivery to Outbox") - -Once the changes are saved in the `DbContext` (typically by the application calling `SaveChangesAsync`), the messages will be written to the database as part of the transaction and will be available to the delivery service. - -The delivery service queries the `OutboxMessage` table for messages published or sent via the Bus Outbox, and attempts to deliver any messages found to the message broker. - -![Delivery to Broker](/outbox-to-broker.png "Delivery to Broker") - -The delivery service uses the _OutboxState_ table to ensure that messages are delivered to the broker in the order they were published/sent. The _OutboxState_ table is also used to lock messages so that multiple instances of the delivery service can coexist without conflict. - -### Consumer Outbox Behavior - -Normally, when messages are published or sent by a consumer or one of its dependencies they are delivered directly to the message broker: - -![Regular Consumer Behavior](/consumer-regular.png "Regular Consumer Behavior") - -When the outbox is configured, the behavior changes. As a message is received, the _inbox_ is used to lock the message by `MessageId`. - -![Consumer Inbox](/consumer-inbox.png "Consumer Inbox") - -When the consumer publishes or sends a message, instead of being delivered to the broker it is stored in the _OutboxMessage_ table. - -![Inbox to Outbox](/inbox-outbox.png "Inbox to Outbox") - -Once the consumer completes and the messages are saved to the outbox, those messages are delivered to the message broker in the order they were produced. - -![Deliver Outbox to Broker](/inbox-outbox-broker.png "Deliver Outbox to Broker") - -If there are issues delivering the messages to the broker, message retry will continue to attempt message delivery. - -For details on configuring the transactional outbox, refer to the [configuration](/documentation/configuration/middleware/outbox) section. - diff --git a/doc/content/3.documentation/5.configuration/0.index.md b/doc/content/3.documentation/5.configuration/0.index.md deleted file mode 100755 index b3cfcbfeec5..00000000000 --- a/doc/content/3.documentation/5.configuration/0.index.md +++ /dev/null @@ -1,506 +0,0 @@ ---- -navigation.title: Overview ---- - -# Configuration - -MassTransit is usable in most .NET application types. MassTransit is easily configured in ASP.NET Core or .NET Generic Host applications (using .NET 6 or later). - -To use MassTransit, add the _MassTransit_ package (from NuGet) and start with the _AddMassTransit_ method shown below. - -```csharp -using MassTransit; - -services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - }); -}); -``` - -In this configuration, the following variables are used: - -| Variable | Type | Description | -|-----------|:----------------------------------|-------------------------------------------------------------------------------------------| -| `x` | `IBusRegistrationConfigurator` | Configure the bus instance (not transport specific) and the underlying service collection | -| `context` | `IBusRegistrationContext` | The configured bus context, also implements `IServiceProvider` | -| `cfg` | `IRabbitMqBusFactoryConfigurator` | Configure the bus specific to the transport (each transport has its own interface type | - -:::alert{type="info"} -The callback passed to the _UsingRabbitMq_ method is invoked after the service collection has been built. Any methods to configure the bus instance (using `x`) should be called outside of this callback. -::: - -Adding MassTransit, as shown above, will configure the service collection with required components, including: - - * Several interfaces (and their implementations, appropriate for the transport specified) - * `IBus` (singleton) - * `IBusControl` (singleton) - * `IReceiveEndpointConnector` (singleton) - * `ISendEndpointProvider` (scoped) - * `IPublishEndpoint` (scoped) - * `IRequestClient` (scoped) - * The bus endpoint with the default settings (not started by default) - * The _MassTransitHostedService_ - * Health checks for the bus (or buses) and receive endpoints - * Using `ILoggerFactory` for log output - -> To configure multiple bus instances in the same service collection, refer to the [MultiBus](/documentation/configuration/multibus) section. - -## Host Options - -MassTransit adds a hosted service so that the generic host can start and stop the bus (or buses, if multiple bus instances are configured). The host options can be configured via _MassTransitHostOptions_ using the _Options_ pattern as shown below. - -```csharp -services.AddOptions() - .Configure(options => - { - }); -``` - -| Option | Description | -|------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| WaitUntilStarted | By default, MassTransit connects to the broker asynchronously. When set to _true_, the MassTransit Hosted Service will block startup until the broker connection has been established. | -| StartTimeout | By default, MassTransit waits infinitely until the broker connection is established. If specified, MassTransit will give up after the timeout has expired. | -| StopTimeout | By default, MassTransit waits infinitely for the bus to stop, including any active message consumers. If specified, MassTransit will force the bus to stop after the timeout has expired. | - -::callout{type="info"} -#summary -The .NET Generic Host has its own internal shutdown timeout. -#content -To configure the Generic Host options so that the bus has sufficient time to stop, configure the host options as shown. -```csharp -services.Configure( - options => options.ShutdownTimeout = TimeSpan.FromMinutes(1)); -``` -:: - -## Transport Options - -Each supported transport has configurable options that can be configured using .NET _options_. In the RabbitMQ example above, the [RabbitMqTransportOptions](/documentation/configuration/transports/rabbitmq#transport-options) should be used to configure the transport. - -The transport can also be configured using the `.Host()` method. - - -## Consumers - -In the configuration shown above, the bus is only configured to publish messages, send messages, and send requests/receive responses. To consume messages, one or more consumers should be added and receive endpoints configured for the added consumers. MassTransit connects each receive endpoint to a queue on the message broker. - -To add a consumer and automatically configure a receive endpoint for the consumer, call one of the [_AddConsumer_](/documentation/configuration/bus/consumers) methods and call _ConfigureEndpoints_ as shown below. - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); -}); -``` - -:::alert{type="info"} -**_ConfigureEndpoints_** should be the last method called after all settings and middleware components have been configured. -::: - -MassTransit will automatically configure a receive endpoint for the _SubmitOrderConsumer_ using the name returned by the configured endpoint name formatter. When the bus is started, the receive endpoint will be started and messages will be delivered from the queue by the transport to an instance of the consumer. - -> All consumer types can be added, including consumers, sagas, saga state machines, and routing slip activities. If a job consumer is added, [additional configuration](/documentation/patterns/job-consumers) is required. - -::callout{type="info"} -#summary -Learn about the default conventions as well as how to tailor the naming style to meet your requirements in this short video: -#content -::div - :video-player{src="https://www.youtube.com/watch?v=bsUlQ93j2MY"} -:: -:: - -## Endpoint Name Formatters - -_ConfigureEndpoints_ uses an `IEndpointNameFormatter` to format the queue names for all supported consumer types. The default endpoint name formatter returns _PascalCase_ class names without the namespace. There are several built-in endpoint name formatters included. For the _SubmitOrderConsumer_, the receive endpoint names would be formatted as shown below. Note that class suffixes such as _Consumer_, _Saga_, and _Activity_ are trimmed from the endpoint name by default. - -| Format | Configuration | Name | -|:-----------|:------------------------------------|:---------------| -| Default | `SetDefaultEndpointNameFormatter` | `SubmitOrder` | -| Snake Case | `SetSnakeCaseEndpointNameFormatter` | `submit_order` | -| Kebab Case | `SetKebabCaseEndpointNameFormatter` | `submit-order` | - -The endpoint name formatters can also be customized by constructing a new instance and configuring MassTransit to use it. - -```csharp -x.SetEndpointNameFormatter(new KebabCaseEndpointNameFormatter(prefix: "Dev"); -``` - -By specifying a prefix, the endpoint name would be `dev-submit-order`. This is useful when sharing a single broker with multiple developers (Amazon SQS is account-wide, for instance). - -## Receive Endpoints - -The previous examples use conventions to configure receive endpoints. Alternatively, receive endpoints can be explicitly configured. - -> When configuring endpoints manually, _ConfigureEndpoints_ should be excluded or be called **after** any explicitly configured receive endpoints. - -To explicitly configure endpoints, use the _ConfigureConsumer_ or _ConfigureConsumers_ method. - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ReceiveEndpoint("order-service", e => - { - e.ConfigureConsumer(context); - }); - }); -}); -``` - -Receive endpoints have transport-independent settings that can be configured. - -| Name | Description | Default | -|:-------------------------|:----------------------------------------------------------------------------|:--------------------------------------------------------------------------| -| PrefetchCount | Number of unacknowledged messages delivered by the broker | max(CPU Count x 2,16) | -| ConcurrentMessageLimit | Number of concurrent messages delivered to consumers | (none, uses PrefetchCount) | -| ConfigureConsumeTopology | Create exchanges/topics on the broker and bind them to the receive endpoint | true | -| PublishFaults | Publish `Fault` events when consumers fault | true | -| DefaultContentType | The default content type for received messages | See [serialization](configuration/integrations/serialization#serializers) | -| SerializerContentType | The default content type for sending/publishing messages | See [serialization](configuration/integrations/serialization#serializers) | - -> The _PrefetchCount_, _ConcurrentMessageLimit_, and serialization settings can be specified at the bus level and will be applied to all receive endpoints. - -In the following example, the _PrefetchCount_ is set to 32 and the _ConcurrentMessageLimit_ is set to 28. - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.PrefetchCount = 32; // applies to all receive endpoints - - cfg.ReceiveEndpoint("order-service", e => - { - e.ConcurrentMessageLimit = 28; // only applies to this endpoint - e.ConfigureConsumer(context); - }); - }); -}); -``` - -> When using _ConfigureConsumer_ with a consumer that has a definition, the _EndpointName_, _PrefetchCount_, and _Temporary_ properties of the consumer definition are not used. - -### Temporary Endpoints - -Some consumers only need to receive messages while connected, and any messages published while disconnected should be discarded. This can be achieved by using a TemporaryEndpointDefinition to configure the receive endpoint. - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingInMemory((context, cfg) => - { - cfg.ReceiveEndpoint(new TemporaryEndpointDefinition(), e => - { - e.ConfigureConsumer(context); - }); - - cfg.ConfigureEndpoints(context); - }); -}); -``` - -### Consumer Definition - -A consumer definition is used to configure the receive endpoint and pipeline behavior for the consumer. When scanning assemblies or namespaces for consumers, consumer definitions are also found and added to the container. The _SubmitOrderConsumer_ and matching definition are shown below. - -```csharp -class SubmitOrderConsumer : - IConsumer -{ - readonly ILogger _logger; - - public SubmitOrderConsumer(ILogger logger) - { - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - _logger.LogInformation("Order Submitted: {OrderId}", context.Message.OrderId); - - await context.Publish(new - { - context.Message.OrderId - }); - } -} - -class SubmitOrderConsumerDefinition : - ConsumerDefinition -{ - public SubmitOrderConsumerDefinition() - { - // override the default endpoint name - EndpointName = "order-service"; - - // limit the number of messages consumed concurrently - // this applies to the consumer only, not the endpoint - ConcurrentMessageLimit = 8; - } - - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) - { - // configure message retry with millisecond intervals - endpointConfigurator.UseMessageRetry(r => r.Intervals(100,200,500,800,1000)); - - // use the outbox to prevent duplicate events from being published - endpointConfigurator.UseInMemoryOutbox(); - } -} -``` - -### Endpoint Definition - -To configure the endpoint for a consumer registration, or override the endpoint configuration in the definition, the `Endpoint` method can be added to the consumer registration. This will create an endpoint definition for the consumer, and register it in the container. This method is available on consumer and saga registrations, with separate execute and compensate endpoint methods for activities. - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(typeof(SubmitOrderConsumerDefinition)) - .Endpoint(e => - { - // override the default endpoint name - e.Name = "order-service-extreme"; - - // specify the endpoint as temporary (may be non-durable, auto-delete, etc.) - e.Temporary = false; - - // specify an optional concurrent message limit for the consumer - e.ConcurrentMessageLimit = 8; - - // only use if needed, a sensible default is provided, and a reasonable - // value is automatically calculated based upon ConcurrentMessageLimit if - // the transport supports it. - e.PrefetchCount = 16; - - // set if each service instance should have its own endpoint for the consumer - // so that messages fan out to each instance. - e.InstanceId = "something-unique"; - }); - - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); -}); -``` - -When the endpoint is configured after the _AddConsumer_ method, the configuration then overrides the endpoint configuration in the consumer definition. However, it cannot override the `EndpointName` if it is specified in the constructor. The order of precedence for endpoint naming is explained below. - -1. Specifying `EndpointName = "submit-order-extreme"` in the constructor which cannot be overridden - - ```csharp - x.AddConsumer() - - public SubmitOrderConsumerDefinition() - { - EndpointName = "submit-order-extreme"; - } - ``` - -2. Specifying `.Endpoint(x => x.Name = "submit-order-extreme")` in the consumer registration, chained to `AddConsumer` - - ```csharp - x.AddConsumer() - .Endpoint(x => x.Name = "submit-order-extreme"); - - public SubmitOrderConsumerDefinition() - { - Endpoint(x => x.Name = "not used"); - } - ``` - -3. Specifying `Endpoint(x => x.Name = "submit-order-extreme")` in the constructor, which creates an endpoint definition - - ```csharp - x.AddConsumer() - - public SubmitOrderConsumerDefinition() - { - Endpoint(x => x.Name = "submit-order-extreme"); - } - ``` - -4. Unspecified, the endpoint name formatter is used (in this case, the endpoint name is `SubmitOrder` using the default formatter) - - ```csharp - x.AddConsumer() - - public SubmitOrderConsumerDefinition() - { - } - ``` - -### Saga Registration - -To add a state machine saga, use the _AddSagaStateMachine_ methods. For a consumer saga, use the _AddSaga_ methods. - -::alert{type="success"} -State machine sagas should be added before class-based sagas, and the class-based saga methods should not be used to add state machine sagas. This may be simplified in the future, but for now, be aware of this registration requirement. -:: - -```csharp -services.AddMassTransit(r => -{ - // add a state machine saga, with the in-memory repository - r.AddSagaStateMachine() - .InMemoryRepository(); - - // add a consumer saga with the in-memory repository - r.AddSaga() - .InMemoryRepository(); - - // add a saga by type, without a repository. The repository should be registered - // in the container elsewhere - r.AddSaga(typeof(OrderSaga)); - - // add a state machine saga by type, including a saga definition for that saga - r.AddSagaStateMachine(typeof(OrderState), typeof(OrderStateDefinition)) - - // add all saga state machines by type - r.AddSagaStateMachines(Assembly.GetExecutingAssembly()); - - // add all sagas in the specified assembly - r.AddSagas(Assembly.GetExecutingAssembly()); - - // add sagas from the namespace containing the type - r.AddSagasFromNamespaceContaining(); - r.AddSagasFromNamespaceContaining(typeof(OrderSaga)); -}); -``` - -To add a saga registration and configure the consumer endpoint in the same expression, a definition can automatically be created. - -```csharp -services.AddMassTransit(r => -{ - r.AddSagaStateMachine() - .NHibernateRepository() - .Endpoint(e => - { - e.Name = "order-state"; - e.ConcurrentMessageLimit = 8; - }); -}); -``` - -Supported saga persistence storage engines are documented in the [saga documentation](/documentation/patterns/saga/) section. - - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.SetKebabCaseEndpointNameFormatter(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); -}); -``` - -And the consumer: - -```csharp -class ValueEnteredEventConsumer : - IConsumer -{ - ILogger _logger; - - public ValueEnteredEventConsumer(ILogger logger) - { - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - _logger.LogInformation("Value: {Value}", context.Message.Value); - } -} -``` - -An ASP.NET Core application can also configure receive endpoints. The consumer, along with the receive endpoint, is configured within the _AddMassTransit_ configuration. Separate registration of the consumer is not required (and discouraged), however, any consumer dependencies should be added to the container separately. Consumers are registered as scoped, and dependencies should be registered as scoped when possible, unless they are singletons. - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ReceiveEndpoint("event-listener", e => - { - e.ConfigureConsumer(context); - }); - }); -}); -``` - -```csharp -class EventConsumer : - IConsumer -{ - ILogger _logger; - - public EventConsumer(ILogger logger) - { - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - _logger.LogInformation("Value: {Value}", context.Message.Value); - } -} -``` - -## Health Checks - -The _AddMassTransit_ method adds bus health checks to the service collection. To configure health checks, map the ready and live endpoints in your ASP.NET application. - -```csharp -app.UseEndpoints(endpoints => -{ - endpoints.MapHealthChecks("/health/ready", new HealthCheckOptions() - { - Predicate = (check) => check.Tags.Contains("ready"), - }); - - endpoints.MapHealthChecks("/health/live", new HealthCheckOptions()); -}); -``` - -## Transports - -:FolderNavigation{path='documentation/configuration/transports'} - -## Persistence - -:FolderNavigation{path='documentation/configuration/persistence'} - -## Middleware - -:FolderNavigation{path='documentation/configuration/middleware'} - -## Scheduling - -:FolderNavigation{path='documentation/configuration/scheduling'} - -## Integrations - -:FolderNavigation{path='documentation/configuration/integration'} diff --git a/doc/content/3.documentation/5.configuration/1.transports/2.rabbitmq.md b/doc/content/3.documentation/5.configuration/1.transports/2.rabbitmq.md deleted file mode 100755 index 19a0e7db77e..00000000000 --- a/doc/content/3.documentation/5.configuration/1.transports/2.rabbitmq.md +++ /dev/null @@ -1,214 +0,0 @@ ---- -navigation.title: RabbitMQ ---- - -# RabbitMQ Configuration - -[![alt MassTransit on NuGet](https://img.shields.io/nuget/v/MassTransit.svg "MassTransit on NuGet")](https://nuget.org/packages/MassTransit.RabbitMQ/) - -With tens of thousands of users, RabbitMQ is one of the most popular open source message brokers. RabbitMQ is lightweight and easy to deploy on premises and in the cloud. RabbitMQ can be deployed in distributed and federated configurations to meet high-scale, high-availability requirements. - -MassTransit fully supports RabbitMQ, including many of the advanced features and capabilities. - -::alert{type="info"} -To get started with RabbitMQ, refer to the [configuration](/documentation/configuration) section which uses RabbitMQ in the examples. -:: - -## Minimal Example - -In the example below, which configures a receive endpoint, consumer, and message type, the bus is configured to use RabbitMQ. - -```csharp -namespace RabbitMqConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public static class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.Host("localhost", "/", h => - { - h.Username("guest"); - h.Password("guest"); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} -``` - -## Broker Topology - -With RabbitMQ, which supports exchanges and queues, messages are _sent_ or _published_ to exchanges and RabbitMQ routes those messages through exchanges to the appropriate queues. - -When the bus is started, MassTransit will create exchanges and queues on the virtual host for the receive endpoint. MassTransit creates durable, _fanout_ exchanges by default, and queues are also durable by default. - -## Configuration - -The configuration includes: - -* The RabbitMQ host - - Host name: `localhost` - - Virtual host: `/` - - User name and password used to connect to the virtual host (credentials are virtual-host specific) -* The receive endpoint - - Queue name: `order-events-listener` - - Consumer: `OrderSubmittedEventConsumer` - - Message type: `OrderSystem.Events.OrderSubmitted` - -| Name | Description | -|:----------------------------------|:------------------------------------------------------------------------------------------------------------------| -| order-events-listener | Queue for the receive endpoint | -| order-events-listener | An exchange, bound to the queue, used to _send_ messages | -| OrderSystem.Events:OrderSubmitted | An exchange, named by the message-type, bound to the _order-events-listener_ exchange, used to _publish_ messages | - -When a message is sent, the endpoint address can be one of two values: - -`exchange:order-events-listener` - -Send the message to the _order-events-listener_ exchange. If the exchange does not exist, it will be created. _MassTransit translates topic: to exchange: when using RabbitMQ, so that topic: addresses can be resolved – since RabbitMQ is the only supported transport that doesn't have topics._ - -`queue:order-events-listener` - -Send the message to the _order-events-listener_ exchange. If the exchange or queue does not exist, they will be created and the exchange will be bound to the queue. - -With either address, RabbitMQ will route the message from the _order-events-listener_ exchange to the _order-events-listener_ queue. - -When a message is published, the message is sent to the _OrderSystem.Events:OrderSubmitted_ exchange. If the exchange does not exist, it will be created. RabbitMQ will route the message from the _OrderSystem.Events:OrderSubmitted_ exchange to the _order-events-listener_ exchange, and subsequently to the _order-events-listener_ queue. If other receive endpoints connected to the same virtual host include consumers that consume the _OrderSubmitted_ message, a copy of the message would be routed to each of those endpoints as well. - -::alert{type="danger"} -If a message is published before starting the bus, so that MassTransit can create the exchanges and queues, the exchange _OrderSystem.Events:OrderSubmitted_ will be created. However, until the bus has been started at least once, there won't be a queue bound to the exchange and any published messages will be lost. Once the bus has been started, the queue will remain bound to the exchange even when the bus is stopped. -:: - -Durable exchanges and queues remain configured on the virtual host, so even if the bus is stopped messages will continue to be routed to the queue. When the bus is restarted, queued messages will be consumed. - -## Transport Options - -All RabbitMQ transport options can be configured using the `.Host()` method. The most commonly used settings can be configured via transport options. - -```csharp -services.AddOptions() - .Configure(options => - { - // configure options manually, but usually bind them to a configuration section - }); -``` - -| Property | Type | Description | -|----------------|--------|---------------------| -| Host | string | Network host name | -| Port | ushort | Network port | -| ManagementPort | ushort | Management API port | -| VHost | string | Virtual host name | -| User | string | Username | -| Pass | string | Password | -| UseSsl | bool | True to use SSL/TLS | - -## Host Configuration - -MassTransit includes several RabbitMQ options that configure the behavior of the entire bus instance. - -| Property | Type | Description | -|----------------------------|----------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| PublisherConfirmation | bool | MassTransit will wait until RabbitMQ confirms messages when publishing or sending messages (default: true) | -| Heartbeat | TimeSpan | The heartbeat interval used by the RabbitMQ client to keep the connection alive | -| RequestedChannelMax | ushort | The maximum number of channels allowed on the connection | -| RequestedConnectionTimeout | TimeSpan | The connection timeout | -| ContinuationTimeout | TImeSpan | Sets the time the client will wait for the broker to response to RPC requests. Increase this value if you are experiencing timeouts from RabbitMQ due to a slow broker instance. | - -#### UseCluster - -MassTransit can connect to a cluster of RabbitMQ virtual hosts and treat them as a single virtual host. To configure a cluster, call the `UseCluster` methods, and add the cluster nodes, each of which becomes part of the virtual host identified by the host name. Each cluster node can specify either a `host` or a `host:port` combination. - -> While this exists, it's generally preferable to configure something like HAProxy in front of a RabbitMQ cluster, instead of using MassTransit's built-in cluster configuration. - -#### ConfigureBatchPublish - -MassTransit will briefly buffer messages before sending them to RabbitMQ, to increase message throughput. While use of the default values is recommended, the batch options can be configured. - -| Property | Type | Default | Description | -|:-------------|:---------|---------|---------------------------------------------------------| -| Enabled | bool | false | Enable or disable batch sends to RabbitMQ | -| MessageLimit | int | 100 | Limit the number of messages per batch | -| SizeLimit | int | 64K | A rough limit of the total message size | -| Timeout | TimeSpan | 1ms | The time to wait for additional messages before sending | - - -```csharp -x.UsingRabbitMq((context, cfg) => -{ - cfg.Host("localhost", h => - { - h.ConfigureBatchPublish(x => - { - x.Enabled = true; - x.Timeout = TimeSpan.FromMilliseconds(2); - }); - }); -}); -``` - -MassTransit includes several receive endpoint level configuration options that control receive endpoint behavior. - -| Property | Type | Description | -|----------------|--------|-------------------------------------------------------------------------------------------------------| -| PrefetchCount | ushort | The number of unacknowledged messages that can be processed concurrently (default based on CPU count) | -| PurgeOnStartup | bool | Removes all messages from the queue when the bus is started (default: false) | -| AutoDelete | bool | If true, the queue will be automatically deleted when the bus is stopped (default: false) | -| Durable | bool | If true, messages are persisted to disk before being acknowledged (default: true) | - -## Additional Examples - -### CloudAMQP - -MassTransit can be used with CloudAMQP, which is a great SaaS-based solution to host your RabbitMQ broker. To configure MassTransit, the host and virtual host must be specified, and _UseSsl_ must be configured. - -```csharp -services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - cfg.Host("wombat.rmq.cloudamqp.com", 5671, "your_vhost", h => - { - h.Username("your_vhost"); - h.Password("your_password"); - - h.UseSsl(s => - { - s.Protocol = SslProtocols.Tls12; - }); - }); - }); -}); -``` - -### AmazonMQ - RabbitMQ - -AmazonMQ now includes [RabbitMQ support](https://us-east-2.console.aws.amazon.com/amazon-mq/home), which means the best message broker can now be used easily on AWS. To configure MassTransit, the AMQPS endpoint address can be used to configure the host as shown below. - -```csharp -services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - cfg.Host(new Uri("amqps://b-12345678-1234-1234-1234-123456789012.mq.us-east-2.amazonaws.com:5671"), h => - { - h.Username("username"); - h.Password("password"); - }); - }); -}); -``` diff --git a/doc/content/3.documentation/5.configuration/1.transports/3.azure-service-bus.md b/doc/content/3.documentation/5.configuration/1.transports/3.azure-service-bus.md deleted file mode 100755 index c59142bddac..00000000000 --- a/doc/content/3.documentation/5.configuration/1.transports/3.azure-service-bus.md +++ /dev/null @@ -1,138 +0,0 @@ ---- -navigation.title: Azure Service Bus ---- - -# Azure Service Bus Configuration - -[![alt MassTransit on NuGet](https://img.shields.io/nuget/v/MassTransit.Azure.ServiceBus.Core.svg "MassTransit on NuGet")](https://nuget.org/packages/MassTransit.Azure.ServiceBus.Core/) - -[![alt MassTransit on NuGet](https://img.shields.io/nuget/dt/MassTransit.Azure.ServiceBus.Core.svg "MassTransit on NuGet")](https://nuget.org/packages/MassTransit.Azure.ServiceBus.Core/) - -MassTransit fully supports Azure Service Bus, including many of the advanced features and capabilities. - -::alert{type="warning"} -The Azure Service Bus transport only supports **Standard** and **Premium** tiers of the Microsoft Azure Service Bus service. Premium tier is recommended for production environments. See [Performance](#performance) section below. -:: - -## Minimal Example - -To configure Azure Service Bus, use the connection string (from the Azure portal) to configure the host as shown below. - -```csharp -namespace ServiceBusConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - x.UsingAzureServiceBus((context, cfg) => - { - cfg.Host("connection-string"); - }); - }); - }) - .Build() - .RunAsync(); - } -} -``` - -## Broker Topology - -With Azure Service Bus (ASB), which supports topics and queues, messages are _sent_ or _published_ to topics and ASB routes those messages through topics to the appropriate queues. - -## Configuration - - -Azure Service Bus queues includes an extensive set a properties that can be configured. All of these are optional, MassTransit uses sensible defaults, but the control is there when needed. - -```csharp -services.AddMassTransit(x => -{ - x.UsingAzureServiceBus((context, cfg) => - { - cfg.Host("connection-string"); - - cfg.ReceiveEndpoint("input-queue", e => - { - // all of these are optional!! - - e.PrefetchCount = 100; - - // number of messages to deliver concurrently - e.ConcurrentMessageLimit = 100; - - // default, but shown for example - e.LockDuration = TimeSpan.FromMinutes(5); - - // lock will be renewed up to 30 minutes - e.MaxAutoRenewDuration = TimeSpan.FromMinutes(30); - }); - }); -}); -``` - -### Host Settings - -| Property | Type | Description | -|----------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| TokenCredential | | Use a specific token-based credential, such as a managed identity token, to access the namespace. You can use the [DefaultAzureCredential](https://docs.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet) to automatically apply any one of several credential types. | -| TransportType | | Change the transport type from the default (AMQP) to use WebSockets | - -### Receive Settings - -| Property | Type | Description | -|----------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| PrefetchCount | int | The number of unacknowledged messages that can be processed concurrently (default based on CPU count) | -| MaxConcurrentCalls | int | How many concurrent messages to dispatch (transport-throttled) | -| LockDuration | TimeSpan | How long to hold message locks (max is 5 minutes) | -| MaxAutoRenewDuration | TimeSpan | How long to renew message locks (maximum consumer duration) | -| RequiresSession | bool | If true, a message SessionId must be specified when sending messages to the queue | -| MaxDeliveryCount | int | How many times the transport will redeliver the message on negative acknowledgment. This is different from retry, this is the transport redelivering the message to a receive endpoint before moving it to the dead letter queue. | - -For example, to configure the transport type to use AMQP over Web Sockets: - -```csharp -cfg.Host(connectionString, h => -{ - h.TransportType = ServiceBusTransportType.AmqpWebSockets; -}); - -``` - -## Additional Examples - -### Example with Azure Managed Identity - -The following example shows how to configure Azure Service Bus using an Azure Managed Identity: - -```csharp -services.AddMassTransit(x => -{ - x.UsingAzureServiceBus((context, cfg) => - { - cfg.Host(new Uri("sb://your-service-bus-namespace.servicebus.windows.net")); - }); -}); -``` - -During local development, in the case of Visual Studio, you can configure the account to use under Options -> Azure Service Authentication. Note that your Azure Active Directory user needs explicit access to the resource and have the 'Azure Service Bus Data Owner' role assigned. - -::alert{type="warning"} -To ensure that Mass Transit has sufficient permissions to perform queue management as well as messaging operations. Your identity & managed identity will need to have the correct role assignments within Azure. - -Assigning the role **Azure Service Bus Data Owner** will provide sufficient permissions for Mass Transit to function on the namespace. -:: - -## Performance - -We **really** recommend that you use the Premium subscription levels for production workloads. We have performed our own testing using [MassTransit Benchmark](https://github.com/MassTransit/MassTransit-Benchmark) on a P4 instance. It is also critical that your application is in the same DC as the ASB instance. From a home test using a 1Gb fiber connection we could not get over 600/second. When running in the same DC as the ASB we were able to acheive 6k/second. This test was done with one instance writing to ASB and another instance reading from ASB, as adding consumption over the same AMQP connection killed throughput. diff --git a/doc/content/3.documentation/5.configuration/1.transports/4.amazon-sqs.md b/doc/content/3.documentation/5.configuration/1.transports/4.amazon-sqs.md deleted file mode 100755 index 01d475a6e6f..00000000000 --- a/doc/content/3.documentation/5.configuration/1.transports/4.amazon-sqs.md +++ /dev/null @@ -1,165 +0,0 @@ ---- -navigation.title: Amazon SQS ---- - -# Amazon SQS Configuration - -[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.AmazonSQS.svg "NuGet")](https://nuget.org/packages/MassTransit.AmazonSQS/) - - -MassTransit combines Amazon SQS (Simple Queue Service) with SNS (Simple Notification Service) to provide both send and publish support. - -Configuring a receive endpoint will use the message topology to create and subscribe SNS topics to SQS queues so that published messages will be delivered to the receive endpoint queue. - -## Minimal Example - -In the example below, the Amazon SQS settings are configured. - -```csharp -namespace AmazonSqsConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - x.UsingAmazonSqs((context, cfg) => - { - cfg.Host("us-east-2", h => - { - h.AccessKey("your-iam-access-key"); - h.SecretKey("your-iam-secret-key"); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} -``` - -## Broker Topology - -With SQS/SNS, which supports topics and queues, messages are _sent_ or _published_ to SNS Topics and then routes those messages through subscriptions to the appropriate SQS Queues. - -When the bus is started, MassTransit will create SNS Topics and SQS Queues for the receive endpoint. - -## Configuration - -The configuration includes: - -* The Amazon SQS host - - Region name: `us-east-2` - - Access key and secret key used to access the resources - -## Additional Examples - -Any topic can be subscribed to a receive endpoint, as shown below. The topic attributes can also be configured, in case the topic needs to be created. - -```csharp -services.AddMassTransit(x => -{ - x.UsingAmazonSqs((context, cfg) => - { - cfg.Host("us-east-2", h => - { - h.AccessKey("your-iam-access-key"); - h.SecretKey("your-iam-secret-key"); - }); - - cfg.ReceiveEndpoint("input-queue", e => - { - // disable the default topic binding - e.ConfigureConsumeTopology = false; - - e.Subscribe("event-topic", s => - { - // set topic attributes - s.TopicAttributes["DisplayName"] = "Public Event Topic"; - s.TopicSubscriptionAttributes["some-subscription-attribute"] = "some-attribute-value"; - s.TopicTags.Add("environment", "development"); - }); - }); - }); -}); -``` - -## Errata - -### Scoping - -Because there is only ever one "SQS/SNS" per AWS account it can be helpful to "Scope" your queues and topics. This will prefix all SQS queues and SNS topics with scope value. - -```csharp -services.AddMassTransit(x => -{ - x.UsingAmazonSqs((context, cfg) => - { - cfg.Host("us-east-2", h => - { - h.AccessKey("your-iam-access-key"); - h.SecretKey("your-iam-secret-key"); - - // specify a scope for all topics - h.Scope("dev", true); - }); - - // additionally include the queues - cfg.ConfigureEndpoints(context, new DefaultEndpointNameFormatter("dev-", false)); - }); -}); -``` - -### Example IAM Policy - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "SqsAccess", - "Effect": "Allow", - "Action": [ - "sqs:SetQueueAttributes", - "sqs:ReceiveMessage", - "sqs:CreateQueue", - "sqs:DeleteMessage", - "sqs:SendMessage", - "sqs:GetQueueUrl", - "sqs:GetQueueAttributes", - "sqs:ChangeMessageVisibility", - "sqs:PurgeQueue", - "sqs:DeleteQueue", - "sqs:TagQueue" - ], - "Resource": "arn:aws:sqs:*:YOUR_ACCOUNT_ID:*" - },{ - "Sid": "SnsAccess", - "Effect": "Allow", - "Action": [ - "sns:GetTopicAttributes", - "sns:CreateTopic", - "sns:Publish", - "sns:Subscribe" - ], - "Resource": "arn:aws:sns:*:YOUR_ACCOUNT_ID:*" - },{ - "Sid": "SnsListAccess", - "Effect": "Allow", - "Action": [ - "sns:ListTopics" - ], - "Resource": "*" - } - ] -} -``` diff --git a/doc/content/3.documentation/5.configuration/2.persistence/azure-table.md b/doc/content/3.documentation/5.configuration/2.persistence/azure-table.md deleted file mode 100755 index e621f3b3889..00000000000 --- a/doc/content/3.documentation/5.configuration/2.persistence/azure-table.md +++ /dev/null @@ -1,59 +0,0 @@ -# Azure Table Storage - -[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.Azure.Cosmos.Table.svg "NuGet")](https://nuget.org/packages/MassTransit.Azure.Cosmos.Table/) - -Azure Tables are exposed in two ways in Azure - via Storage accounts & via the premium offering within Cosmos DB APIs. This persistence supports both implementations and behind the curtains uses the Microsoft.Azure.Cosmos.Table library for communication. - -::alert{type="success"} -Azure Tables currently only supports Optimistic Concurrency. Mass Transit manages the ETag property in Payload Context and uses this property for state machine updates. Concurrency errors can be spotted in logs via standard "Precondition Failed" errors from Table Storage. -:: - -::alert{type="warning"} -Be sure to set DateTime properties as nullable when updated later in the saga. Failure to do this can result in 400 bad requests from Table Storage. -:: - -```csharp -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } -} -``` - -## Container Integration - -To configure a Table as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. - -```csharp -CloudTable cloudTable; -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .AzureTableRepository(endpointUri, key, r => - { - cfg.ConnectionFactory(() => cloudTable); - }); -}); -``` - -The container extension will register the saga repository in the container. - -To configure the saga repository with a specific key formatter, use the code shown below with _KeyFormatter_ configuration extension. - -```csharp -CloudTable cloudTable; -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .AzureTableRepository(endpointUri, key, r => - { - cfg.ConnectionFactory(() => cloudTable); - cfg.KeyFormatter(() => new ConstRowSagaKeyFormatter(typeof(OrderState).Name))) - }); -}); -``` - -Unlike the default `ConstPartitionSagaKeyFormatter`, `ConstRowSagaKeyFormatter` in this example uses `PartitionKey` to store the correlationId which may benefit from [scale-out capability of Tables](https://docs.microsoft.com/en-us/rest/api/storageservices/designing-a-scalable-partitioning-strategy-for-azure-table-storage#scalability). diff --git a/doc/content/3.documentation/5.configuration/2.persistence/entity-framework.md b/doc/content/3.documentation/5.configuration/2.persistence/entity-framework.md deleted file mode 100755 index 350396fa7e2..00000000000 --- a/doc/content/3.documentation/5.configuration/2.persistence/entity-framework.md +++ /dev/null @@ -1,157 +0,0 @@ -# Entity Framework - -[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.EntityFrameworkCore.svg "NuGet")](https://nuget.org/packages/MassTransit.EntityFrameworkCore/) - -An example saga instance is shown below, which is orchestrated using an Automatonymous state machine. The _CorrelationId_ will be the primary key, and _CurrentState_ will be used to store the current state of the saga instance. - -```csharp -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } - - // If using Optimistic concurrency, this property is required - public byte[] RowVersion { get; set; } -} -``` - -The instance properties are configured using a _SagaClassMap_. - -::alert{type="warning"} -The `SagaClassMap` has a default mapping for the `CorrelationId` as the primary key. If you create your own mapping, you must follow the same convention, or at least make it a Clustered Index + Unique, otherwise you will likely experience deadlock exceptions and/or performance issues in high throughput scenarios. -:: - -```csharp -public class OrderStateMap : - SagaClassMap -{ - protected override void Configure(EntityTypeBuilder entity, ModelBuilder model) - { - entity.Property(x => x.CurrentState).HasMaxLength(64); - entity.Property(x => x.OrderDate); - - // If using Optimistic concurrency, otherwise remove this property - entity.Property(x => x.RowVersion).IsRowVersion(); - } -} -``` - -Include the instance map in a _DbContext_ class that will be used by the saga repository. - -```csharp -public class OrderStateDbContext : - SagaDbContext -{ - public OrderStateDbContext(DbContextOptions options) - : base(options) - { - } - - protected override IEnumerable Configurations - { - get { yield return new OrderStateMap(); } - } -} -``` - -## Configuration - -Once the class map and associated _DbContext_ class have been created, the saga repository can be configured with the saga registration, which is done using the configuration method passed to _AddMassTransit_. The following example shows how the repository is configured for using Microsoft Dependency Injection Extensions, which are used by default with Entity Framework Core. - -```csharp -services.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .EntityFrameworkRepository(r => - { - r.ConcurrencyMode = ConcurrencyMode.Pessimistic; // or use Optimistic, which requires RowVersion - - r.AddDbContext((provider,builder) => - { - builder.UseSqlServer(connectionString, m => - { - m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); - m.MigrationsHistoryTable($"__{nameof(OrderStateDbContext)}"); - }); - }); - }); -}); -``` - -### Shared DbContext - -A single `DbContext` can be registered in the container which can then be used to configure sagas that are mapped by the `DbContext`. For example, [Job Consumers](/documentation/patterns/job-consumers) needs three saga repositories, and the Entity Framework Core package includes the `JobServiceSagaDbContext` which can be configured using the `AddSagaRepository` method as shown below. - -```csharp -services.AddDbContext(builder => - builder.UseNpgsql(Configuration.GetConnectionString("JobService"), m => - { - m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); - m.MigrationsHistoryTable($"__{nameof(JobServiceSagaDbContext)}"); - })); - -services.AddMassTransit(x => -{ - x.AddSagaRepository() - .EntityFrameworkRepository(r => - { - r.ExistingDbContext(); - r.LockStatementProvider = new PostgresLockStatementProvider(); - }); - x.AddSagaRepository() - .EntityFrameworkRepository(r => - { - r.ExistingDbContext(); - r.LockStatementProvider = new PostgresLockStatementProvider(); - }); - x.AddSagaRepository() - .EntityFrameworkRepository(r => - { - r.ExistingDbContext(); - r.LockStatementProvider = new PostgresLockStatementProvider(); - }); - - // other configuration, such as consumers, etc. -}); -``` - -The above code using the standard Entity Framework configuration extensions to add the _DbContext_ to the container, using PostgreSQL. Because the job service state machine receive endpoints are configured by _ConfigureJobServiceEndpoints_, the saga repositories must be configured separately. The _AddSagaRepository_ method is used to register a repository for a saga that has already been added, and uses the same extension methods as the _AddSaga_ and _AddSagaStateMachine_ methods. - -Once configured, the job service sagas can be configured as shown below. - -```csharp -cfg.ServiceInstance(options, instance => -{ - instance.ConfigureJobServiceEndpoints(js => - { - js.ConfigureSagaRepositories(context); - }); -}); -``` - -:sample{sample="job-consumer"} - -The sample above is a working example of this configuration style. - -### Multiple DbContext - -Multiple `DbContext` can be registered in the container which can then be used to configure sagas that are mapped by the `DbContext` and injected into other components. Calling the `AddDbContext` extension method will register a scoped `DbContext` by default. For simple scenarios where there is a single `DbContext` this will work. However, in scenarios where there is at least one other `DbContext` the dotnet command that generates Entity Framework migrations will not work. To resolve this issue, you'll need to perform the following steps: - -1. Make sure that all `DbContext` has a constructor that takes `DbContextOptions` instead of `DbContextOptions`. - -2. Run the Entity Framework Core command to create your migrations as shown below. - - ```bash - dotnet ef migrations add InitialCreate -c JobServiceSagaDbContext - ``` - -3. Run the Entity Framework Core command to sync with the database as shown below. - - ```bash - dotnet ef database update -c JobServiceSagaDbContext - ``` - - diff --git a/doc/content/3.documentation/5.configuration/2.persistence/marten.md b/doc/content/3.documentation/5.configuration/2.persistence/marten.md deleted file mode 100755 index 18c5c8906af..00000000000 --- a/doc/content/3.documentation/5.configuration/2.persistence/marten.md +++ /dev/null @@ -1,96 +0,0 @@ -# Marten - -[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.Marten.svg "NuGet")](https://nuget.org/packages/MassTransit.Marten/) - -[Marten][2] is an open source library that provides provides .NET developers with the ability to easily use the proven PostgreSQL database engine and its fantastic [JSON support][1] as a fully fledged document database. To use Marten and PostgreSQL as saga persistence, you need to install `MassTransit.Marten` NuGet package and add some code. - -> MassTransit will automatically configure the _CorrelationId_ property so that Marten will use that property as the primary key. No attribute is necessary. - -```csharp -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } -} -``` - -## Container Integration - -To configure Marten as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. This will configure Marten to connect to the local Marten instance on the default port using Optimistic concurrency. - -```csharp -container.AddMassTransit(cfg => -{ - var connectionString = "host=localhost;port=5432;database=orders;username=web;password=webpw;"; - - cfg.AddSagaStateMachine() - .MartenRepository(connectionString); -}); -``` - -## Optimistic Concurrency - -To use Marten's built-in Optimistic concurrency, use the configuration options to configure the schema. Marten supports optimistic concurrency by using an eTag-like version field in the metadata, which does not require any additional fields in the saga class. - -```csharp -container.AddMassTransit(cfg => -{ - var connectionString = "host=localhost;port=5432;database=orders;username=web;password=webpw;"; - - cfg.AddSagaStateMachine() - .MartenRepository(connectionString, r => - { - r.Schema.For().UseOptimisticConcurrency(true); - }); -}); -``` - -Alternatively, you can add the `UseOptimisticConcurrency` attribute to the class. - -```csharp -[UseOptimisticConcurrency] -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - // ... -} -``` - -## Index Creation - -Marten can create indices for properties, which greatly increases query performance. If your saga is correlating events using other fields, index creation is recommended. For example, if an _OrderNumber_ property was added to the _OrderState_ class, it could be indexed by configuring it in the repository. - -```csharp -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public string OrderNumber { get; set; } - - public DateTime? OrderDate { get; set; } -} -``` - -```csharp -container.AddMassTransit(cfg => -{ - var connectionString = "host=localhost;port=5432;database=orders;username=web;password=webpw;"; - - cfg.AddSagaStateMachine() - .MartenRepository(connectionString, r => - { - r.Schema.For().Index(x => x.OrderNumber); - }); -}); -``` - -Details on how Marten creates indices is available in the [Computed Index](https://martendb.io/documentation/documents/customizing/computed_index/) documentation. - -[1]: https://www.postgresql.org/docs/9.5/static/functions-json.html -[2]: http://jasperfx.github.io/marten/ diff --git a/doc/content/3.documentation/5.configuration/2.persistence/redis.md b/doc/content/3.documentation/5.configuration/2.persistence/redis.md deleted file mode 100755 index e469e46c6db..00000000000 --- a/doc/content/3.documentation/5.configuration/2.persistence/redis.md +++ /dev/null @@ -1,75 +0,0 @@ -# Redis - -[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.Redis.svg "NuGet")](https://nuget.org/packages/MassTransit.Redis/) - -Redis is a very popular key-value store, which is known for being very fast. To support Redis, MassTransit uses the `StackExchange.Redis` library. - -::alert{type="warning"} -Redis only supports event correlation by _CorrelationId_, it does not support queries. If a saga uses expression-based correlation, a _NotImplementedByDesignException_ will be thrown. -:: - -Storing a saga in Redis requires an additional interface, _ISagaVersion_, which has a _Version_ property used for optimistic concurrency. An example saga is shown below. - -```csharp -public class OrderState : - SagaStateMachineInstance, - ISagaVersion -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } - - public int Version { get; set; } -} -``` - -## Configuration - -To configure Redis as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. This will configure Redis to connect to the local Redis instance on the default port using Optimistic concurrency. This will also store the _ConnectionMultiplexer_ in the container as a single instance, which will be disposed by the container. - -```csharp -services.AddMassTransit(x => -{ - const string configurationString = "127.0.0.1"; - - x.AddSagaStateMachine() - .RedisRepository(configurationString); -}); -``` - -The example below includes all the configuration options, in cases where additional settings are required. - -```csharp -services.AddMassTransit(x => -{ - const string configurationString = "127.0.0.1"; - - x.AddSagaStateMachine() - .RedisRepository(r => - { - r.DatabaseConfiguration(configurationString); - - // Default is Optimistic - r.ConcurrencyMode = ConcurrencyMode.Pessimistic; - - // Optional, prefix each saga instance key with the string specified - // resulting dev:c6cfd285-80b2-4c12-bcd3-56a00d994736 - r.KeyPrefix = "dev"; - - // Optional, to customize the lock key - r.LockSuffix = "-lockage"; - - // Optional, the default is 30 seconds - r.LockTimeout = TimeSpan.FromSeconds(90); - });; -}); -``` - -## Concurrency - -Redis supports both Optimistic (default) and Pessimistic concurrency. - -In optimistic mode, the saga instance is not locked before reading, which can ultimately lead to a write conflict if the instance was updated by another message. The _Version_ property is used to compare that the update would not overwrite a previous update. It is recommended that a retry policy is configured (using `UseMessageRetry`, see the [exceptions](/documentation/concepts/exceptions#retry) documentation). - -Pessimistic concurrency uses the Redis lock mechanism. During the message processing, the repository will lock the saga instance before reading it, so that any concurrent attempts to lock the same instance will wait until the current message has completed or the lock timeout expires. diff --git a/doc/content/3.documentation/5.configuration/3.middleware/0.index.md b/doc/content/3.documentation/5.configuration/3.middleware/0.index.md deleted file mode 100644 index 6fe7b12b573..00000000000 --- a/doc/content/3.documentation/5.configuration/3.middleware/0.index.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -navigation.title: Overview ---- - -# Middleware - -MassTransit is built using a network of pipes and filters to dispatch messages. A pipe is composed of a series of filters, each of which is a key atom and are described below. - -A detailed view of MassTransit's [Receive Pipeline](receive.md) is a good example of the sophistication possible. - -Middleware components are configured using extension methods on any pipe configurator `IPipeConfigurator`, and the extension methods all begin with `Use` to separate them from other methods. - -To understand how middleware components are built, an understanding of filters and pipes is needed. - -## Filters - -A filter is a middleware component that performs a specific function, and should adhere to the single responsibility principal – do one thing, one thing only (and hopefully do it well). By sticking to this approach, developers are able to opt-in to each behavior without including unnecessary or unwatched functionality. - -There are many filters included with GreenPipes, and a whole lot more of them are included with MassTransit. In fact, the entire MassTransit message flow is built around pipes and filters. - -Developers can create their own filters. To create a filter, create a class that implements `IFilter`. - -```csharp -public interface IFilter - where T : class, PipeContext -{ - void Probe(ProbeContext context); - - Task Send(T context, IPipe next); -} -``` - -The _Probe_ method is used to interrogate the filter about its behavior. This should describe the filter in a way that a developer would understand its role when looking at a network graph. For example, a transaction filter may add the following to the context. - -```csharp -public void Probe(ProbeContext context) -{ - context.CreateFilterScope("transaction"); -} -``` - -The _Send_ method is used to send contexts through the pipe to each filter. _Context_ is the actual context, and _next_ is used to pass the context to the next filter in the pipe. Send returns a Task, and should always follow the .NET guidelines for asynchronous methods. - -### PipeContext - -The _context_ type has a `PipeContext` constraint, which is another core atom in _GreenPipes_. A pipe context can include _payloads_, which are kept in a last-in, first-out (LIFO) collection. Payloads are identified by _type_, and can be retrieved, added, and updated using the `PipeContext` methods: - -```csharp -public interface PipeContext -{ - /// - /// Used to cancel the execution of the context - /// - CancellationToken CancellationToken { get; } - - /// - /// Checks if a payload is present in the context - /// - bool HasPayloadType(Type payloadType); - - /// - /// Retrieves a payload from the pipe context - /// - /// The payload type - /// The payload - /// - bool TryGetPayload(out T payload) - where T : class; - - /// - /// Returns an existing payload or creates the payload using the factory method provided - /// - /// The payload type - /// The payload factory is the payload is not present - /// The payload - T GetOrAddPayload(PayloadFactory payloadFactory) - where T : class; - - /// - /// Either adds a new payload, or updates an existing payload - /// - /// The payload factory called if the payload is not present - /// The payload factory called if the payload already exists - /// The payload type - /// - T AddOrUpdatePayload(PayloadFactory addFactory, UpdatePayloadFactory updateFactory) - where T : class; -``` - -The payload methods are also used to check if a pipe context is another type of context. For example, to see if the `SendContext` is a `RabbitMqSendContext`, the `TryGetPayload` method should be used instead of trying to pattern match or cast the _context_ parameter. - -```csharp -public async Task Send(SendContext context, IPipe next) -{ - if(context.TryGetPayload(out var rabbitMqSendContext)) - rabbitMqSendContext.Priority = 3; - - return next.Send(context); -} -``` - -::alert{type="warning"} -It is entirely the filter's responsibility to call _Send_ on the _next_ parameter. This gives the filter ultimately control over the context and behavior. It is how the retry filter is able to retry – by controlling the context flow. -:: - -User-defined payloads are easily added, so that subsequent filters can use them. The following example adds a payload. - -```csharp -public class SomePayload -{ - public int Value { get; set; } -} - -public async Task Send(SendContext context, IPipe next) -{ - var payload = context.GetOrAddPayload(() => new SomePayload{Value = 27}); - - return next.Send(context); -} -``` - -::alert{type="info"} -Using interfaces for payload types is recommended, but not required. -:: - -## Pipes - -Filters are combined in sequence to form a pipe. A pipe configurator, along with a pipe builder, is used to configure and build a pipe. - -```csharp -public interface CustomContext : - PipeContext -{ - string SomeThing { get; } -} - -IPipe pipe = Pipe.New(x => -{ - x.UseFilter(new CustomFilter(...)); -}) -``` - -The `IPipe` interface is similar to `IFilter`, but a pipe hides the _next_ parameter as it is part of the pipe's structure. It is the pipe's responsibility to pass the -appropriate _next_ parameter to the individual filters in the pipe. - -```csharp -public interface IPipe - where T : class, PipeContext -{ - Task Send(T context); -} -``` - -Send can be called, passing a context instance as shown. - -```csharp -public class BaseCustomContext : - BasePipeContext, - CustomContext -{ - public string SomeThing { get; set; } -} - -await pipe.Send(new BaseCustomContext { SomeThing = "Hello" }); -``` - - - - - - diff --git a/doc/content/3.documentation/5.configuration/3.middleware/1.filters.md b/doc/content/3.documentation/5.configuration/3.middleware/1.filters.md deleted file mode 100644 index 99c84e820b6..00000000000 --- a/doc/content/3.documentation/5.configuration/3.middleware/1.filters.md +++ /dev/null @@ -1,132 +0,0 @@ -# Filters - -## Kill Switch - -A Kill Switch is used to prevent failing consumers from moving all the messages from the input queue to the error queue. By monitoring message consumption and tracking message successes and failures, a Kill Switch stops the receive endpoint when a trip threshold has been reached. - -Typically, consumer exceptions are transient issues and suspending consumption until a later time when the transient issue may have been resolved. - -::alert{type="info"} -A Kill Switch is the messaging analog of a Circuit Breaker, and operates in a similar manner. However, instead of inducing failure to reduce pressure on a backing service, the kill switch stops consuming messages instead thereby reducing pressure on backing services. - -> Read Martin Fowler's description of the pattern [here](http://martinfowler.com/bliki/CircuitBreaker.html). -:: - -### Configuration - -A Kill Switch can be configured on an individual receive endpoint or all receive endpoints on the bus. To configure a kill switch on all receive endpoints, add the _UseKillSwitch_ method as shown. - -```csharp -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.UseKillSwitch(options => options - .SetActivationThreshold(10) - .SetTripThreshold(0.15) - .SetRestartTimeout(m: 1)); - - cfg.ReceiveEndpoint("some-queue", e => - { - e.Consumer(); - }); -}); -``` - -In the above example, the kill switch will activate after _10_ messages have been consumed. If the ratio of failures/attempts exceeds _15%_, the kill switch will trip and stop the receive endpoint. After _1_ minute, the receive endpoint will be restarted. Once restarted, if exceptions are still observed, the receive endpoint will be stopped again for _1_ minute. - -To configure the kill switch on a receive endpoint, the syntax is the same as shown. - -```csharp -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.ReceiveEndpoint("some-queue", e => - { - e.UseKillSwitch(options => options - .SetActivationThreshold(10) - .SetTripThreshold(0.15) - .SetRestartTimeout(m: 1)); - - e.Consumer(); - }); -}); -``` - -### Options - -| Option | Description | -| ---------------------------- | --------------------------------------------------------- | -| `TrackingPeriod` | The time window for tracking exceptions | -| `TripThreshold` | The percentage of failed messages that triggers the kill switch. Should be 0-100, but seriously like 5-10. | -| `ActivationThreshold` | The number of messages that must be consumed before the kill switch activates. | -| `RestartTimeout` | The wait time before restarting the receive endpoint | -| `ExceptionFilter` | By default, all exceptions are tracked. An exception filter can be configured to only track specific exceptions. | - - - -## Circuit Breaker - -A circuit breaker is used to protect resources (remote, local, or otherwise) from being overloaded when -in a failure state. For example, a remote web site may be unavailable and calling that web site in a -message consumer takes 30-60 seconds to time out. By continuing to call the failing service, the service -may be unable to recover. A circuit breaker detects the repeated failures and trips, preventing further -calls to the service and giving it time to recover. Once the reset interval expires, calls are slowly allowed -back to the service. If it is still failing, the breaker remains open, and the timeout interval resets. -Once the service returns to healthy, calls flow normally as the breaker closes. - -Read Martin Fowler's description of the pattern [here](http://martinfowler.com/bliki/CircuitBreaker.html). - -To add the circuit breaker to a receive endpoint: - -```csharp -e.UseCircuitBreaker(cb => -{ - cb.TrackingPeriod = TimeSpan.FromMinutes(1); - cb.TripThreshold = 15; - cb.ActiveThreshold = 10; - cb.ResetInterval = TimeSpan.FromMinutes(5); -}); -``` - -### Options - -There are four options that can be adjusted on a circuit breaker. - -| Option | Description | -|-------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `TrackingPeriod` | The time window for tracking exceptions | -| `TripThreshold` | This is a percentage, and is based on the ratio of successful to failed attempts. When set to 15, if the ratio exceeds 15%, the circuit breaker opens and remains open until the `ResetInterval` expires. | -| `ActiveThreshold` | that must reach the circuit breaker in a tracking period before the circuit breaker can trip. If set to 10, the trip threshold is not evaluated until at least 10 messages have been received. | -| `ResetInterval` | The period of time between the circuit breaker trip and the first attempt to close the circuit breaker. Messages that reach the circuit breaker during the open period will immediately fail with the same exception that tripped the circuit breaker. | - -## Rate Limiter - -A rate limiter is used to restrict the number of messages processed within a time period. The reason may be -that an API or service only accepts a certain number of calls per minute, and will delay any subsequent attempts -until the rate limiting period has expired. - -::alert{type="warning"} -The rate limiter will delay message delivery until the rate limit expires, so it is best to avoid large time windows -and keep the rate limits sane. Something like 1000 over 10 minutes is a bad idea, versus 100 over a minute. Try to -adjust the values and see what works for you. -:: - -There are two modes that a rate limiter can operate, but only one of them is currently supported (the other may come later). - -To add a rate limiter to a receive endpoint: - -```csharp -cfg.ReceiveEndpoint("customer_update_queue", e => -{ - e.UseRateLimit(1000, TimeSpan.FromSeconds(5)); - // other configuration -}); -``` - -The two arguments supported by the rate limiter include: - -### RateLimit - The number of calls allowed in the time period. - -### Interval - The time interval before the message count is reset to zero. - - diff --git a/doc/content/3.documentation/5.configuration/3.middleware/2.outbox.md b/doc/content/3.documentation/5.configuration/3.middleware/2.outbox.md deleted file mode 100644 index 678a9ca6c34..00000000000 --- a/doc/content/3.documentation/5.configuration/3.middleware/2.outbox.md +++ /dev/null @@ -1,121 +0,0 @@ ---- -navigation.title: Transactional Outbox ---- - -# Transactional Outbox Configuration - -The Transaction Outbox is explained in the [patterns](/documentation/patterns/transactional-outbox) section. This section covers how to configure the transactional outbox using any of the supported databases. - - -## Bus Outbox Options - -The bus outbox has its own configuration settings, which are common across all supported databases. - -| Setting | Description | -|--------------------------|------------------------------------------------------------------------------------------------------| -| MessageDeliveryLimit | The number of messages to deliver at a time from the outbox to the broker | -| MessageDeliveryTimeout | Transport Send timeout when delivering messages to the transport | -| DisableDeliveryService() | Disable the outbox message delivery service, removing the hosted service from the service collection | - - -## Entity Framework Outbox - -The Transactional Outbox for Entity Framework Core uses three tables in the `DbContext` to store messages that are subsequently delivered to the message broker. - -| Table | Description | -|---------------|----------------------------------------------------------------------------------------------------------------------------------------------------| -| InboxState | Tracks received messages by `MessageId` for each endpoint | -| OutboxMessage | Stores messages published or sent using `ConsumeContext`, `IPublishEndpoint`, and `ISendEndpointProvider` | -| OutboxState | Tracks delivery of outbox messages by the delivery service (similar to _InboxState_ but for message sent outside of a consumer via the bus outbox) | - -### Configuration - -> The code below is based upon the [sample application](https://github.com/MassTransit/Sample-Outbox) - -The outbox components are included in the `MassTransit.EntityFrameworkCore` NuGet packages. The code below configures both the bus outbox and the consumer outbox using the default settings. In this case, PostgreSQL is the database engine. - -```csharp -x.AddEntityFrameworkOutbox(o => -{ - // configure which database lock provider to use (Postgres, SqlServer, or MySql) - o.UsePostgres(); - - // enable the bus outbox - o.UseBusOutbox(); -}); -``` - -To configure the _DbContext_ with the appropriate tables, use the extension methods shown below: - -```csharp -public class RegistrationDbContext : - DbContext -{ - public RegistrationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.AddInboxStateEntity(); - modelBuilder.AddOutboxMessageEntity(); - modelBuilder.AddOutboxStateEntity(); - } -} -``` - -To configure the outbox on a receive endpoint, configure the receive endpoint as shown below. The configuration below uses a `SagaDefinition` to configure the receive endpoint, which is added to MassTransit along with the saga state machine. - -```csharp -public class RegistrationStateDefinition : - SagaDefinition -{ - readonly IServiceProvider _provider; - - public RegistrationStateDefinition(IServiceProvider provider) - { - _provider = provider; - } - - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, - ISagaConfigurator consumerConfigurator) - { - endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 500, 1000, 1000, 1000, 1000, 1000)); - - endpointConfigurator.UseEntityFrameworkOutbox(_provider); - } -} -``` - -The definition is added with the saga state machine: - -```csharp -x.AddSagaStateMachine() - .EntityFrameworkRepository(r => - { - r.ExistingDbContext(); - r.UsePostgres(); - }); -``` - -The Entity Framework outbox adds a hosted service which removes delivered _InboxState_ entries after the _DuplicateDetectionWindow_ has elapsed. The Bus Outbox includes an additional hosted service that delivers the outbox messages to the broker. - -### Configuration Options - -The available outbox settings are listed below. - -| Setting | Description | -|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| DuplicateDetectionWindow | The amount of time a message remains in the inbox for duplicate detection (based on MessageId) | -| IsolationLevel | The transaction isolation level to use (Serializable by default) | -| LockStatementProvider | The lock statement provider, needed to execute pessimistic locks. Is set via `UsePostgres`, `UseSqlServer` (the default), or `UseMySql` | -| QueryDelay | The delay between queries once messages are no longer available. When a query returns messages, subsequent queries are performed until no messages are returned after which the QueryDelay is used. | -| QueryMessageLimit | The maximum number of messages to query from the database at a time | -| QueryTimeout | The database query timeout | - -## MongoDB - -(coming soon) diff --git a/doc/content/3.documentation/5.configuration/3.middleware/3.scoped.md b/doc/content/3.documentation/5.configuration/3.middleware/3.scoped.md deleted file mode 100644 index 5b3bb3c87f5..00000000000 --- a/doc/content/3.documentation/5.configuration/3.middleware/3.scoped.md +++ /dev/null @@ -1,371 +0,0 @@ -# Custom - -## Scoped Filters - -Most of the built-in filters are created and added to the pipeline during configuration. This approach is typically sufficient, however, there are scenarios where the filter needs access to other components at runtime. - -Using a scoped filter, combined with a supported dependency injection container (either MSDI or Autofac), allows a new filter instance to be resolved from the container for each message. If a current scope is not available, a new scope will be created using the root container. - -### Filter Classes - -Scoped filters must be generic classes with a single generic argument for the message type. For example, a scoped consume filter would be defined as shown below. - -```csharp -public class TFilter : - IFilter> -``` - -### Supported Filter Contexts - -Scope filters are added using one of the following methods, which are specific to the filter context type. - -| Type | Usage | -|------------------------------|-----------------------------------------------------------| -| `ConsumeContext` | `UseConsumeFilter(typeof(TFilter<>), context)` | -| `SendContext` | `UseSendFilter(typeof(TFilter<>), context)` | -| `PublishContext` | `UsePublishFilter(typeof(TFilter<>), context)` | -| `ExecuteContext` | `UseExecuteActivityFilter(typeof(TFilter<>), context)` | -| `CompensateContext` | `UseCompensateActivityFilter(typeof(TFilter<>), context)` | - -More information could be found inside [Middleware](/documentation/configuration/middleware) section - -### Usage - -To create a `ConsumeContext` filter and add it to the receive endpoint: - -```csharp -public class MyConsumeFilter : - IFilter> - where T : class -{ - public MyConsumeFilter(IMyDependency dependency) { } - - public async Task Send(ConsumeContext context, IPipe> next) { } - - public void Probe(ProbeContext context) { } -} - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - //other configuration - services.AddScoped(); //register dependency - - services.AddConsumer(); - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.ReceiveEndpoint("input-queue", e => - { - e.UseConsumeFilter(typeof(MyConsumeFilter<>), context); //generic filter - - e.ConfigureConsumer(); - }); - }); - }); - } -} -``` - -To create a `SendContext` filter and add it to the send pipeline: - -```csharp -public class MySendFilter : - IFilter> - where T : class -{ - public MySendFilter(IMyDependency dependency) { } - - public async Task Send(SendContext context, IPipe> next) { } - - public void Probe(ProbeContext context) { } -} - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - //other configuration - services.AddScoped(); //register dependency - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.UseSendFilter(typeof(MySendFilter<>), context); //generic filter - }); - }); - } -} -``` - -### Combining Consume And Send/Publish Filters - -A common use case with scoped filters is transferring data between the consumer. This data may be extracted from headers, or could include context or authorization information that needs to be passed from a consumed message context to sent or published messages. In these situations, there _may_ be some special requirements to ensure everything works as expected. - -The following example has both consume and send filters, and utilize a shared dependency to communicate data to outbound messages. - -```csharp -public class MyConsumeFilter : - IFilter> - where T : class -{ - public MyConsumeFilter(MyDependency dependency) { } - - public async Task Send(ConsumeContext context, IPipe> next) { } - - public void Probe(ProbeContext context) { } -} - -public class MySendFilter : - IFilter> - where T : class -{ - public MySendFilter(MyDependency dependency) { } - - public async Task Send(SendContext context, IPipe> next) { } - - public void Probe(ProbeContext context) { } -} - -public class MyDependency -{ - public string SomeValue { get; set; } -} - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddScoped(); - - services.AddMassTransit(x => - { - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseSendFilter(typeof(MySendFilter<>), context); - - cfg.ReceiveEndpoint("input-queue", e => - { - e.UseConsumeFilter(typeof(MyConsumeFilter<>), context); - e.ConfigureConsumer(context); - }); - }); - }); - } -} -``` - -::alert{type="warning"} -When using the InMemoryOutbox with scoped publish or send filters, `UseMessageScope` (for MSDI) or `UseMessageLifetimeScope` (for Autofac) must be configured _before_ the InMemoryOutbox. If `UseMessageRetry` is used, it must come _before_ either `UseMessageScope` or `UseMessageLifetimeScope`. -:: - -Because the InMemoryOutbox delays publishing and sending messages until after the consumer or saga completes, the created container scope will have been disposed. The `UseMessageScope` or `UseMessageLifetimeScope` filters create the scope before the InMemoryOutbox, which is then used by the consumer or saga and any scoped filters (consume, publish, or send). - -The updated receive endpoint configuration using the InMemoryOutbox is shown below. - -```csharp - cfg.ReceiveEndpoint("input-queue", e => - { - e.UseMessageRetry(r => r.Intervals(100, 500, 1000, 2000)); - e.UseMessageScope(context); - e.UseInMemoryOutbox(); - - e.UseConsumeFilter(typeof(MyConsumeFilter<>), context); - e.ConfigureConsumer(context); - }); -``` - - - - -## Pipeline Filters - -Middleware components are configured using extension methods, to make them easy to discover. - -::alert{type="info"} -To be consistent with MassTransit conventions, middleware configuration methods should start with `Use`. -:: - -An example middleware component that would log exceptions to the console is shown below. - -```csharp -Bus.Factory.CreateUsingInMemory(cfg => -{ - cfg.UseExceptionLogger(); -}); -``` - -The extension method creates the pipe specification for the middleware component, which can be added to any pipe. For a component on the message consumption pipeline, use `ConsumeContext` instead of any `PipeContext`. - -```csharp -public static class ExampleMiddlewareConfiguratorExtensions -{ - public static void UseExceptionLogger(this IPipeConfigurator configurator) - where T : class, PipeContext - { - configurator.AddPipeSpecification(new ExceptionLoggerSpecification()); - } -} -``` - -The pipe specification is a class that adds the filter to the pipeline. Additional logic can be included, such as configuring optional settings, etc. using a closure syntax similar to the other configuration classes in MassTransit. - -```csharp -public class ExceptionLoggerSpecification : - IPipeSpecification - where T : class, PipeContext -{ - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - - public void Apply(IPipeBuilder builder) - { - builder.AddFilter(new ExceptionLoggerFilter()); - } -} -``` - -Finally, the middleware component itself is a filter added to the pipeline. All filters have absolute and complete control of the execution context and flow of the message. Pipelines are entirely asynchronous, and expect that asynchronous operations will be performed. - -::alert{type="danger"} -Do not use legacy constructs such as .Wait, .Result, or .WaitAll() as these can cause blocking in the message pipeline. While they might work in same cases, you've been warned! -:: - - -```csharp -public class ExceptionLoggerFilter : - IFilter - where T : class, PipeContext -{ - long _exceptionCount; - long _successCount; - long _attemptCount; - - public void Probe(ProbeContext context) - { - var scope = context.CreateFilterScope("exceptionLogger"); - scope.Add("attempted", _attemptCount); - scope.Add("succeeded", _successCount); - scope.Add("faulted", _exceptionCount); - } - - /// - /// Send is called for each context that is sent through the pipeline - /// - /// The context sent through the pipeline - /// The next filter in the pipe, must be called or the pipe ends here - public async Task Send(T context, IPipe next) - { - try - { - Interlocked.Increment(ref _attemptCount); - - // here the next filter in the pipe is called - await next.Send(context); - - Interlocked.Increment(ref _successCount); - } - catch (Exception ex) - { - Interlocked.Increment(ref _exceptionCount); - - await Console.Out.WriteLineAsync($"An exception occurred: {ex.Message}"); - - // propagate the exception up the call stack - throw; - } - } -} -``` - -The example filter above is stateful. If the filter was stateless, the same filter instance could be used by multiple pipes — worth considering if the filter has high memory requirements. - -### Message Type Filters - -In many cases, the message type is used by a filter. To create an instance of a generic filter that includes the message type, use the configuration observer. - -```csharp -public class MessageFilterConfigurationObserver : - ConfigurationObserver, - IMessageConfigurationObserver -{ - public MessageFilterConfigurationObserver(IConsumePipeConfigurator receiveEndpointConfigurator) - : base(receiveEndpointConfigurator) - { - Connect(this); - } - - public void MessageConfigured(IConsumePipeConfigurator configurator) - where TMessage : class - { - var specification = new MessageFilterPipeSpecification(); - - configurator.AddPipeSpecification(specification); - } -} -``` - -Then, in the specification, the appropriate filter can be created and added to the pipeline. - -```csharp -public class MessageFilterPipeSpecification : - IPipeSpecification> - where T : class -{ - public void Apply(IPipeBuilder> builder) - { - var filter = new MessageFilter(); - - builder.AddFilter(filter); - } - - public IEnumerable Validate() - { - yield break; - } -} -``` - -The filter could then include the message type as a generic type parameter. - -```csharp -public class MessageFilter : - IFilter> - where T : class -{ - public void Probe(ProbeContext context) - { - var scope = context.CreateFilterScope("messageFilter"); - } - - public async Task Send(ConsumeContext context, IPipe> next) - { - // do something - - await next.Send(context); - } -} -``` - -The extension method for the above is shown below (for completeness). - -```csharp -public static class MessageFilterConfigurationExtensions -{ - public static void UseMessageFilter(this IConsumePipeConfigurator configurator) - { - if (configurator == null) - throw new ArgumentNullException(nameof(configurator)); - - var observer = new MessageFilterConfigurationObserver(configurator); - } -} -``` diff --git a/doc/content/3.documentation/5.configuration/3.middleware/transactions.md b/doc/content/3.documentation/5.configuration/3.middleware/transactions.md deleted file mode 100644 index 22cc4627517..00000000000 --- a/doc/content/3.documentation/5.configuration/3.middleware/transactions.md +++ /dev/null @@ -1,340 +0,0 @@ -# Transaction - -::alert{type="warning"} -Transactions, and using a shared transaction, is an advanced concept. Every scenario is different, so this is more of a guideline than a rule. -:: - -The message pipeline in MassTransit is asynchronous, leveraging the Task Parallel Library (TPL) extensively to maximum thread utilization. This means that receiving an individual message may involve several threads over the life cycle of the consumer. To prevent strange things from happening, developers should avoid using any *static* or *thread static* variables as these are one of the main causes of errors in asynchronous programming. - -The .NET `System.Transactions` namespace is a static hound, with many applications following the model of using a transaction scope to wrap a transactional operation. - -```csharp -public class Repository -{ - public void Save(Entity entity) - { - using(var scope = new TransactionScope()) - { - SaveEntity(entity); - - scope.Complete(); - } - } -} -``` - -In this example, the creation of a `TransactionScope` actually sets a static variable, `Transaction.Current`, to the created or ambient transaction. That word *ambient* should be a big clue — it's using a static variable (in this case, it's actually a thread static, but anyway). - -It turns out that the above example is simple, and works, because there are no asynchronous methods. But that also means that the method blocks the thread while the database performs work (which takes an eternity in CPU time). Most databases support asynchronous operations (including Entity Framework), so it is logical to assume that using those methods would increase thread utilization. - -It is also often requested that a set of operations be managed as a *unit of work*. A single transaction is shared across multiple operations that are committed as a single unit. If the commit fails, everything is undone and the message is faulted (or retried, if the retry middleware is used). - -## Usage - -MassTransit includes transaction middleware to share a single committable transaction across any number consumers and any dependencies used by the those consumers. To use the middleware, it must be added to the bus or receive endpoint. - -```csharp -Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.ReceiveEndpoint("event_queue", e => - { - e.UseTransaction(x => - { - Timeout = TimeSpan.FromSeconds(90); - IsolationLevel = IsolationLevel.ReadCommitted; - }); - - e.Consumer(); - }) -}); -``` - -For each message, a new `CommittableTransaction` is created. This transaction can be passed to classes that support transactional operations, such as `DbContext`, `SqlCommand`, and `SqlConnection`. It can also be used to create any `TransactionScope` that may be required to support a synchronous operation. - -To use the transaction directly in a consumer, the transaction can be pulled from the `ConsumeContext`. - -```csharp -public class TransactionalConsumer : - IConsumer -{ - readonly SqlConnection _connection; // ctor injected - - public async Task Consume(ConsumeContext context) - { - var transactionContext = context.GetPayload(); - - _connection.EnlistTransaction(transactionContext.Transaction); - - using (SqlCommand command = new SqlCommand(sql, _connection)) - { - using (var reader = await command.ExecuteReaderAsync()) - { - } - } - - // the connection lifetime should be managed by a container - // or perhaps another more specific middleware component. - } -} -``` - -The connection (and by use of the connection, the command) are enlisted in the transaction. Once the method completes, and control is returned to the transaction middleware, if no exceptions are thrown the transaction is committed (which should complete the database operation). If an exception is thrown, the transaction is rolled back. - -While not shown here, a class that provides the connection, and enlists the connection upon creation, should be added to the container to ensure that the transaction is not enlisted twice (not sure that's a bad thing though, it should be ignored). Also, as long as only a single connection string is enlisted, the DTC should not get involved. Using the same transaction across multiple connection strings is a bad thing, as it will make the DTC come into play which slows the world down significantly. - -## Unit of Work (Buffer) - -Sometimes you just have to integrate with Database first systems, but still want some of the perks that message buses have to offer. A good example is an API with your typical HTTP Requests. You want to manipulate your DB, commit, and then upon success, release the messages to the broker. This is NOT a distributed transaction. There's still a risk that you could have the DB up and the broker down, causing the messages to never be sent to the broker. So you've been warned! - -There are two options to provide this buffer: - -- Transactional Enlistment Bus -- Transactional Bus - -## Transactional Enlistment Bus - -Transports don't typically support transactions, so sending messages during a transaction only to encounter an exception resulting in a transaction rollback may lead to messages that were sent without the transaction being committed. - -::alert{type="info"} -MassTransit has an in-memory outbox to deal with this problem, which can be used within a message consumer. It leverages the durable nature of a message transport to ensure that messages are ultimately sent. There is an extensive article and [video](https://youtu.be/P41IsVAc1nI) explaining the outbox behavior. This approach is preferred to performing transactional database writes outside of a consumer. -:: - -However, sometimes you are coming from the database first and can't get around it. For those situations, MassTransit has a _very simple_ transactional bus which enlists in the current transaction and defers outgoing messages until the transaction is being committed. There is still no rollback, once the messages are delivered to the broker, there is no pulling them back. - - -```csharp -services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - }); - - x.AddTransactionalEnlistmentBus(); -}); -``` - -That is all that's needed. Now here's an example usage within an MVC Action. It's also important to use `TransactionScopeAsyncFlowOption.Enabled` as shown below. - -```csharp -public class MyController : ControllerBase -{ - private readonly IPublishEndpoint _publishEndpoint; - private readonly MyDbContext _dbContext; - - public ValuesController(IPublishEndpoint publishEndpoint, MyDbContext dbContext) - { - _publishEndpoint = publishEndpoint; - _dbContext = dbContext; - } - - [HttpPost] - public async Task Post([FromBody] string value) - { - using(var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) - { - _dbContext.Posts.Add(new Post{...}); - await _dbContext.SaveChangesAsync(); - - await _publishEndpoint.Publish(new PostCreated{...}); - - transaction.Complete(); - } - - return Ok(); - } -} -``` - -Here's an example from within a Console App, with no Container: - -```csharp -public class Program -{ - public static async Task Main() - { - var bus = Bus.Factory.CreateUsingRabbitMq(sbc => - { - sbc.Host("rabbitmq://localhost"); - }); - - await bus.StartAsync(); // This is important! - - var transactionalBus = new TransactionalEnlistmentBus(bus); - - while(/*some condition*/) - { - using(var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) - { - // Do whatever business logic you need. - - await transactionalBus.Publish(new ReportQueued{...}); - await transactionalBus.Send(new CalculateReport{...}); - - // Maybe other business logic - - transaction.Complete(); - } - } - - Console.WriteLine("Press any key to exit"); - await Task.Run(() => Console.ReadKey()); - - await bus.StopAsync(); - } -} -``` - -## Transactional Bus - -Here we are again, another option for holding onto the messages and releasing them as close to the database transaction Commit as possible. We made this alternative because using TransactionScope from the previous section, could [in certain cases](https://github.com/MassTransit/MassTransit/issues/2075) still cause a 2 phase commit escalation (not to mention that TransactionScope doesn't truely have async support, so we make [concessions by calling TaskUtil.Await](https://github.com/MassTransit/MassTransit/blob/develop/src/MassTransit/Transactions/TransactionalBusEnlistment.cs#L83)). So to offer an alternative to these drawbacks, MassTransit provides an Outbox Bus. - -::alert{type="danger"} -Never use the TransactionalBus or TransactionalEnlistmentBus when writing consumers. These tools are very specific and should be used only in the scenarios described. -:: - -The examples will show it's usage in an ASP.NET MVC application, which is where we most commonly use Scoped lifetime for our DbContext and therefore we want the same for our TransactionalBus. You could possibly use it in some console applications, but ones WITHOUT a MT Consumer. Once you have consumers you will ALWAYS use `ConsumeContext` to interact with the bus, and never the `IBus`. - -First Register the outbox bus. - -```csharp -services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - }); - - x.AddTransactionalBus(); -}); -``` - -Then use within your controller. - -```csharp -public class MyController : ControllerBase -{ - private readonly ITransactionalBus _transactionalBus; - private readonly MyDbContext _dbContext; - - public ValuesController(ITransactionalBus transactionalBus, MyDbContext dbContext) - { - _transactionalBus = transactionalBus; - _dbContext = dbContext; - } - - [HttpPost] - public async Task Post([FromBody] string value) - { - using(var transaction = await _dbContext.Database.BeginTransactionAsync()) - { - try - { - _dbContext.Posts.Add(new Post{...}); - await _dbContext.SaveChangesAsync(); - - await _transactionalBus.Publish(new PostCreated{...}); - - await transaction.CommitAsync(); - await _transactionalBus.Release(); // Immediately after CommitAsync - } - catch (Exception) - { - transaction.Rollback(); - } - - } - - return Ok(); - } -} -``` - -One option to remove some of the boilerplate of opening a transaction each Action that writes to the DB is to make a Filter. You can then include all of the boilerplate code to begin the transaction, and release the outbox. - -```csharp -public class DbContextTransactionFilter : TypeFilterAttribute -{ - public DbContextTransactionFilter() - : base(typeof(DbContextTransactionFilterImpl)) - { - } - - // This will be scoped per http request - private class DbContextTransactionFilterImpl : IAsyncActionFilter - { - private readonly MyDbContext _db; - private readonly ILogger _logger; - private readonly ITransactionalBus _transactionalBus; - - public DbContextTransactionFilterImpl( - MyDbContext db, - ILogger logger, - ITransactionalBus transactionalBus) - { - _db = db; - _logger = logger; - _transactionalBus = transactionalBus; - } - - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - using var transaction = await _db.Database.BeginTransactionAsync(); - - try - { - var actionExecuted = await next(); - if (actionExecuted.Exception != null && !actionExecuted.ExceptionHandled) - { - await transaction.RollbackAsync(); - } - else - { - await transaction.CommitAsync(); - await _transactionalBus.Release(); // Immediately after CommitAsync - } - } - catch (Exception) - { - try - { - await transaction.RollbackAsync(); - } - catch (Exception e) - { - // Swallow failed rollback - _logger.LogWarning(e, "Tried to rollback transaction but failed, swallow exception."); - } - - throw; - } - } - } -} -``` - -Now your Controller Action will look like: - -```csharp -public class MyController : ControllerBase -{ - private readonly ITransactionalBus _transactionalBus; - private readonly MyDbContext _dbContext; - - public ValuesController(ITransactionalBus transactionalBus, MyDbContext dbContext) - { - _transactionalBus = transactionalBus; - _dbContext = dbContext; - } - - [HttpPost] - [DbContextTransactionFilter] - public async Task Post([FromBody] string value) - { - _dbContext.Posts.Add(new Post{...}); - await _dbContext.SaveChangesAsync(); - - await _transactionalBus.Publish(new PostCreated{...}); - - return Ok(); - } -} -``` diff --git a/doc/content/3.documentation/5.configuration/4.scheduling.md b/doc/content/3.documentation/5.configuration/4.scheduling.md deleted file mode 100644 index 4a1bb665d9c..00000000000 --- a/doc/content/3.documentation/5.configuration/4.scheduling.md +++ /dev/null @@ -1,328 +0,0 @@ ---- -navigation.title: Scheduling ---- - -# Scheduling Configuration - -Time is important, particularly in distributed applications. Sophisticated systems need to schedule things, and MassTransit has extensive scheduling support. - -MassTransit supports two different methods of message scheduling: - -1. Scheduler-based, using either Quartz.NET or Hangfire, where the scheduler runs in a service and schedules messages using a queue. -2. Transport-based, using the transports built-in message scheduling/delay capabilities. In some cases, such as RabbitMQ, this requires an additional plug-in to be installed and configured. - -> Recurring schedules are only supported by Quartz.NET or Hangfire. - -## Configuration - -Depending upon the scheduling method used, the bus must be configured to use the appropriate scheduler. - -::code-group - - ::code-block{label="Quartz/Hangfire" quartz} - ```csharp - services.AddMassTransit(x => - { - Uri schedulerEndpoint = new Uri("queue:scheduler"); - - x.AddMessageScheduler(schedulerEndpoint); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseMessageScheduler(schedulerEndpoint); - - cfg.ConfigureEndpoints(context); - }); - }); - ``` - :: - - ::code-block{label="RabbitMQ" rabbitmq} - ```csharp - services.AddMassTransit(x => - { - x.AddDelayedMessageScheduler(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseDelayedMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - ``` - :: - - ::code-block{label="Azure Service Bus" azuresb} - ```csharp - services.AddMassTransit(x => - { - x.AddServiceBusMessageScheduler(); - - x.UsingAzureServiceBus((context, cfg) => - { - cfg.UseServiceBusMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - ``` - :: - - ::code-block{label="Amazon SQS" sqs} - ```csharp - services.AddMassTransit(x => - { - x.AddDelayedMessageScheduler(); - - x.UsingAmazonSqs((context, cfg) => - { - cfg.UseDelayedMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - ``` - :: - - ::code-block{label="ActiveMQ" activemq} - ```csharp - services.AddMassTransit(x => - { - x.AddDelayedMessageScheduler(); - - x.UsingActiveMq((context, cfg) => - { - cfg.UseDelayedMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - ``` - :: - -:: - -::callout{type="info"} -#summary -RabbitMQ - -#content -When using RabbitMQ, MassTransit uses the Delayed Exchange plug-in to schedule messages. - -The plug-in can be downloaded from [GitHub][1]. A [Docker Image](https://hub.docker.com/r/masstransit/rabbitmq) with RabbitMQ ready to run, including the delayed exchange plug-in is also available. -:: - -::callout{type="info"} -#summary -Azure Service Bus - -#content -Azure Service Bus supports message cancellation, unlike the other transports. -:: - -::callout{type="info"} -#summary -Amazon SQS - -#content -Scheduled messages cannot be canceled when using the Amazon SQS message scheduler -:: - -## Usage - -To use the message scheduler (outside of a consumer), resolve _IMessageScheduler_ from the container. - -### Consumer - -To schedule messages from a consumer, use any of the _ConsumeContext_ extension methods, such as _ScheduleSend_, to schedule messages. - -```csharp -services.AddMassTransit(x => -{ - Uri schedulerEndpoint = new Uri("queue:scheduler"); - - x.AddMessageScheduler(schedulerEndpoint); - - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseMessageScheduler(schedulerEndpoint); - - cfg.ConfigureEndpoints(context); - }); -}); -``` - -```csharp -public class ScheduleNotificationConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - Uri notificationService = new Uri("queue:notification-service"); - - await context.ScheduleSend(notificationService, - context.Message.DeliveryTime, new() - { - EmailAddress = context.Message.EmailAddress, - Body = context.Message.Body - }); - } -} -``` - -```csharp -public record ScheduleNotification -{ - public DateTime DeliveryTime { get; init; } - public string EmailAddress { get; init; } - public string Body { get; init; } -} -``` - -```csharp -public record SendNotification -{ - public string EmailAddress { get; init; } - public string Body { get; init; } -} -``` - -The message scheduler, specified during bus configuration, will be used to schedule the message. - -### Scope - -To schedule messages from a bus, use _IMessageScheduler_ from the container (or create a new one using the bus and appropriate scheduler). - -```csharp -services.AddMassTransit(x => -{ - Uri schedulerEndpoint = new Uri("queue:scheduler"); - - x.AddMessageScheduler(schedulerEndpoint); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseMessageScheduler(schedulerEndpoint); - - cfg.ConfigureEndpoints(context); - }); -}); -``` - -```csharp -await using var scope = provider.CreateAsyncScope(); - -var scheduler = scope.ServiceProvider.GetRequiredService(); - -await scheduler.SchedulePublish( - DateTime.UtcNow + TimeSpan.FromSeconds(30), new() - { - EmailAddress = "frank@nul.org", - Body = "Thank you for signing up for our awesome newsletter!" - }); -``` - -```csharp -public record SendNotification -{ - public string EmailAddress { get; init; } - public string Body { get; init; } -} -``` - -### Recurring Messages - -You can also schedule a message to be send to you periodically. This functionality uses the Quartz.Net periodic -schedule feature and requires some knowledge of cron expressions. - -To request a recurring message, you need to use `ScheduleRecurringSend` extension method, which is available -for both `Context` and `SendEndpoint`. This message requires a schedule object as a parameter, which must -implement `RecurringSchedule` interface. Since this interface is rather broad, you can use the default -abstract implementation `DefaultRecurringSchedule` as the base class for your own schedule. - -```csharp -public class PollExternalSystemSchedule : DefaultRecurringSchedule -{ - public PollExternalSystemSchedule() - { - CronExpression = "0 0/1 * 1/1 * ? *"; // this means every minute - } -} - -public class PollExternalSystem {} -``` - -```csharp -var schedulerEndpoint = await bus.GetSendEndpoint(_schedulerAddress); - -var scheduledRecurringMessage = await schedulerEndpoint.ScheduleRecurringSend( - InputQueueAddress, new PollExternalSystemSchedule(), new PollExternalSystem()); -``` - -When you stop your service or just have any other need to tell Quartz service to stop sending you -these recurring messages, you can use the return value of `ScheduleRecurringSend` to cancel the recurring schedule. - -```csharp -await bus.CancelScheduledRecurringMessage(scheduledRecurringMessage); -``` - -You can also cancel using schedule id and schedule group values, which are part of the recurring schedule object. - -## Quartz.NET - -To host Quartz.NET with MassTransit, configure Quartz and MassTransit as shown below. - -```csharp -services.AddQuartz(q => -{ - q.UseMicrosoftDependencyInjectionJobFactory(); -}); -``` - -```csharp -services.AddMassTransit(x => -{ - x.AddPublishMessageScheduler(); - - x.AddQuartzConsumers(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UsePublishMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); -}); -``` - -## Hangfire - -```csharp -services.AddHangfire(h => -{ - h.UseRecommendedSerializerSettings(); - h.UseMemoryStorage(); -}); -``` - -```csharp -services.AddMassTransitTestHarness(x => -{ - x.AddPublishMessageScheduler(); - - x.AddHangfireConsumers(); - - x.UsingInMemory((context, cfg) => - { - cfg.UsePublishMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); -}) -``` - - -[1]: https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/ diff --git a/doc/content/3.documentation/5.configuration/bus/consumers.md b/doc/content/3.documentation/5.configuration/bus/consumers.md deleted file mode 100644 index cae297bee47..00000000000 --- a/doc/content/3.documentation/5.configuration/bus/consumers.md +++ /dev/null @@ -1,149 +0,0 @@ -# Consumers - -To understand consumers and how to create one, refer to the [Consumers](/documentation/concepts/consumers) section. - -## Adding Consumers - -Consumers are added inside the `AddMassTransit` configuration using any of the following methods. - -```csharp -AddConsumer(); -``` - -Adds a consumer. - -```csharp -AddConsumer(); -``` - -Adds a consumer with a matching consumer definition. - -```csharp -AddConsumer(cfg => -{ - cfg.ConcurrentMessageLimit = 8; -}); -``` - -Adds a consumer with a matching consumer definition and configures the consumer pipeline. - -```csharp -AddConsumer(typeof(MyConsumer)); -``` - -Adds a consumer by type. - -```csharp -AddConsumer(typeof(MyConsumer), typeof(MyConsumerDefinition)); -``` - -Adds a consumer with a matching consumer definition by type. - -```csharp -void AddConsumers(params Type[] types); -``` - -Adds the specified consumers and consumer definitions. When consumer definitions are included they will be added with the matching consumer type. - -```csharp -void AddConsumers(params Assembly[] assemblies); -``` - -Adds all consumers and consumer definitions in the specified an assembly or assemblies. - -```csharp -void AddConsumers(Func filter, params Assembly[] assemblies); -``` - -Adds the consumers and any matching consumer definitions in the specified an assembly or assemblies that pass the filter. The filter is only called for consumer types. - -## Batch Options - -```csharp -AddConsumer(cfg => -{ - cfg.Options(options => options - .SetMessageLimit(100) - .SetTimeLimit(s: 1) - .SetTimeLimitStart(BatchTimeLimitStart.FromLast) - .GroupBy(x => x.CustomerId) - .SetConcurrencyLimit(10)); -}); -``` - -Adds a batch consumer and configures the batch options. - -## Job Options - -```csharp -AddConsumer(cfg => -{ - cfg.Options>(options => options - .SetMessageLimit(100) - .SetTimeLimit(s: 1) - .SetTimeLimitStart(BatchTimeLimitStart.FromLast) - .GroupBy(x => x.CustomerId) - .SetConcurrencyLimit(10)); -}); -``` - -Adds a job consumer and configures the job options. - - -## Configuring Consumers - -Consumers are automatically configured when `ConfigureEndpoints` is called, which is highly recommended. The endpoint configuration can be mostly customized using either a consumer definition or by specifying the endpoint configuration inline. - -To manually configure a consumer on a receive endpoint, use one of the following methods. - -::card{icon="icon-park-outline:info"} -#title -Order Matters -#description -Manually configured receive endpoints should be configured **before** calling _ConfigureEndpoints_. -:: - -::alert{type="warning"} -Manually configured receive endpoints should be configured **before** calling _ConfigureEndpoints_. -:: - -```csharp -cfg.ReceiveEndpoint("manually-configured", e => -{ - // configure endpoint-specific settings first - e.SomeEndpointSetting = someValue; - - // configure any required middleware components next - e.UseMessageRetry(r => r.Interval(5, 1000)); - - // configure the consumer last - e.ConfigureConsumer(context); -}); - -// configure any remaining consumers, sagas, etc. -cfg.ConfigureEndpoints(context); -``` - -#### Configuration Methods - -```csharp -ConfigureConsumer(context); -``` - -Configures the consumer on the receive endpoint. - -```csharp -ConfigureConsumer(context, consumer => -{ - // configure consumer-specific middleware -}); -``` - -Configures the consumer on the receive endpoint and applies the additional consumer configuration to the consumer pipeline. - -```csharp -ConfigureConsumers(context); -``` - -Configures all consumers that haven't been configured on the receive endpoint. - diff --git a/doc/content/3.documentation/5.configuration/bus/sagas.md b/doc/content/3.documentation/5.configuration/bus/sagas.md deleted file mode 100644 index d6de21b8f46..00000000000 --- a/doc/content/3.documentation/5.configuration/bus/sagas.md +++ /dev/null @@ -1,109 +0,0 @@ -# Sagas - -To understand sagas and how to create one, refer to the [Sagas](/documentation/concepts/sagas) section. - -## Adding Sagas - -Sagas are added inside the `AddMassTransit` configuration using any of the following methods. - -```csharp -AddSaga(); -``` - -Adds a saga. - -```csharp -AddSaga(); -``` - -Adds a saga with a matching saga definition. - -```csharp -AddSaga(cfg => -{ - cfg.ConcurrentMessageLimit = 8; -}); -``` - -Adds a saga with a matching saga definition and configures the saga pipeline. - -```csharp -AddSaga(typeof(MySaga)); -``` - -Adds a saga by type. - -```csharp -AddSaga(typeof(MySaga), typeof(MySagaDefinition)); -``` - -Adds a saga with a matching saga definition by type. - -```csharp -AddSagas(params Type[] types); -``` - -Adds the specified sagas and saga definitions. When saga definitions are included they will be added with the matching saga type. - -```csharp -AddSagas(params Assembly[] assemblies); -``` - -Adds all sagas and saga definitions in the specified an assembly or assemblies. - -```csharp -AddSagas(Func filter, params Assembly[] assemblies); -``` - -Adds the sagas and any matching saga definitions in the specified an assembly or assemblies that pass the filter. The filter is only called for saga types. - - -## Configuring Sagas - -Sagas are automatically configured when `ConfigureEndpoints` is called, which is highly recommended. The endpoint configuration can be mostly customized using either a saga definition or by specifying the endpoint configuration inline. - -To manually configure a saga on a receive endpoint, use one of the following methods. - -::alert{type="warning"} -Manually configured receive endpoints should be configured **before** calling _ConfigureEndpoints_. -:: - -```csharp -cfg.ReceiveEndpoint("manually-configured", e => -{ - // configure endpoint-specific settings first - e.SomeEndpointSetting = someValue; - - // configure any required middleware components next - e.UseMessageRetry(r => r.Interval(5, 1000)); - - // configure the saga last - e.ConfigureSaga(context); -}); - -// configure any remaining consumers, sagas, etc. -cfg.ConfigureEndpoints(context); -``` - -#### Configuration Methods - -```csharp -ConfigureSaga(context); -``` - -Configures the saga on the receive endpoint. - -```csharp -ConfigureSaga(context, saga => -{ - // configure saga-specific middleware -}); -``` - -Configures the saga on the receive endpoint and applies the additional saga configuration to the saga pipeline. - -```csharp -ConfigureSagas(context); -``` - -Configures all sagas that haven't been configured on the receive endpoint. diff --git a/doc/content/3.documentation/5.configuration/integrations/logging.md b/doc/content/3.documentation/5.configuration/integrations/logging.md deleted file mode 100644 index 63cce186d37..00000000000 --- a/doc/content/3.documentation/5.configuration/integrations/logging.md +++ /dev/null @@ -1,43 +0,0 @@ -# Logging - -The MassTransit framework has fully adopted the [`Microsoft.Extensions.Logging`](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0) framework. -So, it will use whatever logging configuration is already in your container. - -## Basic Configuration - -By integrating with `Microsoft.Extensions.Logging` the basic configuration is no configuration. :tada: -When you run a project using the HostBuilder features of .Net you will get a basic logging experience right -out of the box. - -## Serilog - -At MassTransit, we are big fans of [Serilog](https://serilog.net/) and use this default configuration as a starting point in -most projects. - -```sh -dotnet add package Serilog.Extensions.Hosting -dotnet add package Serilog -dotnet add package Serilog.Sinks.Console -``` - -```csharp -public static IHostBuilder CreateHostBuilder(string[] args) -{ - return Host.CreateDefaultBuilder(args) - .UseSerilog((host, log) => - { - if (host.HostingEnvironment.IsProduction()) - log.MinimumLevel.Information(); - else - log.MinimumLevel.Debug(); - - log.MinimumLevel.Override("Microsoft", LogEventLevel.Warning); - log.MinimumLevel.Override("Quartz", LogEventLevel.Information); - log.WriteTo.Console(); - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); -} -``` diff --git a/doc/content/3.documentation/5.configuration/integrations/serialization.md b/doc/content/3.documentation/5.configuration/integrations/serialization.md deleted file mode 100644 index 98f2e64331a..00000000000 --- a/doc/content/3.documentation/5.configuration/integrations/serialization.md +++ /dev/null @@ -1,154 +0,0 @@ ---- -navigation.title: Serialization ---- - -# Message Serialization - -In MassTransit, developers specify types for messages. MassTransit's serializers then perform the hard work of converting the types to the serializer format ( -such as JSON, XML, BSON, etc.) and then back again. - -To interoperate with other languages and platforms, the message structure is important. - -## Message Envelope - -MassTransit encapsulates messages in an envelope before they are serialized. An example JSON message envelope is shown below: - -```json -{ - "messageId": "181c0000-6393-3630-36a4-08daf4e7c6da", - "requestId": "ef375b18-69ee-4a9e-b5ec-44ee1177a27e", - "correlationId": null, - "conversationId": null, - "initiatorId": null, - "sourceAddress": "rabbitmq://localhost/source", - "destinationAddress": "rabbitmq://localhost/destination", - "responseAddress": "rabbitmq://localhost/response", - "faultAddress": "rabbitmq://localhost/fault", - "messageType": [ - "urn:message:Company.Project:SubmitOrder" - ], - "message": { - "orderId": "181c0000-6393-3630-36a4-08daf4e7c6da", - "timestamp": "2023-01-12T21:55:53.714Z" - }, - "expirationTime": null, - "sentTime": "2023-01-12T21:55:53.715882Z", - "headers": { - "Application-Header": "SomeValue" - }, - "host": { - "machineName": "MyComputer", - "processName": "dotnet", - "processId": 427, - "assembly": "TestProject", - "assemblyVersion": "2.11.1.93", - "frameworkVersion": "6.0.7", - "massTransitVersion": "8.0.10.0", - "operatingSystemVersion": "Unix 12.6.2" - } -} -``` - -| Property | Type | Notes | Set | -|:-------------------|:--------:|:------------|:---:| -| messageId | Guid | Recommended | Y | -| correlationId | Guid | Optional | | -| requestId | Guid | Situational | R | -| initiatorId | Guid | Optional | | -| conversationId | Guid | Optional | Y | -| sourceAddress | Uri | Optional | Y | -| destinationAddress | Uri | Optional | Y | -| responseAddress | Uri | Situational | R | -| faultAddress | Uri | Optional | | -| expirationTime | ISO-8601 | Situational | S | -| sentTime | ISO-8601 | Optional | Y | -| messageType | Urn\[\] | Required | Y | - -> Set indicates whether the property is automatically set by MassTransit when producing messages. _Yes_, _Requests_ only, or _Situational_. - -### Message Type - -MassTransit stores the supported .NET types for a message as an array of URNs, which include the namespace and name of the message type. All interfaces and -superclasses of the message type are included. The namespace and name are formatted as shown below. - -`urn:message:Namespace:TypeName` - -A few examples of valid message types: - -```text -urn:message:MyProject.Messages:UpdateAccount -urn:message:MyProject.Messages.Events:AccountUpdated -urn:message:MyProject:ChangeAccount -urn:message:MyProject.AccountService:MyService+AccountUpdatedEvent -``` - -> The last one is a nested class, as indicated by the '+' symbol. - -## Raw JSON - -When using a serializer that doesn't wrap the message in an envelope (_application/json_), the above message would be reduced to the simple JSON below. - -```json -{ - "orderId": "181c0000-6393-3630-36a4-08daf4e7c6da", - "timestamp": "2023-01-12T21:55:53.714Z" -} -``` - -If the _RawSerializerOptions.AddTransportHeaders_ option is specified when configuring a raw JSON serializer, the following transport headers will be set if the header value is present. - -| Header Name | Type | Notes | -|:---------------------|:--------:|:----------------------------------------------------------| -| MessageId | Guid | | -| CorrelationId | Guid | | -| RequestId | Guid | | -| MT-InitiatorId | Guid | | -| ConversationId | Guid | | -| MT-Source-Address | Uri | | -| MT-Response-Address | Uri | | -| MT-Fault-Address | Uri | | -| MT-MessageType | Urn\[\] | Multiple message types separated by ; | -| MT-Host-Info | string | JSON serialized host info | -| MT-OriginalMessageId | Guid | For redelivered messages with a newly generated MessageId | - -MassTransit provides several options when dealing with raw JSON messages. The options can be specified on the _UseRawJsonSerializer_ method._RawSerializerOptions_ includes the following flags: - -| Option | Value | Default | Notes | -|:--------------------|:-----:|:-------:|:-------------------------------------------------------------| -| AnyMessageType | 1 | Y | Messages will match any consumed message type | -| AddTransportHeaders | 2 | Y | MassTransit will add the above headers to outbound messages | -| CopyHeaders | 4 | N | Received message headers will be copied to outbound messages | - -In cases where MassTransit is used and raw JSON messages are preferred, the non-default options are recommended. - -```csharp -cfg.UseRawJsonSerializer(RawSerializerOptions.AddTransportHeaders | RawSerializerOptions.CopyHeaders); -``` - -## Serializers - -MassTransit include support for several commonly used serialization packages. - -### System Text Json - -MassTransit uses _System.Text.Json_ by default to serialize and deserialize JSON messages. - -| Content Type | Format | Configuration Method | -|:-------------------------------------|:----------------------|:----------------------------------| -| **application/vnd.masstransit+json** | **JSON (w/envelope)** | `UseJsonSerializer` **(default)** | -| application/json | JSON | `UseRawJsonSerializer` | - -### Newtonsoft - -The [MassTransit.Newtonsoft](https://nuget.org/packages/MassTransit.Newtonsoft) package adds the following serializer types. Prior to MassTransit v8, Newtonsoft was the default message serializer. - -| Content Type | Format | Configuration Method | -|:-----------------------------------|:--------------------|:---------------------------------| -| application/vnd.masstransit+json | JSON (w/envelope) | `UseNewtonsoftJsonSerializer` | -| application/json | JSON | `UseNewtonsoftRawJsonSerializer` | -| application/vnd.masstransit+bson | BSON (w/envelope) | `UseBsonSerializer` | -| application/vnd.masstransit+xml | XML (w/envelope) | `UseXmlSerializer` | -| application/xml | XML | `UseRawXmlSerializer` | -| application/vnd.masstransit+aes | Binary (w/envelope) | `UseEncryptedSerializer` | -| application/vnd.masstransit.v2+aes | Binary (w/envelope) | `UseEncryptedSerializerV2` | - diff --git a/doc/content/3.documentation/5.configuration/multibus.md b/doc/content/3.documentation/5.configuration/multibus.md deleted file mode 100644 index e0fdb46cb5e..00000000000 --- a/doc/content/3.documentation/5.configuration/multibus.md +++ /dev/null @@ -1,152 +0,0 @@ -# MultiBus - -_pronounced mool-tee-buss_ - -MassTransit is designed so that most applications only need a single bus, and that is the recommended approach. Using a single bus, with however many receive endpoints are needed, minimizes complexity and ensures efficient broker resource utilization. Consistent with this guidance, container configuration using the `AddMassTransit` method registers the appropriate types so that they are available to other components, as well as consumers, sagas, and activities. - -However, with broader use of cloud-based platforms comes a greater variety of messaging transports, not to mention HTTP as a transfer protocol. As application sophistication increases, connecting to multiple message transports and/or brokers is becoming more common. Therefore, rather than force developers to create their own solutions, MassTransit has the ability to configure additional bus instances within specific dependency injection containers. - -> And by specific, right now it is very specific: Microsoft.Extensions.DependencyInjection. Though technically, any container that supports `IServiceCollection` for configuration _might_ work. - -## Standard Configuration - -To review, the configuration for a single bus is shown below. - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(); - x.AddRequestClient(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); -}); -``` - -This configures the container so that there is a bus, using RabbitMQ, with a single consumer _SubmitOrderConsumer_, using automatic endpoint configuration. The MassTransit hosted service, which configures the bus health checks and starts/stop the bus via `IHostedService`, is also added to the container. - -There are several interfaces added to the container using this configuration: - -| Interface | Lifestyle | Notes | -|:------------------------------|:----------|:-----------------------------------------------------------------------| -| `IBusControl` | Singleton | Used to start/stop the bus (not typically used) | -| `IBus` | Singleton | Publish/Send on this bus, starting a new conversation | -| `ISendEndpointProvider` | Scoped | Send messages from consumer dependencies, ASP.NET Controllers | -| `IPublishEndpoint` | Scoped | Publish messages from consumer dependencies, ASP.NET Controllers | -| `IClientFactory` | Singleton | Used to create request clients (singleton, or within scoped consumers) | -| `IRequestClient` | Scoped | Used to send requests | -| `ConsumeContext` | Scoped | Available in any message scope, such as a consumer, saga, or activity | - -When a consumer, a saga, or an activity is consuming a message the _ConsumeContext_ is available in the container scope. When the consumer is created using the container, the consumer and any dependencies are created within that scope. If a dependency includes _ISendEndpointProvider_, _IPublishEndpoint_, or even _ConsumeContext_ (should not be the first choice, but totally okay) on the constructor, all three of those interfaces result in the same reference which is great because it ensures that messages sent and/or published by the consumer or its dependencies includes the proper correlation identifiers and monitoring activity headers. - -## MultiBus Configuration - -To support multiple bus instances in a single container, the interface behaviors described above had to be considered carefully. There are expectations as to how these interfaces behave, and it was important to ensure consistent behavior whether an application has one, two, or a dozen bus instances (please, not a dozen – think of the children). A way to differentiate between different bus instances ensuring that sent or published messages end up on the right queues or topics is needed. The ability to configure each bus instance separately, yet leverage the power of a single shared container is also a must. - -To configure additional bus instances, create a new interface that includes _IBus_. Then, using that interface, configure the additional bus using the `AddMassTransit` method, which is included in the **_MassTransit.MultiBus_** namespace. - -```csharp -public interface ISecondBus : - IBus -{ -} -``` - -```csharp -services.AddMassTransit(x => -{ - x.AddConsumer(); - x.AddRequestClient(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); -}); - -services.AddMassTransit(x => -{ - x.AddConsumer(); - x.AddRequestClient(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.Host("remote-host"); - - cfg.ConfigureEndpoints(context); - }); -}); -``` - -This configures the container so that there is an additional bus, using RabbitMQ, with a single consumer _AllocateInventoryConsumer_, using automatic endpoint configuration. Only a single hosted service is required that will start all bus instances so there is no need to add it twice. - -Notable differences in the new method: - -- The generic argument, _ISecondBus_, is the type that will be added to the container instead of _IBus_. This ensures that access to the additional bus is directly available without confusion. - -The registered interfaces are slightly different for additional bus instances. - -| Interface | Lifestyle | Notes | -|:------------------------------|:----------|:------------------------------------------------------------------------| -| `IBusControl` | N/A | Not registered, but automatically started/stopped by the hosted service | -| `IBus` | N/A | Not registered, the new bus interface is registered instead | -| `ISecondBus` | Singleton | Publish/Send on this bus, starting a new conversation | -| `ISendEndpointProvider` | Scoped | Send messages from consumer dependencies only | -| `IPublishEndpoint` | Scoped | Publish messages from consumer dependencies only | -| `IClientFactory` | N/A | Registered as an instance-specific client factory | -| `IRequestClient` | Scoped | Created using the specific bus instance | -| `ConsumeContext` | Scoped | Available in any message scope, such as a consumer, saga, or activity | - -For consumers or dependencies that need to send or publish messages to a different bus instance, a dependency on that specific bus interface (such as _IBus_, or _ISecondBus_) would be added. - -::alert{type="warning"} -Some things do not work across bus instances. As stated above, calling Send or Publish on an IBus (or other bus instance interface) starts a new conversation. Middleware components such as the _InMemoryOutbox_ currently do not buffer messages across bus instances. -:: - -### Bus Interface Types - -In the example above, which should be the most common of this hopefully uncommon use, the _ISecondBus_ interface is all that is required. MassTransit creates a dynamic class to delegate the `IBus` methods to the bus instance. However, it is possible to specify a class that implements _ISecondBus_ instead. - -To specify a class, as well as take advantage of the container to bring additional properties along with it, take a look at the following types and configuration. - -```csharp -public interface IThirdBus : - IBus -{ -} - -class ThirdBus : - BusInstance, - IThirdBus -{ - public ThirdBus(IBusControl busControl, ISomeService someService) - : base(busControl) - { - SomeService = someService; - } - - public ISomeService SomeService { get; } -} - -public interface ISomeService -{ -} -``` - -```csharp -services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - cfg.Host("third-host"); - }); -}); -``` - -This would add a third bus instance, the same as the second, but using the instance class specified. The class is resolved from the container and given `IBusControl`, which must be passed to the base class ensuring that it is properly configured. - - - - diff --git a/doc/content/3.documentation/5.configuration/observability.md b/doc/content/3.documentation/5.configuration/observability.md deleted file mode 100644 index 459de449645..00000000000 --- a/doc/content/3.documentation/5.configuration/observability.md +++ /dev/null @@ -1,348 +0,0 @@ -# Observability - -## Monitoring - -### Open Telemetry - -OpenTelemetry is an open-source standard for distributed tracing, which allows you to collect and analyze data about the performance of your systems. MassTransit can be configured to use OpenTelemetry to instrument message handling, so that you can collect telemetry data about messages as they flow through your system. - -By using OpenTelemetry with MassTransit, you can gain insights into the performance of your systems, which can help you to identify and troubleshoot issues, and to improve the overall performance of your application. - - -### Application Insights - -Application Insights (part of Azure Monitor) is able to capture and record metrics from MassTransit. It can also be configured as a log sink for logging. - -[Create an Application Insights resource](https://docs.microsoft.com/en-us/azure/application-insights/app-insights-create-new-resource#create-an-application-insights-resource-1) - -[Copy the instrumentation key](https://docs.microsoft.com/en-us/azure/application-insights/app-insights-create-new-resource#copy-the-instrumentation-key) - -To configure an application to use Application Insights with MassTransit: - -> Requires NuGets `MassTransit`, `Microsoft.ApplicationInsights.DependencyCollector` -> -> (for logging, add `Microsoft.Extensions.Logging.ApplicationInsights`) - -```csharp -using System; -using System.Reflection; -using System.Threading.Tasks; -using MassTransit; - -namespace Example -{ - public class MyMessageConsumerConsumer : - MassTransit.IConsumer - { - public async Task Consume(ConsumeContext context) - { - await Console.Out.WriteLineAsync($"Received: {context.Message.Value}"); - } - } - - // Message Definition - public class MyMessage - { - public string Value { get; set; } - } - - public class Program - { - public static async Task Main(string[] args) - { - var module = new DependencyTrackingTelemetryModule(); - module.IncludeDiagnosticSourceActivities.Add("MassTransit"); - - TelemetryConfiguration configuration = TelemetryConfiguration.CreateDefault(); - configuration.InstrumentationKey = ""; - configuration.TelemetryInitializers.Add(new HttpDependenciesParsingTelemetryInitializer()); - - var telemetryClient = new TelemetryClient(configuration); - module.Initialize(configuration); - - var loggerOptions = new ApplicationInsightsLoggerOptions(); - - var applicationInsightsLoggerProvider = new ApplicationInsightsLoggerProvider(Options.Create(configuration), - Options.Create(loggerOptions)); - - ILoggerFactory factory = new LoggerFactory(); - factory.AddProvider(applicationInsightsLoggerProvider); - - LogContext.ConfigureCurrentLogContext(factory); - - var busControl = Bus.Factory.CreateUsingInMemory(cfg => - { - cfg.ReceiveEndpoint("my_queue", ec => - { - ec.Consumer(); - }); - }); - - using(busControl.StartAsync()) - { - await busControl.Publish(new MyMessage{Value = "Hello, World."}); - - await Task.Run(() => Console.ReadLine()); - } - - module.Dispose(); - - telemetryClient.Flush(); - await Task.Delay(5000); - - configuration.Dispose(); - } - } -} -``` - -### Prometheus - -[![alt NuGet](https://img.shields.io/nuget/v/MassTransit.Prometheus.svg "NuGet")](https://nuget.org/packages/MassTransit.Prometheus/) - -MassTransit supports Prometheus metric capture, which provides useful observability into the bus, endpoints, consumers, and messages. - -> The `prometheus-net` library is used as the Prometheus client since it is mentioned on the Prometheus client list. - -#### Installation - -```bash -$ dotnet add package prometheus-net.AspNetCore -$ dotnet add package MassTransit.Prometheus -``` - -#### Configuration - -To configure the bus to capture metrics, add the `UsePrometheusMetrics()` method to your bus configuration. - -```csharp -services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - cfg.UsePrometheusMetrics(serviceName: "order_service"); - }); -}); -``` - -To then mount the metrics to `/metrics` go to your Startup.cs and add - -```csharp -app.UseEndpoints(endpoints => -{ - endpoints.MapMetrics(); -}); -``` - -> For more details, see the [Prometheus-Net Documentation](https://github.com/prometheus-net/prometheus-net#aspnet-core-exporter-middleware). - -#### Metrics Captured - -The metrics captured by MassTransit are listed below. - -| Name | Description | -|:----------------------------------------|:-------------------------------------------------------------------------------------| -| mt_receive_total | Total number of messages received | -| mt_receive_fault_total | Total number of messages receive faults | -| mt_receive_duration_seconds | Elapsed time spent receiving messages, in seconds | -| mt_receive_in_progress | Number of messages being received | -| mt_consume_total | Total number of messages consumed | -| mt_consume_fault_total | Total number of message consume faults | -| mt_consume_retry_total | Total number of message consume retries | -| mt_consume_duration_seconds | Elapsed time spent consuming a message, in seconds | -| mt_delivery_duration_seconds | Elapsed time between when the message was sent and when it was consumed, in seconds. | -| mt_publish_total | Total number of messages published | -| mt_publish_fault_total | Total number of message publish faults | -| mt_send_total | Total number of messages sent | -| mt_send_fault_total | Total number of message send faults | -| mt_bus | Number of bus instances | -| mt_endpoint | Number of receive endpoint instances | -| mt_consumer_in_progress | Number of consumers in progress | -| mt_handler_in_progress | Number of handlers in progress | -| mt_saga_in_progress | Number of sagas in progress | -| mt_activity_execute_in_progress | Number of activity executions in progress | -| mt_activity_compensate_in_progress | Number of activity compensations in progress | -| mt_activity_execute_total | Total number of activities executed | -| mt_activity_execute_fault_total | Total number of activity executions faults | -| mt_activity_execute_duration_seconds | Elapsed time spent executing an activity, in seconds | -| mt_activity_compensate_total | Total number of activities compensated | -| mt_activity_compensate_failure_total | Total number of activity compensation failures | -| mt_activity_compensate_duration_seconds | Elapsed time spent compensating an activity, in seconds | - -#### Labels - -For the metrics above, labels are specified where appropriate. - -| Name | Description | -|:-----------------|:----------------------------------------------------| -| service_name | The service name specified at bus configuration | -| endpoint_address | The endpoint address | -| message_type | The message type for the metric | -| consumer_type | The consumer, saga, or activity type for the metric | -| activity_name | The activity name | -| argument_type | The activity execute argument type | -| log_type | The activity compensate log type | -| exception_type | The exception type for a fault metric | - -#### Example Docker Compose - -```yaml -version: "3.7" - -services: - prometheus: - image: prom/prometheus - ports: - - "9090:9090" -``` - -**Example MassTransit Prometheus Config File** - -> You can use the domain `host.docker.internal` to access process running on the host machine. - -```yaml -global: - scrape_interval: 10s - -scrape_configs: - - job_name: masstransit - tls_config: - insecure_skip_verify: true - scheme: https - static_configs: - - targets: - - 'host.docker.internal:5001' -``` - -## Lifetime Observers - -MassTransit supports several message observers allowing received, consumed, sent, and published messages to be monitored. There is a bus observer as well, so that the bus life cycle can be monitored. - -::alert{type="warning"} -Observers should not be used to modify or intercept messages. To intercept messages to add headers or modify message content, create a new or use an existing middleware component. -:: - -### Bus - -To observe bus life cycle events, create a class which implements `IBusObserver`. To configure a bus observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. - -```csharp -services.AddBusObserver(); -``` - -```csharp -services.AddBusObserver(provider => new BusObserver()); -``` - -### Receive Endpoint - -To configure a receive endpoint observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. - -```csharp -services.AddReceiveEndpointObserver(); -``` - -```csharp -services.AddReceiveEndpointObserver(provider => new ReceiveEndpointObserver()); -``` - -## Pipeline Observers - -### Receive - -To observe messages as they are received by the transport, create a class that implements the `IReceiveObserver` interface, and connect it to the bus as shown below. - -To configure a receive observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. When a container is not being used, the `ConnectReceiveObserver` bus method can be used instead. - -```csharp -services.AddReceiveObserver(); -``` - -```csharp -services.AddReceiveObserver(provider => new ReceiveObserver()); -``` - -### Consume - -If the `ReceiveContext` isn't fascinating enough for you, perhaps the actual consumption of messages might float your boat. A consume observer implements the `IConsumeObserver` interface, as shown below. - -To configure a consume observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. When a container is not being used, the `ConnectConsumeObserver` bus method can be used instead. - -```csharp -services.AddConsumeObserver(); -``` - -```csharp -services.AddConsumeObserver(provider => new ConsumeObserver()); -``` - -#### Consume Message - -Okay, so it's obvious that if you've read this far you want a more specific observer, one that only is called when a specific message type is consumed. We have you covered there too, as shown below. - -To connect the observer, use the `ConnectConsumeMessageObserver` method before starting the bus. - -> The `ConsumeMessageObserver` interface may be deprecated at some point, it's sort of a legacy observer that isn't recommended. - -### Send - -Okay, so, incoming messages are not your thing. We get it, you're all about what goes out. It's cool. It's better to send than to receive. Or is that give? Anyway, a send observer is also available. - -To configure a send observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer -creation. When a container is not being used, the `ConnectSendObserver` bus method can be used instead. - -```csharp -services.AddSendObserver(); -``` - -```csharp -services.AddSendObserver(provider => new SendObserver()); -``` - -### Publish - -In addition to send, publish is also observable. Because the semantics matter, absolutely. Using the MessageId to link them up as it's unique for each message. Remember that Publish and Send are two distinct operations so if you want to observe all messages that are leaving your service, you have to connect both Publish and Send observers. - -To configure a public observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer -creation. When a container is not being used, the `ConnectPublishObserver` bus method can be used instead. - -```csharp -services.AddPublishObserver(); -``` - -```csharp -services.AddPublishObserver(provider => new PublishObserver()); -``` - -## State Machine Observers - -### Event - -To observe events consumed by a saga state machine, use an `IEventObserver` where `T` is the saga instance type. - -To configure an event observer, add it to the container using one of the methods shown below. The factory method version allows customization of the -observer creation. - -```csharp -services.AddEventObserver>(); -``` - -```csharp -services.AddEventObserver(provider => new EventObserver()); -``` - -### State - -To observe state changes that happen in a saga state machine, use an `IStateObserver` where `T` is the saga instance type. - -To configure a state observer, add it to the container using one of the methods shown below. The factory method version allows customization of the -observer creation. - -```csharp -services.AddStateObserver>(); -``` - -```csharp -services.AddStateObserver(provider => new StateObserver()); -``` - diff --git a/doc/content/3.documentation/2.transports/0.index.md b/doc/content/3.documentation/6.transports/0.index.md similarity index 100% rename from doc/content/3.documentation/2.transports/0.index.md rename to doc/content/3.documentation/6.transports/0.index.md diff --git a/doc/content/3.documentation/6.transports/2.rabbitmq.md b/doc/content/3.documentation/6.transports/2.rabbitmq.md new file mode 100644 index 00000000000..44853d5523c --- /dev/null +++ b/doc/content/3.documentation/6.transports/2.rabbitmq.md @@ -0,0 +1,323 @@ +--- +navigation.title: RabbitMQ +--- + +# RabbitMQ Transport + +RabbitMQ is an open-source message broker software that implements the Advanced Message Queuing Protocol (AMQP). It is written in the Erlang programming +language and is built on the Open Telecom Platform framework for clustering and failover. + +RabbitMQ can be used to decouple and distribute systems by sending messages between them. It supports a variety of messaging patterns, including point-to-point, +publish/subscribe, and request/response. + +RabbitMQ provides features such as routing, reliable delivery, and message persistence. It also has a built-in management interface that allows for monitoring +and management of the broker, queues, and connections. Additionally, it supports various plugins, such as the RabbitMQ Management Plugin, that provide +additional functionality. + +## Topology + +The send and publish topologies are extended to support RabbitMQ features, and make it possible to configure how exchanges are created. + +### Exchanges + +In RabbitMQ, an exchange is a component that receives messages from producers and routes them to one or more queues based on a set of rules called bindings. +Exchanges are used to decouple the producer of a message from the consumer, by allowing messages to be sent to multiple queues and/or consumers. + +There are several types of exchanges in RabbitMQ, each with its own routing algorithm: +| Exchange Type | Routing Algorithm | +|------------------|----------------------------------------------------------------------| +| Direct exchange | route messages to queues based on an exact match of the routing key | +| Fanout exchange | route messages to all bound queues | +| Topic exchange | route messages to queues based on a pattern match of the routing key | +| Headers exchange | route messages to queues based on the headers of the message | + +When a message is published to an exchange, the exchange applies the routing algorithm based on the routing key and the bindings to determine which queues the +message should be sent to. The message is then sent to each of the queues that it matches. + +Exchanges allow for more complex routing and message distribution strategies, as they allow to route messages based on different criteria, such as routing key, +headers, or patterns. + +When a message is published, MassTransit sends it to an exchange that is named based upon the message type. Using topology, the exchange name, as well as the +exchange properties can be configured to support a custom behavior. + +To configure the properties used when an exchange is created, the publish topology can be configured during bus creation: + +```csharp +cfg.Publish(x => +{ + x.Durable = false; // default: true + x.AutoDelete = true; // default: false + x.ExchangeType = "fanout"; // default, allows any valid exchange type +}); + +cfg.Publish(x => +{ + x.Exclude = true; // do not create an exchange for this type +}); +``` + +### Exchange Binding + +To bind an exchange to a receive endpoint: + +```csharp +cfg.ReceiveEndpoint("input-queue", e => +{ + e.Bind("exchange-name"); + e.Bind(); +}) +``` + +The above will create two exchange bindings, one between the `exchange-name` exchange and the `input-queue` exchange and a second between the exchange name +matching the `MessageType` and the same `input-queue` exchange. + +The properties of the exchange binding may also be configured: + +```csharp +cfg.ReceiveEndpoint("input-queue", e => +{ + e.Bind("exchange-name", x => + { + x.Durable = false; + x.AutoDelete = true; + x.ExchangeType = "direct"; + x.RoutingKey = "8675309"; + }); +}) +``` + +The above will create an exchange binding between the `exchange-name` and the `input-queue` exchange, using the configured properties. + +### RoutingKey + +The routing key on published/sent messages can be configured by convention, allowing the same method to be used for messages which implement a common interface +type. If no common type is shared, each message type may be configured individually using various conventional selectors. Alternatively, developers may create +their own convention to fit their needs. + +When configuring a bus, the send topology can be used to specify a routing key formatter for a particular message type. + +```csharp +public record SubmitOrder +{ + public string CustomerType { get; init; } + public Guid TransactionId { get; init; } + // ... +} +``` + +```csharp +cfg.Send(x => +{ + // use customerType for the routing key + x.UseRoutingKeyFormatter(context => context.Message.CustomerType); + + // multiple conventions can be set, in this case also CorrelationId + x.UseCorrelationId(context => context.Message.TransactionId); +}); + +// Keeping in mind that the default exchange config for your published type will be the full typename of your message +// we explicitly specify which exchange the message will be published to. So it lines up with the exchange we are binding our +// consumers too. +cfg.Message(x => x.SetEntityName("submitorder")); + +// Also if your publishing your message: because publishing a message will, by default, send it to a fanout queue. +// We specify that we are sending it to a direct queue instead. In order for the routingkeys to take effect. +cfg.Publish(x => x.ExchangeType = ExchangeType.Direct); +``` + +The consumer could then be created: + +```csharp +public class OrderConsumer : + IConsumer +{ + public async Task Consume(ConsumeContext context) + { + + } +} +``` + +And then connected to a receive endpoint: + +```csharp +cfg.ReceiveEndpoint("priority-orders", x => +{ + x.ConfigureConsumeTopology = false; + + x.Consumer(); + + x.Bind("submitorder", s => + { + s.RoutingKey = "PRIORITY"; + s.ExchangeType = ExchangeType.Direct; + }); +}); + +cfg.ReceiveEndpoint("regular-orders", x => +{ + x.ConfigureConsumeTopology = false; + + x.Consumer(); + + x.Bind("submitorder", s => + { + s.RoutingKey = "REGULAR"; + s.ExchangeType = ExchangeType.Direct; + }); +}); +``` + +This would split the messages sent to the exchange, by routing key, to the proper endpoint, using the CustomerType property. + +## Endpoint Address + +A RabbitMQ endpoint address supports the following query string parameters: + +| Parameter | Type | Description | Implies | +|----------------------|--------|----------------------------------------------------------|------------------------------------| +| temporary | bool | Temporary endpoint | durable = false, autodelete = true | +| durable | bool | Save messages to disk | | +| autodelete | bool | Delete when bus is stopped | | +| bind | bool | Bind exchange to queue | | +| queue | string | Bind to queue name | bind = true | +| type | string | Exchange type (fanout, direct, topic) | | +| delayedtype | string | (Internal) delayed target exchange type | type = x-delayed-message | +| alternateexchange | string | Alternate exchange name | | +| bindexchange | string | Bind additional exchange | Queues Only | +| singleactiveconsumer | bool | (Internal) Receive endpoint has a single active consumer | Queues Only | + +## Broker Topology + +In this example topology, two commands and events are used. + +First, the event contracts that are supported by an endpoint that receives files from a customer. + +```csharp +namespace Acme; + +public interface FileReceived +{ + Guid FileId { get; } + DateTime Timestamp { get; } + Uri Location { get; } +} + +public interface CustomerDataReceived +{ + DateTime Timestamp { get; } + string CustomerId { get; } + string SourceAddress { get; } + Uri Location { get; } +} +``` + +Second, the command contract for processing a file that was received. + +```csharp +namespace Acme; + +public interface ProcessFile +{ + Guid FileId { get; } + Uri Location { get; } +} +``` + +The above contracts are used by the consumers to receive messages. From a publishing or sending perspective, two classes are created by the event producer and +the command sender which implement these interfaces. + +```csharp +namespace Acme; + +public record FileReceivedEvent : + FileReceived, + CustomerDataReceived +{ + public Guid FileId { get; init; } + public DateTime Timestamp { get; init; } + public Uri Location { get; init; } + public string CustomerId { get; init; } + public string SourceAddress { get; init; } +} +``` + +And the command class. + +```csharp +namespace Acme; + +public record ProcessFileCommand : + ProcessFile +{ + public Guid FileId { get; init; } + public Uri Location { get; init; } +} +``` + +The consumers for these message contracts are as below. + +```csharp +class FileReceivedConsumer : + IConsumer +{ +} + +class CustomerAuditConsumer : + IConsumer +{ +} + +class ProcessFileConsumer : + IConsumer +{ +} +``` + +:::alert{type="info"} +The broker topology can be customized using the [topology API](/documentation/configuration/topology). +::: + +### Publish + +These are the exchanges and queues for the example above showing the topology for a Publish of a [polymorphic message that uses inheritance](/documentation/concepts/messages#message-inheritance): + +:::alert{type="info"} +MassTransit publishes messages to the message type exchange, which in turn means that copies are routed to all the subscribers by the RabbitMQ exchange. This approach was [based on an article][2] on how to maximize routing performance in RabbitMQ. +::: + +![Publish topology RabbitMQ](/rabbitmq-topology-publish.svg) + +### Send + +These are the exchanges and queues for the example above showing the topology for a Send: + +![Send Topology for RabbitMQ](/rabbitmq-topology-send.svg) + +### Fault + +These are the exchanges and queues used when messages fail. The failing message gets forwarded to an `_error` queue by default. The following diagram shows which Exchanges and Queues are used when a message fails to be processed and is deadlettered for the example above. + +![Fault topology for RabbitMQ](/rabbitmq-topology-fault.svg) + +Go to [Exceptions to learn more on exception and faults](/documentation/concepts/exceptions) + +[2]: http://spring.io/blog/2011/04/01/routing-topologies-for-performance-and-scalability-with-rabbitmq/ + +## Retrying messages + +The RabbitMQ Management UI has the ability to retry faulted messages when used in conjunction with the [Shovel plugin](https://www.rabbitmq.com/docs/shovel). Faulted messages by default end up in the `*_error` queue that corresponds with the consumer queue. + +![Error Queue](/rabbitmq-managementui-errorqueue.png) + +Before returning a message the message can be inspected by first fetching a message via the button `Get Message(s)` which returns a raw view of the message properties and payload: + +![Get Message](/rabbitmq-managementui-getmessage.png) + +After inspection, **all messsages** currently stored in the error queue can be 'shoveled' back the the original queue. + +![Move Messages](/rabbitmq-managementui-movemessage.png) + +:::alert{type="info"} +For advanced alternatives to managing failures see [Exceptions - Managing Faults](/documentation/concepts/exceptions#managing-faults) +::: diff --git a/doc/content/3.documentation/6.transports/3.azure-service-bus.md b/doc/content/3.documentation/6.transports/3.azure-service-bus.md new file mode 100644 index 00000000000..f3932998ca5 --- /dev/null +++ b/doc/content/3.documentation/6.transports/3.azure-service-bus.md @@ -0,0 +1,326 @@ +--- +navigation.title: Azure Service Bus +--- + +# Azure Service Bus Transport + +Azure Service Bus is a messaging service from Microsoft Azure that allows for communication between decoupled systems. It offers a reliable and secure platform for asynchronous transfer of data and state. It supports a variety of messaging patterns, including queuing, publish/subscribe, and request/response. + +With Service Bus, you can create messaging entities such as queues, topics, and subscriptions. Queues provide one-to-one messaging, where each message is consumed by a single receiver. Topics and subscriptions provide one-to-many messaging, where a message is delivered to multiple subscribers. + +Service Bus also provides advanced features such as partitioning and auto-scaling, which allow for high availability and scalability. Additionally, it offers a dead letter queue, which is a special queue that stores undelivered or expired messages. + +## Topology + +The send and publish topologies are extended to support the Azure Service Bus features, and make it possible to configure how topics are created. + +### Topics + +An Azure Service Bus Topic is a messaging entity that allows for one-to-many messaging, where a message is delivered to multiple subscribers. Topics are built on top of Azure Service Bus Queues and provide additional functionality for publish/subscribe messaging patterns. + +When a message is sent to a topic, it is automatically broadcast to all subscribers that have a subscription to that topic. Subscriptions are used to filter messages that are delivered to the subscribers. Subscribers can create multiple subscriptions to a topic, each with its own filter, to receive only the messages that are of interest to them. + +Topics also provide a feature called Session-based messaging, which allows for guaranteed ordering of messages, and the ability to send and receive messages in a stateful manner. + +Topics provide a robust and scalable messaging infrastructure for building distributed systems, where multiple services or systems can subscribe to a topic and receive messages that are relevant to them. Topics also support advanced features such as partitioning and auto-scaling, which allow for high availability and scalability. + +To specify properties used when a topic is created, the publish topology can be configured during bus creation: + +```csharp +cfg.Publish(x => +{ + x.EnablePartitioning = true; +}); +``` + +### PartitionKey + +When publishing messages to an Azure Service Bus topic, you can use the PartitionKey property to specify a value that will be used to partition the messages across multiple topic partitions. This can be useful in situations where you want to ensure that related messages are always delivered to the same partition, and thus will be guaranteed to be processed in the order they were sent. + +By setting a PartitionKey, all messages with the same key will be sent to the same partition, and thus will be received by consumers in the order they were sent. This is particularly useful when building distributed systems that require strict ordering of messages, such as event sourcing or stream processing. + +Another use case for the PartitionKey is when you have a large number of messages and want to distribute them evenly across multiple partitions for better performance, this way the messages are load balanced across all the partitions. + +It's important to note that when you use a PartitionKey, it's important to choose a key that will result in an even distribution of messages across partitions, to avoid overloading a single partition. + +The PartitionKey on published/sent messages can be configured by convention, allowing the same method to be used for messages which implement a common interface type. If no common type is shared, each message type may be configured individually using various conventional selectors. Alternatively, developers may create their own convention to fit their needs. + +When configuring a bus, the send topology can be used to specify a routing key formatter for a particular message type. + +```csharp +public record SubmitOrder +{ + public string CustomerId { get; init; } + public Guid TransactionId { get; init; } +} +``` + +```csharp +cfg.Send(x => +{ + x.UsePartitionKeyFormatter(context => context.Message.CustomerId); +}); +``` + +### SessionId + +When publishing messages to an Azure Service Bus Topic, you can use the SessionId property to specify a value that will be used to group messages together in a session. This can be useful in situations where you want to ensure that related messages are always delivered together, and thus will be guaranteed to be processed in the order they were sent. + +A session is a logical container for messages, and all messages within a session have a guaranteed order of delivery. This means that messages with the same SessionId will be delivered in the order they were sent, regardless of the order they were received by the topic. + +A common use case for sessions is when you have a set of related messages that need to be processed together. For example, if you are sending a series of commands to control a device, you would want to ensure that the commands are delivered in the order they were sent and that all related commands are delivered together. + +Another use case for sessions is when you have a large number of messages and want to ensure that each consumer processes the messages in a specific order. + +It's important to note that when you use sessions, the consumers must be able to process the messages in the order they were sent, otherwise messages might get stuck in the session and cause delays. + +The SessionId on published/sent messages can be configured by convention, allowing the same method to be used for messages which implement a common interface type. If no common type is shared, each message type may be configured individually using various conventional selectors. Alternatively, developers may create their own convention to fit their needs. + +When configuring a bus, the send topology can be used to specify a routing key formatter for a particular message type. + +```csharp +public record UpdateUserStatus +{ + public Guid UserId { get; init; } + public string Status { get; init; } +} +``` + +```csharp +cfg.Send(x => +{ + x.UseSessionIdFormatter(context => context.Message.UserId); +}); +``` + +## Subscriptions + +In Azure, topics and topic subscriptions provide a mechanism for one-to-many communication (versus queues that are designed for one-to-one). A topic subscription acts as a virtual queue. To subscribe to a topic subscription directly the `SubscriptionEndpoint` should be used: + +```csharp +cfg.SubscriptionEndpoint("subscription-name", e => +{ + e.ConfigureConsumer(provider); +}) +``` + +Note that a topic subscription's messages can be forwarded to a receive endpoint (an Azure Service Bus queue), in the following way. Behind the scenes MassTransit is setting up [Service Bus Auto-forwarding](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-auto-forwarding) between a topic subscription and a queue. + +```csharp +cfg.ReceiveEndpoint("input-queue", e => +{ + e.Subscribe("topic-name"); + e.Subscribe(); +}) +``` + +The properties of the topic subscription may also be configured: + +```csharp +cfg.ReceiveEndpoint("input-queue", e => +{ + e.Subscribe("topic-name", x => + { + x.AutoDeleteOnIdle = TimeSpan.FromMinutes(60); + }); +}) +``` + +### Subscription Filters + +MassTransit supports the configuration of subscription rules and filters, which can be used to filter messages as they are delivered to either the subscription endpoint or forwarded to the receive endpoint. + +To specify a subscription filter: + +```csharp +cfg.ReceiveEndpoint("input-queue", e => +{ + e.Subscribe("topic-name", x => + { + x.Filter = new SqlRuleFilter("1 = 1"); + }); +}) +``` + +### Saga State Machine Event Filter + +This is an advanced scenario in which a saga state machine has an event that needs to filter messages from the topic via the subscription. + +First, configure the event, which is defined in the saga state machine, so that it does not configure the consume topology. + +```csharp +public class FilteredSagaStateMachine : + MassTransitStateMachine +{ + public FilteredSagaStateMachine() + { + Event(() => FilteredEvent, x => x.ConfigureConsumeTopology = false); + } + + public Event FilteredEvent { get; } +} +``` + +> Note that this may cause the saga state machine to be difficult to unit test, since events will no longer be automatically routed to the saga's receive endpoint. + +Next, add a saga definition for the saga and explicitly subscribe to the event type + +```csharp +public class FilteredSagaDefinition : + SagaDefinition +{ + protected virtual void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, + ISagaConfigurator sagaConfigurator) + { + if(endpointConfigurator is IServiceBusReceiveEndpointConfigurator sb) + { + sb.Subscribe("subscription-name", x => + { + x.Rule = new CreateRuleOptions("Only47", new SqlRuleFilter("ClientId = 47")); + }); + } + } +} +``` + +Finally, add the saga state machine and the definition when configuring MassTransit. + +```csharp +services.AddMassTransit(x => +{ + x.AddSagaStateMachine(); +}); +``` + +## Broker Topology + +Two commands and events are used in this example. + +These are the event contracts for a consumer that receives files from a customer: + +```csharp +namespace Acme; + +public interface FileReceived +{ + Guid FileId { get; } + DateTime Timestamp { get; } + Uri Location { get; } +} + +public interface CustomerDataReceived +{ + DateTime Timestamp { get; } + string CustomerId { get; } + string SourceAddress { get; } + Uri Location { get; } +} +``` + +Here is the command contract for processing a file that was received. + +```csharp +namespace Acme; + +public interface ProcessFile +{ + Guid FileId { get; } + Uri Location { get; } +} +``` + +The above contracts are used by the consumers to receive messages. From a publishing or sending perspective, two classes are created by the event producer and the command sender which implement these interfaces. + +```csharp +namespace Acme; + +public record FileReceivedEvent : + FileReceived, + CustomerDataReceived +{ + public Guid FileId { get; init; } + public DateTime Timestamp { get; init; } + public Uri Location { get; init; } + public string CustomerId { get; init; } + public string SourceAddress { get; init; } +} +``` + +And the command class: + +```csharp +namespace Acme; + +public record ProcessFileCommand : + ProcessFile +{ + public Guid FileId { get; init; } + public Uri Location { get; init; } +} +``` + +The consumers for these message contracts are shown below: + +```csharp +namespace Acme; + +class FileReceivedConsumer : + IConsumer +{ +} + +class CustomerAuditConsumer : + IConsumer +{ +} + +class ProcessFileConsumer : + IConsumer +{ +} +``` + +:::alert{type="info"} +The broker topology can be customized using the [topology API](/documentation/configuration/topology). +::: + +### Send + +These are the topics and queues for the example above when Sending a message: + +![Send topology for Azure Service Bus](/azure-topology-send.svg) + +### Publish + +These are the topics and queues for the example above when Publishing a [polymorphic message that uses inheritance](/documentation/concepts/messages#message-inheritance): + +![Publish topology for Azure Service Bus](/azure-topology-publish.svg) + +### Fault + +These are the topics and queues used when messages fail. The failing message gets forwarded to an `_error` queue by default. The following diagram shows which topics and queues are used when a message fails to be processed and is deadlettered for the example above. + +:::alert{type="info"} +The diagram shows the non-default usage of the [Azure Service Bus dead-letter queue](/documentation/configuration/transports/azure-service-bus#using-service-bus-dead-letter-queues). +::: + +![Fault topology for Azure Service Bus](/azure-topology-fault.svg) + +Go to [Exceptions to learn more on exception and faults](/documentation/concepts/exceptions) + +## Retrying messages + +The Azure Service Bus Portal provides a method to retry faulted messages by doing the following: + +1. Open the Service Bus namespace +2. Select the queue that has failed messages +3. Select 'Service Bus Explorer' +4. Select the 'Dead-letter' tab + +This will open a view of the dead-letter queue and provides an option to select one or more messages. The selected messages can be retried by selecting `Re-send selected messages` + +![Dead-letter view](/servicebus-deadletter-view.png) + +:::alert{type="info"} +For advanced alternatives to managing failures see [Exceptions - Managing Faults](/documentation/concepts/exceptions#managing-faults) +::: diff --git a/doc/content/3.documentation/6.transports/4.amazon-sqs.md b/doc/content/3.documentation/6.transports/4.amazon-sqs.md new file mode 100644 index 00000000000..98c82781669 --- /dev/null +++ b/doc/content/3.documentation/6.transports/4.amazon-sqs.md @@ -0,0 +1,149 @@ +--- +navigation.title: Amazon SQS +title: Amazon SQS Transport +--- + +# Amazon SQS + +::alert{type="info"} +Amazon SQS does not support [polymorphic message](/documentation/concepts/messages#message-inheritance) dispatch +:: + +Amazon Simple Queue Service (SQS) is a fully managed message queuing service that enables you to decouple and scale microservices, distributed systems, and serverless applications. SQS eliminates the complexity and overhead associated with managing and operating message oriented middleware, and empowers developers to focus on differentiating work. + +With SQS, you can send, store, and receive messages between software components at any volume, without losing messages or requiring other services to be always available. SQS makes it simple and cost-effective to decouple and coordinate the components of a cloud application. + +SQS offers two types of queues, Standard and FIFO (First-In-First-Out). Standard queues offer best-effort ordering, which ensures that messages are generally delivered in the order in which they are sent. FIFO queues guarantee that messages are processed exactly once, in the order that they are sent, and they are designed to prevent duplicates. + + +## Amazon SNS + +Amazon Simple Notification Service (SNS) is a fully managed messaging service that enables you to send messages to multiple subscribers or endpoints. SNS supports multiple protocols including HTTP, HTTPS, email, and Lambda, and it can be used to send push notifications to mobile devices, or to process messages asynchronously using AWS Lambda. + +SNS allows you to send a message to a "topic" which is a logical access point and communication channel. Subscribers can then subscribe to that topic to receive the messages. + +SNS also provides a feature called fan-out delivery, which enables messages to be delivered to multiple subscribers in parallel, this allows SNS to handle high-throughput and burst traffic, and can improve the overall performance of your application. + +MassTransit uses SNS to route published messages to SQS queues. + +## Broker Topology + +The following messages are used in this example: + +Here is the command contract for processing a file that was received: + +```csharp +namespace Acme; + +public record ProcessFile +{ +} +``` + +These are the event contracts for a consumer that receives files from a customer: + +```csharp +namespace Acme; + +public record FileReceivedEvent +{ +} +``` + +The consumers for these message contracts are shown below: + +```csharp +class ProcessFileConsumer : + IConsumer +{ +} + +class FileReceivedConsumer : + IConsumer +{ +} + +class CustomerAuditConsumer : + IConsumer +{ +} +``` + +:::alert{type="info"} +The broker topology can be customized using the [topology API](/documentation/configuration/topology). +::: + +### Send + +These are the exchanges and queues for the example above when Sending a message: + +![Send topology for Azure Service Bus](/amazonsqs-topology-send.svg) + +### Publish + +These are the topics and queues for the example above when Publishing a message: + +![Publish topology for Azure Service Bus](/amazonsqs-topology-publish.svg) + +### Fault + +These are the exchanges and queues used when messages fail. The failing message gets forwarded to an `_error` queue by default. The following diagram shows which exchanges and queues are used when a message fails to be processed and is deadlettered for the example above. + +![Fault topology for Azure Service Bus](/amazonsqs-topology-fault.svg) + +Go to [Exceptions to learn more on exception and faults](/documentation/concepts/exceptions) + +## Retrying messages + +Faulted messages by default are forwarded to the corresponding `*_error` queue: + +![Error queue](/amazonsqs-errorqueue.png) + +Messages can be inspected by: + +1. Selecting the queue +2. Selecting `Send and receive messages` +3. In the **Receive messages** panel, select `Poll for messages` + +A list of message appears and a message can be inspected by clicking it: + +![Message details](/amazonsqs-message-details.gif) + +Configure the consumer queue its dead-letter queue: + +1. Select a consumer queue, for example `BillOrder` +2. Select `Edit` in the **Dead-letter queue** panel +3. Enable `Set this queue to receive undeliverable messages` in the **Dead-letter queue** panel +4. Select the corresponding consumer queue (here `arn:aws:sqs:***:***:BillOrder_error` ) +5. Select `Save` + +![Configure consumer queue its dead-letter queue](/amasonsqs-select-dlq.gif) + +The `_error` dead-letter needs to be configured to set a re-drive (return) queue; + +1. Select a consumer `_error` queue, for example `BillOrder_error` +2. Select `Edit` in the **Dead-letter queue** panel +3. Enable the **Redrive allow policy** panel +4. Select `By queue` +5. Select the consumer queue, for example `BillOrder `(here `arn:aws:sqs:***:***:BillOrder` ) +6. Select `Save` + +![Configure the 'redrive' queue (return queue)](/amazonsqs-select-redrive.gif) + +The `Start DLQ redrive` button in the upper-right corner should now be enabled. + +AmazonSQS isn't aware that MassTransit forwarded the messages currently in the queue from another queue unless it actually forwarded the messages it self because the delivery count was exceeded. This requires the following steps to set a custom destination: + +1. Select `Start DLQ redrive` +2. Select `Redrive to a custom destination` +3. Select the correct consumer queue, for example `BillOrder` +4. Scroll down and select 1 or more messages +5. Select `DLQ redrive` in the lower-right corner + +:::alert{type="warning"} +This will re-drive all messages in the dead-letter queue, not just the polled messages. +::: + +:::alert{type="info"} +For advanced alternatives to managing failures see [Exceptions - Managing Faults](/documentation/concepts/exceptions#managing-faults) +::: diff --git a/doc/content/3.documentation/2.transports/5.activemq.md b/doc/content/3.documentation/6.transports/5.activemq.md similarity index 100% rename from doc/content/3.documentation/2.transports/5.activemq.md rename to doc/content/3.documentation/6.transports/5.activemq.md diff --git a/doc/content/3.documentation/2.transports/_dir.yml b/doc/content/3.documentation/6.transports/_dir.yml similarity index 100% rename from doc/content/3.documentation/2.transports/_dir.yml rename to doc/content/3.documentation/6.transports/_dir.yml diff --git a/doc/content/3.documentation/2.transports/in-memory.md b/doc/content/3.documentation/6.transports/in-memory.md similarity index 100% rename from doc/content/3.documentation/2.transports/in-memory.md rename to doc/content/3.documentation/6.transports/in-memory.md diff --git a/doc/content/3.documentation/2.transports/kafka.md b/doc/content/3.documentation/6.transports/kafka.md similarity index 100% rename from doc/content/3.documentation/2.transports/kafka.md rename to doc/content/3.documentation/6.transports/kafka.md diff --git a/doc/content/3.documentation/6.transports/sql.md b/doc/content/3.documentation/6.transports/sql.md new file mode 100644 index 00000000000..4a151e3b7f7 --- /dev/null +++ b/doc/content/3.documentation/6.transports/sql.md @@ -0,0 +1,358 @@ +--- +navigation.title: SQL/DB +--- + +# SQL Database Transport + +In the realm of distributed systems and message-oriented architectures, a reliable and efficient message transport is a crucial aspect. + +PostgreSQL and Microsoft SQL Server are renowned and feature-rich relational database management systems. When combined with the power of +MassTransit, these database engines emerge as a formidable choice for implementing a robust and scalable message transport. + +By leveraging either of these databases as the underlying message storage and delivery mechanism, developers can harness the reliability, durability, +and transactional capabilities of the database, while benefiting from MassTransit's extensive support for message-based communication patterns. + +This integration presents an enticing proposition for building resilient and high-performance distributed systems that can seamlessly handle +complex message flows and enable reliable communication between components. + +## Details + +The SQL transport: + +- Stores messages, queues, topics, and subscriptions using tables, indices, and functions/stored procedures +- Requires no custom extensions or additional services +- Uses pure SQL via DbConnection, DbCommand, and DbDataReader (no Entity Framework required) +- Behaves like a true message broker, similar to RabbitMQ, Azure Service Bus, or Amazon SQS + - Messages are locked, locks are automatically renewed, and messages are acknowledged/removed once successfully consumed + - Competing consumer (load balancing) to scale out service instances + - Delayed redelivery (second-level retry) is implemented at the transport layer, rescheduling messages and adding exception headers +- Uses PostgreSQL's `LISTEN`/`NOTIFY` channels to reduce polling frequency while still enabling immediate message delivery + +### Features + +The SQL transport supports: + +- Durable messages, stored as JSON, with headers and metadata stored in separate columns +- Publish/subscribe messaging using polymorphic, topic-based routing +- Topic-to-topic and topic-to-queue subscriptions, enabling sophisticated message routing options +- Multiple subscription types including _All_ (fan-out), _Routing Key_ (direct), and _Pattern_ (topic) +- Dead-letter (*_skipped*) and error sub-queues with functions to move messages back into the main queue +- Message scheduling, including cancellation +- Delayed redelivery (second-level retry) +- Message priority, at the message level +- Partitioned message consumption, enabling fair message consumption across tenants, customers, etc. and ordered message delivery +- Supports all consumer types, including consumers, sagas, state machines, and routing slips +- Transactional Outbox using Entity Framework Core + +### Sample + +:sample{sample="sql-transport"} + +## Configuration + +The SQL transport is configured with `UsingPostgres` or `UsingSqlServer`. + +### PostgreSQL + +```csharp +services.AddMassTransit(x => +{ + x.AddSqlMessageScheduler(); + + x.UsingPostgres((context, cfg) => + { + cfg.UseSqlMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +### SQL Server + +```csharp +services.AddMassTransit(x => +{ + x.AddSqlMessageScheduler(); + + x.UsingSqlServer((context, cfg) => + { + cfg.UseSqlMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +## SqlTransportOptions + +To configure the SQL transport options, the standard .NET options pattern should be used. + +### Connection String + +A standard connection string can be used to configure the SQL transport. In the example below, the configured connection string is retrieved and set +on the `SqlTrasnportOptions`. + +```csharp +var connectionString = builder.Configuration.GetConnectionString("Db"); + +builder.Services.AddOptions() + .Configure(options => + { + options.ConnectionString = connectionString; + }); +``` + +In the `appsettings.json`, the connection string should be configured. For PostgreSQL this may be something like: + +```json +{ + "ConnectionStrings": { + "Db": "Server=localhost;Port=5432;user id=postgres;password=Password12!;database=my_app;" + }, + "AllowedHosts": "*" +} +``` + +### Options + +Additionally, individual options can be specified, as shown below. This might be the case when you want to change the schema name or the role created by the +migration script. If the username and password used by the application do not have administrative rights to the database, a separate admin username and password +can also be specified. + +```csharp +services.AddOptions().Configure(options => +{ + options.Host = "localhost"; + options.Database = "sample"; + options.Schema = "transport"; // the schema for the transport-related tables, etc. + options.Role = "transport"; // the role to assign for all created tables, functions, etc. + options.Username = "masstransit"; // the application-level credentials to use + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = builder.Username; // the admin credentials to create the tables, etc. + options.AdminPassword = builder.Password; +}); +``` + +:::alert{type="info"} +If the `AdminUsername` and `AdminPassword` are not specified, the `Username` and `Password` are used instead and may need elevated permissions to +allow creation of the database and/or infrastructure. +::: + +### Migrations + +To automatically create the database, tables, roles, functions, and other related database elements, a hosted service is available. The migration hosted +service should be added **BEFORE** `AddMassTransit` in the configuration to ensure the database has been created/configured before starting the bus. + +```csharp +services.AddPostgresMigrationHostedService(); +// OR +services.AddSqlServerMigrationHostedService(); +``` + +To use an existing database (which may be the case with Azure SQL or Azure PostreSQL), you can skip database creation but still create all the tables and +functions/stored procedure required. + +```csharp +services.AddPostgresMigrationHostedService(x => +{ + x.CreateDatabase = false; + x.CreateInfrastructure = true; // this is the default, but shown for completeness +}); +``` + +::alert{type="danger"} +Specifying `DeleteDatabase = true` is only recommended for unit tests! +:: + +::alert{type="info"} +For SQL Server, replace `AddPostgresMigrationHostedService` with `AddSqlServerMigrationHostedService`. +:: + +## Topic Subscriptions + +Several topic and queue subscription types are supported. + +### All + +By default, subscriptions are created with the `All` subscription type so that all messages +published and/or sent to the topic are delivered to the destination (either a queue or another topic). + +### Routing Key + +The `RoutingKey` subscription type is used to filter messages so that only messages with a matching routing key are delivered to the destination. +When adding a routing key subscription, it's usually necessary to disable the automatic topology configuration so that an `All` subscription won't be +added for the consumer. + +```csharp +e.ConfigureConsumeTopology = false; + +e.Subscribe(m => +{ + m.SubscriptionType = SqlSubscriptionType.RoutingKey; + m.RoutingKey = "8675309"; +}); +``` + +Messages can then be published with a _RoutingKey_ so that they are properly routed: + +```csharp +await publishEndpoint.Publish(new CustomerUpdatedEvent(NewId.NextGuid()), + x => x.SetRoutingKey("8675309")); +``` + +### Pattern + +The `Pattern` subscription type is used to filter messages so that only messages with a regular expression matching the routing key are delivered to the +destination. +When adding a pattern subscription, it's usually necessary to disable the automatic topology configuration so that an `All` subscription won't be +added for the consumer. + +```csharp +e.ConfigureConsumeTopology = false; + +e.Subscribe(m => +{ + m.SubscriptionType = SqlSubscriptionType.Pattern; + m.RoutingKey = "^[A-Z]+$"; +}); +``` + +Messages can then be published with a _RoutingKey_ so that they are properly routed: + +```csharp +await publishEndpoint.Publish(new CustomerUpdatedEvent(NewId.NextGuid()), + x => x.SetRoutingKey("ABCDEFG")); +``` + +## Partitioned Queues + +The SQL transport support message-level partition keys and messages can be consumed by partition key. This promotes fairness in how messages are delivered, +particularly in customer- or tenant-based applications to avoid an individual customer or tenant from blocking others due to high message volume. Consuming by +partition key can limit the number of messages consumed per partition key which evens out message delivery. + +### Set Partition Key + +Messages published or sent can specify the partition key with the `Publish` or `Send` call as shown. + +```csharp +await publishEndpoint.Publish(new CustomerUpdatedEvent(NewId.NextGuid()), + x => x.SetPartitionKey("CustomerA")); +``` + +Messages can also be configured to automatically set the partition key based on the message content by configuring a send convention during bus configuration. + +```csharp +x.UsingSqlServer((context, cfg) => +{ + cfg.SendTopology.UsePartitionKeyFormatter(x => x.Message.CustomerId); +}); +``` + +Typically, it's easier to combine the message convention configuration into an extension methods and use that when configuring the bus:. + +```csharp +public static class MessageConventionExtensions +{ + public static void UseMessagePartitionKeyFormatters(this IBusFactoryConfigurator cfg) + { + cfg.SendTopology.UsePartitionKeyFormatter(x => x.Message.CustomerId); + cfg.SendTopology.UsePartitionKeyFormatter(x => x.Message.CustomerId); + cfg.SendTopology.UsePartitionKeyFormatter(x => x.Message.CustomerId); + } +} +``` + +Then, use the extension method when configuring the bus. + +```csharp +x.UsingSqlServer((context, cfg) => +{ + cfg.UseMessagePartitionKeyFormatters(); +}); +``` + +### Set Receive Mode + +The SQL transport supports multiple receive modes when configuring a receive endpoint. To enable partitioned delivery, one of the partitioned receive modes +must be configured. + +| Receive Mode | Description | +|------------------------------|----------------------------------------------------------------------------------------------------| +| Normal | Standard priority-first FIFO (first-in, first-out) order | +| Partitioned | Priority-first FIFO with only one message per PartitionKey concurrently | +| PartitionedConcurrent | Priority-first FIFO with up to `ConcurrentDeliveryLimit` messages per PartitionKey concurrently | +| PartitionedOrdered | Explicit in-order FIFO with one message per PartitionKey concurrently | +| PartitionedOrderedConcurrent | Explicit in-order FIFO with up to `ConcurrentDeliveryLimit` messages per PartitionKey concurrently | + +There are a few notable aspects of these receive modes, including: + +- When using a partitioned receive mode, messages are partitioned across **ALL** scaled out consumer instances. This delivery mechanism is unique to the + SQL transport and enables scaling across high node counts and prevents a single partition key from saturating multiple consumer instances. +- Ordered receive modes are guaranteed to be in order, even when performing message redelivery or scheduling messages for future consumption. For example, if + message 1 is scheduled for two minutes in the future, and message 2 and 3 with the same partition key are published any time after message 1, messages 2 and 3 + will _only_ be consumed after message 1 has been consumed. + +The receive mode can be set when configuring the receive endpoint, or it can be added to the consumer endpoint configuration as shown. + +```csharp +x.AddConsumer() + .Endpoint(e => e.AddConfigureEndpointCallback(cfg => + { + if (cfg is ISqlReceiveEndpointConfigurator sql) + sql.SetReceiveMode(SqlReceiveMode.Partitioned); + })); +``` + +When settings a concurrent receive mode, the _ConcurrentDeliveryLimit_ should also be specified. This is useful when using a batch consumer. + +```csharp +x.AddConsumer(c => c.Options(o => +{ + o.GroupBy(m => m.PartitionKey()); + o.SetConcurrencyLimit(10); + o.SetMessageLimit(10); + o.SetTimeLimit(ms: 10); +})) +.Endpoint(e => e.AddConfigureEndpointCallback(cfg => +{ + if (cfg is ISqlReceiveEndpointConfigurator sql) + { + sql.SetReceiveMode(SqlReceiveMode.PartitionedConcurrent); + sql.ConcurrentDeliveryLimit = 10; + } +})); +``` + +### Job Sagas + +When using the SQL transport with the job saga state machines, use the partitioned receive mode for the most reliable performance and concurrency. There are +two convenience methods that ensure the transport and sagas are properly configured: `SetPartitionedReceiveMode` and `UseJobSagaPartitionKeyFormatters`. + +The method usage is shown below. + +```csharp +services.AddMassTransit(x => +{ + x.AddSqlMessageScheduler(); + + x.AddJobSagaStateMachines() + .SetPartitionedReceiveMode() // set job saga endpoints to partitioned + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.UsePostgres(); + }); + + x.UsingPostgres((context, cfg) => + { + cfg.UseSqlMessageScheduler(); + cfg.UseJobSagaPartitionKeyFormatters(); // partition key conventions + + cfg.ConfigureEndpoints(context); + }); +}); +``` + +`UseJobSagaPartitionKeyFormatters` configures the partition key conventions so that the `PartitionKey` property is automatically set for the messages used by +the job saga state machines and consumers. diff --git a/doc/content/4.support/1.support-channels.md b/doc/content/4.support/1.support-channels.md index 7844ac0fa7e..10b71c6d1e6 100644 --- a/doc/content/4.support/1.support-channels.md +++ b/doc/content/4.support/1.support-channels.md @@ -2,17 +2,21 @@ Community support is offered through the following channels. These channels are monitored regularly, but response time is not guaranteed. If you need immediate help with MassTransit, consider [Commercial Support](/support). -## Stack Overflow +## GitHub Discussions -There is a MassTransit tag on [Stack Overflow][1], which has many questions that have already been asked. Several developers regularly monitor this tag for new questions, so that's a great place to start. Be sure to search and see if your question has already been asked, that is the fastest way to an answer if someone else has already experienced the same issue. +Questions, ideas, and suggestions can be posted on [GitHub Discussions](https://github.com/MassTransit/MassTransit/discussions). This is a great place to post code samples, share ideas, and get help using MassTransit. -Before you just post your question, however, spend a few moments to compose your thoughts and formulate your question. There is nothing as pointless as simply telling us "MassTransit does not work for me" with no further information to give any clue to why. Before you post, search the web to see if your question has already been asked or even answered. And if it has been, you will already have your answer. +Before you post, however, spend a few moments to compose your thoughts and formulate your question. There is nothing as pointless as simply telling us "MassTransit does not work for me" with no further information to give any clue to why. Before you post, search the web to see if your question has already been asked or even answered. And if it has been, you will already have your answer. -## GitHub Discussions +> If you have an issue _using_ MassTransit, this is the place to start **before** creating an issue. -Questions, ideas, and suggestions can be posted on [GitHub Discussions](https://github.com/MassTransit/MassTransit/discussions). Similar to Stack Overflow, but integrated with GitHub, this is a great place to post code samples, share ideas, and get help using MassTransit. The same guidelines above apply to discussions. +## GitHub Issues -> If you have an issue _using_ MassTransit, this is the place to start **before** creating an issue. +Please **do not open an issue on GitHub**, unless you have spotted an actual bug in MassTransit. If you are unsure, first create a discussion. If we confirm it's a bug, we'll ask you to [create the issue][3], or we will create the issue from the discussion itself. + +**Issues are not the place for questions, and they'll likely be closed.** + +This policy is in place to avoid bugs being drowned out in a pile of sensible suggestions for future enhancements and calls for help from people who forget to check back if they get it and so on. ## Discord @@ -24,17 +28,6 @@ MassTransit has an active Discord server, which is a great place to get quick an There are several seasons of videos [available on YouTube](https://www.youtube.com/playlist?list=PLx8uyNNs1ri2MBx6BjPum5j9_MMdIfM9C), and new episodes are added fairly regularly. There are also many short-format [Commutes](https://youtube.com/playlist?list=PLx8uyNNs1ri2_ldsW1aPb7_8E2FI7ZtaI) that cover quick topics. -## Twitter - -You might be able to get some attention on Twitter, and you're highly encouraged to tweet about MassTransit. Feel free to tag `@mtproj` (strangely, MassTransit is a noisy search term -- go figure). - -## GitHub Issues - -Please **do not open an issue on GitHub**, unless you have spotted an actual bug in MassTransit. If you are unsure, pursue one of the alternate options first. If we confirm it's a bug, we'll ask you to [create the issue][3]. - -**Issues are not the place for questions, and they'll likely be closed.** - -This policy is in place to avoid bugs being drowned out in a pile of sensible suggestions for future enhancements and calls for help from people who forget to check back if they get it and so on. [1]: http://stackoverflow.com/questions/tagged/masstransit [3]: https://github.com/masstransit/masstransit/issues diff --git a/doc/content/4.support/4.upgrade.md b/doc/content/4.support/4.upgrade.md index 8efa58e2518..e6e0b6689d1 100644 --- a/doc/content/4.support/4.upgrade.md +++ b/doc/content/4.support/4.upgrade.md @@ -1,5 +1,54 @@ # Upgrading +## Version 8.3 + +With MassTransit 8.3 there are a few core underlying changes that may require attention when upgrading from an earlier version. + +### Job Consumers + +Support for recurring and scheduled job consumers is now available. To support these new features, the job saga state machines have new properties that must be persisted. The entity framework maps have been updated, to be sure to update any migrations in your applications and plan for the migrations to be run when or before the new versions are deployed. + +> The [Job Consumer documentation](/documentation/patterns/job-consumers) has also been updated to include all new and existing features. + +The `NotifyCompleted`, `NotifyFaulted`, `NotifyCanceled`, and `NotifyStarted` methods have been removed from the `JobContext` interface. These were _never_ meant to be called by a job consumer. The underlying job service components report the job state, there is no need for consumers to do it. + +Job consumer behavior determines the resulting state: + +- If the `Run` method completes without throwing an exception, the job is Completed. +- If the `Run` method throws an exception, the job is Faulted. +- If the `Run` method throws an `OperationCanceledException` where the token is `context.CancellationToken`, the job is Canceled. + +When checking for cancellation, or calling methods that can be canceled, the `context.CancellationToken` should be used. If checking for cancellation explicitly, using `context.CancellationToken.IsCancellationRequested`, the exception should be throwing using `context.CancellationToken.ThrowIfCancellationRequested()`. + + + +### Transactional Outbox + +The transactional outbox entity `OutboxMessage` now has foreign key relationships to `InboxState` and `OutboxState`. To meet this requirement, all three entity types must always be configured/included (previously you could leave one or the other off and the outbox would still function). The migrations should also be updated to include the new constraints. + +## Version 8.1 + +MassTransit version 8.1 is focused on improving cross-component integration between various components like the (mediator <-> bus, bus1 <-> bus2, etc). In previous versions of MassTransit, the `ConsumeContext` was used to send messages. This approach worked well for a long time, but as more components like the Mediator, MultiBus, and Riders became available, issues arose with resolving the correct `ConsumeContext`. + +To address this issue, MassTransit v8.1 introduces a new capability to keep track of the owning component of the `ConsumeContext`. When the `ConsumeContext` is owned by another component, the library only copies necessary data such as headers, payloads, and source address. This change opens up the possibility of consuming message by the Mediator and sending it directly to the bus by resolving `IPublishEndpoint` or `ISendEndpointProvider`. + +As this is a minor release, we have made every effort to ensure minimal impact on existing customer integrations. However, to use this capability, small changes are required. Previously, `IServiceProvider` was used as a parameter to most configuration methods, with this change `IRegistrationContext` should be used instead. + +### Sagas +In MassTransit v8.1, the registration of `ISagaRepository` in the container has been updated. Previously, this interface was responsible for both retrieving and querying sagas from the repository. With this release, we have decided to separate these responsibilities, resulting in the registering of two additional interfaces in container: + +- `ILoadSagaRepository` - should be used to load sagas by id. +- `IQuerySagaRepository` - should be used to query saga ids by expression. + +Both of these interfaces are registered in the container as singletons. +::alert{type="warning"} +The registration `ISagaRepository` will be removed from the container, so it is recommended to start using these new interfaces instead. +:: + +### Job Service State Machines + +In Mass Transit v8.1 the job type saga now includes a name property. This may require a database migration depending on the persistence provider chosen. + ## Version 8 MassTransit v8 is the first major release since the availability of .NET 6. MassTransit v8 works a significant portion of the underlying components into a more manageable solution structure. Focused on the developer experience, while maintaining compatibility with previous versions, this release brings together the entire MassTransit stack. @@ -36,7 +85,7 @@ To continue using Newtonsoft for serialization, add the `MassTransit.Newtonsoft` - `UseXmlSerializer` - `UseBsonSerializer` -### Hosted Service +### AddMassTransitHostedService (deprecated) Previous versions of MassTransit required the use of the `MassTransit.AspNetCore` package to support registration of MassTransit's hosted service. This package is no longer required, and MassTransit will automatically add an `IHostedService` for MassTransit. diff --git a/doc/content/4.support/packages.md b/doc/content/4.support/packages.md index f733261672e..eb0cf59a3cd 100644 --- a/doc/content/4.support/packages.md +++ b/doc/content/4.support/packages.md @@ -23,6 +23,8 @@ The following NuGet packages are the currently supported. * [MassTransit.WebJobs.ServiceBus](https://nuget.org/packages/MassTransit.WebJobs.ServiceBus/) * [MassTransit.WebJobs.EventHubs](https://nuget.org/packages/MassTransit.WebJobs.EventHubs/) * [MassTransit.RabbitMQ](https://nuget.org/packages/MassTransit.RabbitMQ/) +* [MassTransit.SqlTransport.PostgreSQL](https://nuget.org/packages/MassTransit.SqlTransport.PostgreSQL/) +* [MassTransit.SqlTransport.SqlServer](https://nuget.org/packages/MassTransit.SqlTransport.SqlServer/) * **Riders** * [MassTransit.EventHub](https://nuget.org/packages/MassTransit.EventHub/) * [MassTransit.Kafka](https://nuget.org/packages/MassTransit.Kafka/) @@ -53,12 +55,12 @@ The following NuGet packages are the currently supported. * [MassTransit.Interop.NServiceBus](https://nuget.org/packages/MassTransit.Interop.NServiceBus/) * [MassTransit.Newtonsoft](https://nuget.org/packages/MassTransit.Newtonsoft/) +* [MassTransit.MessagePack](https://nuget.org/packages/MassTransit.MessagePack/) ### Other * [MassTransit.Analyzers](https://nuget.org/packages/MassTransit.Analyzers/) * [MassTransit.SignalR](https://nuget.org/packages/MassTransit.SignalR/) -* [MassTransit.Prometheus](https://nuget.org/packages/MassTransit.Prometheus/) * [MassTransit.StateMachineVisualizer](https://nuget.org/packages/MassTransit.StateMachineVisualizer/) * [MassTransit.TestFramework](https://nuget.org/packages/MassTransit.TestFramework/) @@ -92,6 +94,7 @@ The following packages from earlier versions of MassTransit are no longer suppor * MassTransit.Ninject * MassTransit.NLog * MassTransit.Platform.Abstractions +* MassTransit.Prometheus * MassTransit.Reactive * MassTransit.SerilogIntegration * MassTransit.SimpleInjector diff --git a/doc/content/samples/sample-kafka.md b/doc/content/samples/sample-kafka.md new file mode 100644 index 00000000000..f301af2dcd7 --- /dev/null +++ b/doc/content/samples/sample-kafka.md @@ -0,0 +1,7 @@ +--- +repo: "MassTransit/Sample-Kafka" +useTitle: false +youtube: CJ_srcJiIKs +--- + +This sample shows how to use MassTransit with Kafka, including Confluent Cloud. diff --git a/doc/content/samples/sql-transport.md b/doc/content/samples/sql-transport.md new file mode 100644 index 00000000000..864a44a3d26 --- /dev/null +++ b/doc/content/samples/sql-transport.md @@ -0,0 +1,7 @@ +--- +repo: "MassTransit/Sample-DbTransport" +useTitle: false +youtube: abc +--- + +Shows how to use the SQL Database Transport, including bus configuration, Entity Framework Core saga state machine persistence, and the transactional outbox. diff --git a/doc/content/samples/web-application-factory.md b/doc/content/samples/web-application-factory.md new file mode 100644 index 00000000000..c468888865c --- /dev/null +++ b/doc/content/samples/web-application-factory.md @@ -0,0 +1,7 @@ +--- +repo: "MassTransit/Sample-WebApplicationFactory" +useTitle: false +youtube: Uzme7vInDz0 +--- + +This sample shows how to use MassTransit's container-based test harness with the WebApplicationFactory, without requiring the application under test to know about the test harness. diff --git a/doc/lib/content.test.ts b/doc/lib/content.test.ts index aee947a5432..c1cd10893e0 100644 --- a/doc/lib/content.test.ts +++ b/doc/lib/content.test.ts @@ -236,7 +236,7 @@ const items : Omit[] = [ "_locale": "en", "_empty": false, "title": "Azure Table Storage", - "description": "Azure Tables are exposed in two ways in Azure - via Storage accounts & via the premium offering within Cosmos DB APIs. This persistence supports both implementations and behind the curtains uses the Microsoft.Azure.Cosmos.Table library for communication.", + "description": "Azure Tables are exposed in two ways in Azure - via Storage accounts & via the premium offering within Cosmos DB APIs. This persistence supports both implementations and behind the curtains uses the Azure.Data.Tables library for communication.", "_type": "markdown", "_id": "content:2.documentation:5.configuration:2.persistence:azure-table.md", "_source": "content", diff --git a/doc/nuxt.config.ts b/doc/nuxt.config.ts index 686ccbbcc19..acb5f8cbe3e 100755 --- a/doc/nuxt.config.ts +++ b/doc/nuxt.config.ts @@ -1,39 +1,45 @@ export default defineNuxtConfig({ - extends: '@nuxt-themes/docus', - css: ['~/assets/css/main.css'], - colorMode: { - preference: 'dark' - }, - content: { - documentDriven: true, - highlight: { - theme: { - dark: 'material-darker', - default: 'material-lighter' - }, - preload: ['json', 'shell', 'markdown', 'yaml', 'bash', 'csharp'] - }, - navigation: { - fields: ['icon', 'titleTemplate', 'aside'] - } - }, - postcss: { - plugins: { - 'tailwindcss/nesting': {}, - tailwindcss: {}, - autoprefixer: {}, - }, - }, - runtimeConfig: { - public: { - algolia: { - applicationId: process.env.ALGOLIA_APP_ID, - apiKey: process.env.ALGOLIA_API_KEY, - langAttribute: 'lang', - docSearch: { - indexName: 'masstransit_io' - } - } - } - } + extends: '@nuxt-themes/docus', + css: ['~/assets/css/main.css'], + + colorMode: { + preference: 'dark' + }, + + content: { + documentDriven: true, + highlight: { + theme: { + dark: 'github-dark', + default: 'github-light' + }, + preload: ['json', 'shell', 'markdown', 'yaml', 'bash', 'csharp', 'sql'] + }, + navigation: { + fields: ['icon', 'titleTemplate', 'aside'] + } + }, + + postcss: { + plugins: { + 'tailwindcss/nesting': {}, + tailwindcss: {}, + autoprefixer: {}, + }, + }, + + runtimeConfig: { + public: { + algolia: { + applicationId: process.env.ALGOLIA_APP_ID, + apiKey: process.env.ALGOLIA_API_KEY, + langAttribute: 'lang', + docSearch: { + indexName: 'masstransit_io' + } + } + } + }, + + compatibilityDate: '2024-07-28' }) diff --git a/doc/package-lock.json b/doc/package-lock.json new file mode 100644 index 00000000000..24ce142e4a7 --- /dev/null +++ b/doc/package-lock.json @@ -0,0 +1,21153 @@ +{ + "name": "masstransit-docus", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "masstransit-docus", + "version": "0.1.0", + "dependencies": { + "@docsearch/js": "3", + "autoprefixer": "^10.4.13", + "postcss": "^8.4.20", + "tailwindcss": "^3.2.4" + }, + "devDependencies": { + "@nuxt-themes/docus": "^1.15.0", + "@nuxtjs/algolia": "^1.5.0", + "@types/chai": "^4.3.4", + "@types/jest": "^29.2.4", + "babel-jest": "^29.3.1", + "chai": "^4.5.0", + "jest": "^29.3.1", + "nuxt": "^3.12.4", + "ts-jest": "^29.0.3", + "vue-gtag-next": "^1.14.0" + } + }, + "node_modules/@algolia/autocomplete-core": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.9.3.tgz", + "integrity": "sha512-009HdfugtGCdC4JdXUbVJClA0q0zh24yyePn+KUGk3rP7j8FEe/m5Yo/z65gn6nP/cM39PxpzqKrL7A6fP6PPw==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-plugin-algolia-insights": "1.9.3", + "@algolia/autocomplete-shared": "1.9.3" + } + }, + "node_modules/@algolia/autocomplete-plugin-algolia-insights": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.9.3.tgz", + "integrity": "sha512-a/yTUkcO/Vyy+JffmAnTWbr4/90cLzw+CC3bRbhnULr/EM0fGNvM13oQQ14f2moLMcVDyAx/leczLlAOovhSZg==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.9.3" + }, + "peerDependencies": { + "search-insights": ">= 1 < 3" + } + }, + "node_modules/@algolia/autocomplete-preset-algolia": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.9.3.tgz", + "integrity": "sha512-d4qlt6YmrLMYy95n5TB52wtNDr6EgAIPH81dvvvW8UmuWRgxEtY0NJiPwl/h95JtG2vmRM804M0DSwMCNZlzRA==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-shared": "1.9.3" + }, + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/autocomplete-shared": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.9.3.tgz", + "integrity": "sha512-Wnm9E4Ye6Rl6sTTqjoymD+l8DjSTHsHboVRYrKgEt8Q7UHm9nYbqhN/i0fhUYA3OAEH7WA8x3jfpnmJm3rKvaQ==", + "license": "MIT", + "peerDependencies": { + "@algolia/client-search": ">= 4.9.1 < 6", + "algoliasearch": ">= 4.9.1 < 6" + } + }, + "node_modules/@algolia/cache-browser-local-storage": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.24.0.tgz", + "integrity": "sha512-t63W9BnoXVrGy9iYHBgObNXqYXM3tYXCjDSHeNwnsc324r4o5UiVKUiAB4THQ5z9U5hTj6qUvwg/Ez43ZD85ww==", + "license": "MIT", + "dependencies": { + "@algolia/cache-common": "4.24.0" + } + }, + "node_modules/@algolia/cache-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.24.0.tgz", + "integrity": "sha512-emi+v+DmVLpMGhp0V9q9h5CdkURsNmFC+cOS6uK9ndeJm9J4TiqSvPYVu+THUP8P/S08rxf5x2P+p3CfID0Y4g==", + "license": "MIT" + }, + "node_modules/@algolia/cache-in-memory": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.24.0.tgz", + "integrity": "sha512-gDrt2so19jW26jY3/MkFg5mEypFIPbPoXsQGQWAi6TrCPsNOSEYepBMPlucqWigsmEy/prp5ug2jy/N3PVG/8w==", + "license": "MIT", + "dependencies": { + "@algolia/cache-common": "4.24.0" + } + }, + "node_modules/@algolia/client-account": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.24.0.tgz", + "integrity": "sha512-adcvyJ3KjPZFDybxlqnf+5KgxJtBjwTPTeyG2aOyoJvx0Y8dUQAEOEVOJ/GBxX0WWNbmaSrhDURMhc+QeevDsA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/client-search": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-account/node_modules/@algolia/client-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-account/node_modules/@algolia/client-search": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", + "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.24.0.tgz", + "integrity": "sha512-y8jOZt1OjwWU4N2qr8G4AxXAzaa8DBvyHTWlHzX/7Me1LX8OayfgHexqrsL4vSBcoMmVw2XnVW9MhL+Y2ZDJXg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/client-search": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-analytics/node_modules/@algolia/client-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-analytics/node_modules/@algolia/client-search": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", + "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.0.0.tgz", + "integrity": "sha512-6N5Qygv/Z/B+rPufnPDLNWgsMf1uubMU7iS52xLcQSLiGlTS4f9eLUrmNXSzHccP33uoFi6xN9craN1sZi5MPQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.24.0.tgz", + "integrity": "sha512-l5FRFm/yngztweU0HdUzz1rC4yoWCFo3IF+dVIVTfEPg906eZg5BOd1k0K6rZx5JzyyoP4LdmOikfkfGsKVE9w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-personalization/node_modules/@algolia/client-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.0.0.tgz", + "integrity": "sha512-QdDYMzoxYZ3axzBy6CHe+M+NlOGvHEFTa2actchGnp25Uu0N6lyVNivT7nph+P1XoxgAD08cWbeJD3wWQXnpng==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.0.0", + "@algolia/requester-browser-xhr": "5.0.0", + "@algolia/requester-node-http": "5.0.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/events": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", + "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@algolia/logger-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.24.0.tgz", + "integrity": "sha512-LLUNjkahj9KtKYrQhFKCzMx0BY3RnNP4FEtO+sBybCjJ73E8jNdaKJ/Dd8A/VA4imVHP5tADZ8pn5B8Ga/wTMA==", + "license": "MIT" + }, + "node_modules/@algolia/logger-console": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.24.0.tgz", + "integrity": "sha512-X4C8IoHgHfiUROfoRCV+lzSy+LHMgkoEEU1BbKcsfnV0i0S20zyy0NLww9dwVHUWNfPPxdMU+/wKmLGYf96yTg==", + "license": "MIT", + "dependencies": { + "@algolia/logger-common": "4.24.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.24.0.tgz", + "integrity": "sha512-P9kcgerfVBpfYHDfVZDvvdJv0lEoCvzNlOy2nykyt5bK8TyieYyiD0lguIJdRZZYGre03WIAFf14pgE+V+IBlw==", + "license": "MIT", + "dependencies": { + "@algolia/cache-browser-local-storage": "4.24.0", + "@algolia/cache-common": "4.24.0", + "@algolia/cache-in-memory": "4.24.0", + "@algolia/client-common": "4.24.0", + "@algolia/client-search": "4.24.0", + "@algolia/logger-common": "4.24.0", + "@algolia/logger-console": "4.24.0", + "@algolia/requester-browser-xhr": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/requester-node-http": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/recommend/node_modules/@algolia/client-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/recommend/node_modules/@algolia/client-search": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", + "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/@algolia/recommend/node_modules/@algolia/requester-browser-xhr": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.24.0.tgz", + "integrity": "sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0" + } + }, + "node_modules/@algolia/recommend/node_modules/@algolia/requester-node-http": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.24.0.tgz", + "integrity": "sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.0.0.tgz", + "integrity": "sha512-oOoQhSpg/RGiGHjn/cqtYpHBkkd+5M/DCi1jmfW+ZOvLVx21QVt6PbWIJoKJF85moNFo4UG9pMBU35R1MaxUKQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.24.0.tgz", + "integrity": "sha512-k3CXJ2OVnvgE3HMwcojpvY6d9kgKMPRxs/kVohrwF5WMr2fnqojnycZkxPoEg+bXm8fi5BBfFmOqgYztRtHsQA==", + "license": "MIT" + }, + "node_modules/@algolia/requester-fetch": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-4.24.0.tgz", + "integrity": "sha512-qgZu2gbKLPEQGMg20UAwmJ1v1qQfRtmKhw6r511iYEeZXBrhuzS9lPf8qOCOUsHud96nYzw39C257Y15mO6rOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.0.0.tgz", + "integrity": "sha512-FwCdugzpnW0wxbgWPauAz5vhmWGQnjZa5DCl9PBbIoDNEy/NIV8DmiL9CEA+LljQdDidG0l0ijojcTNaRRtPvQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "@algolia/client-common": "5.0.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/transporter": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.24.0.tgz", + "integrity": "sha512-86nI7w6NzWxd1Zp9q3413dRshDqAzSbsQjhcDhPIatEFiZrL1/TjnHL8S7jVKFePlIMzDsZWXAXwXzcok9c5oA==", + "license": "MIT", + "dependencies": { + "@algolia/cache-common": "4.24.0", + "@algolia/logger-common": "4.24.0", + "@algolia/requester-common": "4.24.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.7.tgz", + "integrity": "sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/highlight": "^7.24.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.2.tgz", + "integrity": "sha512-bYcppcpKBvX4znYaPEeFau03bp89ShqNMLs+rmdptMw+heSZh9+z84d2YG+K7cYLbWwzdjtDoW/uqZmPjulClQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", + "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/helper-compilation-targets": "^7.25.2", + "@babel/helper-module-transforms": "^7.25.2", + "@babel/helpers": "^7.25.0", + "@babel/parser": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.2", + "@babel/types": "^7.25.2", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.0.tgz", + "integrity": "sha512-3LEEcj3PVW8pW2R1SR1M89g/qrYk/m/mB/tLqn7dn4sbBUQyTqnlod+II2U4dqiGtUmkcnAmkMDralTFZttRiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.24.7.tgz", + "integrity": "sha512-BaDeOonYvhdKw+JoMVkAixAAJzG2jVPIwWoKBPdYuY9b452e2rPuI9QPYh3KpofZ3pW2akOmwZLOiOsHMiqRAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", + "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.25.2", + "@babel/helper-validator-option": "^7.24.8", + "browserslist": "^4.23.1", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.0.tgz", + "integrity": "sha512-GYM6BxeQsETc9mnct+nIIpf63SAyzvyYN7UB/IlTyd+MBg06afFGp0mIeUqGyWgS2mxad6vqbMrHVlaL3m70sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/helper-replace-supers": "^7.25.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/traverse": "^7.25.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.8.tgz", + "integrity": "sha512-LABppdt+Lp/RlBxqrh4qgf1oEH/WxdzQNDJIu5gC/W1GyvPVrOBiItmmM8wan2fm4oYqFuFfkXmlGpLQhPY8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.8", + "@babel/types": "^7.24.8" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", + "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", + "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.24.7", + "@babel/helper-simple-access": "^7.24.7", + "@babel/helper-validator-identifier": "^7.24.7", + "@babel/traverse": "^7.25.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.24.7.tgz", + "integrity": "sha512-jKiTsW2xmWwxT1ixIdfXUZp+P5yURx2suzLZr5Hi64rURpDYdMW0pv+Uf17EYk2Rd428Lx4tLsnjGJzYKDM/6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", + "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.25.0.tgz", + "integrity": "sha512-q688zIvQVYtZu+i2PsdIu/uWGRpfxzr5WESsfpShfZECkO+d2o+WROWezCi/Q6kJ0tfPa5+pUGUlfx2HhrA3Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.24.8", + "@babel/helper-optimise-call-expression": "^7.24.7", + "@babel/traverse": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", + "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.24.7.tgz", + "integrity": "sha512-IO+DLT3LQUElMbpzlatRASEyQtfhSE0+m465v++3jyyXeBTBUjtVZg28/gHeV5mrTJqvEKhKroBGAvhW+qPHiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.24.7", + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", + "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.0.tgz", + "integrity": "sha512-MjgLZ42aCm0oGjJj8CtSM3DB8NOOf8h2l7DCTePJs29u+v7yO/RBX9nShlKMgFnRks/Q4tBAe7Hxnov9VkGwLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.7.tgz", + "integrity": "sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.24.7.tgz", + "integrity": "sha512-RL9GR0pUG5Kc8BUWLNDm2T5OpYwSX15r98I0IkgmRQTXuELq/OynH8xtMTMvTJFjXbMWFVTKtYkTaYQsuAwQlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.7", + "@babel/helper-plugin-utils": "^7.24.7", + "@babel/plugin-syntax-decorators": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.24.7.tgz", + "integrity": "sha512-Ui4uLJJrRV1lb38zg1yYTmRKmiZLiftDEvZN2iq3kd9kUFU+PttmzTbAFC2ucRk/XJmtek6G23gPsuZbhrT8fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.7.tgz", + "integrity": "sha512-hbX+lKKeUMGihnK8nvKqmXBInriT3GVjzXKFriV3YC6APGxMbP8RZNFwy91+hocLXq90Mta+HshoB31802bb8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.7.tgz", + "integrity": "sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.7.tgz", + "integrity": "sha512-c/+fVeJBB0FeKsFvwytYiUD+LBvhHjGSI0g446PRGdSVGZLRNArBUno2PETbAly3tpiNAQR5XaZ+JslxkotsbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.2.tgz", + "integrity": "sha512-lBwRvjSmqiMYe/pS0+1gggjJleUJi7NzjvQ1Fkqtt69hBa/0t1YuW/MLQMAPixfwaQOHUXsd6jeU3Z+vdGv3+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.24.7", + "@babel/helper-create-class-features-plugin": "^7.25.0", + "@babel/helper-plugin-utils": "^7.24.8", + "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", + "@babel/plugin-syntax-typescript": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.0.tgz", + "integrity": "sha512-7dRy4DwXwtzBrPbZflqxnvfxLF8kdZXPkhymtDeFoFqE6ldzjQFgYTtYIFARcLEYDrqfBfYcZt1WqFxRoyC9Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/standalone": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/standalone/-/standalone-7.25.3.tgz", + "integrity": "sha512-uR+EoBqIIIvKGCG7fOj7HKupu3zVObiMfdEwoPZfVCPpcWJaZ1PkshaP5/6cl6BKAm1Zcv25O1rf+uoQ7V8nqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.0.tgz", + "integrity": "sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/parser": "^7.25.0", + "@babel/types": "^7.25.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.3.tgz", + "integrity": "sha512-HefgyP1x754oGCsKmV5reSmtV7IXj/kpaE1XYY+D9G5PvKKoFfSbiS4M77MdjuwlZKDIKFCffq9rPU+H/s3ZdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/types": "^7.25.2", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", + "integrity": "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=16.13" + } + }, + "node_modules/@cloudflare/kv-asset-handler/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@csstools/cascade-layer-name-parser": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.13.tgz", + "integrity": "sha512-MX0yLTwtZzr82sQ0zOjqimpZbzjMaK/h2pmlrLK7DCzlmiZLYFpoO94WmN1akRVo6ll/TdpHb53vihHLUMyvng==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^2.7.1", + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.7.1.tgz", + "integrity": "sha512-2SJS42gxmACHgikc1WGesXLIT8d/q2l0UFM7TaEeIzdFCE/FPMtTiizcPGGJtlPo2xuQzY09OhrLTzRxqJqwGw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^2.4.1" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.4.1.tgz", + "integrity": "sha512-eQ9DIktFJBhGjioABJRtUucoWR2mwllurfnM8LuNGAqX3ViZXaUchqk+1s7jjtkFiT9ySdACsFEA3etErkALUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/@docsearch/css": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.6.1.tgz", + "integrity": "sha512-VtVb5DS+0hRIprU2CO6ZQjK2Zg4QU5HrDM1+ix6rT0umsYvFvatMAnf97NHZlVWDaaLlx7GRfR/7FikANiM2Fg==", + "license": "MIT" + }, + "node_modules/@docsearch/js": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@docsearch/js/-/js-3.6.1.tgz", + "integrity": "sha512-erI3RRZurDr1xES5hvYJ3Imp7jtrXj6f1xYIzDzxiS7nNBufYWPbJwrmMqWC5g9y165PmxEmN9pklGCdLi0Iqg==", + "license": "MIT", + "dependencies": { + "@docsearch/react": "3.6.1", + "preact": "^10.0.0" + } + }, + "node_modules/@docsearch/react": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.6.1.tgz", + "integrity": "sha512-qXZkEPvybVhSXj0K7U3bXc233tk5e8PfhoZ6MhPOiik/qUQxYC+Dn9DnoS7CxHQQhHfCvTiN0eY9M12oRghEXw==", + "license": "MIT", + "dependencies": { + "@algolia/autocomplete-core": "1.9.3", + "@algolia/autocomplete-preset-algolia": "1.9.3", + "@docsearch/css": "3.6.1", + "algoliasearch": "^4.19.1" + }, + "peerDependencies": { + "@types/react": ">= 16.8.0 < 19.0.0", + "react": ">= 16.8.0 < 19.0.0", + "react-dom": ">= 16.8.0 < 19.0.0", + "search-insights": ">= 1 < 3" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "search-insights": { + "optional": true + } + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", + "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", + "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", + "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", + "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", + "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", + "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", + "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", + "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", + "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", + "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", + "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", + "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", + "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", + "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", + "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", + "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", + "integrity": "sha512-EV6+ovTsEXCPAp58g2dD68LxoP/wK5pRvgy0J/HxPGB009omFPv3Yet0HiaqvrIrgPTBuC6wCH1LTOY91EO5hQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", + "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", + "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", + "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", + "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", + "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", + "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", + "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@iconify/vue": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@iconify/vue/-/vue-4.1.2.tgz", + "integrity": "sha512-CQnYqLiQD5LOAaXhBrmj1mdL2/NCJvwcC4jtW2Z8ukhThiFkLDkutarTOV2trfc9EXqUqRs0KqXOL9pZ/IyysA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/types": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/cyberalien" + }, + "peerDependencies": { + "vue": ">=3" + } + }, + "node_modules/@ioredis/commands": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.2.0.tgz", + "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "license": "MIT", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@kwsites/file-exists": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/file-exists/-/file-exists-1.1.1.tgz", + "integrity": "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1" + } + }, + "node_modules/@kwsites/promise-deferred": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz", + "integrity": "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@netlify/functions": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@netlify/functions/-/functions-2.8.1.tgz", + "integrity": "sha512-+6wtYdoz0yE06dSa9XkP47tw5zm6g13QMeCwM3MmHx1vn8hzwFa51JtmfraprdkL7amvb7gaNM+OOhQU1h6T8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@netlify/serverless-functions-api": "1.19.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@netlify/node-cookies": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@netlify/node-cookies/-/node-cookies-0.1.0.tgz", + "integrity": "sha512-OAs1xG+FfLX0LoRASpqzVntVV/RpYkgpI0VrUnw2u0Q1qiZUzcPffxRK8HF3gc4GjuhG5ahOEMJ9bswBiZPq0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.16.0 || >=16.0.0" + } + }, + "node_modules/@netlify/serverless-functions-api": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/@netlify/serverless-functions-api/-/serverless-functions-api-1.19.1.tgz", + "integrity": "sha512-2KYkyluThg1AKfd0JWI7FzpS4A/fzVVGYIf6AM4ydWyNj8eI/86GQVLeRgDoH7CNOxt243R5tutWlmHpVq0/Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@netlify/node-cookies": "^0.1.0", + "urlpattern-polyfill": "8.0.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nuxt-themes/docus": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@nuxt-themes/docus/-/docus-1.15.0.tgz", + "integrity": "sha512-V2kJ5ecGUxXcEovXeQkJBPYfQwjmjaxB5fnl2XaQV+S2Epcn+vhPWShSlL6/WXzLPiAkQFdwbBj9xedTvXgjkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt-themes/elements": "^0.9.5", + "@nuxt-themes/tokens": "^1.9.1", + "@nuxt-themes/typography": "^0.11.0", + "@nuxt/content": "^2.8.5", + "@nuxthq/studio": "^1.0.0", + "@vueuse/integrations": "^10.4.1", + "@vueuse/nuxt": "^10.4.1", + "focus-trap": "^7.5.3", + "fuse.js": "^6.6.2" + } + }, + "node_modules/@nuxt-themes/elements": { + "version": "0.9.5", + "resolved": "https://registry.npmjs.org/@nuxt-themes/elements/-/elements-0.9.5.tgz", + "integrity": "sha512-uAA5AiIaT1SxCBjNIURJyCDPNR27+8J+t3AWuzWyhbNPr3L1inEcETZ3RVNzFdQE6mx7MGAMwFBqxPkOUhZQuA==", + "dev": true, + "dependencies": { + "@nuxt-themes/tokens": "^1.9.1", + "@vueuse/core": "^9.13.0" + } + }, + "node_modules/@nuxt-themes/tokens": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@nuxt-themes/tokens/-/tokens-1.9.1.tgz", + "integrity": "sha512-5C28kfRvKnTX8Tux+xwyaf+2pxKgQ53dC9l6C33sZwRRyfUJulGDZCFjKbuNq4iqVwdGvkFSQBYBYjFAv6t75g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxtjs/color-mode": "^3.2.0", + "@vueuse/core": "^9.13.0", + "pinceau": "^0.18.8" + } + }, + "node_modules/@nuxt-themes/typography": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@nuxt-themes/typography/-/typography-0.11.0.tgz", + "integrity": "sha512-TqyvD7sDWnqGmL00VtuI7JdmNTPL5/g957HCAWNzcNp+S20uJjW/FXSdkM76d4JSVDHvBqw7Wer3RsqVhqvA4w==", + "dev": true, + "dependencies": { + "@nuxtjs/color-mode": "^3.2.0", + "nuxt-config-schema": "^0.4.5", + "nuxt-icon": "^0.3.3", + "pinceau": "^0.18.8", + "ufo": "^1.1.1" + } + }, + "node_modules/@nuxt/content": { + "version": "2.13.2", + "resolved": "https://registry.npmjs.org/@nuxt/content/-/content-2.13.2.tgz", + "integrity": "sha512-9AmX7iG8+1MaWia8XLe1TyzoLrTaIhchas19w6VxqZI0dEoQCGslEcdOxy8xLrdGVFuy6MObBwU8SZgpQB9pyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.12.4", + "@nuxtjs/mdc": "^0.8.3", + "@vueuse/core": "^10.11.0", + "@vueuse/head": "^2.0.0", + "@vueuse/nuxt": "^10.11.0", + "consola": "^3.2.3", + "defu": "^6.1.4", + "destr": "^2.0.3", + "json5": "^2.2.3", + "knitwork": "^1.1.0", + "listhen": "^1.7.2", + "mdast-util-to-string": "^4.0.0", + "mdurl": "^2.0.0", + "micromark": "^4.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-types": "^2.0.0", + "minisearch": "^7.0.2", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "scule": "^1.3.0", + "shiki": "^1.10.3", + "slugify": "^1.6.6", + "socket.io-client": "^4.7.5", + "ufo": "^1.5.4", + "unist-util-stringify-position": "^4.0.0", + "unstorage": "^1.10.2", + "ws": "^8.18.0" + } + }, + "node_modules/@nuxt/content/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nuxt/content/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nuxt/content/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nuxt/content/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@nuxt/content/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@nuxt/devalue": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nuxt/devalue/-/devalue-2.0.2.tgz", + "integrity": "sha512-GBzP8zOc7CGWyFQS6dv1lQz8VVpz5C2yRszbXufwG/9zhStTIH50EtD87NmWbTMwXDvZLNg8GIpb1UFdH93JCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@nuxt/devtools": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@nuxt/devtools/-/devtools-1.3.9.tgz", + "integrity": "sha512-tFKlbUPgSXw4tyD8xpztQtJeVn3egdKbFCV0xc92FbfGbclAyaa3XhKA2tMWXEGZQpykAWMRNrGWN24FtXFA6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@nuxt/devtools-kit": "1.3.9", + "@nuxt/devtools-wizard": "1.3.9", + "@nuxt/kit": "^3.12.2", + "@vue/devtools-core": "7.3.3", + "@vue/devtools-kit": "7.3.3", + "birpc": "^0.2.17", + "consola": "^3.2.3", + "cronstrue": "^2.50.0", + "destr": "^2.0.3", + "error-stack-parser-es": "^0.1.4", + "execa": "^7.2.0", + "fast-glob": "^3.3.2", + "fast-npm-meta": "^0.1.1", + "flatted": "^3.3.1", + "get-port-please": "^3.1.2", + "hookable": "^5.5.3", + "image-meta": "^0.2.0", + "is-installed-globally": "^1.0.0", + "launch-editor": "^2.8.0", + "local-pkg": "^0.5.0", + "magicast": "^0.3.4", + "nypm": "^0.3.9", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.1.2", + "rc9": "^2.1.2", + "scule": "^1.3.0", + "semver": "^7.6.2", + "simple-git": "^3.25.0", + "sirv": "^2.0.4", + "unimport": "^3.7.2", + "vite-plugin-inspect": "^0.8.4", + "vite-plugin-vue-inspector": "^5.1.2", + "which": "^3.0.1", + "ws": "^8.17.1" + }, + "bin": { + "devtools": "cli.mjs" + }, + "peerDependencies": { + "vite": "*" + } + }, + "node_modules/@nuxt/devtools-kit": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-1.3.9.tgz", + "integrity": "sha512-tgr/F+4BbI53/JxgaXl3cuV9dMuCXMsd4GEXN+JqtCdAkDbH3wL79GGWx0/6I9acGzRsB6UZ1H6U96nfgcIrAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.12.2", + "@nuxt/schema": "^3.12.3", + "execa": "^7.2.0" + }, + "peerDependencies": { + "vite": "*" + } + }, + "node_modules/@nuxt/devtools-kit/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@nuxt/devtools-kit/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@nuxt/devtools-kit/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-kit/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-kit/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-kit/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-kit/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-kit/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-wizard": { + "version": "1.3.9", + "resolved": "https://registry.npmjs.org/@nuxt/devtools-wizard/-/devtools-wizard-1.3.9.tgz", + "integrity": "sha512-WMgwWWuyng+Y6k7sfBI95wYnec8TPFkuYbHHOlYQgqE9dAewPisSbEm3WkB7p/W9UqxpN8mvKN5qUg4sTmEpgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3", + "diff": "^5.2.0", + "execa": "^7.2.0", + "global-directory": "^4.0.1", + "magicast": "^0.3.4", + "pathe": "^1.1.2", + "pkg-types": "^1.1.2", + "prompts": "^2.4.2", + "rc9": "^2.1.2", + "semver": "^7.6.2" + }, + "bin": { + "devtools-wizard": "cli.mjs" + } + }, + "node_modules/@nuxt/devtools-wizard/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@nuxt/devtools-wizard/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@nuxt/devtools-wizard/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-wizard/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-wizard/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-wizard/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-wizard/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools-wizard/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nuxt/devtools-wizard/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools/node_modules/execa": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-7.2.0.tgz", + "integrity": "sha512-UduyVP7TLB5IcAQl+OzLyLcS/l32W/GLg+AhHJ+ow40FOk2U3SAllPwR44v4vmdFwIWqpdwxxpQbF1n5ta9seA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.1", + "human-signals": "^4.3.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^3.0.7", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": "^14.18.0 || ^16.14.0 || >=18.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@nuxt/devtools/node_modules/human-signals": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-4.3.1.tgz", + "integrity": "sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14.18.0" + } + }, + "node_modules/@nuxt/devtools/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nuxt/devtools/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/devtools/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@nuxt/kit": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.12.4.tgz", + "integrity": "sha512-aNRD1ylzijY0oYolldNcZJXVyxdGzNTl+Xd0UYyFQCu9f4wqUZqQ9l+b7arCEzchr96pMK0xdpvLcS3xo1wDcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/schema": "3.12.4", + "c12": "^1.11.1", + "consola": "^3.2.3", + "defu": "^6.1.4", + "destr": "^2.0.3", + "globby": "^14.0.2", + "hash-sum": "^2.0.0", + "ignore": "^5.3.1", + "jiti": "^1.21.6", + "klona": "^2.0.6", + "knitwork": "^1.1.0", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.1.3", + "scule": "^1.3.0", + "semver": "^7.6.3", + "ufo": "^1.5.4", + "unctx": "^2.3.1", + "unimport": "^3.9.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/kit/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nuxt/schema": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.12.4.tgz", + "integrity": "sha512-H7FwBV4ChssMaeiLyPdVLOLUa0326ebp3pNbJfGgFt7rSoKh1MmgjorecA8JMxOQZziy3w6EELf4+5cgLh/F1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "compatx": "^0.1.8", + "consola": "^3.2.3", + "defu": "^6.1.4", + "hookable": "^5.5.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.3", + "scule": "^1.3.0", + "std-env": "^3.7.0", + "ufo": "^1.5.4", + "uncrypto": "^0.1.3", + "unimport": "^3.9.0", + "untyped": "^1.4.2" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/@nuxt/telemetry": { + "version": "2.5.4", + "resolved": "https://registry.npmjs.org/@nuxt/telemetry/-/telemetry-2.5.4.tgz", + "integrity": "sha512-KH6wxzsNys69daSO0xUv0LEBAfhwwjK1M+0Cdi1/vxmifCslMIY7lN11B4eywSfscbyVPAYJvANyc7XiVPImBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.11.2", + "ci-info": "^4.0.0", + "consola": "^3.2.3", + "create-require": "^1.1.1", + "defu": "^6.1.4", + "destr": "^2.0.3", + "dotenv": "^16.4.5", + "git-url-parse": "^14.0.0", + "is-docker": "^3.0.0", + "jiti": "^1.21.0", + "mri": "^1.2.0", + "nanoid": "^5.0.7", + "ofetch": "^1.3.4", + "parse-git-config": "^3.0.0", + "pathe": "^1.1.2", + "rc9": "^2.1.2", + "std-env": "^3.7.0" + }, + "bin": { + "nuxt-telemetry": "bin/nuxt-telemetry.mjs" + } + }, + "node_modules/@nuxt/telemetry/node_modules/ci-info": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz", + "integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@nuxt/vite-builder": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/@nuxt/vite-builder/-/vite-builder-3.12.4.tgz", + "integrity": "sha512-5v3y6SkshJurZYJWHtc7+NGeCgptsreCSguBCZVzJxYdsPFdMicLoxjTt8IGAHWjkGVONrX+K8NBSFFgnx40jQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "3.12.4", + "@rollup/plugin-replace": "^5.0.7", + "@vitejs/plugin-vue": "^5.0.5", + "@vitejs/plugin-vue-jsx": "^4.0.0", + "autoprefixer": "^10.4.19", + "clear": "^0.1.0", + "consola": "^3.2.3", + "cssnano": "^7.0.4", + "defu": "^6.1.4", + "esbuild": "^0.23.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "externality": "^1.0.2", + "get-port-please": "^3.1.2", + "h3": "^1.12.0", + "knitwork": "^1.1.0", + "magic-string": "^0.30.10", + "mlly": "^1.7.1", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.1.3", + "postcss": "^8.4.39", + "rollup-plugin-visualizer": "^5.12.0", + "std-env": "^3.7.0", + "strip-literal": "^2.1.0", + "ufo": "^1.5.4", + "unenv": "^1.10.0", + "unplugin": "^1.11.0", + "vite": "^5.3.4", + "vite-node": "^2.0.3", + "vite-plugin-checker": "^0.7.2", + "vue-bundle-renderer": "^2.1.0" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + }, + "peerDependencies": { + "vue": "^3.3.4" + } + }, + "node_modules/@nuxt/vite-builder/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@nuxt/vite-builder/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@nuxthq/studio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@nuxthq/studio/-/studio-1.1.2.tgz", + "integrity": "sha512-YVEiIuU+5cLZ0qdLsRAYuFE395XoYf87UTR5xwxxpw9++uhlyLiQyO7JIXTTWIOdEiMHt8frrrLJBBPd5tHAeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.11.2", + "defu": "^6.1.4", + "git-url-parse": "^14.0.0", + "nuxt-component-meta": "^0.6.4", + "parse-git-config": "^3.0.0", + "pkg-types": "^1.1.1", + "socket.io-client": "^4.7.5", + "ufo": "^1.5.3", + "untyped": "^1.4.2" + } + }, + "node_modules/@nuxtjs/algolia": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@nuxtjs/algolia/-/algolia-1.10.2.tgz", + "integrity": "sha512-vN+CSnZmRNWiwzLRQZb5gt+qGgCUYp5Uwd3//f3VWH/gUSlWw+fP71Azvzx3WdAr8Gjsp75U1JPdrHaXGbxG1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/cache-in-memory": "^4.14.2", + "@algolia/recommend": "^4.12.2", + "@algolia/requester-fetch": "^4.23.2", + "@nuxt/kit": "^3.7.0", + "algoliasearch": "^4.11.0", + "instantsearch.css": "^7.4.5", + "metadata-scraper": "^0.2.49", + "storyblok-algolia-indexer": "^1.1.0", + "vue-instantsearch": "^4.3.2" + } + }, + "node_modules/@nuxtjs/color-mode": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/@nuxtjs/color-mode/-/color-mode-3.4.4.tgz", + "integrity": "sha512-VSNJVGnRIjiGmfbMa0cN+rwNRowDRTL/wku/z5MpKSanVo3khIRitBNqNviso1l3T+LW0pLHeXBNp6L8g/l1EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.12.4", + "pathe": "^1.1.2", + "pkg-types": "^1.1.3", + "semver": "^7.6.3" + } + }, + "node_modules/@nuxtjs/color-mode/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@nuxtjs/mdc": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/@nuxtjs/mdc/-/mdc-0.8.3.tgz", + "integrity": "sha512-FqvJFWkBN9u2FeWog+7+C0aIOx0WIu61TYgAXPmmIOVVua6s2mXQsMyF3fXY2M56QBIaYJzK/SYN+5FGr5GNTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.12.2", + "@shikijs/transformers": "^1.10.0", + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "@vue/compiler-core": "^3.4.31", + "consola": "^3.2.3", + "debug": "^4.3.5", + "defu": "^6.1.4", + "destr": "^2.0.3", + "detab": "^3.0.2", + "github-slugger": "^2.0.0", + "hast-util-to-string": "^3.0.0", + "mdast-util-to-hast": "^13.2.0", + "micromark-util-sanitize-uri": "^2.0.0", + "ohash": "^1.1.3", + "parse5": "^7.1.2", + "pathe": "^1.1.2", + "property-information": "^6.5.0", + "rehype-external-links": "^3.0.0", + "rehype-raw": "^7.0.0", + "rehype-slug": "^6.0.0", + "rehype-sort-attribute-values": "^5.0.0", + "rehype-sort-attributes": "^5.0.0", + "remark-emoji": "^5.0.0", + "remark-gfm": "^4.0.0", + "remark-mdc": "^3.2.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.0", + "scule": "^1.3.0", + "shiki": "^1.10.0", + "ufo": "^1.5.3", + "unified": "^11.0.5", + "unist-builder": "^4.0.0", + "unist-util-visit": "^5.0.0", + "unwasm": "^0.3.9" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.4.1.tgz", + "integrity": "sha512-HNjmfLQEVRZmHRET336f20H/8kOozUGwk7yajvsonjNxbj2wBTK1WsQuHkD5yYh9RxFGL2EyDHryOihOwUoKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.4.1", + "@parcel/watcher-darwin-arm64": "2.4.1", + "@parcel/watcher-darwin-x64": "2.4.1", + "@parcel/watcher-freebsd-x64": "2.4.1", + "@parcel/watcher-linux-arm-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-glibc": "2.4.1", + "@parcel/watcher-linux-arm64-musl": "2.4.1", + "@parcel/watcher-linux-x64-glibc": "2.4.1", + "@parcel/watcher-linux-x64-musl": "2.4.1", + "@parcel/watcher-win32-arm64": "2.4.1", + "@parcel/watcher-win32-ia32": "2.4.1", + "@parcel/watcher-win32-x64": "2.4.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.4.1.tgz", + "integrity": "sha512-LOi/WTbbh3aTn2RYddrO8pnapixAziFl6SMxHM69r3tvdSm94JtCenaKgk1GRg5FJ5wpMCpHeW+7yqPlvZv7kg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.4.1.tgz", + "integrity": "sha512-ln41eihm5YXIY043vBrrHfn94SIBlqOWmoROhsMVTSXGh0QahKGy77tfEywQ7v3NywyxBBkGIfrWRHm0hsKtzA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.4.1.tgz", + "integrity": "sha512-yrw81BRLjjtHyDu7J61oPuSoeYWR3lDElcPGJyOvIXmor6DEo7/G2u1o7I38cwlcoBHQFULqF6nesIX3tsEXMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.4.1.tgz", + "integrity": "sha512-TJa3Pex/gX3CWIx/Co8k+ykNdDCLx+TuZj3f3h7eOjgpdKM+Mnix37RYsYU4LHhiYJz3DK5nFCCra81p6g050w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.4.1.tgz", + "integrity": "sha512-4rVYDlsMEYfa537BRXxJ5UF4ddNwnr2/1O4MHM5PjI9cvV2qymvhwZSFgXqbS8YoTk5i/JR0L0JDs69BUn45YA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.4.1.tgz", + "integrity": "sha512-BJ7mH985OADVLpbrzCLgrJ3TOpiZggE9FMblfO65PlOCdG++xJpKUJ0Aol74ZUIYfb8WsRlUdgrZxKkz3zXWYA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.4.1.tgz", + "integrity": "sha512-p4Xb7JGq3MLgAfYhslU2SjoV9G0kI0Xry0kuxeG/41UfpjHGOhv7UoUDAz/jb1u2elbhazy4rRBL8PegPJFBhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.4.1.tgz", + "integrity": "sha512-s9O3fByZ/2pyYDPoLM6zt92yu6P4E39a03zvO0qCHOTjxmt3GHRMLuRZEWhWLASTMSrrnVNWdVI/+pUElJBBBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.4.1.tgz", + "integrity": "sha512-L2nZTYR1myLNST0O632g0Dx9LyMNHrn6TOt76sYxWLdff3cB22/GZX2UPtJnaqQPdCRoszoY5rcOj4oMTtp5fQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-wasm": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-wasm/-/watcher-wasm-2.4.1.tgz", + "integrity": "sha512-/ZR0RxqxU/xxDGzbzosMjh4W6NdYFMqq2nvo2b8SLi7rsl/4jkL8S5stIikorNkdR50oVDvqb/3JT05WM+CRRA==", + "bundleDependencies": [ + "napi-wasm" + ], + "dev": true, + "license": "MIT", + "dependencies": { + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "napi-wasm": "^1.1.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-wasm/node_modules/napi-wasm": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.4.1.tgz", + "integrity": "sha512-Uq2BPp5GWhrq/lcuItCHoqxjULU1QYEcyjSO5jqqOK8RNFDBQnenMMx4gAl3v8GiWa59E9+uDM7yZ6LxwUIfRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.4.1.tgz", + "integrity": "sha512-maNRit5QQV2kgHFSYwftmPBxiuK5u4DXjbXx7q6eKjq5dsLXZ4FJiVvlcw35QXzk0KrUecJmuVFbj4uV9oYrcw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.4.1.tgz", + "integrity": "sha512-+DvS92F9ezicfswqrvIRM2njcYJbd5mb9CUgtrHCHmvn7pPPa+nMDRu1o1bYYz/l5IB2NVGNJWiH7h1E58IF2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.25", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz", + "integrity": "sha512-j7P6Rgr3mmtdkeDGTe0E/aYyWEWVtc5yFXtHCRHs28/jptDEWfaVOc5T7cblqy1XKPPfCxJc/8DwQ5YgLOZOVQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-alias": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-alias/-/plugin-alias-5.1.0.tgz", + "integrity": "sha512-lpA3RZ9PdIG7qqhEfv79tBffNaoDuukFDrmhLqg9ifv99u/ehn+lOg30x2zmhf8AQqQUZaMk/B9fZraQ6/acDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "slash": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-alias/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@rollup/plugin-commonjs": { + "version": "25.0.8", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-25.0.8.tgz", + "integrity": "sha512-ZEZWTK5n6Qde0to4vS9Mr5x/0UZoqCxPVR9KRUjU4kA2sO7GEUn1fop0DAwpO6z0Nw/kJON9bDmSxdWxO/TT1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@rollup/plugin-commonjs/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@rollup/plugin-inject": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz", + "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.2.3", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.2.3.tgz", + "integrity": "sha512-j/lym8nf5E21LwBT4Df1VD6hRO2L2iwUeUmP7litikRsVp1H6NWx20NEp0Y7su+7XGc476GnXXc4kFeZNGmaSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-builtin-module": "^3.2.1", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-replace": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@rollup/plugin-replace/-/plugin-replace-5.0.7.tgz", + "integrity": "sha512-PqxSfuorkHz/SPpyngLyg5GCEkOcee9M1bkxiVDr41Pd61mqP1PLOoDPbpl44SB2mQGKwV/In74gqQmGITOhEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "magic-string": "^0.30.3" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.0.tgz", + "integrity": "sha512-XTIWOPPcpvyKI6L1NHo0lFlCyznUEyPmPY1mc3KpPVDYulHSTvyeLNVW00QTLIAFNhR3kYnJTQHeGqU4M3n09g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.22.4.tgz", + "integrity": "sha512-Fxamp4aEZnfPOcGA8KSNEohV8hX7zVHOemC8jVBoBUHu5zpJK/Eu3uJwt6BMgy9fkvzxDaurgj96F/NiLukF2w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.22.4.tgz", + "integrity": "sha512-VXoK5UMrgECLYaMuGuVTOx5kcuap1Jm8g/M83RnCHBKOqvPPmROFJGQaZhGccnsFtfXQ3XYa4/jMCJvZnbJBdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.22.4.tgz", + "integrity": "sha512-xMM9ORBqu81jyMKCDP+SZDhnX2QEVQzTcC6G18KlTQEzWK8r/oNZtKuZaCcHhnsa6fEeOBionoyl5JsAbE/36Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.22.4.tgz", + "integrity": "sha512-aJJyYKQwbHuhTUrjWjxEvGnNNBCnmpHDvrb8JFDbeSH3m2XdHcxDd3jthAzvmoI8w/kSjd2y0udT+4okADsZIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.22.4.tgz", + "integrity": "sha512-j63YtCIRAzbO+gC2L9dWXRh5BFetsv0j0va0Wi9epXDgU/XUi5dJKo4USTttVyK7fGw2nPWK0PbAvyliz50SCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.22.4.tgz", + "integrity": "sha512-dJnWUgwWBX1YBRsuKKMOlXCzh2Wu1mlHzv20TpqEsfdZLb3WoJW2kIEsGwLkroYf24IrPAvOT/ZQ2OYMV6vlrg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.22.4.tgz", + "integrity": "sha512-AdPRoNi3NKVLolCN/Sp4F4N1d98c4SBnHMKoLuiG6RXgoZ4sllseuGioszumnPGmPM2O7qaAX/IJdeDU8f26Aw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.22.4.tgz", + "integrity": "sha512-Gl0AxBtDg8uoAn5CCqQDMqAx22Wx22pjDOjBdmG0VIWX3qUBHzYmOKh8KXHL4UpogfJ14G4wk16EQogF+v8hmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.22.4.tgz", + "integrity": "sha512-3aVCK9xfWW1oGQpTsYJJPF6bfpWfhbRnhdlyhak2ZiyFLDaayz0EP5j9V1RVLAAxlmWKTDfS9wyRyY3hvhPoOg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.22.4.tgz", + "integrity": "sha512-ePYIir6VYnhgv2C5Xe9u+ico4t8sZWXschR6fMgoPUK31yQu7hTEJb7bCqivHECwIClJfKgE7zYsh1qTP3WHUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.22.4.tgz", + "integrity": "sha512-GqFJ9wLlbB9daxhVlrTe61vJtEY99/xB3C8e4ULVsVfflcpmR6c8UZXjtkMA6FhNONhj2eA5Tk9uAVw5orEs4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.22.4.tgz", + "integrity": "sha512-87v0ol2sH9GE3cLQLNEy0K/R0pz1nvg76o8M5nhMR0+Q+BBGLnb35P0fVz4CQxHYXaAOhE8HhlkaZfsdUOlHwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.22.4.tgz", + "integrity": "sha512-UV6FZMUgePDZrFjrNGIWzDo/vABebuXBhJEqrHxrGiU6HikPy0Z3LfdtciIttEUQfuDdCn8fqh7wiFJjCNwO+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.22.4.tgz", + "integrity": "sha512-BjI+NVVEGAXjGWYHz/vv0pBqfGoUH0IGZ0cICTn7kB9PyjrATSkX+8WkguNjWoj2qSr1im/+tTGRaY+4/PdcQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.22.4.tgz", + "integrity": "sha512-SiWG/1TuUdPvYmzmYnmd3IEifzR61Tragkbx9D3+R8mzQqDBz8v+BvZNDlkiTtI9T15KYZhP0ehn3Dld4n9J5g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.22.4.tgz", + "integrity": "sha512-j8pPKp53/lq9lMXN57S8cFz0MynJk8OWNuUnXct/9KCpKU7DgU3bYMJhwWmcqC0UU29p8Lr0/7KEVcaM6bf47Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@shikijs/core": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.14.1.tgz", + "integrity": "sha512-KyHIIpKNaT20FtFPFjCQB5WVSTpLR/n+jQXhWHWVUMm9MaOaG9BGOG0MSyt7yA4+Lm+4c9rTc03tt3nYzeYSfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/transformers": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@shikijs/transformers/-/transformers-1.14.1.tgz", + "integrity": "sha512-JJqL8QBVCJh3L61jqqEXgFq1cTycwjcGj7aSmqOEsbxnETM9hRlaB74QuXvY/fVJNjbNt8nvWo0VwAXKvMSLRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "shiki": "1.14.1" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", + "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/commons/node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/chai": { + "version": "4.3.17", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.17.tgz", + "integrity": "sha512-zmZ21EWzR71B4Sscphjief5djsLre50M6lI622OSySTmn9DB3j+C3kWroHfBQWXbOBwbgg/M8CG/hUxDLIloow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", + "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/dom-speech-recognition": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz", + "integrity": "sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/google.maps": { + "version": "3.55.12", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.55.12.tgz", + "integrity": "sha512-Q8MsLE+YYIrE1H8wdN69YHHAF8h7ApvF5MiMXh/zeCpP9Ut745mV9M0F4X4eobZ2WJe9k8tW2ryYjLa87IO2Sg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/hogan.js": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hogan.js/-/hogan.js-3.0.5.tgz", + "integrity": "sha512-/uRaY3HGPWyLqOyhgvW9Aa43BNnLZrNeQxl2p8wqId4UHMfPKolSB+U7BlZyO1ng7MkLnyEAItsBzCG0SDhqrA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.5.12", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", + "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "0.7.34", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", + "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.4.0.tgz", + "integrity": "sha512-49AbMDwYUz7EXxKU/r7mXOsxwFr4BYbvB7tWYxVuLdb2ibd30ijjXINSMAHiEEZk5PCRBmW1gUeisn2VMKt3cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/@types/qs": { + "version": "6.9.15", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", + "integrity": "sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", + "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unhead/dom": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@unhead/dom/-/dom-1.9.16.tgz", + "integrity": "sha512-aZIAnnc89Csi1vV4mtlHYI765B7m1yuaXUuQiYHwr6glE9FLyy2X87CzEci4yPH/YbkKm0bGQRfcxXq6Eq0W7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unhead/schema": "1.9.16", + "@unhead/shared": "1.9.16" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/schema": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@unhead/schema/-/schema-1.9.16.tgz", + "integrity": "sha512-V2BshX+I6D2wN4ys5so8RQDUgsggsxW9FVBiuQi4h8oPWtHclogxzDiHa5BH2TgvNIoUxLnLYNAShMGipmVuUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookable": "^5.5.3", + "zhead": "^2.2.4" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/shared": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@unhead/shared/-/shared-1.9.16.tgz", + "integrity": "sha512-pfJnArULCY+GBr7OtYyyxihRiQLkT31TpyK6nUKIwyax4oNOGyhNfk0RFzNq16BwLg60d1lrc5bd5mZGbfClMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unhead/schema": "1.9.16" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/ssr": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@unhead/ssr/-/ssr-1.9.16.tgz", + "integrity": "sha512-8R1qt4VAemX4Iun/l7DnUBJqmxA/KaUSc2+/hRYPJYOopXdCWkoaxC1K1ROX2vbRF7qmjdU5ik/a27kSPN94gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unhead/schema": "1.9.16", + "@unhead/shared": "1.9.16" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/@unhead/vue": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@unhead/vue/-/vue-1.9.16.tgz", + "integrity": "sha512-kpMWWwm8cOwo4gw4An43pz30l2CqNtmJpX5Xsu79rwf6Viq8jHAjk6BGqyKy220M2bpa0Va4fnR532SgGO1YgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unhead/schema": "1.9.16", + "@unhead/shared": "1.9.16", + "hookable": "^5.5.3", + "unhead": "1.9.16" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + }, + "peerDependencies": { + "vue": ">=2.7 || >=3" + } + }, + "node_modules/@unocss/reset": { + "version": "0.50.8", + "resolved": "https://registry.npmjs.org/@unocss/reset/-/reset-0.50.8.tgz", + "integrity": "sha512-2WoM6O9VyuHDPAnvCXr7LBJQ8ZRHDnuQAFsL1dWXp561Iq2l9whdNtPuMcozLGJGUUrFfVBXIrHY4sfxxScgWg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vercel/nft": { + "version": "0.26.5", + "resolved": "https://registry.npmjs.org/@vercel/nft/-/nft-0.26.5.tgz", + "integrity": "sha512-NHxohEqad6Ra/r4lGknO52uc/GrWILXAMs1BB4401GTqww0fw1bAqzpG1XHuDO+dprg4GvsD9ZLLSsdo78p9hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.5", + "@rollup/pluginutils": "^4.0.0", + "acorn": "^8.6.0", + "acorn-import-attributes": "^1.9.2", + "async-sema": "^3.1.1", + "bindings": "^1.4.0", + "estree-walker": "2.0.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.2", + "node-gyp-build": "^4.2.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "nft": "out/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@vercel/nft/node_modules/@rollup/pluginutils": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-4.2.1.tgz", + "integrity": "sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "estree-walker": "^2.0.1", + "picomatch": "^2.2.2" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/@vitejs/plugin-vue": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.1.2.tgz", + "integrity": "sha512-nY9IwH12qeiJqumTCLJLE7IiNx7HZ39cbHaysEUd+Myvbz9KAqd2yq+U01Kab1R/H1BmiyM2ShTYlNH32Fzo3A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@vitejs/plugin-vue-jsx": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-4.0.1.tgz", + "integrity": "sha512-7mg9HFGnFHMEwCdB6AY83cVK4A6sCqnrjFYF4WIlebYAQVVJ/sC/CiTruVdrRlhrFoeZ8rlMxY9wYpPTIRhhAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.24.7", + "@vue/babel-plugin-jsx": "^1.2.2" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "vite": "^5.0.0", + "vue": "^3.0.0" + } + }, + "node_modules/@volar/language-core": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.4.1.tgz", + "integrity": "sha512-EIY+Swv+TjsWpxOxujjMf1ZXqOjg9MT2VMXZ+1dKva0wD8W0L6EtptFFcCJdBbcKmGMFkr57Qzz9VNMWhs3jXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "1.4.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.4.1.tgz", + "integrity": "sha512-bZ46ad72dsbzuOWPUtJjBXkzSQzzSejuR3CT81+GvTEI2E994D8JPXzM3tl98zyCNnjgs4OkRyliImL1dvJ5BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.2.2" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@volar/typescript/node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/typescript/node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript/node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@volar/vue-language-core": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/@volar/vue-language-core/-/vue-language-core-1.6.5.tgz", + "integrity": "sha512-IF2b6hW4QAxfsLd5mePmLgtkXzNi+YnH6ltCd80gb7+cbdpFMjM1I+w+nSg2kfBTyfu+W8useCZvW89kPTBpzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "1.4.1", + "@volar/source-map": "1.4.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/compiler-sfc": "^3.3.0", + "@vue/reactivity": "^3.3.0", + "@vue/shared": "^3.3.0", + "minimatch": "^9.0.0", + "muggle-string": "^0.2.2", + "vue-template-compiler": "^2.7.14" + } + }, + "node_modules/@volar/vue-language-core/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@volar/vue-language-core/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue-macros/common": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@vue-macros/common/-/common-1.12.2.tgz", + "integrity": "sha512-+NGfhrPvPNOb3Wg9PNPEXPe0HTXmVe6XJawL1gi3cIjOSGIhpOdvmMT2cRuWb265IpA/PeL5Sqo0+DQnEDxLvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.0", + "@rollup/pluginutils": "^5.1.0", + "@vue/compiler-sfc": "^3.4.34", + "ast-kit": "^1.0.1", + "local-pkg": "^0.5.0", + "magic-string-ast": "^0.6.2" + }, + "engines": { + "node": ">=16.14.0" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.2.25" + }, + "peerDependenciesMeta": { + "vue": { + "optional": true + } + } + }, + "node_modules/@vue/babel-helper-vue-transform-on": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.2.2.tgz", + "integrity": "sha512-nOttamHUR3YzdEqdM/XXDyCSdxMA9VizUKoroLX6yTyRtggzQMHXcmwh8a7ZErcJttIBIc9s68a1B8GZ+Dmvsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/babel-plugin-jsx": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.2.2.tgz", + "integrity": "sha512-nYTkZUVTu4nhP199UoORePsql0l+wj7v/oyQjtThUVhJl1U+6qHuoVhIvR3bf7eVKjbCK+Cs2AWd7mi9Mpz9rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "~7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/template": "^7.23.9", + "@babel/traverse": "^7.23.9", + "@babel/types": "^7.23.9", + "@vue/babel-helper-vue-transform-on": "1.2.2", + "@vue/babel-plugin-resolve-type": "1.2.2", + "camelcase": "^6.3.0", + "html-tags": "^3.3.1", + "svg-tags": "^1.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + } + } + }, + "node_modules/@vue/babel-plugin-jsx/node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@vue/babel-plugin-jsx/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@vue/babel-plugin-resolve-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@vue/babel-plugin-resolve-type/-/babel-plugin-resolve-type-1.2.2.tgz", + "integrity": "sha512-EntyroPwNg5IPVdUJupqs0CFzuf6lUrVvCspmv2J1FITLeGnUCuoGNNk78dgCusxEiYj6RMkTJflGSxk5aIC4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/helper-module-imports": "~7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/parser": "^7.23.9", + "@vue/compiler-sfc": "^3.4.15" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@vue/babel-plugin-resolve-type/node_modules/@babel/helper-module-imports": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.15.tgz", + "integrity": "sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.38.tgz", + "integrity": "sha512-8IQOTCWnLFqfHzOGm9+P8OPSEDukgg3Huc92qSG49if/xI2SAwLHQO2qaPQbjCWPBcQoO1WYfXfTACUrWV3c5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/shared": "3.4.38", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.38.tgz", + "integrity": "sha512-Osc/c7ABsHXTsETLgykcOwIxFktHfGSUDkb05V61rocEfsFDcjDLH/IHJSNJP+/Sv9KeN2Lx1V6McZzlSb9EhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.4.38", + "@vue/shared": "3.4.38" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.38.tgz", + "integrity": "sha512-s5QfZ+9PzPh3T5H4hsQDJtI8x7zdJaew/dCGgqZ2630XdzaZ3AD8xGZfBqpT8oaD/p2eedd+pL8tD5vvt5ZYJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.24.7", + "@vue/compiler-core": "3.4.38", + "@vue/compiler-dom": "3.4.38", + "@vue/compiler-ssr": "3.4.38", + "@vue/shared": "3.4.38", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.10", + "postcss": "^8.4.40", + "source-map-js": "^1.2.0" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.38.tgz", + "integrity": "sha512-YXznKFQ8dxYpAz9zLuVvfcXhc31FSPFDcqr0kyujbOwNhlmaNvL2QfIy+RZeJgSn5Fk54CWoEUeW+NVBAogGaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.4.38", + "@vue/shared": "3.4.38" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.3.tgz", + "integrity": "sha512-0MiMsFma/HqA6g3KLKn+AGpL1kgKhFWszC9U29NfpWK5LE7bjeXxySWJrOJ77hBz+TBrBQ7o4QJqbPbqbs8rJw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/devtools-core": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-core/-/devtools-core-7.3.3.tgz", + "integrity": "sha512-i6Bwkx4OwfY0QVHjAdsivhlzZ2HMj7fbNRYJsWspQ+dkA1f3nTzycPqZmVUsm2TGkbQlhTMhCAdDoP97JKoc+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-kit": "^7.3.3", + "@vue/devtools-shared": "^7.3.3", + "mitt": "^3.0.1", + "nanoid": "^3.3.4", + "pathe": "^1.1.2", + "vite-hot-client": "^0.2.3" + } + }, + "node_modules/@vue/devtools-core/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/@vue/devtools-kit": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/@vue/devtools-kit/-/devtools-kit-7.3.3.tgz", + "integrity": "sha512-m+dFI57BrzKYPKq73mt4CJ5GWld5OLBseLHPHGVP7CaILNY9o1gWVJWAJeF8XtQ9LTiMxZSaK6NcBsFuxAhD0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-shared": "^7.3.3", + "birpc": "^0.2.17", + "hookable": "^5.5.3", + "mitt": "^3.0.1", + "perfect-debounce": "^1.0.0", + "speakingurl": "^14.0.1", + "superjson": "^2.2.1" + } + }, + "node_modules/@vue/devtools-shared": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/@vue/devtools-shared/-/devtools-shared-7.3.8.tgz", + "integrity": "sha512-1NiJbn7Yp47nPDWhFZyEKpB2+5/+7JYv8IQnU0ccMrgslPR2dL7u1DIyI7mLqy4HN1ll36gQy0k8GqBYSFgZJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "rfdc": "^1.4.1" + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core/node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@vue/language-core/node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@vue/language-core/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@vue/language-core/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@vue/language-core/node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vue/reactivity": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.38.tgz", + "integrity": "sha512-4vl4wMMVniLsSYYeldAKzbk72+D3hUnkw9z8lDeJacTxAkXeDAP1uE9xr2+aKIN0ipOL8EG2GPouVTH6yF7Gnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/shared": "3.4.38" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.38.tgz", + "integrity": "sha512-21z3wA99EABtuf+O3IhdxP0iHgkBs1vuoCAsCKLVJPEjpVqvblwBnTj42vzHRlWDCyxu9ptDm7sI2ZMcWrQqlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.4.38", + "@vue/shared": "3.4.38" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.38.tgz", + "integrity": "sha512-afZzmUreU7vKwKsV17H1NDThEEmdYI+GCAK/KY1U957Ig2NATPVjCROv61R19fjZNzMmiU03n79OMnXyJVN0UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.4.38", + "@vue/runtime-core": "3.4.38", + "@vue/shared": "3.4.38", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.38.tgz", + "integrity": "sha512-NggOTr82FbPEkkUvBm4fTGcwUY8UuTsnWC/L2YZBmvaQ4C4Jl/Ao4HHTB+l7WnFCt5M/dN3l0XLuyjzswGYVCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.4.38", + "@vue/shared": "3.4.38" + }, + "peerDependencies": { + "vue": "3.4.38" + } + }, + "node_modules/@vue/shared": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.38.tgz", + "integrity": "sha512-q0xCiLkuWWQLzVrecPb0RMsNWyxICOjPrcrwxTUEHb1fsnvni4dcuyG7RT/Ie7VPTvnjzIaWzRMUBsrqNj/hhw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/core/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/head": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vueuse/head/-/head-2.0.0.tgz", + "integrity": "sha512-ykdOxTGs95xjD4WXE4na/umxZea2Itl0GWBILas+O4oqS7eXIods38INvk3XkJKjqMdWPcpCyLX/DioLQxU1KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unhead/dom": "^1.7.0", + "@unhead/schema": "^1.7.0", + "@unhead/ssr": "^1.7.0", + "@unhead/vue": "^1.7.0" + }, + "peerDependencies": { + "vue": ">=2.7 || >=3" + } + }, + "node_modules/@vueuse/integrations": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/integrations/-/integrations-10.11.1.tgz", + "integrity": "sha512-Y5hCGBguN+vuVYTZmdd/IMXLOdfS60zAmDmFYc4BKBcMUPZH1n4tdyDECCPjXm0bNT3ZRUy1xzTLGaUje8Xyaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vueuse/core": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "async-validator": "^4", + "axios": "^1", + "change-case": "^4", + "drauu": "^0.3", + "focus-trap": "^7", + "fuse.js": "^6", + "idb-keyval": "^6", + "jwt-decode": "^3", + "nprogress": "^0.2", + "qrcode": "^1.5", + "sortablejs": "^1", + "universal-cookie": "^6" + }, + "peerDependenciesMeta": { + "async-validator": { + "optional": true + }, + "axios": { + "optional": true + }, + "change-case": { + "optional": true + }, + "drauu": { + "optional": true + }, + "focus-trap": { + "optional": true + }, + "fuse.js": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "jwt-decode": { + "optional": true + }, + "nprogress": { + "optional": true + }, + "qrcode": { + "optional": true + }, + "sortablejs": { + "optional": true + }, + "universal-cookie": { + "optional": true + } + } + }, + "node_modules/@vueuse/integrations/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/integrations/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/nuxt": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/nuxt/-/nuxt-10.11.1.tgz", + "integrity": "sha512-UiaYSIwOkmUVn8Gl1AqtLWYR12flO+8sEu9X0Y1fNjSR7EWy9jMuiCvOGqwtoeTsqfHrivl0d5HfMzr11GFnMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.12.1", + "@vueuse/core": "10.11.1", + "@vueuse/metadata": "10.11.1", + "local-pkg": "^0.5.0", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "nuxt": "^3.0.0" + } + }, + "node_modules/@vueuse/nuxt/node_modules/@types/web-bluetooth": { + "version": "0.0.20", + "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz", + "integrity": "sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/nuxt/node_modules/@vueuse/core": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-10.11.1.tgz", + "integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.20", + "@vueuse/metadata": "10.11.1", + "@vueuse/shared": "10.11.1", + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/nuxt/node_modules/@vueuse/metadata": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-10.11.1.tgz", + "integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/nuxt/node_modules/@vueuse/shared": { + "version": "10.11.1", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-10.11.1.tgz", + "integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue-demi": ">=0.14.8" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/nuxt/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared/node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dev": true, + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-attributes": { + "version": "1.9.5", + "resolved": "https://registry.npmjs.org/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", + "integrity": "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/algoliasearch": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.24.0.tgz", + "integrity": "sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==", + "license": "MIT", + "dependencies": { + "@algolia/cache-browser-local-storage": "4.24.0", + "@algolia/cache-common": "4.24.0", + "@algolia/cache-in-memory": "4.24.0", + "@algolia/client-account": "4.24.0", + "@algolia/client-analytics": "4.24.0", + "@algolia/client-common": "4.24.0", + "@algolia/client-personalization": "4.24.0", + "@algolia/client-search": "4.24.0", + "@algolia/logger-common": "4.24.0", + "@algolia/logger-console": "4.24.0", + "@algolia/recommend": "4.24.0", + "@algolia/requester-browser-xhr": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/requester-node-http": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/algoliasearch-helper": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.22.3.tgz", + "integrity": "sha512-2eoEz8mG4KHE+DzfrBTrCmDPxVXv7aZZWPojAJFtARpxxMO6lkos1dJ+XDCXdPvq7q3tpYWRi6xXmVQikejtpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/client-common": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.24.0.tgz", + "integrity": "sha512-bc2ROsNL6w6rqpl5jj/UywlIYC21TwSSoFHKl01lYirGMW+9Eek6r02Tocg4gZ8HAw3iBvu6XQiM3BEbmEMoiA==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/client-search": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.24.0.tgz", + "integrity": "sha512-uRW6EpNapmLAD0mW47OXqTP8eiIx5F6qN9/x/7HHO6owL3N1IXqydGwW5nhDFBrV+ldouro2W1VX3XlcUXEFCA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "4.24.0", + "@algolia/requester-common": "4.24.0", + "@algolia/transporter": "4.24.0" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/requester-browser-xhr": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.24.0.tgz", + "integrity": "sha512-Z2NxZMb6+nVXSjF13YpjYTdvV3032YTBSGm2vnYvYPA6mMxzM3v5rsCiSspndn9rzIW4Qp1lPHBvuoKJV6jnAA==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0" + } + }, + "node_modules/algoliasearch/node_modules/@algolia/requester-node-http": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.24.0.tgz", + "integrity": "sha512-JF18yTjNOVYvU/L3UosRcvbPMGT9B+/GQWNWnenIImglzNVGpyzChkXLnrSf6uxwVNO6ESGu6oN8MqcGQcjQJw==", + "license": "MIT", + "dependencies": { + "@algolia/requester-common": "4.24.0" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "license": "MIT" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/archiver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/archiver/-/archiver-7.0.1.tgz", + "integrity": "sha512-ZcbTaIqJOfCc03QwD468Unz/5Ir8ATtvAHsK+FdXbDIbGfihqh9mrvdcYunQzqn4HrvWWaFyaxJhGZagaJJpPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.2", + "async": "^3.2.4", + "buffer-crc32": "^1.0.0", + "readable-stream": "^4.0.0", + "readdir-glob": "^1.1.2", + "tar-stream": "^3.0.0", + "zip-stream": "^6.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-5.0.2.tgz", + "integrity": "sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^10.0.0", + "graceful-fs": "^4.2.0", + "is-stream": "^2.0.1", + "lazystream": "^1.0.0", + "lodash": "^4.17.15", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/archiver-utils/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/archiver-utils/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/archiver-utils/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/assert": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz", + "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "is-nan": "^1.3.2", + "object-is": "^1.1.5", + "object.assign": "^4.1.4", + "util": "^0.12.5" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/ast-kit": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-1.1.0.tgz", + "integrity": "sha512-RlNqd4u6c/rJ5R+tN/ZTtyNrH8X0NHCvyt6gD8RHa3JjzxxHWoyaU0Ujk3Zjbh7IZqrYl1Sxm6XzZifmVxXxHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "pathe": "^1.1.2" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/ast-types": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.15.2.tgz", + "integrity": "sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-walker-scope": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz", + "integrity": "sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.3", + "ast-kit": "^1.0.1" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/async": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.5.tgz", + "integrity": "sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/async-sema": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz", + "integrity": "sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.4.tgz", + "integrity": "sha512-DukmaFRnY6AzAALSH4J2M3k6PkaC+MfaAGdEERRWcC9q3/TWQwLpHR8ZRLKTdQ3aBDL64EdluRDjJqKw+BPZEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/b4a": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.6.tgz", + "integrity": "sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.0.tgz", + "integrity": "sha512-ldYss8SbBlWva1bs28q78Ju5Zq1F+8BrqBZZ0VFhLBvhh6lCpC2o3gDJi/5DRLs9FgYZCnmPYIVFU4lRXCkyUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" + }, + "node_modules/bare-events": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.4.2.tgz", + "integrity": "sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==", + "dev": true, + "license": "Apache-2.0", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/birpc": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-0.2.17.tgz", + "integrity": "sha512-+hkTxhot+dWsLpp3gia5AkVHIsKlZybNT5gIYiDlNzJrmYPcTM9k5/w2uaj3IPpd7LlEYpmCj4Jj1nC41VhDFg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/buffer-crc32": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz", + "integrity": "sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/c12": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/c12/-/c12-1.11.1.tgz", + "integrity": "sha512-KDU0TvSvVdaYcQKQ6iPHATGz/7p/KiVjPg4vQrB6Jg/wX9R0yl5RZxWm9IoZqaIHD2+6PZd81+KMGwRr/lRIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.6.0", + "confbox": "^0.1.7", + "defu": "^6.1.4", + "dotenv": "^16.4.5", + "giget": "^1.2.3", + "jiti": "^1.21.6", + "mlly": "^1.7.1", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.1.1", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.4" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", + "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chai": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", + "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.3", + "deep-eql": "^4.1.3", + "get-func-name": "^2.0.2", + "loupe": "^2.3.6", + "pathval": "^1.1.1", + "type-detect": "^4.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/check-error": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", + "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/chroma-js": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz", + "integrity": "sha512-BLHvCB9s8Z1EV4ethr6xnkl/P2YRFOGqfgvuMG/MyCbZPrTA+NeiByY6XvgF0zP4/2deU2CXnWyMa3zu1LqQ3A==", + "dev": true, + "license": "(BSD-3-Clause AND Apache-2.0)" + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.3.1.tgz", + "integrity": "sha512-a3KdPAANPbNE4ZUv9h6LckSl9zLsYOP4MBmhIPkRaeyybt+r4UghLvq+xw/YwUcC1gqylCkL4rdVs3Lwupjm4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/clear": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/clear/-/clear-0.1.0.tgz", + "integrity": "sha512-qMjRnoL+JDPJHeLePZJuao6+8orzHMGP04A8CdwCNsKhRbOnKRjefxONR7bwILT3MHecxKBjHkKL/tkZ8r4Uzw==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/clipboardy": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/clipboardy/-/clipboardy-4.0.0.tgz", + "integrity": "sha512-5mOlNS0mhX0707P2I0aZ2V/cmHUEO/fL7VFLqszkhUsxt7RwnmrInf/eEQKlf5GzvYeHIjT+Ov1HRfNmymlG0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^8.0.1", + "is-wsl": "^3.1.0", + "is64bit": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/clipboardy/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/clipboardy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/clipboardy/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/clipboardy/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true, + "license": "MIT" + }, + "node_modules/compatx": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/compatx/-/compatx-0.1.8.tgz", + "integrity": "sha512-jcbsEAR81Bt5s1qOFymBufmCbXCXbk0Ql+K5ouj6gCyx2yHlu6AgmGIi9HxfKixpUDO5bCFJUHQ5uM6ecbTebw==", + "dev": true, + "license": "MIT" + }, + "node_modules/compress-commons": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz", + "integrity": "sha512-6FqVXeETqWPoGcfzrXb37E50NP0LXT8kAMu5ooZayhWWdgEY4lBEEcbQNXtkuKQsGduxiIcI4gOTsxTmuq/bSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "crc32-stream": "^6.0.0", + "is-stream": "^2.0.1", + "normalize-path": "^3.0.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", + "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.2.3.tgz", + "integrity": "sha512-I5qxpzLv+sJhTVEoLYNcTW+bThDCPsit0vLNKShZx6rLtpilNpmmeTPaeqJb9ZE9dV3DGaeby6Vuhrw38WjeyQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie-es": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", + "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "dev": true, + "license": "MIT" + }, + "node_modules/copy-anything": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-3.0.5.tgz", + "integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-what": "^4.1.8" + }, + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/crc-32": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz", + "integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "crc32": "bin/crc32.njs" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/crc32-stream": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-6.0.0.tgz", + "integrity": "sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==", + "dev": true, + "license": "MIT", + "dependencies": { + "crc-32": "^1.2.0", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/create-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/croner": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/croner/-/croner-8.1.1.tgz", + "integrity": "sha512-1VdUuRnQP4drdFkS8NKvDR1NBgevm8TOuflcaZEKsxw42CxonjW/2vkj1AKlinJb4ZLwBcuWF9GiPr7FQc6AQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0" + } + }, + "node_modules/cronstrue": { + "version": "2.50.0", + "resolved": "https://registry.npmjs.org/cronstrue/-/cronstrue-2.50.0.tgz", + "integrity": "sha512-ULYhWIonJzlScCCQrPUG5uMXzXxSixty4djud9SS37DoNxDdkeRocxzHuAo4ImRBUK+mAuU5X9TSwEDccnnuPg==", + "dev": true, + "license": "MIT", + "bin": { + "cronstrue": "bin/cli.js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crossws": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.2.4.tgz", + "integrity": "sha512-DAxroI2uSOgUKLz00NX6A8U/8EE3SZHmIND+10jkVSaypvyt57J5JEOxAQOL6lQxyzi/wZbTIwssU1uy69h5Vg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "uWebSockets.js": "*" + }, + "peerDependenciesMeta": { + "uWebSockets.js": { + "optional": true + } + } + }, + "node_modules/css-declaration-sorter": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14 || ^16 || >=18" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cssnano": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.0.5.tgz", + "integrity": "sha512-Aq0vqBLtpTT5Yxj+hLlLfNPFuRQCDIjx5JQAhhaedQKLNDvDGeVziF24PS+S1f0Z5KCxWvw0QVI3VNHNBITxVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^7.0.5", + "lilconfig": "^3.1.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-preset-default": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.5.tgz", + "integrity": "sha512-Jbzja0xaKwc5JzxPQoc+fotKpYtWEu4wQLMQe29CM0FjjdRjA4omvbGHl2DTGgARKxSTpPssBsok+ixv8uTBqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^5.0.0", + "postcss-calc": "^10.0.1", + "postcss-colormin": "^7.0.2", + "postcss-convert-values": "^7.0.3", + "postcss-discard-comments": "^7.0.2", + "postcss-discard-duplicates": "^7.0.1", + "postcss-discard-empty": "^7.0.0", + "postcss-discard-overridden": "^7.0.0", + "postcss-merge-longhand": "^7.0.3", + "postcss-merge-rules": "^7.0.3", + "postcss-minify-font-values": "^7.0.0", + "postcss-minify-gradients": "^7.0.0", + "postcss-minify-params": "^7.0.2", + "postcss-minify-selectors": "^7.0.3", + "postcss-normalize-charset": "^7.0.0", + "postcss-normalize-display-values": "^7.0.0", + "postcss-normalize-positions": "^7.0.0", + "postcss-normalize-repeat-style": "^7.0.0", + "postcss-normalize-string": "^7.0.0", + "postcss-normalize-timing-functions": "^7.0.0", + "postcss-normalize-unicode": "^7.0.2", + "postcss-normalize-url": "^7.0.0", + "postcss-normalize-whitespace": "^7.0.0", + "postcss-ordered-values": "^7.0.1", + "postcss-reduce-initial": "^7.0.2", + "postcss-reduce-transforms": "^7.0.0", + "postcss-svgo": "^7.0.1", + "postcss-unique-selectors": "^7.0.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/cssnano-utils": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.0.tgz", + "integrity": "sha512-Uij0Xdxc24L6SirFr25MlwC2rCFX6scyUmuKpzI+JQ7cyqDEwD42fJ0xfB3yLfOnRDU5LKGgjQ9FA6LYh76GWQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/db0": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/db0/-/db0-0.1.4.tgz", + "integrity": "sha512-Ft6eCwONYxlwLjBXSJxw0t0RYtA5gW9mq8JfBXn9TtC0nDPlqePAhpv9v4g9aONBi6JI1OXHTKKkUYGd+BOrCA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@libsql/client": "^0.5.2", + "better-sqlite3": "^9.4.3", + "drizzle-orm": "^0.29.4" + }, + "peerDependenciesMeta": { + "@libsql/client": { + "optional": true + }, + "better-sqlite3": { + "optional": true + }, + "drizzle-orm": { + "optional": true + } + } + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", + "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dedent": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", + "integrity": "sha512-NHQtfOOW68WD8lgypbLA5oT+Bt0xXJhiYvoR6SmmNXZfpzOGXwdKWmcwG8N7PwVVWV3eF/68nmD9BaJSsTBhyQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", + "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.3.tgz", + "integrity": "sha512-2N3BOUU4gYMpTP24s5rF5iP7BDr7uNTCs4ozw3kf/eKfvWSIu93GEBi5m427YoyJoeOzQ5smuu4nNAPGb8idSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detab": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/detab/-/detab-3.0.2.tgz", + "integrity": "sha512-7Bp16Bk8sk0Y6gdXiCtnpGbghn8atnTJdd/82aWvS5ESnlcNvgUc10U2NYS0PAiDSGjWiI8qs/Cv1b2uSGdQ8w==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.0.0.tgz", + "integrity": "sha512-gO+/OMXF7488D+u3ue+G7Y4AA3ZmUnB3eHJXmBTgNHvr4ZNzl36A0ZtG+XCRNYCkYx/bFmw4qtkoFLa+wSrwAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "license": "Apache-2.0" + }, + "node_modules/diff": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", + "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dir-glob/node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "license": "MIT" + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domino": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/domino/-/domino-2.1.6.tgz", + "integrity": "sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-8.0.2.tgz", + "integrity": "sha512-xaBe6ZT4DHPkg0k4Ytbvn5xoxgpG0jOS1dYxSOwAHPuNLjP3/OzN0gH55SrLqpx8cBfSaVt91lXYkApjb+nYdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^3.8.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true, + "license": "MIT" + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/ejs": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", + "integrity": "sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jake": "^10.8.5" + }, + "bin": { + "ejs": "bin/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.11", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.11.tgz", + "integrity": "sha512-R1CccCDYqndR25CaXFd6hp/u9RaaMcftMkphmvuepXr5b1vfLkRml6aWVeBhXJ7rbevHkKEMJtz8XqPf7ffmew==", + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoticon": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", + "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", + "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", + "integrity": "sha512-LMHl3dXhTcfv8gM4kEzIUeTQ+7fpdA0l2tUf34BddXPkz2A5xJ5L/Pchd5BL6rdccM9QGvu0sWZzK1Z1t4wwyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser-es": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-0.1.5.tgz", + "integrity": "sha512-xHku1X40RO+fO8yJ8Wh2f2rZWVjqyhb1zgq1yZ8aZRQkv6OOKhKWRUaht3eSCUbAOBaKIgM+ykwFLE+QUxgGeg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/errx": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/errx/-/errx-0.1.0.tgz", + "integrity": "sha512-fZmsRiDNv07K6s2KkKFTiD2aIvECa7++PKyD5NC32tpRw46qZA3sOz+aM+/V9V0GDHxVTKLziveV4JhzBHDp9Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", + "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.2.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.23.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.23.1.tgz", + "integrity": "sha512-VVNz/9Sa0bs5SELtn3f7qhJCDPCF5oMEl5cO9/SSinpE9hbPVvxbd572HH5AKiP7WD8INO53GgfDDhRjkylHEg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.23.1", + "@esbuild/android-arm": "0.23.1", + "@esbuild/android-arm64": "0.23.1", + "@esbuild/android-x64": "0.23.1", + "@esbuild/darwin-arm64": "0.23.1", + "@esbuild/darwin-x64": "0.23.1", + "@esbuild/freebsd-arm64": "0.23.1", + "@esbuild/freebsd-x64": "0.23.1", + "@esbuild/linux-arm": "0.23.1", + "@esbuild/linux-arm64": "0.23.1", + "@esbuild/linux-ia32": "0.23.1", + "@esbuild/linux-loong64": "0.23.1", + "@esbuild/linux-mips64el": "0.23.1", + "@esbuild/linux-ppc64": "0.23.1", + "@esbuild/linux-riscv64": "0.23.1", + "@esbuild/linux-s390x": "0.23.1", + "@esbuild/linux-x64": "0.23.1", + "@esbuild/netbsd-x64": "0.23.1", + "@esbuild/openbsd-arm64": "0.23.1", + "@esbuild/openbsd-x64": "0.23.1", + "@esbuild/sunos-x64": "0.23.1", + "@esbuild/win32-arm64": "0.23.1", + "@esbuild/win32-ia32": "0.23.1", + "@esbuild/win32-x64": "0.23.1" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/externality": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/externality/-/externality-1.0.2.tgz", + "integrity": "sha512-LyExtJWKxtgVzmgtEHyQtLFpw1KFhQphF9nTG8TpAIVkiI/xQ3FJh75tRFLYl4hkn7BNIIdLJInuDAavX35pMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "enhanced-resolve": "^5.14.1", + "mlly": "^1.3.0", + "pathe": "^1.1.1", + "ufo": "^1.1.2" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-npm-meta": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/fast-npm-meta/-/fast-npm-meta-0.1.1.tgz", + "integrity": "sha512-uS9DjGncI/9XZ6HJFrci0WzSi++N8Jskbb2uB7+9SQlrgA3VaLhXhV9Gl5HwIGESHkayYYZFGnVNhJwRDKCWIA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "dev": true, + "license": "MIT" + }, + "node_modules/filelist": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", + "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.0.1" + } + }, + "node_modules/filelist/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/filelist/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/flat/-/flat-6.0.1.tgz", + "integrity": "sha512-/3FfIa8mbrg3xE7+wAhWeV+bd7L2Mof+xtZb5dRDKZ+wDvYJK4WDYeIOuOhre5Yv5aQObZrlbRmk3RTSiuQBtw==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" + }, + "node_modules/focus-trap": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.4.tgz", + "integrity": "sha512-N7kHdlgsO/v+iD/dMoJKtsSqs5Dz/dXZVebRgJw23LDk+jMi/974zyiOYDziY2JPp8xivq9BmUGwIJMiuSBi7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tabbable": "^6.2.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/foreground-child/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuse.js": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.6.2.tgz", + "integrity": "sha512-cJaJkxCCxC8qIIcPBF9yGxY0W/tVZS3uEISDxhYIdtk8OL93pe+6Zj7LjCqVV4dzbqcriOZ+kQ/NE4RXZHsIGA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", + "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port-please": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.1.2.tgz", + "integrity": "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/giget": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/giget/-/giget-1.2.3.tgz", + "integrity": "sha512-8EHPljDvs7qKykr6uw8b+lqLiUc/vUg+KVTI0uND4s63TdsZM2Xus3mflvF0DDG9SiM4RlCkFGL+7aAjRmV7KA==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.3", + "nypm": "^0.3.8", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "tar": "^6.2.0" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, + "node_modules/git-config-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/git-config-path/-/git-config-path-2.0.0.tgz", + "integrity": "sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/git-up": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/git-up/-/git-up-7.0.0.tgz", + "integrity": "sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-ssh": "^1.4.0", + "parse-url": "^8.1.0" + } + }, + "node_modules/git-url-parse": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/git-url-parse/-/git-url-parse-14.1.0.tgz", + "integrity": "sha512-8xg65dTxGHST3+zGpycMMFZcoTzAdZ2dOtu4vmgIfkTFnVHBxHMzBC2L1k8To7EmrSiHesT8JgPLT91VKw1B5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "git-up": "^7.0.0" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/gzip-size": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-7.0.0.tgz", + "integrity": "sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/h3": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.12.0.tgz", + "integrity": "sha512-Zi/CcNeWBXDrFNlV0hUBJQR9F7a96RjMeAZweW/ZWkR9fuXrMcvKnSA63f/zZ9l0GgQOZDVHGvXivNN9PWOwhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie-es": "^1.1.0", + "crossws": "^0.2.4", + "defu": "^6.1.4", + "destr": "^2.0.3", + "iron-webcrypto": "^1.1.1", + "ohash": "^1.1.3", + "radix3": "^1.1.2", + "ufo": "^1.5.3", + "uncrypto": "^0.1.3", + "unenv": "^1.9.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", + "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/hash-sum": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", + "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.1.tgz", + "integrity": "sha512-Er/Iixbc7IEa7r/XLtuG52zoqn/b3Xng/w6aZQ0xGVxzhw5xUFxcRqdPzP6yFi/4HBYRaifaI5fQ1RH8n0ZeOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^8.0.0", + "property-information": "^6.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.0.4.tgz", + "integrity": "sha512-LHE65TD2YiNsHD3YuXcKPHXPLuYh/gjp12mOfU8jxSrm1f/yJpsb0F/KKljS6U9LJoP0Ux+tCe8iJ2AsPzTdgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", + "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.0.tgz", + "integrity": "sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-8.0.0.tgz", + "integrity": "sha512-dMOtzCEd3ABUeSIISmrETiKuyydk1w0pa+gE/uormcTpSYuaNJPbX1NU3JLyscSLjwAQM8bWMhhIlnCqnRvDTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^6.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/hogan.js": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", + "integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==", + "dev": true, + "dependencies": { + "mkdirp": "0.3.0", + "nopt": "1.0.10" + }, + "bin": { + "hulk": "bin/hulk" + } + }, + "node_modules/hogan.js/node_modules/mkdirp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", + "integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==", + "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", + "dev": true, + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, + "node_modules/hogan.js/node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/hookable": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-5.5.3.tgz", + "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/htm": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/htm/-/htm-3.1.1.tgz", + "integrity": "sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-shutdown": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/http-shutdown/-/http-shutdown-1.2.2.tgz", + "integrity": "sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/httpxy": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/httpxy/-/httpxy-0.1.5.tgz", + "integrity": "sha512-hqLDO+rfststuyEUTWObQK6zHEEmZ/kaIP2/zclGGZn6X8h/ESTWg+WKecQ/e5k4nPswjzZD+q2VqZIbr15CoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-meta": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/image-meta/-/image-meta-0.2.1.tgz", + "integrity": "sha512-K6acvFaelNxx8wc2VjbIzXKDVB0Khs0QT35U6NkGfTdCmjLNcO2945m7RFNR9/RPVFm48hq7QPzK8uGH18HCGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/instantsearch-ui-components": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/instantsearch-ui-components/-/instantsearch-ui-components-0.8.0.tgz", + "integrity": "sha512-EzV7cR5+18sjmR6DMdv8yL9WuS2hUxrkqbByiLmHnJFbB4TZ4Q7oZDAn43bOItWZ2TxMK3GoxNbB/ZhWjsptPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.1.2" + } + }, + "node_modules/instantsearch.css": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/instantsearch.css/-/instantsearch.css-7.4.5.tgz", + "integrity": "sha512-iIGBYjCokU93DDB8kbeztKtlu4qVEyTg1xvS6iSO1YvqRwkIZgf0tmsl/GytsLdZhuw8j4wEaeYsCzNbeJ/zEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/instantsearch.js": { + "version": "4.73.4", + "resolved": "https://registry.npmjs.org/instantsearch.js/-/instantsearch.js-4.73.4.tgz", + "integrity": "sha512-QdvExJthRBXpRaX9lzey+2sqUIzlOZEpd8N5wZyLYYs6WjDHIwrNPOzmOv7VHLBBHGqZ6YkXoCoegj5zm9QI8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@algolia/events": "^4.0.1", + "@types/dom-speech-recognition": "^0.0.1", + "@types/google.maps": "^3.45.3", + "@types/hogan.js": "^3.0.0", + "@types/qs": "^6.5.3", + "algoliasearch-helper": "3.22.3", + "hogan.js": "^3.0.2", + "htm": "^3.0.0", + "instantsearch-ui-components": "0.8.0", + "preact": "^10.10.0", + "qs": "^6.5.1 < 6.10", + "search-insights": "^2.15.0" + }, + "peerDependencies": { + "algoliasearch": ">= 3.1 < 6" + } + }, + "node_modules/ioredis": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", + "integrity": "sha512-2YZsvl7jopIa1gaePkeMtd9rAcSjOOjPtpcLlOeusyO+XH2SK5ZcT+UCrElPP+WVIInh2TzeI4XW9ENaSLVVHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ioredis/commands": "^1.1.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-absolute-url": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz", + "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz", + "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-installed-globally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-1.0.0.tgz", + "integrity": "sha512-K55T22lfpQ63N4KEN57jZUAaAYqYHEe8veb/TycJRk9DdSCLLcovXz/mL6mOnhQaZsQGwPhuFopdQIlqGSEjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-directory": "^4.0.1", + "is-path-inside": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-nan": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz", + "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-4.0.0.tgz", + "integrity": "sha512-lJJV/5dYS+RcL8uQdBDW9c9uWFLLBNRyFhnAKXw5tVqLlKZ4RMGZKv+YQ/IA3OhD+RpbJa1LLFM1FQPGyIXvOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-reference": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-1.2.1.tgz", + "integrity": "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/is-ssh": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/is-ssh/-/is-ssh-1.4.0.tgz", + "integrity": "sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.1" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", + "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "which-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-what": { + "version": "4.1.16", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-4.1.16.tgz", + "integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.13" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is64bit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is64bit/-/is64bit-2.0.0.tgz", + "integrity": "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "system-architecture": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jake": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", + "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "async": "^3.2.3", + "chalk": "^4.0.2", + "filelist": "^1.0.4", + "minimatch": "^3.1.2" + }, + "bin": { + "jake": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jake/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jake/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jake/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jake/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jake/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jake/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/knitwork": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/knitwork/-/knitwork-1.1.0.tgz", + "integrity": "sha512-oHnmiBUVHz1V+URE77PNot2lv3QiYU2zQf1JjOVkMt3YDKGbu8NAFr+c4mcNOhdsGrB/VpVbRwPwhiXrPhxQbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/kolorist": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", + "integrity": "sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/launch-editor": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.1.tgz", + "integrity": "sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/lazystream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/lazystream/-/lazystream-1.0.1.tgz", + "integrity": "sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "^2.0.5" + }, + "engines": { + "node": ">= 0.6.3" + } + }, + "node_modules/lazystream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/lazystream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/lazystream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, + "node_modules/listhen": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/listhen/-/listhen-1.7.2.tgz", + "integrity": "sha512-7/HamOm5YD9Wb7CFgAZkKgVPA96WwhcTQoqtm2VTZGVbVVn3IWKRBTgrU7cchA3Q8k9iCsG8Osoi9GX4JsGM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.4.1", + "@parcel/watcher-wasm": "^2.4.1", + "citty": "^0.1.6", + "clipboardy": "^4.0.0", + "consola": "^3.2.3", + "crossws": "^0.2.0", + "defu": "^6.1.4", + "get-port-please": "^3.1.2", + "h3": "^1.10.2", + "http-shutdown": "^1.2.2", + "jiti": "^1.21.0", + "mlly": "^1.6.1", + "node-forge": "^1.3.1", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "ufo": "^1.4.0", + "untun": "^0.1.3", + "uqr": "^0.1.2" + }, + "bin": { + "listen": "bin/listhen.mjs", + "listhen": "bin/listhen.mjs" + } + }, + "node_modules/local-pkg": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", + "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.4.2", + "pkg-types": "^1.0.3" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash._reinterpolate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", + "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.template": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", + "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash._reinterpolate": "^3.0.0", + "lodash.templatesettings": "^4.0.0" + } + }, + "node_modules/lodash.templatesettings": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", + "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash._reinterpolate": "^3.0.0" + } + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/loupe": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", + "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/magic-string-ast": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/magic-string-ast/-/magic-string-ast-0.6.2.tgz", + "integrity": "sha512-oN3Bcd7ZVt+0VGEs7402qR/tjgjbM7kPlH/z7ufJnzTLVBzXJITRHOJiwMmmYMgZfdoWQsfQcY+iKlxiBppnMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.10" + }, + "engines": { + "node": ">=16.14.0" + } + }, + "node_modules/magicast": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", + "integrity": "sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.1.tgz", + "integrity": "sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.1.tgz", + "integrity": "sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.0.0.tgz", + "integrity": "sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.0.tgz", + "integrity": "sha512-FyzMsduZZHSc3i0Px3PQcBT4WJY/X/RCtEJKuybiC6sjPqLv7h1yqAkmILZtuxMSsUyaLUWNp71+vQH2zqp5cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.0.0.tgz", + "integrity": "sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", + "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", + "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/metadata-scraper": { + "version": "0.2.61", + "resolved": "https://registry.npmjs.org/metadata-scraper/-/metadata-scraper-0.2.61.tgz", + "integrity": "sha512-ECV8r10nIVgn7Y5vY8lnlvi9vF1YgYBJjn2R1zrOcKRe47ra9Yg25ZE1ejL3Equqi8u2Mp346KHqIcR4PLdyTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "domino": "^2.1.6", + "got": "^11.8.1" + } + }, + "node_modules/micromark": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", + "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.1.tgz", + "integrity": "sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.0.tgz", + "integrity": "sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", + "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", + "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", + "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", + "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", + "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.0.tgz", + "integrity": "sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", + "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", + "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", + "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", + "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", + "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", + "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", + "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", + "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", + "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", + "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.1.tgz", + "integrity": "sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", + "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", + "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", + "dev": true, + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/minisearch": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/minisearch/-/minisearch-7.1.0.tgz", + "integrity": "sha512-tv7c/uefWdEhcu6hvrfTihflgeEi2tN6VV7HJnCjK6VxM75QQJh4t9FwJCsA2EsRS8LCnu3W87CuGPWMocOLCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdist": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/mkdist/-/mkdist-1.5.4.tgz", + "integrity": "sha512-GEmKYJG5K1YGFIq3t0K3iihZ8FTgXphLf/4UjbmpXIAtBFn4lEjXk3pXNTSfy7EtcEXhp2Nn1vzw5pIus6RY3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "autoprefixer": "^10.4.19", + "citty": "^0.1.6", + "cssnano": "^7.0.4", + "defu": "^6.1.4", + "esbuild": "^0.23.0", + "fast-glob": "^3.3.2", + "jiti": "^1.21.6", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.1.3", + "postcss": "^8.4.39", + "postcss-nested": "^6.0.1", + "semver": "^7.6.2" + }, + "bin": { + "mkdist": "dist/cli.cjs" + }, + "peerDependencies": { + "sass": "^1.77.8", + "typescript": ">=5.5.3", + "vue-tsc": "^1.8.27 || ^2.0.21" + }, + "peerDependenciesMeta": { + "sass": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/mkdist/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mlly": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.1.tgz", + "integrity": "sha512-rrVRZRELyQzrIUAVMHxP97kv+G786pHmOKzuFII8zDYahFBS7qnHh2AlYSl1GAHhaMPCz6/oHjVMcfFYgFYHgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.3", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/muggle-string/-/muggle-string-0.2.2.tgz", + "integrity": "sha512-YVE1mIJ4VpUMqZObFndk9CJu6DBJR/GB13p3tXuNbwD4XExaI5EOuRl6BHeIDxIqXZVxSfAC+y6U1Z/IxCfKUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.0.7.tgz", + "integrity": "sha512-oLxFY2gd2IqnjcYyOXD8XGCftpGtZP2AbHbOkthDkvRywH5ayNtPVy9YlOPcHckXzbLTCHpkb7FB+yuxKV13pQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/nitropack": { + "version": "2.9.7", + "resolved": "https://registry.npmjs.org/nitropack/-/nitropack-2.9.7.tgz", + "integrity": "sha512-aKXvtNrWkOCMsQbsk4A0qQdBjrJ1ZcvwlTQevI/LAgLWLYc5L7Q/YiYxGLal4ITyNSlzir1Cm1D2ZxnYhmpMEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cloudflare/kv-asset-handler": "^0.3.4", + "@netlify/functions": "^2.8.0", + "@rollup/plugin-alias": "^5.1.0", + "@rollup/plugin-commonjs": "^25.0.8", + "@rollup/plugin-inject": "^5.0.5", + "@rollup/plugin-json": "^6.1.0", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-replace": "^5.0.7", + "@rollup/plugin-terser": "^0.4.4", + "@rollup/pluginutils": "^5.1.0", + "@types/http-proxy": "^1.17.14", + "@vercel/nft": "^0.26.5", + "archiver": "^7.0.1", + "c12": "^1.11.1", + "chalk": "^5.3.0", + "chokidar": "^3.6.0", + "citty": "^0.1.6", + "consola": "^3.2.3", + "cookie-es": "^1.1.0", + "croner": "^8.0.2", + "crossws": "^0.2.4", + "db0": "^0.1.4", + "defu": "^6.1.4", + "destr": "^2.0.3", + "dot-prop": "^8.0.2", + "esbuild": "^0.20.2", + "escape-string-regexp": "^5.0.0", + "etag": "^1.8.1", + "fs-extra": "^11.2.0", + "globby": "^14.0.1", + "gzip-size": "^7.0.0", + "h3": "^1.12.0", + "hookable": "^5.5.3", + "httpxy": "^0.1.5", + "ioredis": "^5.4.1", + "jiti": "^1.21.6", + "klona": "^2.0.6", + "knitwork": "^1.1.0", + "listhen": "^1.7.2", + "magic-string": "^0.30.10", + "mime": "^4.0.3", + "mlly": "^1.7.1", + "mri": "^1.2.0", + "node-fetch-native": "^1.6.4", + "ofetch": "^1.3.4", + "ohash": "^1.1.3", + "openapi-typescript": "^6.7.6", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.1.1", + "pretty-bytes": "^6.1.1", + "radix3": "^1.1.2", + "rollup": "^4.18.0", + "rollup-plugin-visualizer": "^5.12.0", + "scule": "^1.3.0", + "semver": "^7.6.2", + "serve-placeholder": "^2.0.2", + "serve-static": "^1.15.0", + "std-env": "^3.7.0", + "ufo": "^1.5.3", + "uncrypto": "^0.1.3", + "unctx": "^2.3.1", + "unenv": "^1.9.0", + "unimport": "^3.7.2", + "unstorage": "^1.10.2", + "unwasm": "^0.3.9" + }, + "bin": { + "nitro": "dist/cli/index.mjs", + "nitropack": "dist/cli/index.mjs" + }, + "engines": { + "node": "^16.11.0 || >=17.0.0" + }, + "peerDependencies": { + "xml2js": "^0.6.2" + }, + "peerDependenciesMeta": { + "xml2js": { + "optional": true + } + } + }, + "node_modules/nitropack/node_modules/@esbuild/aix-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", + "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/android-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", + "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/android-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", + "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/android-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", + "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/darwin-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", + "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/darwin-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", + "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/freebsd-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", + "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/freebsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", + "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-arm": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", + "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", + "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", + "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-loong64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", + "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-mips64el": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", + "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-ppc64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", + "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-riscv64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", + "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-s390x": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", + "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/linux-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", + "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/netbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", + "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/openbsd-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", + "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/sunos-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", + "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/win32-arm64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", + "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/win32-ia32": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", + "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/@esbuild/win32-x64": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", + "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/nitropack/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/nitropack/node_modules/esbuild": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", + "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.20.2", + "@esbuild/android-arm": "0.20.2", + "@esbuild/android-arm64": "0.20.2", + "@esbuild/android-x64": "0.20.2", + "@esbuild/darwin-arm64": "0.20.2", + "@esbuild/darwin-x64": "0.20.2", + "@esbuild/freebsd-arm64": "0.20.2", + "@esbuild/freebsd-x64": "0.20.2", + "@esbuild/linux-arm": "0.20.2", + "@esbuild/linux-arm64": "0.20.2", + "@esbuild/linux-ia32": "0.20.2", + "@esbuild/linux-loong64": "0.20.2", + "@esbuild/linux-mips64el": "0.20.2", + "@esbuild/linux-ppc64": "0.20.2", + "@esbuild/linux-riscv64": "0.20.2", + "@esbuild/linux-s390x": "0.20.2", + "@esbuild/linux-x64": "0.20.2", + "@esbuild/netbsd-x64": "0.20.2", + "@esbuild/openbsd-x64": "0.20.2", + "@esbuild/sunos-x64": "0.20.2", + "@esbuild/win32-arm64": "0.20.2", + "@esbuild/win32-ia32": "0.20.2", + "@esbuild/win32-x64": "0.20.2" + } + }, + "node_modules/nitropack/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nitropack/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.1.3.tgz", + "integrity": "sha512-E2WEOVsgs7O16zsURJ/eH8BqhF029wGpEOnv7Urwdo2wmQanOACwJQh0devF9D9RhoZru0+9JXIS0dBXIAz+lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.4.tgz", + "integrity": "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp-build": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", + "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", + "dev": true, + "license": "MIT", + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "license": "MIT" + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "dev": true, + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/nuxi": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/nuxi/-/nuxi-3.12.0.tgz", + "integrity": "sha512-6vRdiXTw9SajEQOUi6Ze/XaIXzy1q/sD5UqHQSv3yqTu7Pot5S7fEihNXV8LpcgLz+9HzjVt70r7jYe7R99c2w==", + "dev": true, + "license": "MIT", + "bin": { + "nuxi": "bin/nuxi.mjs", + "nuxi-ng": "bin/nuxi.mjs", + "nuxt": "bin/nuxi.mjs", + "nuxt-cli": "bin/nuxi.mjs" + }, + "engines": { + "node": "^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/nuxt": { + "version": "3.12.4", + "resolved": "https://registry.npmjs.org/nuxt/-/nuxt-3.12.4.tgz", + "integrity": "sha512-/ddvyc2kgYYIN2UEjP8QIz48O/W3L0lZm7wChIDbOCj0vF/yLLeZHBaTb3aNvS9Hwp269nfjrm8j/mVxQK4RhA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/devalue": "^2.0.2", + "@nuxt/devtools": "^1.3.9", + "@nuxt/kit": "3.12.4", + "@nuxt/schema": "3.12.4", + "@nuxt/telemetry": "^2.5.4", + "@nuxt/vite-builder": "3.12.4", + "@unhead/dom": "^1.9.16", + "@unhead/ssr": "^1.9.16", + "@unhead/vue": "^1.9.16", + "@vue/shared": "^3.4.32", + "acorn": "8.12.1", + "c12": "^1.11.1", + "chokidar": "^3.6.0", + "compatx": "^0.1.8", + "consola": "^3.2.3", + "cookie-es": "^1.1.0", + "defu": "^6.1.4", + "destr": "^2.0.3", + "devalue": "^5.0.0", + "errx": "^0.1.0", + "esbuild": "^0.23.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "globby": "^14.0.2", + "h3": "^1.12.0", + "hookable": "^5.5.3", + "ignore": "^5.3.1", + "jiti": "^1.21.6", + "klona": "^2.0.6", + "knitwork": "^1.1.0", + "magic-string": "^0.30.10", + "mlly": "^1.7.1", + "nitropack": "^2.9.7", + "nuxi": "^3.12.0", + "nypm": "^0.3.9", + "ofetch": "^1.3.4", + "ohash": "^1.1.3", + "pathe": "^1.1.2", + "perfect-debounce": "^1.0.0", + "pkg-types": "^1.1.3", + "radix3": "^1.1.2", + "scule": "^1.3.0", + "semver": "^7.6.3", + "std-env": "^3.7.0", + "strip-literal": "^2.1.0", + "ufo": "^1.5.4", + "ultrahtml": "^1.5.3", + "uncrypto": "^0.1.3", + "unctx": "^2.3.1", + "unenv": "^1.10.0", + "unimport": "^3.9.0", + "unplugin": "^1.11.0", + "unplugin-vue-router": "^0.10.0", + "unstorage": "^1.10.2", + "untyped": "^1.4.2", + "vue": "^3.4.32", + "vue-bundle-renderer": "^2.1.0", + "vue-devtools-stub": "^0.1.0", + "vue-router": "^4.4.0" + }, + "bin": { + "nuxi": "bin/nuxt.mjs", + "nuxt": "bin/nuxt.mjs" + }, + "engines": { + "node": "^14.18.0 || >=16.10.0" + }, + "peerDependencies": { + "@parcel/watcher": "^2.1.0", + "@types/node": "^14.18.0 || >=16.10.0" + }, + "peerDependenciesMeta": { + "@parcel/watcher": { + "optional": true + }, + "@types/node": { + "optional": true + } + } + }, + "node_modules/nuxt-component-meta": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/nuxt-component-meta/-/nuxt-component-meta-0.6.6.tgz", + "integrity": "sha512-Y5/tuZuZOlD4GluAjcTU6JlhtEeg7/92VEfoV814t2uTuZK+b9RokJeGtrMotXuCJ4vuT1Is7M+pkPm+vY/tXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.9.1", + "citty": "^0.1.5", + "scule": "^1.1.1", + "typescript": "^5.3.3", + "vue-component-meta": "^1.8.27" + }, + "bin": { + "nuxt-component-meta": "bin/nuxt-component-meta.mjs" + } + }, + "node_modules/nuxt-config-schema": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/nuxt-config-schema/-/nuxt-config-schema-0.4.6.tgz", + "integrity": "sha512-kHLWJFynj5QrxVZ1MjY2xmDaTSN1BCMLGExA+hMMLoCb3wn9TJlDVqnE/nSdUJPMRkNn/NQ5WP9NLA9vlAXRUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nuxt/kit": "^3.4.2", + "defu": "^6.1.2", + "jiti": "^1.18.2", + "pathe": "^1.0.0", + "untyped": "^1.3.2" + } + }, + "node_modules/nuxt-icon": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/nuxt-icon/-/nuxt-icon-0.3.3.tgz", + "integrity": "sha512-KdhJAigBGTP8/YIFZ3orwetk40AgLq6VQ5HRYuDLmv5hiDptor9Ro+WIdZggHw7nciRxZvDdQkEwi9B5G/jrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@iconify/vue": "^4.1.0", + "@nuxt/kit": "^3.3.1", + "nuxt-config-schema": "^0.4.5" + } + }, + "node_modules/nuxt/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nuxt/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/nuxt/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/nypm": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.3.9.tgz", + "integrity": "sha512-BI2SdqqTHg2d4wJh8P9A1W+bslg33vOE9IZDY6eR2QC+Pu1iNBVZUqczrd43rJb+fMzHU7ltAYKsEFY/kHMFcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.2.3", + "execa": "^8.0.1", + "pathe": "^1.1.2", + "pkg-types": "^1.1.1", + "ufo": "^1.5.3" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": "^14.16.0 || >=16.10.0" + } + }, + "node_modules/nypm/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/nypm/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/nypm/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/nypm/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/nypm/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/object-is": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", + "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", + "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.5", + "define-properties": "^1.2.1", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ofetch": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.3.4.tgz", + "integrity": "sha512-KLIET85ik3vhEfS+3fDlc/BAZiAp+43QEC/yCo5zkNoY2YaKvNkOaFr/6wCFgFH1kuYQM5pMNi0Tg8koiIemtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "destr": "^2.0.3", + "node-fetch-native": "^1.6.3", + "ufo": "^1.5.3" + } + }, + "node_modules/ohash": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-1.1.3.tgz", + "integrity": "sha512-zuHHiGTYTA1sYJ/wZN+t5HKZaH23i4yI1HMwbuXm24Nid7Dv0KcuRlKoNKS9UNfAVSBlnGLcuQrnOKWOZoEGaw==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/openapi-typescript": { + "version": "6.7.6", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-6.7.6.tgz", + "integrity": "sha512-c/hfooPx+RBIOPM09GSxABOZhYPblDoyaGhqBkD/59vtpN21jEuWKDlM0KYTvqJVlSYjKs0tBcIdeXKChlSPtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-colors": "^4.1.3", + "fast-glob": "^3.3.2", + "js-yaml": "^4.1.0", + "supports-color": "^9.4.0", + "undici": "^5.28.4", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + } + }, + "node_modules/openapi-typescript/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/openapi-typescript/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/openapi-typescript/node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "license": "BlueOak-1.0.0" + }, + "node_modules/paneer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/paneer/-/paneer-0.1.0.tgz", + "integrity": "sha512-SZfJe/y9fbpeXZU+Kf7cSG2G7rnGP50hUYzCvcWyhp7hYzA3YXGthpkGfv6NSt0oo6QbcRyKwycg/6dpG5p8aw==", + "deprecated": "Please migrate to https://github.com/unjs/magicast", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.15", + "@types/estree": "^1.0.0", + "recast": "^0.22.0" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parse-entities": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.1.tgz", + "integrity": "sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse-git-config": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parse-git-config/-/parse-git-config-3.0.0.tgz", + "integrity": "sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "git-config-path": "^2.0.0", + "ini": "^1.3.5" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/parse-git-config/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-path": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", + "integrity": "sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog==", + "dev": true, + "license": "MIT", + "dependencies": { + "protocols": "^2.0.0" + } + }, + "node_modules/parse-url": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/parse-url/-/parse-url-8.1.0.tgz", + "integrity": "sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-path": "^7.0.0" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" + }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pinceau": { + "version": "0.18.9", + "resolved": "https://registry.npmjs.org/pinceau/-/pinceau-0.18.9.tgz", + "integrity": "sha512-GJ+l8a5Y+7PP/diwuajJhd2QONTIFkk2YXjrVTh7QKC3sMQEphpLH6ZJfXSeeSonQ0/BnhrrMi9a5e14mmqXug==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "playground" + ], + "dependencies": { + "@unocss/reset": "^0.50.3", + "@volar/vue-language-core": "^1.2.0", + "acorn": "^8.8.2", + "chroma-js": "^2.4.2", + "consola": "^3.0.1", + "csstype": "^3.1.1", + "defu": "^6.1.2", + "magic-string": "^0.30.0", + "nanoid": "^4.0.1", + "ohash": "^1.0.0", + "paneer": "^0.1.0", + "pathe": "^1.1.0", + "postcss-custom-properties": "13.1.4", + "postcss-dark-theme-class": "0.7.3", + "postcss-nested": "^6.0.1", + "recast": "^0.22.0", + "scule": "^1.0.0", + "style-dictionary-esm": "^1.3.7", + "unbuild": "^1.1.2", + "unplugin": "^1.1.0" + } + }, + "node_modules/pinceau/node_modules/nanoid": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-4.0.2.tgz", + "integrity": "sha512-7ZtY5KTCNheRGfEFxnedV5zFiORN1+Y1N6zvPTnHQd8ENUvfaDBeuJDZb2bN/oXwXxu3qkTXDzy57W5vAmDTBw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^14 || ^16 || >=18" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-types": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.3.tgz", + "integrity": "sha512-+JrgthZG6m3ckicaOB74TwQ+tBWsFl3qVQg7mN8ulwSOElJ7gBhKzj2VkCPnZ4NlF6kEquYU+RIYNVAvzd54UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.7", + "mlly": "^1.7.1", + "pathe": "^1.1.2" + } + }, + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-calc": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.0.2.tgz", + "integrity": "sha512-DT/Wwm6fCKgpYVI7ZEWuPJ4az8hiEHtCUeYjZXqU7Ou4QqYh1Df2yCQ7Ca6N7xqKPFkxN3fhf+u9KSoOCJNAjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12 || ^20.9 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.38" + } + }, + "node_modules/postcss-colormin": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.2.tgz", + "integrity": "sha512-YntRXNngcvEvDbEjTdRWGU606eZvB5prmHG4BF0yLmVpamXbpsRJzevyy6MZVyuecgzI2AWAlvFi8DAeCqwpvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-convert-values": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.3.tgz", + "integrity": "sha512-yJhocjCs2SQer0uZ9lXTMOwDowbxvhwFVrZeS6NPEij/XXthl73ggUmfwVvJM+Vaj5gtCKJV1jiUu4IhAUkX/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-custom-properties": { + "version": "13.1.4", + "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-13.1.4.tgz", + "integrity": "sha512-iSAdaZrM3KMec8cOSzeTUNXPYDlhqsMJHpt62yrjwG6nAnMtRHPk5JdMzGosBJtqEahDolvD5LNbcq+EZ78o5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/cascade-layer-name-parser": "^1.0.0", + "@csstools/css-parser-algorithms": "^2.0.0", + "@csstools/css-tokenizer": "^2.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, + "node_modules/postcss-dark-theme-class": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/postcss-dark-theme-class/-/postcss-dark-theme-class-0.7.3.tgz", + "integrity": "sha512-M9vtfh8ORzQsVdT9BWb+xpEDAzC7nHBn7wVc988/JkEVLPupKcUnV0jw7RZ8sSj0ovpqN1POf6PLdt19JCHfhQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-discard-comments": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.2.tgz", + "integrity": "sha512-/Hje9Ls1IYcB9duELO/AyDUJI6aQVY3h5Rj1ziXgaLYCTi1iVBLnjg/TS0D6NszR/kDG6I86OwLmAYe+bvJjiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-duplicates": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.1.tgz", + "integrity": "sha512-oZA+v8Jkpu1ct/xbbrntHRsfLGuzoP+cpt0nJe5ED2FQF8n8bJtn7Bo28jSmBYwqgqnqkuSXJfSUEE7if4nClQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-empty": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.0.tgz", + "integrity": "sha512-e+QzoReTZ8IAwhnSdp/++7gBZ/F+nBq9y6PomfwORfP7q9nBpK5AMP64kOt0bA+lShBFbBDcgpJ3X4etHg4lzA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-discard-overridden": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.0.tgz", + "integrity": "sha512-GmNAzx88u3k2+sBTZrJSDauR0ccpE24omTQCVmaTTZFz1du6AasspjaUPMJ2ud4RslZpoFKyf+6MSPETLojc6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "license": "MIT", + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-merge-longhand": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.3.tgz", + "integrity": "sha512-8waYomFxshdv6M9Em3QRM9MettRLDRcH2JQi2l0Z1KlYD/vhal3gbkeSES0NuACXOlZBB0V/B0AseHZaklzWOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^7.0.3" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-merge-rules": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.3.tgz", + "integrity": "sha512-2eSas2p3voPxNfdI5sQrvIkMaeUHpVc3EezgVs18hz/wRTQAC9U99tp9j3W5Jx9/L3qHkEDvizEx/LdnmumIvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^5.0.0", + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-font-values": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.0.tgz", + "integrity": "sha512-2ckkZtgT0zG8SMc5aoNwtm5234eUx1GGFJKf2b1bSp8UflqaeFzR50lid4PfqVI9NtGqJ2J4Y7fwvnP/u1cQog==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-gradients": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.0.tgz", + "integrity": "sha512-pdUIIdj/C93ryCHew0UgBnL2DtUS3hfFa5XtERrs4x+hmpMYGhbzo6l/Ir5de41O0GaKVpK1ZbDNXSY6GkXvtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-params": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.2.tgz", + "integrity": "sha512-nyqVLu4MFl9df32zTsdcLqCFfE/z2+f8GE1KHPxWOAmegSo6lpV2GNy5XQvrzwbLmiU7d+fYay4cwto1oNdAaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-minify-selectors": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.3.tgz", + "integrity": "sha512-SxTgUQSgBk6wEqzQZKEv1xQYIp9UBju6no9q+npohzSdhuSICQdkqmD1UMKkZWItS3olJSJMDDEY9WOJ5oGJew==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-normalize-charset": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.0.tgz", + "integrity": "sha512-ABisNUXMeZeDNzCQxPxBCkXexvBrUHV+p7/BXOY+ulxkcjUZO0cp8ekGBwvIh2LbCwnWbyMPNJVtBSdyhM2zYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-display-values": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.0.tgz", + "integrity": "sha512-lnFZzNPeDf5uGMPYgGOw7v0BfB45+irSRz9gHQStdkkhiM0gTfvWkWB5BMxpn0OqgOQuZG/mRlZyJxp0EImr2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-positions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.0.tgz", + "integrity": "sha512-I0yt8wX529UKIGs2y/9Ybs2CelSvItfmvg/DBIjTnoUSrPxSV7Z0yZ8ShSVtKNaV/wAY+m7bgtyVQLhB00A1NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-repeat-style": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.0.tgz", + "integrity": "sha512-o3uSGYH+2q30ieM3ppu9GTjSXIzOrRdCUn8UOMGNw7Af61bmurHTWI87hRybrP6xDHvOe5WlAj3XzN6vEO8jLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-string": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.0.tgz", + "integrity": "sha512-w/qzL212DFVOpMy3UGyxrND+Kb0fvCiBBujiaONIihq7VvtC7bswjWgKQU/w4VcRyDD8gpfqUiBQ4DUOwEJ6Qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-timing-functions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.0.tgz", + "integrity": "sha512-tNgw3YV0LYoRwg43N3lTe3AEWZ66W7Dh7lVEpJbHoKOuHc1sLrzMLMFjP8SNULHaykzsonUEDbKedv8C+7ej6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-unicode": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.2.tgz", + "integrity": "sha512-ztisabK5C/+ZWBdYC+Y9JCkp3M9qBv/XFvDtSw0d/XwfT3UaKeW/YTm/MD/QrPNxuecia46vkfEhewjwcYFjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-url": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.0.tgz", + "integrity": "sha512-+d7+PpE+jyPX1hDQZYG+NaFD+Nd2ris6r8fPTBAjE8z/U41n/bib3vze8x7rKs5H1uEw5ppe9IojewouHk0klQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-normalize-whitespace": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.0.tgz", + "integrity": "sha512-37/toN4wwZErqohedXYqWgvcHUGlT8O/m2jVkAfAe9Bd4MzRqlBmXrJRePH0e9Wgnz2X7KymTgTOaaFizQe3AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-ordered-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.1.tgz", + "integrity": "sha512-irWScWRL6nRzYmBOXReIKch75RRhNS86UPUAxXdmW/l0FcAsg0lvAXQCby/1lymxn/o0gVa6Rv/0f03eJOwHxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-utils": "^5.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-initial": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.2.tgz", + "integrity": "sha512-pOnu9zqQww7dEKf62Nuju6JgsW2V0KRNBHxeKohU+JkHd/GAH5uvoObqFLqkeB2n20mr6yrlWDvo5UBU5GnkfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-reduce-transforms": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.0.tgz", + "integrity": "sha512-pnt1HKKZ07/idH8cpATX/ujMbtOGhUfE+m8gbqwJE05aTaNw8gbo34a2e3if0xc0dlu75sUOiqvwCGY3fzOHew==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-svgo": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.0.1.tgz", + "integrity": "sha512-0WBUlSL4lhD9rA5k1e5D8EN5wCEyZD6HJk0jIvRxl+FDVOMlJ7DePHYWGGVc5QRqrJ3/06FTXM0bxjmJpmTPSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.3.2" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-unique-selectors": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.2.tgz", + "integrity": "sha512-CjSam+7Vf8cflJQsHrMS0P2hmy9u0+n/P001kb5eAszLmhjMqrt/i5AqQuNFihhViwDvEAezqTmXqaYXL2ugMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "license": "MIT" + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/preact": { + "version": "10.23.2", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.23.2.tgz", + "integrity": "sha512-kKYfePf9rzKnxOAKDpsWhg/ysrHPqT+yQ7UW4JjdnqjFIeNUnNcEJvhuA8fDenxAGWzUqtd51DfVg7xp/8T9NA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/pretty-bytes": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", + "integrity": "sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/property-information": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", + "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/protocols": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/protocols/-/protocols-2.0.1.tgz", + "integrity": "sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/queue-tick": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/queue-tick/-/queue-tick-1.0.1.tgz", + "integrity": "sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==", + "dev": true, + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readable-stream": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz", + "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/readdir-glob": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/readdir-glob/-/readdir-glob-1.1.3.tgz", + "integrity": "sha512-v05I2k7xN8zXvPD9N+z/uhXPaj0sUFCe2rcWZIpBsqxfP7xXFQ0tipAd/wjj1YxWyWtUS5IDJpOG82JKt2EAVA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "minimatch": "^5.1.0" + } + }, + "node_modules/readdir-glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/readdir-glob/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/recast": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.22.0.tgz", + "integrity": "sha512-5AAx+mujtXijsEavc5lWXBPQqrM4+Dl5qNH96N2aNeuJFUzpiiToKPsxQD/zAIJHspz7zz0maX0PCtCTFVlixQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert": "^2.0.0", + "ast-types": "0.15.2", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "dev": true, + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/rehype-external-links": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz", + "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-is-element": "^3.0.0", + "is-absolute-url": "^4.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-raw": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", + "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sort-attribute-values": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rehype-sort-attribute-values/-/rehype-sort-attribute-values-5.0.0.tgz", + "integrity": "sha512-dQdHdCIRnpiU+BkrLSqH+aM4lWJyLqGzv49KvH4gHj+JxYwNqvGhoTXckS3AJu4V9ZutwsTcawP0pC7PhwX0tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-sort-attributes": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/rehype-sort-attributes/-/rehype-sort-attributes-5.0.0.tgz", + "integrity": "sha512-6tJUH4xHFcdO85CZRwAcEtHNCzjZ9V9S0VZLgo1pzbN04qy8jiVCZ3oAxDmBVG3Rth5b1xFTDet5WG/UYZeJLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-emoji": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-5.0.1.tgz", + "integrity": "sha512-QCqTSvcZ65Ym+P+VyBKd4JfJfh7icMl7cIOGVmPMzWkDtdD8pQ0nQG7yxGolVIiMzSx90EZ7SwNiVpYpfTxn7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.4", + "emoticon": "^4.0.1", + "mdast-util-find-and-replace": "^3.0.1", + "node-emoji": "^2.1.3", + "unified": "^11.0.4" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.0.tgz", + "integrity": "sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/remark-mdc/-/remark-mdc-3.2.1.tgz", + "integrity": "sha512-MLNqQE7ryygOA3TtH4hKmIvmjFAqTMzCs2zrMzXs4MWJXYM2vbtdwR2NfgcN3vxIp5Pllgq3oLGuKgQSs8J19w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.3", + "@types/unist": "^3.0.2", + "flat": "^6.0.1", + "js-yaml": "^4.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.1.0", + "micromark": "^4.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.1.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.1", + "scule": "^1.3.0", + "stringify-entities": "^4.0.3", + "unified": "^11.0.4", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/remark-mdc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/remark-mdc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.0.tgz", + "integrity": "sha512-z3tJrAs2kIs1AqIIy6pzHmAHlF1hWQ+OdY4/hv+Wxe35EhyLKcajL33iUEn3ScxtFox9nUvRufR/Zre8Q08H/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lowercase-keys": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.22.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.22.4.tgz", + "integrity": "sha512-vD8HJ5raRcWOyymsR6Z3o6+RzfEPCnVLMFJ6vRslO1jt4LO6dUo5Qnpg7y4RkZFM2DMe3WUirkI5c16onjrc6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.22.4", + "@rollup/rollup-android-arm64": "4.22.4", + "@rollup/rollup-darwin-arm64": "4.22.4", + "@rollup/rollup-darwin-x64": "4.22.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.22.4", + "@rollup/rollup-linux-arm-musleabihf": "4.22.4", + "@rollup/rollup-linux-arm64-gnu": "4.22.4", + "@rollup/rollup-linux-arm64-musl": "4.22.4", + "@rollup/rollup-linux-powerpc64le-gnu": "4.22.4", + "@rollup/rollup-linux-riscv64-gnu": "4.22.4", + "@rollup/rollup-linux-s390x-gnu": "4.22.4", + "@rollup/rollup-linux-x64-gnu": "4.22.4", + "@rollup/rollup-linux-x64-musl": "4.22.4", + "@rollup/rollup-win32-arm64-msvc": "4.22.4", + "@rollup/rollup-win32-ia32-msvc": "4.22.4", + "@rollup/rollup-win32-x64-msvc": "4.22.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup-plugin-visualizer": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.12.0.tgz", + "integrity": "sha512-8/NU9jXcHRs7Nnj07PF2o4gjxmm9lXIrZ8r175bT9dK8qoLlvKTwRMArRCMgpMGlq8CTLugRvEmyMeMXIU2pNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "open": "^8.4.0", + "picomatch": "^2.3.1", + "source-map": "^0.7.4", + "yargs": "^17.5.1" + }, + "bin": { + "rollup-plugin-visualizer": "dist/bin/cli.js" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "rollup": "2.x || 3.x || 4.x" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/rollup-plugin-visualizer/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 8" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/search-insights": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.16.3.tgz", + "integrity": "sha512-hSHy/s4Zk2xibhj9XTCACB+1PqS+CaJxepGNBhKc/OsHRpqvHAUAm5+uZ6kJJbGXn0pb3XqekHjg6JAqPExzqg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-placeholder": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/serve-placeholder/-/serve-placeholder-2.0.2.tgz", + "integrity": "sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4" + } + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true, + "license": "ISC" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/shiki": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.14.1.tgz", + "integrity": "sha512-FujAN40NEejeXdzPt+3sZ3F2dx1U24BY2XTY01+MG8mbxCiA2XukXdcbyMyLAHJ/1AUUnQd1tZlvIjefWWEJeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/core": "1.14.1", + "@types/hast": "^3.0.4" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/simple-git": { + "version": "3.25.0", + "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.25.0.tgz", + "integrity": "sha512-KIY5sBnzc4yEcJXW7Tdv4viEz8KyG+nU0hay+DWZasvdFOYKeUZ6Xc25LUHHjw0tinPT7O1eY6pzX7pRT1K8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@kwsites/file-exists": "^1.1.1", + "@kwsites/promise-deferred": "^1.1.1", + "debug": "^4.3.5" + }, + "funding": { + "type": "github", + "url": "https://github.com/steveukx/git-js?sponsor=1" + } + }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/smob": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", + "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "dev": true, + "license": "MIT" + }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/speakingurl": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/speakingurl/-/speakingurl-14.0.1.tgz", + "integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/storyblok-algolia-indexer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/storyblok-algolia-indexer/-/storyblok-algolia-indexer-1.1.0.tgz", + "integrity": "sha512-Kuy4FAzhdNC//LtZ+FJ3kZ92W9FVe7auUicAj4e2of5j4VE2BXYtg/TdAWtekjvA4vvzHYoYCt1sPPDe45n29A==", + "dev": true, + "license": "MIT", + "dependencies": { + "algoliasearch": "^4.12.0", + "axios": "^1.3.4", + "storyblok-js-client": "^5.8.0" + } + }, + "node_modules/storyblok-js-client": { + "version": "5.14.4", + "resolved": "https://registry.npmjs.org/storyblok-js-client/-/storyblok-js-client-5.14.4.tgz", + "integrity": "sha512-9yY33tvfO3Cbe25h/l6K0T3IxJeOOyGl/pCwsd55woT0ZEBhIiEZoMumpRsyLjD5AedW9KXLbLjMXhHazkepCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamx": { + "version": "2.18.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.18.0.tgz", + "integrity": "sha512-LLUC1TWdjVdn1weXGcSxyTR3T4+acB6tVGXT95y0nGbca4t4o/ng1wKAGTljm9VicuCVLvRlqFYXYy5GwgM7sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-fifo": "^1.3.2", + "queue-tick": "^1.0.1", + "text-decoder": "^1.1.0" + }, + "optionalDependencies": { + "bare-events": "^2.2.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "dev": true, + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", + "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", + "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/style-dictionary-esm": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/style-dictionary-esm/-/style-dictionary-esm-1.9.2.tgz", + "integrity": "sha512-MR+ppTqzkJJtXH6UyDJ0h4h4ekBCePA8A8xlYNuL0tLj2K+ngyuxoe0AvCHQ7sJVX8O5WK2z32ANSgIcF4mGxw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "chalk": "^5.3.0", + "change-case": "^4.1.2", + "commander": "^11.1.0", + "consola": "^3.2.3", + "fast-glob": "^3.3.2", + "glob": "^10.3.10", + "jiti": "^1.21.0", + "json5": "^2.2.3", + "jsonc-parser": "^3.2.0", + "lodash.template": "^4.5.0", + "tinycolor2": "^1.6.0" + }, + "bin": { + "style-dictionary": "bin/style-dictionary.js" + } + }, + "node_modules/style-dictionary-esm/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/style-dictionary-esm/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/style-dictionary-esm/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/style-dictionary-esm/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/style-dictionary-esm/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/stylehacks": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.3.tgz", + "integrity": "sha512-4DqtecvI/Nd+2BCvW9YEF6lhBN5UM50IJ1R3rnEAhBwbCKf4VehRf+uqvnVArnBayjYD/WtT3g0G/HSRxWfTRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.23.3", + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/sucrase/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sucrase/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/superjson": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/superjson/-/superjson-2.2.1.tgz", + "integrity": "sha512-8iGv75BYOa0xRJHK5vRLEjE2H/i4lulTjzpUXic3Eg8akftYjkmQDa8JARQ42rlczXyFR3IeRoeFCc7RxHsYZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "copy-anything": "^3.0.2" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg-tags": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", + "dev": true + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/system-architecture": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/system-architecture/-/system-architecture-0.1.0.tgz", + "integrity": "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "dev": true, + "license": "MIT" + }, + "node_modules/tailwindcss": { + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", + "license": "MIT", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss/node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-stream": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz", + "integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/terser": { + "version": "5.31.6", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.31.6.tgz", + "integrity": "sha512-PQ4DAriWzKj+qgehQ7LK5bQqCFNMmlhjR2PFFLuqGCpuCAauxemVBWwWOxo3UIwWQx8+Pr61Df++r76wDmkQBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.1.tgz", + "integrity": "sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinycolor2": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.6.0.tgz", + "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "license": "Apache-2.0" + }, + "node_modules/ts-jest": { + "version": "29.2.4", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.2.4.tgz", + "integrity": "sha512-3d6tgDyhCI29HlpwIq87sNuI+3Q6GLTTCeYRHCs7vDz+/3GCMwEtV9jezLyl4ZtnBgx00I7hm8PCP8cTksMGrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "0.x", + "ejs": "^3.1.10", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.3", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "^7.5.3", + "yargs-parser": "^21.0.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tslib": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", + "dev": true, + "license": "0BSD" + }, + "node_modules/type-detect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", + "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.4.tgz", + "integrity": "sha512-UsUk3byDzKd04EyoZ7U4DOlxQaD14JUKQl6/P7wiX4FNvUfm3XL246n9W5AmqwW5RSFJ27NAuM0iLscAOYUiGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.5.3.tgz", + "integrity": "sha512-GykOvZwgDWZlTQMtp5jrD4BVL+gNn2NVlVafjcFUJ7taY20tqYdwdoWBFy6GBJsNTZe1GkGPkSl5knQAjtgceg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unbuild": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/unbuild/-/unbuild-1.2.1.tgz", + "integrity": "sha512-J4efk69Aye43tWcBPCsLK7TIRppGrEN4pAlDzRKo3HSE6MgTSTBxSEuE3ccx7ixc62JvGQ/CoFXYqqF2AHozow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-alias": "^5.0.0", + "@rollup/plugin-commonjs": "^24.1.0", + "@rollup/plugin-json": "^6.0.0", + "@rollup/plugin-node-resolve": "^15.0.2", + "@rollup/plugin-replace": "^5.0.2", + "@rollup/pluginutils": "^5.0.2", + "chalk": "^5.2.0", + "consola": "^3.0.2", + "defu": "^6.1.2", + "esbuild": "^0.17.16", + "globby": "^13.1.4", + "hookable": "^5.5.3", + "jiti": "^1.18.2", + "magic-string": "^0.30.0", + "mkdist": "^1.2.0", + "mlly": "^1.2.0", + "mri": "^1.2.0", + "pathe": "^1.1.0", + "pkg-types": "^1.0.2", + "pretty-bytes": "^6.1.0", + "rollup": "^3.20.2", + "rollup-plugin-dts": "^5.3.0", + "scule": "^1.0.0", + "typescript": "^5.0.4", + "untyped": "^1.3.2" + }, + "bin": { + "unbuild": "dist/cli.mjs" + } + }, + "node_modules/unbuild/node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/@rollup/plugin-commonjs": { + "version": "24.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-commonjs/-/plugin-commonjs-24.1.0.tgz", + "integrity": "sha512-eSL45hjhCWI0jCCXcNtLVqM5N1JlBGvlFfY0m6oOYnLCJ6N0qEXoZql4sY2MOUArzhH4SA/qBpTxvvZp2Sc+DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "commondir": "^1.0.1", + "estree-walker": "^2.0.2", + "glob": "^8.0.3", + "is-reference": "1.2.1", + "magic-string": "^0.27.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.68.0||^3.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/unbuild/node_modules/@rollup/plugin-commonjs/node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/unbuild/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/unbuild/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/unbuild/node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/unbuild/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/unbuild/node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unbuild/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/unbuild/node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/unbuild/node_modules/rollup-plugin-dts": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-5.3.1.tgz", + "integrity": "sha512-gusMi+Z4gY/JaEQeXnB0RUdU82h1kF0WYzCWgVmV4p3hWXqelaKuCvcJawfeg+EKn2T1Ie+YWF2OiN1/L8bTVg==", + "dev": true, + "license": "LGPL-3.0", + "dependencies": { + "magic-string": "^0.30.2" + }, + "engines": { + "node": ">=v14.21.3" + }, + "funding": { + "url": "https://github.com/sponsors/Swatinem" + }, + "optionalDependencies": { + "@babel/code-frame": "^7.22.5" + }, + "peerDependencies": { + "rollup": "^3.0", + "typescript": "^4.1 || ^5.0" + } + }, + "node_modules/unbuild/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/unctx": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/unctx/-/unctx-2.3.1.tgz", + "integrity": "sha512-PhKke8ZYauiqh3FEMVNm7ljvzQiph0Mt3GBRve03IJm7ukfaON2OBK795tLwhbyfzknuRRkW0+Ze+CQUmzOZ+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.8.2", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.0", + "unplugin": "^1.3.1" + } + }, + "node_modules/unctx/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "6.19.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.6.tgz", + "integrity": "sha512-e/vggGopEfTKSvj4ihnOLTsqhrKRN3LeO6qSN/GxohhuRv8qH9bNQ4B8W7e/vFL+0XTnmHPB4/kegunZGA4Org==", + "dev": true, + "license": "MIT" + }, + "node_modules/unenv": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-1.10.0.tgz", + "integrity": "sha512-wY5bskBQFL9n3Eca5XnhH6KbUo/tfvkwm9OpcdCvLaeA7piBNbavbOKJySEwQ1V0RH6HvNlSAFRTpvTqgKRQXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "consola": "^3.2.3", + "defu": "^6.1.4", + "mime": "^3.0.0", + "node-fetch-native": "^1.6.4", + "pathe": "^1.1.2" + } + }, + "node_modules/unenv/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/unhead": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/unhead/-/unhead-1.9.16.tgz", + "integrity": "sha512-FOoXkuRNDwt7PUaNE0LXNCb6RCz4vTpkGymz4tJ8rcaG5uUJ0lxGK536hzCFwFw3Xkp3n+tkt2yCcbAZE/FOvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@unhead/dom": "1.9.16", + "@unhead/schema": "1.9.16", + "@unhead/shared": "1.9.16", + "hookable": "^5.5.3" + }, + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unimport": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/unimport/-/unimport-3.10.0.tgz", + "integrity": "sha512-/UvKRfWx3mNDWwWQhR62HsoM3wxHwYdTq8ellZzMOHnnw4Dp8tovgthyW7DjTrbjDL+i4idOp06voz2VKlvrLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.0", + "acorn": "^8.12.1", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.2", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "pkg-types": "^1.1.3", + "scule": "^1.3.0", + "strip-literal": "^2.1.0", + "unplugin": "^1.12.0" + } + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unist-builder": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-builder/-/unist-builder-4.0.0.tgz", + "integrity": "sha512-wmRFnH+BLpZnTKpc5L7O67Kac89s9HMrtELpnNaE6TAobq5DTZZs5YaTQfAZBA9bFPECx2uVAPO31c+GVug8mg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", + "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", + "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", + "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unplugin": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-1.12.2.tgz", + "integrity": "sha512-bEqQxeC7rxtxPZ3M5V4Djcc4lQqKPgGe3mAWZvxcSmX5jhGxll19NliaRzQSQPrk4xJZSGniK3puLWpRuZN7VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.12.1", + "chokidar": "^3.6.0", + "webpack-sources": "^3.2.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-vue-router": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/unplugin-vue-router/-/unplugin-vue-router-0.10.7.tgz", + "integrity": "sha512-5KEh7Swc1L2Xh5WOD7yQLeB5bO3iTw+Hst7qMxwmwYcPm9qVrtrRTZUftn2Hj4is17oMKgqacyWadjQzwW5B/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.25.2", + "@rollup/pluginutils": "^5.1.0", + "@vue-macros/common": "^1.12.2", + "ast-walker-scope": "^0.6.2", + "chokidar": "^3.6.0", + "fast-glob": "^3.3.2", + "json5": "^2.2.3", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.11", + "mlly": "^1.7.1", + "pathe": "^1.1.2", + "scule": "^1.3.0", + "unplugin": "^1.12.1", + "yaml": "^2.5.0" + }, + "peerDependencies": { + "vue-router": "^4.4.0" + }, + "peerDependenciesMeta": { + "vue-router": { + "optional": true + } + } + }, + "node_modules/unstorage": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.10.2.tgz", + "integrity": "sha512-cULBcwDqrS8UhlIysUJs2Dk0Mmt8h7B0E6mtR+relW9nZvsf/u4SkAYyNliPiPW7XtFNb5u3IUMkxGxFTTRTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^3.6.0", + "destr": "^2.0.3", + "h3": "^1.11.1", + "listhen": "^1.7.2", + "lru-cache": "^10.2.0", + "mri": "^1.2.0", + "node-fetch-native": "^1.6.2", + "ofetch": "^1.3.3", + "ufo": "^1.4.0" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.5.0", + "@azure/cosmos": "^4.0.0", + "@azure/data-tables": "^13.2.2", + "@azure/identity": "^4.0.1", + "@azure/keyvault-secrets": "^4.8.0", + "@azure/storage-blob": "^12.17.0", + "@capacitor/preferences": "^5.0.7", + "@netlify/blobs": "^6.5.0 || ^7.0.0", + "@planetscale/database": "^1.16.0", + "@upstash/redis": "^1.28.4", + "@vercel/kv": "^1.0.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.3.2" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/untun": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/untun/-/untun-0.1.3.tgz", + "integrity": "sha512-4luGP9LMYszMRZwsvyUd9MrxgEGZdZuZgpVQHEEX0lCYFESasVRvZd0EYpCkOIbJKHMuv0LskpXc/8Un+MJzEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "citty": "^0.1.5", + "consola": "^3.2.3", + "pathe": "^1.1.1" + }, + "bin": { + "untun": "bin/untun.mjs" + } + }, + "node_modules/untyped": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/untyped/-/untyped-1.4.2.tgz", + "integrity": "sha512-nC5q0DnPEPVURPhfPQLahhSTnemVtPzdx7ofiRxXpOB2SYnb3MfdU3DVGyJdS8Lx+tBWeAePO8BfU/3EgksM7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.7", + "@babel/standalone": "^7.23.8", + "@babel/types": "^7.23.6", + "defu": "^6.1.4", + "jiti": "^1.21.0", + "mri": "^1.2.0", + "scule": "^1.2.0" + }, + "bin": { + "untyped": "dist/cli.mjs" + } + }, + "node_modules/unwasm": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/unwasm/-/unwasm-0.3.9.tgz", + "integrity": "sha512-LDxTx/2DkFURUd+BU1vUsF/moj0JsoTvl+2tcg2AUOiEzVturhGGx17/IMgGvKUYdZwr33EJHtChCJuhu9Ouvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "knitwork": "^1.0.0", + "magic-string": "^0.30.8", + "mlly": "^1.6.1", + "pathe": "^1.1.2", + "pkg-types": "^1.0.3", + "unplugin": "^1.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/uqr": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/uqr/-/uqr-0.1.2.tgz", + "integrity": "sha512-MJu7ypHq6QasgF5YRTjqscSzQp/W11zoUk6kvmlH+fmWEs63Y0Eib13hYFwAzagRJcVY8WVnlV+eBDUGMJ5IbA==", + "dev": true, + "license": "MIT" + }, + "node_modules/urlpattern-polyfill": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-8.0.2.tgz", + "integrity": "sha512-Qp95D4TPJl1kC9SKigDcqgyM2VDVO4RiJc2d4qe5GrYm+zbIQCWWKAFaJNQ4BhdFeDGwBmAxqJBwWSJDb9T3BQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vfile": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.2.tgz", + "integrity": "sha512-zND7NlS8rJYb/sPqkb13ZvbbUoExdbi4w3SfRrMq6R3FvnLQmmfpajJNITuuYm6AZ5uao9vy4BAos3EXBPf2rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", + "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "5.4.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.6.tgz", + "integrity": "sha512-IeL5f8OO5nylsgzd9tq4qD2QqI0k2CQLGrWD0rCN0EQJZpBK5vJAx0I+GDkMOXxQX/OfFHMuLIx6ddAxGX/k+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-hot-client": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/vite-hot-client/-/vite-hot-client-0.2.3.tgz", + "integrity": "sha512-rOGAV7rUlUHX89fP2p2v0A2WWvV3QMX2UYq0fRqsWSvFvev4atHWqjwGoKaZT1VTKyLGk533ecu3eyd0o59CAg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0" + } + }, + "node_modules/vite-node": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite-plugin-checker": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.7.2.tgz", + "integrity": "sha512-xeYeJbG0gaCaT0QcUC4B2Zo4y5NR8ZhYenc5gPbttrZvraRFwkEADCYwq+BfEHl9zYz7yf85TxsiGoYwyyIjhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "ansi-escapes": "^4.3.0", + "chalk": "^4.1.1", + "chokidar": "^3.5.1", + "commander": "^8.0.0", + "fast-glob": "^3.2.7", + "fs-extra": "^11.1.0", + "npm-run-path": "^4.0.1", + "strip-ansi": "^6.0.0", + "tiny-invariant": "^1.1.0", + "vscode-languageclient": "^7.0.0", + "vscode-languageserver": "^7.0.0", + "vscode-languageserver-textdocument": "^1.0.1", + "vscode-uri": "^3.0.2" + }, + "engines": { + "node": ">=14.16" + }, + "peerDependencies": { + "@biomejs/biome": ">=1.7", + "eslint": ">=7", + "meow": "^9.0.0", + "optionator": "^0.9.1", + "stylelint": ">=13", + "typescript": "*", + "vite": ">=2.0.0", + "vls": "*", + "vti": "*", + "vue-tsc": ">=2.0.0" + }, + "peerDependenciesMeta": { + "@biomejs/biome": { + "optional": true + }, + "eslint": { + "optional": true + }, + "meow": { + "optional": true + }, + "optionator": { + "optional": true + }, + "stylelint": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vls": { + "optional": true + }, + "vti": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/vite-plugin-checker/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/vite-plugin-checker/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/vite-plugin-checker/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/vite-plugin-checker/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite-plugin-checker/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/vite-plugin-checker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/vite-plugin-checker/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/vite-plugin-inspect": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/vite-plugin-inspect/-/vite-plugin-inspect-0.8.5.tgz", + "integrity": "sha512-JvTUqsP1JNDw0lMZ5Z/r5cSj81VK2B7884LO1DC3GMBhdcjcsAnJjdWq7bzQL01Xbh+v60d3lju3g+z7eAtNew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.10", + "@rollup/pluginutils": "^5.1.0", + "debug": "^4.3.5", + "error-stack-parser-es": "^0.1.4", + "fs-extra": "^11.2.0", + "open": "^10.1.0", + "perfect-debounce": "^1.0.0", + "picocolors": "^1.0.1", + "sirv": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "vite": "^3.1.0 || ^4.0.0 || ^5.0.0-0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/vite-plugin-inspect/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-inspect/node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/vite-plugin-vue-inspector": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vite-plugin-vue-inspector/-/vite-plugin-vue-inspector-5.1.3.tgz", + "integrity": "sha512-pMrseXIDP1Gb38mOevY+BvtNGNqiqmqa2pKB99lnLsADQww9w9xMbAfT4GB6RUoaOkSPrtlXqpq2Fq+Dj2AgFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.23.0", + "@babel/plugin-proposal-decorators": "^7.23.0", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-transform-typescript": "^7.22.15", + "@vue/babel-plugin-jsx": "^1.1.5", + "@vue/compiler-dom": "^3.3.4", + "kolorist": "^1.8.0", + "magic-string": "^0.30.4" + }, + "peerDependencies": { + "vite": "^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0" + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/vscode-jsonrpc": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz", + "integrity": "sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0 || >=10.0.0" + } + }, + "node_modules/vscode-languageclient": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz", + "integrity": "sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimatch": "^3.0.4", + "semver": "^7.3.4", + "vscode-languageserver-protocol": "3.16.0" + }, + "engines": { + "vscode": "^1.52.0" + } + }, + "node_modules/vscode-languageclient/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/vscode-languageserver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz", + "integrity": "sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.16.0" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz", + "integrity": "sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A==", + "dev": true, + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "6.0.0", + "vscode-languageserver-types": "3.16.0" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz", + "integrity": "sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA==", + "dev": true, + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz", + "integrity": "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.38.tgz", + "integrity": "sha512-f0ZgN+mZ5KFgVv9wz0f4OgVKukoXtS3nwET4c2vLBGQR50aI8G0cqbFtLlX9Yiyg3LFGBitruPHt2PxwTduJEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.4.38", + "@vue/compiler-sfc": "3.4.38", + "@vue/runtime-dom": "3.4.38", + "@vue/server-renderer": "3.4.38", + "@vue/shared": "3.4.38" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-bundle-renderer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vue-bundle-renderer/-/vue-bundle-renderer-2.1.0.tgz", + "integrity": "sha512-uZ+5ZJdZ/b43gMblWtcpikY6spJd0nERaM/1RtgioXNfWFbjKlUwrS8HlrddN6T2xtptmOouWclxLUkpgcVX3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ufo": "^1.5.3" + } + }, + "node_modules/vue-component-meta": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-component-meta/-/vue-component-meta-1.8.27.tgz", + "integrity": "sha512-j3WJsyQHP4TDlvnjHc/eseo0/eVkf0FaCpkqGwez5zD+Tj31onBzWZEXTnWKs8xRj0n3dMNYdy3SpiS6NubSvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "path-browserify": "^1.0.1", + "vue-component-type-helpers": "1.8.27" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-component-type-helpers": { + "version": "1.8.27", + "resolved": "https://registry.npmjs.org/vue-component-type-helpers/-/vue-component-type-helpers-1.8.27.tgz", + "integrity": "sha512-0vOfAtI67UjeO1G6UiX5Kd76CqaQ67wrRZiOe7UAb9Jm6GzlUr/fC7CV90XfwapJRjpCMaZFhv1V0ajWRmE9Dg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-devtools-stub": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz", + "integrity": "sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-gtag-next": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/vue-gtag-next/-/vue-gtag-next-1.14.0.tgz", + "integrity": "sha512-iJl+cOG2GU5NuxqzSSIpt03WVOvZqyKB9TOy7d55KiuvRklcnb2nlqxW5B/a3/sbIt7fla+XEkRyMCcoz0zAHw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0-rc.11" + } + }, + "node_modules/vue-instantsearch": { + "version": "4.19.2", + "resolved": "https://registry.npmjs.org/vue-instantsearch/-/vue-instantsearch-4.19.2.tgz", + "integrity": "sha512-Bl6TW+Y1twTSbRfVb50l59dCLt46U+9aiV9/o/2HFgDDm3Gqec5Jq9Fe4Q2jjN+SqE+RSrL67XSwQjeHPFnJgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "instantsearch-ui-components": "0.8.0", + "instantsearch.js": "4.73.4", + "mitt": "^2.1.0" + }, + "peerDependencies": { + "@vue/server-renderer": "^3.1.2", + "algoliasearch": ">= 3.32.0 < 6", + "vue": "^2.6.0 || >=3.0.0-rc.0", + "vue-server-renderer": "^2.6.11" + }, + "peerDependenciesMeta": { + "@vue/server-renderer": { + "optional": true + }, + "vue-server-renderer": { + "optional": true + } + } + }, + "node_modules/vue-instantsearch/node_modules/mitt": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", + "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vue-router": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.3.tgz", + "integrity": "sha512-sv6wmNKx2j3aqJQDMxLFzs/u/mjA9Z5LCgy6BE0f7yFWMjrPLnS/sPNn8ARY/FXw6byV18EFutn5lTO6+UsV5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", + "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zhead": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/zhead/-/zhead-2.2.4.tgz", + "integrity": "sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/harlan-zw" + } + }, + "node_modules/zip-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-6.0.1.tgz", + "integrity": "sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "archiver-utils": "^5.0.0", + "compress-commons": "^6.0.2", + "readable-stream": "^4.0.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/doc/package.json b/doc/package.json index d7af42e25b5..2856f350eae 100755 --- a/doc/package.json +++ b/doc/package.json @@ -9,21 +9,20 @@ "preview": "nuxi preview" }, "devDependencies": { - "@nuxt-themes/docus": "^1.0.2", + "@nuxt-themes/docus": "^1.15.0", "@nuxtjs/algolia": "^1.5.0", "@types/chai": "^4.3.4", "@types/jest": "^29.2.4", "babel-jest": "^29.3.1", - "chai": "^4.3.7", + "chai": "^4.5.0", "jest": "^29.3.1", - "nuxt": "^3.0.0", + "nuxt": "^3.12.4", "ts-jest": "^29.0.3", "vue-gtag-next": "^1.14.0" }, "dependencies": { "@docsearch/js": "3", "autoprefixer": "^10.4.13", - "mermaid": "^9.2.2", "postcss": "^8.4.20", "tailwindcss": "^3.2.4" } diff --git a/doc/public/amasonsqs-select-dlq.gif b/doc/public/amasonsqs-select-dlq.gif new file mode 100644 index 00000000000..ea8f223a5af Binary files /dev/null and b/doc/public/amasonsqs-select-dlq.gif differ diff --git a/doc/public/amazonsqs-errorqueue.png b/doc/public/amazonsqs-errorqueue.png new file mode 100644 index 00000000000..a1542f3a9af Binary files /dev/null and b/doc/public/amazonsqs-errorqueue.png differ diff --git a/doc/public/amazonsqs-message-details.gif b/doc/public/amazonsqs-message-details.gif new file mode 100644 index 00000000000..e2dafd5c0ad Binary files /dev/null and b/doc/public/amazonsqs-message-details.gif differ diff --git a/doc/public/amazonsqs-select-redrive.gif b/doc/public/amazonsqs-select-redrive.gif new file mode 100644 index 00000000000..99c72ec1624 Binary files /dev/null and b/doc/public/amazonsqs-select-redrive.gif differ diff --git a/doc/public/amazonsqs-topology-fault.svg b/doc/public/amazonsqs-topology-fault.svg new file mode 100644 index 00000000000..462305842c2 --- /dev/null +++ b/doc/public/amazonsqs-topology-fault.svg @@ -0,0 +1,3 @@ + + +
Copy/delete message  to retry
✉️ Fault Message
✉️ ProcessFile
(+error details)
✉️ ProcessFile

MassTransit-Fault--Acme-ProcessFile--

Topic

ProcessFileConsumer

Consumer

ProcessFile

Queue

ProcessFile_error

Queue
\ No newline at end of file diff --git a/doc/public/amazonsqs-topology-publish.svg b/doc/public/amazonsqs-topology-publish.svg new file mode 100644 index 00000000000..385cb7fab56 --- /dev/null +++ b/doc/public/amazonsqs-topology-publish.svg @@ -0,0 +1,3 @@ + + +

Acme.FileReceivedEvent

Topic

CustomerAudit

Queue

FileReceived

Queue

FileReceivedConsumer

Consumer

CustomerAuditConsumer

Consumer

Publish

\ No newline at end of file diff --git a/doc/public/amazonsqs-topology-send.svg b/doc/public/amazonsqs-topology-send.svg new file mode 100644 index 00000000000..10aab203192 --- /dev/null +++ b/doc/public/amazonsqs-topology-send.svg @@ -0,0 +1,3 @@ + + +

ProcessFile

Queue

Send

ProcessFileConsumer

\ No newline at end of file diff --git a/doc/public/azure-topology-fault.svg b/doc/public/azure-topology-fault.svg new file mode 100644 index 00000000000..d81578506c0 --- /dev/null +++ b/doc/public/azure-topology-fault.svg @@ -0,0 +1,3 @@ + + +
Redeliver via Azure Service Bus portal to retry
✉️ Fault Message
✉️ ProcessFile
(+error details)
✉️ ProcessFile

MassTransit-Fault--Acme-ProcessFile--

Topic

ProcessFile

Dead-letter queue

ProcessFileConsumer

Consumer

ProcessFile

Queue
\ No newline at end of file diff --git a/doc/public/azure-topology-publish.svg b/doc/public/azure-topology-publish.svg new file mode 100644 index 00000000000..c50210b2931 --- /dev/null +++ b/doc/public/azure-topology-publish.svg @@ -0,0 +1,3 @@ + + +

Acme.FileReceivedEvent

Topic

Acme.FileReceived

Topic

Acme.CustomerDataReceived

Topic

CustomerAudit

Queue

FileReceiver

Queue

FileReceivedConsumer

Consumer

CustomerAuditConsumer

Consumer

Publish

Acme.FileReceived

Subscription

FileReceived

Subscription

CustomerAudit

Subscription

Acme.CustomerDataReceived

Subscription
\ No newline at end of file diff --git a/doc/public/azure-topology-send.svg b/doc/public/azure-topology-send.svg new file mode 100644 index 00000000000..d99ed7d9bc1 --- /dev/null +++ b/doc/public/azure-topology-send.svg @@ -0,0 +1,3 @@ + + +

ProcessFile

Queue

Send

ProcessFileConsumer

\ No newline at end of file diff --git a/doc/public/rabbitmq-managementui-errorqueue.png b/doc/public/rabbitmq-managementui-errorqueue.png new file mode 100644 index 00000000000..73252a0534b Binary files /dev/null and b/doc/public/rabbitmq-managementui-errorqueue.png differ diff --git a/doc/public/rabbitmq-managementui-getmessage.png b/doc/public/rabbitmq-managementui-getmessage.png new file mode 100644 index 00000000000..7da179c8a98 Binary files /dev/null and b/doc/public/rabbitmq-managementui-getmessage.png differ diff --git a/doc/public/rabbitmq-managementui-movemessage.png b/doc/public/rabbitmq-managementui-movemessage.png new file mode 100644 index 00000000000..625979c71a5 Binary files /dev/null and b/doc/public/rabbitmq-managementui-movemessage.png differ diff --git a/doc/public/rabbitmq-publish-topology.png b/doc/public/rabbitmq-publish-topology.png deleted file mode 100644 index db96b669bb7..00000000000 Binary files a/doc/public/rabbitmq-publish-topology.png and /dev/null differ diff --git a/doc/public/rabbitmq-send-topology.png b/doc/public/rabbitmq-send-topology.png deleted file mode 100644 index b5d3e89d501..00000000000 Binary files a/doc/public/rabbitmq-send-topology.png and /dev/null differ diff --git a/doc/public/rabbitmq-topology-fault.svg b/doc/public/rabbitmq-topology-fault.svg new file mode 100644 index 00000000000..93afc960c2e --- /dev/null +++ b/doc/public/rabbitmq-topology-fault.svg @@ -0,0 +1,3 @@ + + +
shovel manually to retry
✉️ Fault Message
✉️ ProcessFile
(+error details)
✉️ ProcessFile

ProcessFile

Queue

ProcessFile

Exchange

ProcessFile_error

Exchange

MassTransit:Fault--ProcessFile

Exchange

ProcessFile_error

Queue

ProcessFileConsumer

Consumer

MassTransit:Fault

Exchange
\ No newline at end of file diff --git a/doc/public/rabbitmq-topology-publish.svg b/doc/public/rabbitmq-topology-publish.svg new file mode 100644 index 00000000000..564ff5adf63 --- /dev/null +++ b/doc/public/rabbitmq-topology-publish.svg @@ -0,0 +1,3 @@ + + +

Acme.FileReceivedEvent

Exchange

Acme.FileReceived

Exchange

Acme.CustomerDataReceived

Exchange

CustomerAudit

Queue

FileReceived

Queue

FileReceived

Exchange

CustomerAudit

Exchange

FileReceivedConsumer

Consumer

CustomerAuditConsumer

Consumer

Publish

\ No newline at end of file diff --git a/doc/public/rabbitmq-topology-send.svg b/doc/public/rabbitmq-topology-send.svg new file mode 100644 index 00000000000..9104ab07d81 --- /dev/null +++ b/doc/public/rabbitmq-topology-send.svg @@ -0,0 +1,3 @@ + + +

ProcessFile

Exchange

ProcessFile

Queue

Send

ProcessFileConsumer

\ No newline at end of file diff --git a/doc/public/servicebus-deadletter-view.png b/doc/public/servicebus-deadletter-view.png new file mode 100644 index 00000000000..1c3824ae2c0 Binary files /dev/null and b/doc/public/servicebus-deadletter-view.png differ diff --git a/doc/server/middleware/redirects.ts b/doc/server/middleware/redirects.ts index 7f5c19c25ba..c9fe92abe04 100644 --- a/doc/server/middleware/redirects.ts +++ b/doc/server/middleware/redirects.ts @@ -198,8 +198,8 @@ const mapping: { [key: string]: string } = { '/architecture/packages.html': '/support/packages', '/architecture/green-cache.html': '/documentation/concepts/messages', '/architecture/nservicebus.html': '/documentation/configuration/integrations/nsb', - '/architecture/interoperability.html': '/documentation/configuration/integrations/serialization', - '/advanced/interoperability.html': '/documentation/configuration/integrations/serialization', + '/architecture/interoperability.html': '/documentation/configuration/serialization', + '/advanced/interoperability.html': '/documentation/configuration/serialization', '/architecture/history.html': '/documentation/concepts/messages', '/architecture/encrypted-messages.html': '/documentation/concepts/messages', '/architecture/newid.html': '/documentation/patterns/newid', @@ -208,7 +208,12 @@ const mapping: { [key: string]: string } = { '/getting-started/upgrade-v6.html': '/support/upgrade', '/getting-started/index.html': '/documentation/concepts/messages', '/getting-started/live-coding.html': '/documentation/concepts/messages', - '/discord.html': '/support/support-channels' + '/discord.html': '/support/support-channels', + '/obsolete': '/documentation/configuration/obsolete', + + '/documentation/configuration/integrations/serialization': '/documentation/configuration/serialization', + '/documentation/patterns/routing-slip': '/documentation/concepts/routing-slips', + '/documentation/patterns/routing-slip/configuration': '/documentation/concepts/routing-slips' } // flip the map, so we ignore good routes diff --git a/doc/transport-topologies.drawio b/doc/transport-topologies.drawio new file mode 100644 index 00000000000..a0cf775768f --- /dev/null +++ b/doc/transport-topologies.drawio @@ -0,0 +1,899 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/doc/vercel.json b/doc/vercel.json index bff83cb7cb3..be9e442b740 100644 --- a/doc/vercel.json +++ b/doc/vercel.json @@ -1,3 +1,22 @@ { - "trailingSlash": false + "trailingSlash": false, + "headers": [ + { + "source": "/(.*)", + "headers": [ + { + "key": "Cache-Control", + "value": "public, max-age=60" + }, + { + "key": "CDN-Cache-Control", + "value": "max-age=900" + }, + { + "key": "Vercel-CDN-Cache-Control", + "value": "public, max-age=3600" + } + ] + } + ] } diff --git a/doc/yarn.lock b/doc/yarn.lock deleted file mode 100644 index 3913b034c3b..00000000000 --- a/doc/yarn.lock +++ /dev/null @@ -1,8640 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@algolia/autocomplete-core@1.7.4": - version "1.7.4" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-core/-/autocomplete-core-1.7.4.tgz#85ff36b2673654a393c8c505345eaedd6eaa4f70" - integrity sha512-daoLpQ3ps/VTMRZDEBfU8ixXd+amZcNJ4QSP3IERGyzqnL5Ch8uSRFt/4G8pUvW9c3o6GA4vtVv4I4lmnkdXyg== - dependencies: - "@algolia/autocomplete-shared" "1.7.4" - -"@algolia/autocomplete-preset-algolia@1.7.4": - version "1.7.4" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.7.4.tgz#610ee1d887962f230b987cba2fd6556478000bc3" - integrity sha512-s37hrvLEIfcmKY8VU9LsAXgm2yfmkdHT3DnA3SgHaY93yjZ2qL57wzb5QweVkYuEBZkT2PIREvRoLXC2sxTbpQ== - dependencies: - "@algolia/autocomplete-shared" "1.7.4" - -"@algolia/autocomplete-shared@1.7.4": - version "1.7.4" - resolved "https://registry.yarnpkg.com/@algolia/autocomplete-shared/-/autocomplete-shared-1.7.4.tgz#78aea1140a50c4d193e1f06a13b7f12c5e2cbeea" - integrity sha512-2VGCk7I9tA9Ge73Km99+Qg87w0wzW4tgUruvWAn/gfey1ZXgmxZtyIRBebk35R1O8TbK77wujVtCnpsGpRy1kg== - -"@algolia/cache-browser-local-storage@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.14.3.tgz#b9e0da012b2f124f785134a4d468ee0841b2399d" - integrity sha512-hWH1yCxgG3+R/xZIscmUrWAIBnmBFHH5j30fY/+aPkEZWt90wYILfAHIOZ1/Wxhho5SkPfwFmT7ooX2d9JeQBw== - dependencies: - "@algolia/cache-common" "4.14.3" - -"@algolia/cache-common@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-common/-/cache-common-4.14.3.tgz#a78e9faee3dfec018eab7b0996e918e06b476ac7" - integrity sha512-oZJofOoD9FQOwiGTzyRnmzvh3ZP8WVTNPBLH5xU5JNF7drDbRT0ocVT0h/xB2rPHYzOeXRrLaQQBwRT/CKom0Q== - -"@algolia/cache-in-memory@4.14.3", "@algolia/cache-in-memory@^4.14.2": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/cache-in-memory/-/cache-in-memory-4.14.3.tgz#96cefb942aeb80e51e6a7e29f25f4f7f3439b736" - integrity sha512-ES0hHQnzWjeioLQf5Nq+x1AWdZJ50znNPSH3puB/Y4Xsg4Av1bvLmTJe7SY2uqONaeMTvL0OaVcoVtQgJVw0vg== - dependencies: - "@algolia/cache-common" "4.14.3" - -"@algolia/client-account@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/client-account/-/client-account-4.14.3.tgz#6d7d032a65c600339ce066505c77013d9a9e4966" - integrity sha512-PBcPb0+f5Xbh5UfLZNx2Ow589OdP8WYjB4CnvupfYBrl9JyC1sdH4jcq/ri8osO/mCZYjZrQsKAPIqW/gQmizQ== - dependencies: - "@algolia/client-common" "4.14.3" - "@algolia/client-search" "4.14.3" - "@algolia/transporter" "4.14.3" - -"@algolia/client-analytics@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/client-analytics/-/client-analytics-4.14.3.tgz#ca409d00a8fff98fdcc215dc96731039900055dc" - integrity sha512-eAwQq0Hb/aauv9NhCH5Dp3Nm29oFx28sayFN2fdOWemwSeJHIl7TmcsxVlRsO50fsD8CtPcDhtGeD3AIFLNvqw== - dependencies: - "@algolia/client-common" "4.14.3" - "@algolia/client-search" "4.14.3" - "@algolia/requester-common" "4.14.3" - "@algolia/transporter" "4.14.3" - -"@algolia/client-common@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/client-common/-/client-common-4.14.3.tgz#c44e48652b2121a20d7a40cfd68d095ebb4191a8" - integrity sha512-jkPPDZdi63IK64Yg4WccdCsAP4pHxSkr4usplkUZM5C1l1oEpZXsy2c579LQ0rvwCs5JFmwfNG4ahOszidfWPw== - dependencies: - "@algolia/requester-common" "4.14.3" - "@algolia/transporter" "4.14.3" - -"@algolia/client-personalization@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/client-personalization/-/client-personalization-4.14.3.tgz#8f71325035aa2a5fa7d1d567575235cf1d6c654f" - integrity sha512-UCX1MtkVNgaOL9f0e22x6tC9e2H3unZQlSUdnVaSKpZ+hdSChXGaRjp2UIT7pxmPqNCyv51F597KEX5WT60jNg== - dependencies: - "@algolia/client-common" "4.14.3" - "@algolia/requester-common" "4.14.3" - "@algolia/transporter" "4.14.3" - -"@algolia/client-search@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/client-search/-/client-search-4.14.3.tgz#cf1e77549f5c3e73408ffe6441ede985fde69da0" - integrity sha512-I2U7xBx5OPFdPLA8AXKUPPxGY3HDxZ4r7+mlZ8ZpLbI8/ri6fnu6B4z3wcL7sgHhDYMwnAE8Xr0AB0h3Hnkp4A== - dependencies: - "@algolia/client-common" "4.14.3" - "@algolia/requester-common" "4.14.3" - "@algolia/transporter" "4.14.3" - -"@algolia/events@^4.0.1": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@algolia/events/-/events-4.0.1.tgz#fd39e7477e7bc703d7f893b556f676c032af3950" - integrity sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ== - -"@algolia/logger-common@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-common/-/logger-common-4.14.3.tgz#87d4725e7f56ea5a39b605771b7149fff62032a7" - integrity sha512-kUEAZaBt/J3RjYi8MEBT2QEexJR2kAE2mtLmezsmqMQZTV502TkHCxYzTwY2dE7OKcUTxi4OFlMuS4GId9CWPw== - -"@algolia/logger-console@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/logger-console/-/logger-console-4.14.3.tgz#1f19f8f0a5ef11f01d1f9545290eb6a89b71fb8a" - integrity sha512-ZWqAlUITktiMN2EiFpQIFCJS10N96A++yrexqC2Z+3hgF/JcKrOxOdT4nSCQoEPvU4Ki9QKbpzbebRDemZt/hw== - dependencies: - "@algolia/logger-common" "4.14.3" - -"@algolia/recommend@^4.12.2": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/recommend/-/recommend-4.14.3.tgz#facb5b84c71be3c74f5b8c12e12c61fe358d4a0d" - integrity sha512-prN7XcOy9FCpZ1ltr6GZxvUFGlzTyZq7JjhSsoyYMvIO4EXKnVp5M9LB8kwCN0RvUH1/XKXJBrNkE0/q5227oA== - dependencies: - "@algolia/cache-browser-local-storage" "4.14.3" - "@algolia/cache-common" "4.14.3" - "@algolia/cache-in-memory" "4.14.3" - "@algolia/client-common" "4.14.3" - "@algolia/client-search" "4.14.3" - "@algolia/logger-common" "4.14.3" - "@algolia/logger-console" "4.14.3" - "@algolia/requester-browser-xhr" "4.14.3" - "@algolia/requester-common" "4.14.3" - "@algolia/requester-node-http" "4.14.3" - "@algolia/transporter" "4.14.3" - -"@algolia/requester-browser-xhr@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.14.3.tgz#bcf55cba20f58fd9bc95ee55793b5219f3ce8888" - integrity sha512-AZeg2T08WLUPvDncl2XLX2O67W5wIO8MNaT7z5ii5LgBTuk/rU4CikTjCe2xsUleIZeFl++QrPAi4Bdxws6r/Q== - dependencies: - "@algolia/requester-common" "4.14.3" - -"@algolia/requester-common@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-common/-/requester-common-4.14.3.tgz#2d02fbe01afb7ae5651ae8dfe62d6c089f103714" - integrity sha512-RrRzqNyKFDP7IkTuV3XvYGF9cDPn9h6qEDl595lXva3YUk9YSS8+MGZnnkOMHvjkrSCKfoLeLbm/T4tmoIeclw== - -"@algolia/requester-node-http@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/requester-node-http/-/requester-node-http-4.14.3.tgz#72389e1c2e5d964702451e75e368eefe85a09d8f" - integrity sha512-O5wnPxtDRPuW2U0EaOz9rMMWdlhwP0J0eSL1Z7TtXF8xnUeeUyNJrdhV5uy2CAp6RbhM1VuC3sOJcIR6Av+vbA== - dependencies: - "@algolia/requester-common" "4.14.3" - -"@algolia/transporter@4.14.3": - version "4.14.3" - resolved "https://registry.yarnpkg.com/@algolia/transporter/-/transporter-4.14.3.tgz#5593036bd9cf2adfd077fdc3e81d2e6118660a7a" - integrity sha512-2qlKlKsnGJ008exFRb5RTeTOqhLZj0bkMCMVskxoqWejs2Q2QtWmsiH98hDfpw0fmnyhzHEt0Z7lqxBYp8bW2w== - dependencies: - "@algolia/cache-common" "4.14.3" - "@algolia/logger-common" "4.14.3" - "@algolia/requester-common" "4.14.3" - -"@algolia/ui-components-highlight-vdom@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@algolia/ui-components-highlight-vdom/-/ui-components-highlight-vdom-1.2.1.tgz#c430c9f090ef8c68477ef4b685324ed231ce0c13" - integrity sha512-IlYgIaCUEkz9ezNbwugwKv991oOHhveyq6nzL0F1jDzg1p3q5Yj/vO4KpNG910r2dwGCG3nEm5GtChcLnarhFA== - dependencies: - "@algolia/ui-components-shared" "1.2.1" - "@babel/runtime" "^7.0.0" - -"@algolia/ui-components-shared@1.2.1", "@algolia/ui-components-shared@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@algolia/ui-components-shared/-/ui-components-shared-1.2.1.tgz#62e3a04fc11623f149312942cd764d4528dd994c" - integrity sha512-a7mYHf/GVQfhAx/HRiMveKkFvHspQv/REdG+C/FIOosiSmNZxX7QebDwJkrGSmDWdXO12D0Qv1xn3AytFcEDlQ== - -"@ampproject/remapping@^2.1.0": - version "2.2.0" - resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.2.0.tgz#56c133824780de3174aed5ab6834f3026790154d" - integrity sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w== - dependencies: - "@jridgewell/gen-mapping" "^0.1.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.12.13", "@babel/code-frame@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.18.6.tgz#3b25d38c89600baa2dcc219edfa88a74eb2c427a" - integrity sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q== - dependencies: - "@babel/highlight" "^7.18.6" - -"@babel/compat-data@^7.20.5": - version "7.20.14" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.20.14.tgz#4106fc8b755f3e3ee0a0a7c27dde5de1d2b2baf8" - integrity sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw== - -"@babel/core@^7.11.6", "@babel/core@^7.12.3", "@babel/core@^7.20.12", "@babel/core@^7.20.5": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.20.12.tgz#7930db57443c6714ad216953d1356dac0eb8496d" - integrity sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg== - dependencies: - "@ampproject/remapping" "^2.1.0" - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-compilation-targets" "^7.20.7" - "@babel/helper-module-transforms" "^7.20.11" - "@babel/helpers" "^7.20.7" - "@babel/parser" "^7.20.7" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.12" - "@babel/types" "^7.20.7" - convert-source-map "^1.7.0" - debug "^4.1.0" - gensync "^1.0.0-beta.2" - json5 "^2.2.2" - semver "^6.3.0" - -"@babel/generator@^7.20.7", "@babel/generator@^7.7.2": - version "7.20.14" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.20.14.tgz#9fa772c9f86a46c6ac9b321039400712b96f64ce" - integrity sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg== - dependencies: - "@babel/types" "^7.20.7" - "@jridgewell/gen-mapping" "^0.3.2" - jsesc "^2.5.1" - -"@babel/helper-annotate-as-pure@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz#eaa49f6f80d5a33f9a5dd2276e6d6e451be0a6bb" - integrity sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-compilation-targets@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz#a6cd33e93629f5eb473b021aac05df62c4cd09bb" - integrity sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ== - dependencies: - "@babel/compat-data" "^7.20.5" - "@babel/helper-validator-option" "^7.18.6" - browserslist "^4.21.3" - lru-cache "^5.1.1" - semver "^6.3.0" - -"@babel/helper-create-class-features-plugin@^7.20.12": - version "7.20.12" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.12.tgz#4349b928e79be05ed2d1643b20b99bb87c503819" - integrity sha512-9OunRkbT0JQcednL0UFvbfXpAsUXiGjUk0a7sN8fUXX7Mue79cUSMjHGDRRi/Vz9vYlpIhLV5fMD5dKoMhhsNQ== - dependencies: - "@babel/helper-annotate-as-pure" "^7.18.6" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/helper-replace-supers" "^7.20.7" - "@babel/helper-skip-transparent-expression-wrappers" "^7.20.0" - "@babel/helper-split-export-declaration" "^7.18.6" - -"@babel/helper-environment-visitor@^7.18.9": - version "7.18.9" - resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be" - integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg== - -"@babel/helper-function-name@^7.19.0": - version "7.19.0" - resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz#941574ed5390682e872e52d3f38ce9d1bef4648c" - integrity sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w== - dependencies: - "@babel/template" "^7.18.10" - "@babel/types" "^7.19.0" - -"@babel/helper-hoist-variables@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678" - integrity sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-member-expression-to-functions@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.20.7.tgz#a6f26e919582275a93c3aa6594756d71b0bb7f05" - integrity sha512-9J0CxJLq315fEdi4s7xK5TQaNYjZw+nDVpVqr1axNGKzdrdwYBD5b4uKv3n75aABG0rCCTK8Im8Ww7eYfMrZgw== - dependencies: - "@babel/types" "^7.20.7" - -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e" - integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-module-transforms@^7.20.11": - version "7.20.11" - resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz#df4c7af713c557938c50ea3ad0117a7944b2f1b0" - integrity sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-module-imports" "^7.18.6" - "@babel/helper-simple-access" "^7.20.2" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/helper-validator-identifier" "^7.19.1" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.10" - "@babel/types" "^7.20.7" - -"@babel/helper-optimise-call-expression@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz#9369aa943ee7da47edab2cb4e838acf09d290ffe" - integrity sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.10.4", "@babel/helper-plugin-utils@^7.12.13", "@babel/helper-plugin-utils@^7.14.5", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.19.0", "@babel/helper-plugin-utils@^7.20.2", "@babel/helper-plugin-utils@^7.8.0": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz#d1b9000752b18d0877cff85a5c376ce5c3121629" - integrity sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ== - -"@babel/helper-replace-supers@^7.20.7": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.20.7.tgz#243ecd2724d2071532b2c8ad2f0f9f083bcae331" - integrity sha512-vujDMtB6LVfNW13jhlCrp48QNslK6JXi7lQG736HVbHz/mbf4Dc7tIRh1Xf5C0rF7BP8iiSxGMCmY6Ci1ven3A== - dependencies: - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-member-expression-to-functions" "^7.20.7" - "@babel/helper-optimise-call-expression" "^7.18.6" - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/helper-simple-access@^7.20.2": - version "7.20.2" - resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz#0ab452687fe0c2cfb1e2b9e0015de07fc2d62dd9" - integrity sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA== - dependencies: - "@babel/types" "^7.20.2" - -"@babel/helper-skip-transparent-expression-wrappers@^7.20.0": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz#fbe4c52f60518cab8140d77101f0e63a8a230684" - integrity sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg== - dependencies: - "@babel/types" "^7.20.0" - -"@babel/helper-split-export-declaration@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz#7367949bc75b20c6d5a5d4a97bba2824ae8ef075" - integrity sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA== - dependencies: - "@babel/types" "^7.18.6" - -"@babel/helper-string-parser@^7.19.4": - version "7.19.4" - resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz#38d3acb654b4701a9b77fb0615a96f775c3a9e63" - integrity sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw== - -"@babel/helper-validator-identifier@^7.18.6", "@babel/helper-validator-identifier@^7.19.1": - version "7.19.1" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz#7eea834cf32901ffdc1a7ee555e2f9c27e249ca2" - integrity sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w== - -"@babel/helper-validator-option@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz#bf0d2b5a509b1f336099e4ff36e1a63aa5db4db8" - integrity sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw== - -"@babel/helpers@^7.20.7": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.20.13.tgz#e3cb731fb70dc5337134cadc24cbbad31cc87ad2" - integrity sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg== - dependencies: - "@babel/template" "^7.20.7" - "@babel/traverse" "^7.20.13" - "@babel/types" "^7.20.7" - -"@babel/highlight@^7.18.6": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.18.6.tgz#81158601e93e2563795adcbfbdf5d64be3f2ecdf" - integrity sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g== - dependencies: - "@babel/helper-validator-identifier" "^7.18.6" - chalk "^2.0.0" - js-tokens "^4.0.0" - -"@babel/parser@^7.1.0", "@babel/parser@^7.14.7", "@babel/parser@^7.16.4", "@babel/parser@^7.20.13", "@babel/parser@^7.20.7": - version "7.20.15" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.20.15.tgz#eec9f36d8eaf0948bb88c87a46784b5ee9fd0c89" - integrity sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg== - -"@babel/plugin-syntax-async-generators@^7.8.4": - version "7.8.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz#a983fb1aeb2ec3f6ed042a210f640e90e786fe0d" - integrity sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-bigint@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz#4c9a6f669f5d0cdf1b90a1671e9a146be5300cea" - integrity sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-class-properties@^7.8.3": - version "7.12.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz#b5c987274c4a3a82b89714796931a6b53544ae10" - integrity sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA== - dependencies: - "@babel/helper-plugin-utils" "^7.12.13" - -"@babel/plugin-syntax-import-meta@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz#ee601348c370fa334d2207be158777496521fd51" - integrity sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz#01ca21b668cd8218c9e640cb6dd88c5412b2c96a" - integrity sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-jsx@^7.0.0", "@babel/plugin-syntax-jsx@^7.7.2": - version "7.18.6" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz#a8feef63b010150abd97f1649ec296e849943ca0" - integrity sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q== - dependencies: - "@babel/helper-plugin-utils" "^7.18.6" - -"@babel/plugin-syntax-logical-assignment-operators@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz#ca91ef46303530448b906652bac2e9fe9941f699" - integrity sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-nullish-coalescing-operator@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" - integrity sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-numeric-separator@^7.8.3": - version "7.10.4" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz#b9b070b3e33570cd9fd07ba7fa91c0dd37b9af97" - integrity sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug== - dependencies: - "@babel/helper-plugin-utils" "^7.10.4" - -"@babel/plugin-syntax-object-rest-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz#60e225edcbd98a640332a2e72dd3e66f1af55871" - integrity sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-catch-binding@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz#6111a265bcfb020eb9efd0fdfd7d26402b9ed6c1" - integrity sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-optional-chaining@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz#4f69c2ab95167e0180cd5336613f8c5788f7d48a" - integrity sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg== - dependencies: - "@babel/helper-plugin-utils" "^7.8.0" - -"@babel/plugin-syntax-top-level-await@^7.8.3": - version "7.14.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz#c1cfdadc35a646240001f06138247b741c34d94c" - integrity sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw== - dependencies: - "@babel/helper-plugin-utils" "^7.14.5" - -"@babel/plugin-syntax-typescript@^7.20.0", "@babel/plugin-syntax-typescript@^7.7.2": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz#4e9a0cfc769c85689b77a2e642d24e9f697fc8c7" - integrity sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ== - dependencies: - "@babel/helper-plugin-utils" "^7.19.0" - -"@babel/plugin-transform-typescript@^7.20.2": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.20.13.tgz#e3581b356b8694f6ff450211fe6774eaff8d25ab" - integrity sha512-O7I/THxarGcDZxkgWKMUrk7NK1/WbHAg3Xx86gqS6x9MTrNL6AwIluuZ96ms4xeDe6AVx6rjHbWHP7x26EPQBA== - dependencies: - "@babel/helper-create-class-features-plugin" "^7.20.12" - "@babel/helper-plugin-utils" "^7.20.2" - "@babel/plugin-syntax-typescript" "^7.20.0" - -"@babel/runtime@^7.0.0": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.13.tgz#7055ab8a7cff2b8f6058bf6ae45ff84ad2aded4b" - integrity sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA== - dependencies: - regenerator-runtime "^0.13.11" - -"@babel/standalone@^7.20.12": - version "7.20.15" - resolved "https://registry.yarnpkg.com/@babel/standalone/-/standalone-7.20.15.tgz#ef82f1a9789d21d8b23f74d9fa8acecbe6ced02c" - integrity sha512-B3LmZ1NHlTb2eFEaw8rftZc730Wh9MlmsH8ubb6IjsNoIk9+SQ2aAA0nrm/1806+PftPRAACPClmKTu8PG7Tew== - -"@babel/template@^7.0.0", "@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.3.3": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.20.7.tgz#a15090c2839a83b02aa996c0b4994005841fd5a8" - integrity sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - -"@babel/traverse@^7.0.0", "@babel/traverse@^7.20.10", "@babel/traverse@^7.20.12", "@babel/traverse@^7.20.13", "@babel/traverse@^7.20.7", "@babel/traverse@^7.7.2": - version "7.20.13" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.20.13.tgz#817c1ba13d11accca89478bd5481b2d168d07473" - integrity sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ== - dependencies: - "@babel/code-frame" "^7.18.6" - "@babel/generator" "^7.20.7" - "@babel/helper-environment-visitor" "^7.18.9" - "@babel/helper-function-name" "^7.19.0" - "@babel/helper-hoist-variables" "^7.18.6" - "@babel/helper-split-export-declaration" "^7.18.6" - "@babel/parser" "^7.20.13" - "@babel/types" "^7.20.7" - debug "^4.1.0" - globals "^11.1.0" - -"@babel/types@^7.0.0", "@babel/types@^7.18.6", "@babel/types@^7.19.0", "@babel/types@^7.20.0", "@babel/types@^7.20.2", "@babel/types@^7.20.7", "@babel/types@^7.3.0", "@babel/types@^7.3.3": - version "7.20.7" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.20.7.tgz#54ec75e252318423fc07fb644dc6a58a64c09b7f" - integrity sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg== - dependencies: - "@babel/helper-string-parser" "^7.19.4" - "@babel/helper-validator-identifier" "^7.19.1" - to-fast-properties "^2.0.0" - -"@bcoe/v8-coverage@^0.2.3": - version "0.2.3" - resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" - integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== - -"@braintree/sanitize-url@^6.0.0": - version "6.0.2" - resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.2.tgz#6110f918d273fe2af8ea1c4398a88774bb9fc12f" - integrity sha512-Tbsj02wXCbqGmzdnXNk0SOF19ChhRU70BsroIi4Pm6Ehp56in6vch94mfbdQ17DozxkL3BAVjbZ4Qc1a0HFRAg== - -"@cloudflare/kv-asset-handler@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.0.tgz#11f0af0749a400ddadcca16dcd6f4696d7036991" - integrity sha512-9CB/MKf/wdvbfkUdfrj+OkEwZ5b7rws0eogJ4293h+7b6KX5toPwym+VQKmILafNB9YiehqY0DlNrDcDhdWHSQ== - dependencies: - mime "^3.0.0" - -"@csstools/cascade-layer-name-parser@^1.0.0": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-1.0.1.tgz#5957adeb71be8159e543d37a9c48e124dcd6c32e" - integrity sha512-SAAi5DpgJJWkfTvWSaqkgyIsTawa83hMwKrktkj6ra2h+q6ZN57vOGZ6ySHq6RSo+CbP64fA3aPChPBRDDUgtw== - -"@csstools/css-parser-algorithms@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.0.1.tgz#ff02629c7c95d1f4f8ea84d5ef1173461610535e" - integrity sha512-B9/8PmOtU6nBiibJg0glnNktQDZ3rZnGn/7UmDfrm2vMtrdlXO3p7ErE95N0up80IRk9YEtB5jyj/TmQ1WH3dw== - -"@csstools/css-tokenizer@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@csstools/css-tokenizer/-/css-tokenizer-2.0.1.tgz#cb1e11752db57e69d9aa0e84c3105a25845d4055" - integrity sha512-sYD3H7ReR88S/4+V5VbKiBEUJF4FqvG+8aNJkxqoPAnbhFziDG22IDZc4+h+xA63SfgM+h15lq5OnLeCxQ9nPA== - -"@docsearch/css@3.3.3": - version "3.3.3" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-3.3.3.tgz#f9346c9e24602218341f51b8ba91eb9109add434" - integrity sha512-6SCwI7P8ao+se1TUsdZ7B4XzL+gqeQZnBc+2EONZlcVa0dVrk0NjETxozFKgMv0eEGH8QzP1fkN+A1rH61l4eg== - -"@docsearch/js@3": - version "3.3.3" - resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-3.3.3.tgz#70725a7a8fe92d221fcf0593263b936389d3728f" - integrity sha512-2xAv2GFuHzzmG0SSZgf8wHX0qZX8n9Y1ZirKUk5Wrdc+vH9CL837x2hZIUdwcPZI9caBA+/CzxsS68O4waYjUQ== - dependencies: - "@docsearch/react" "3.3.3" - preact "^10.0.0" - -"@docsearch/react@3.3.3": - version "3.3.3" - resolved "https://registry.yarnpkg.com/@docsearch/react/-/react-3.3.3.tgz#907b6936a565f880b4c0892624b4f7a9f132d298" - integrity sha512-pLa0cxnl+G0FuIDuYlW+EBK6Rw2jwLw9B1RHIeS4N4s2VhsfJ/wzeCi3CWcs5yVfxLd5ZK50t//TMA5e79YT7Q== - dependencies: - "@algolia/autocomplete-core" "1.7.4" - "@algolia/autocomplete-preset-algolia" "1.7.4" - "@docsearch/css" "3.3.3" - algoliasearch "^4.0.0" - -"@esbuild/android-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz#cf91e86df127aa3d141744edafcba0abdc577d23" - integrity sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg== - -"@esbuild/android-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.6.tgz#b11bd4e4d031bb320c93c83c137797b2be5b403b" - integrity sha512-YnYSCceN/dUzUr5kdtUzB+wZprCafuD89Hs0Aqv9QSdwhYQybhXTaSTcrl6X/aWThn1a/j0eEpUBGOE7269REg== - -"@esbuild/android-arm@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.16.17.tgz#025b6246d3f68b7bbaa97069144fb5fb70f2fff2" - integrity sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw== - -"@esbuild/android-arm@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.6.tgz#ac6b5674da2149997f6306b3314dae59bbe0ac26" - integrity sha512-bSC9YVUjADDy1gae8RrioINU6e1lCkg3VGVwm0QQ2E1CWcC4gnMce9+B6RpxuSsrsXsk1yojn7sp1fnG8erE2g== - -"@esbuild/android-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.16.17.tgz#c820e0fef982f99a85c4b8bfdd582835f04cd96e" - integrity sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ== - -"@esbuild/android-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.6.tgz#18c48bf949046638fc209409ff684c6bb35a5462" - integrity sha512-MVcYcgSO7pfu/x34uX9u2QIZHmXAB7dEiLQC5bBl5Ryqtpj9lT2sg3gNDEsrPEmimSJW2FXIaxqSQ501YLDsZQ== - -"@esbuild/darwin-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz#edef4487af6b21afabba7be5132c26d22379b220" - integrity sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w== - -"@esbuild/darwin-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.6.tgz#b3fe19af1e4afc849a07c06318124e9c041e0646" - integrity sha512-bsDRvlbKMQMt6Wl08nHtFz++yoZHsyTOxnjfB2Q95gato+Yi4WnRl13oC2/PJJA9yLCoRv9gqT/EYX0/zDsyMA== - -"@esbuild/darwin-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz#42829168730071c41ef0d028d8319eea0e2904b4" - integrity sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg== - -"@esbuild/darwin-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.6.tgz#f4dacd1ab21e17b355635c2bba6a31eba26ba569" - integrity sha512-xh2A5oPrYRfMFz74QXIQTQo8uA+hYzGWJFoeTE8EvoZGHb+idyV4ATaukaUvnnxJiauhs/fPx3vYhU4wiGfosg== - -"@esbuild/freebsd-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz#1f4af488bfc7e9ced04207034d398e793b570a27" - integrity sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw== - -"@esbuild/freebsd-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.6.tgz#ea4531aeda70b17cbe0e77b0c5c36298053855b4" - integrity sha512-EnUwjRc1inT4ccZh4pB3v1cIhohE2S4YXlt1OvI7sw/+pD+dIE4smwekZlEPIwY6PhU6oDWwITrQQm5S2/iZgg== - -"@esbuild/freebsd-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz#636306f19e9bc981e06aa1d777302dad8fddaf72" - integrity sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug== - -"@esbuild/freebsd-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.6.tgz#1896170b3c9f63c5e08efdc1f8abc8b1ed7af29f" - integrity sha512-Uh3HLWGzH6FwpviUcLMKPCbZUAFzv67Wj5MTwK6jn89b576SR2IbEp+tqUHTr8DIl0iDmBAf51MVaP7pw6PY5Q== - -"@esbuild/linux-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz#a003f7ff237c501e095d4f3a09e58fc7b25a4aca" - integrity sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g== - -"@esbuild/linux-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.6.tgz#967dfb951c6b2de6f2af82e96e25d63747f75079" - integrity sha512-bUR58IFOMJX523aDVozswnlp5yry7+0cRLCXDsxnUeQYJik1DukMY+apBsLOZJblpH+K7ox7YrKrHmJoWqVR9w== - -"@esbuild/linux-arm@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz#b591e6a59d9c4fe0eeadd4874b157ab78cf5f196" - integrity sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ== - -"@esbuild/linux-arm@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.6.tgz#097a0ee2be39fed3f37ea0e587052961e3bcc110" - integrity sha512-7YdGiurNt7lqO0Bf/U9/arrPWPqdPqcV6JCZda4LZgEn+PTQ5SMEI4MGR52Bfn3+d6bNEGcWFzlIxiQdS48YUw== - -"@esbuild/linux-ia32@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz#24333a11027ef46a18f57019450a5188918e2a54" - integrity sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg== - -"@esbuild/linux-ia32@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.6.tgz#a38a789d0ed157495a6b5b4469ec7868b59e5278" - integrity sha512-ujp8uoQCM9FRcbDfkqECoARsLnLfCUhKARTP56TFPog8ie9JG83D5GVKjQ6yVrEVdMie1djH86fm98eY3quQkQ== - -"@esbuild/linux-loong64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz#d5ad459d41ed42bbd4d005256b31882ec52227d8" - integrity sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ== - -"@esbuild/linux-loong64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.6.tgz#ae3983d0fb4057883c8246f57d2518c2af7cf2ad" - integrity sha512-y2NX1+X/Nt+izj9bLoiaYB9YXT/LoaQFYvCkVD77G/4F+/yuVXYCWz4SE9yr5CBMbOxOfBcy/xFL4LlOeNlzYQ== - -"@esbuild/linux-mips64el@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz#4e5967a665c38360b0a8205594377d4dcf9c3726" - integrity sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw== - -"@esbuild/linux-mips64el@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.6.tgz#15fbbe04648d944ec660ee5797febdf09a9bd6af" - integrity sha512-09AXKB1HDOzXD+j3FdXCiL/MWmZP0Ex9eR8DLMBVcHorrWJxWmY8Nms2Nm41iRM64WVx7bA/JVHMv081iP2kUA== - -"@esbuild/linux-ppc64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz#206443a02eb568f9fdf0b438fbd47d26e735afc8" - integrity sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g== - -"@esbuild/linux-ppc64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.6.tgz#38210094e8e1a971f2d1fd8e48462cc65f15ef19" - integrity sha512-AmLhMzkM8JuqTIOhxnX4ubh0XWJIznEynRnZAVdA2mMKE6FAfwT2TWKTwdqMG+qEaeyDPtfNoZRpJbD4ZBv0Tg== - -"@esbuild/linux-riscv64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz#c351e433d009bf256e798ad048152c8d76da2fc9" - integrity sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw== - -"@esbuild/linux-riscv64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.6.tgz#bc3c66d5578c3b9951a6ed68763f2a6856827e4a" - integrity sha512-Y4Ri62PfavhLQhFbqucysHOmRamlTVK10zPWlqjNbj2XMea+BOs4w6ASKwQwAiqf9ZqcY9Ab7NOU4wIgpxwoSQ== - -"@esbuild/linux-s390x@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz#661f271e5d59615b84b6801d1c2123ad13d9bd87" - integrity sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w== - -"@esbuild/linux-s390x@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.6.tgz#d7ba7af59285f63cfce6e5b7f82a946f3e6d67fc" - integrity sha512-SPUiz4fDbnNEm3JSdUW8pBJ/vkop3M1YwZAVwvdwlFLoJwKEZ9L98l3tzeyMzq27CyepDQ3Qgoba44StgbiN5Q== - -"@esbuild/linux-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz#e4ba18e8b149a89c982351443a377c723762b85f" - integrity sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw== - -"@esbuild/linux-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.6.tgz#ba51f8760a9b9370a2530f98964be5f09d90fed0" - integrity sha512-a3yHLmOodHrzuNgdpB7peFGPx1iJ2x6m+uDvhP2CKdr2CwOaqEFMeSqYAHU7hG+RjCq8r2NFujcd/YsEsFgTGw== - -"@esbuild/netbsd-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz#7d4f4041e30c5c07dd24ffa295c73f06038ec775" - integrity sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA== - -"@esbuild/netbsd-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.6.tgz#e84d6b6fdde0261602c1e56edbb9e2cb07c211b9" - integrity sha512-EanJqcU/4uZIBreTrnbnre2DXgXSa+Gjap7ifRfllpmyAU7YMvaXmljdArptTHmjrkkKm9BK6GH5D5Yo+p6y5A== - -"@esbuild/openbsd-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz#970fa7f8470681f3e6b1db0cc421a4af8060ec35" - integrity sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg== - -"@esbuild/openbsd-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.6.tgz#cf4b9fb80ce6d280a673d54a731d9c661f88b083" - integrity sha512-xaxeSunhQRsTNGFanoOkkLtnmMn5QbA0qBhNet/XLVsc+OVkpIWPHcr3zTW2gxVU5YOHFbIHR9ODuaUdNza2Vw== - -"@esbuild/sunos-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz#abc60e7c4abf8b89fb7a4fe69a1484132238022c" - integrity sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw== - -"@esbuild/sunos-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.6.tgz#a6838e246079b24d962b9dcb8d208a3785210a73" - integrity sha512-gnMnMPg5pfMkZvhHee21KbKdc6W3GR8/JuE0Da1kjwpK6oiFU3nqfHuVPgUX2rsOx9N2SadSQTIYV1CIjYG+xw== - -"@esbuild/win32-arm64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz#7b0ff9e8c3265537a7a7b1fd9a24e7bd39fcd87a" - integrity sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw== - -"@esbuild/win32-arm64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.6.tgz#ace0186e904d109ea4123317a3ba35befe83ac21" - integrity sha512-G95n7vP1UnGJPsVdKXllAJPtqjMvFYbN20e8RK8LVLhlTiSOH1sd7+Gt7rm70xiG+I5tM58nYgwWrLs6I1jHqg== - -"@esbuild/win32-ia32@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz#e90fe5267d71a7b7567afdc403dfd198c292eb09" - integrity sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig== - -"@esbuild/win32-ia32@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.6.tgz#7fb3f6d4143e283a7f7dffc98a6baf31bb365c7e" - integrity sha512-96yEFzLhq5bv9jJo5JhTs1gI+1cKQ83cUpyxHuGqXVwQtY5Eq54ZEsKs8veKtiKwlrNimtckHEkj4mRh4pPjsg== - -"@esbuild/win32-x64@0.16.17": - version "0.16.17" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz#c5a1a4bfe1b57f0c3e61b29883525c6da3e5c091" - integrity sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q== - -"@esbuild/win32-x64@0.17.6": - version "0.17.6" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.6.tgz#563ff4277f1230a006472664fa9278a83dd124da" - integrity sha512-n6d8MOyUrNp6G4VSpRcgjs5xj4A91svJSaiwLIDWVWEsZtpN5FA9NlBbZHDmAJc2e8e6SF4tkBD3HAvPF+7igA== - -"@iconify/types@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@iconify/types/-/types-2.0.0.tgz#ab0e9ea681d6c8a1214f30cd741fe3a20cc57f57" - integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg== - -"@iconify/vue@^4.0.2": - version "4.1.0" - resolved "https://registry.yarnpkg.com/@iconify/vue/-/vue-4.1.0.tgz#ce3dc1b34b08fe2a1e34ef9e0c860796a18e76ea" - integrity sha512-rBQVxNoSDooqgWkQg2MqkIHkH/huNuvXGqui5wijc1zLnU7TKzbBHW9VGmbnV4asNTmIHmqV4Nvt0M2rZ/9nHA== - dependencies: - "@iconify/types" "^2.0.0" - -"@ioredis/commands@^1.1.1": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@ioredis/commands/-/commands-1.2.0.tgz#6d61b3097470af1fdbbe622795b8921d42018e11" - integrity sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg== - -"@istanbuljs/load-nyc-config@^1.0.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz#fd3db1d59ecf7cf121e80650bb86712f9b55eced" - integrity sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ== - dependencies: - camelcase "^5.3.1" - find-up "^4.1.0" - get-package-type "^0.1.0" - js-yaml "^3.13.1" - resolve-from "^5.0.0" - -"@istanbuljs/schema@^0.1.2": - version "0.1.3" - resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.3.tgz#e45e384e4b8ec16bce2fd903af78450f6bf7ec98" - integrity sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA== - -"@jest/console@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-29.4.2.tgz#f78374905c2454764152904a344a2d5226b0ef09" - integrity sha512-0I/rEJwMpV9iwi9cDEnT71a5nNGK9lj8Z4+1pRAU2x/thVXCDnaTGrvxyK+cAqZTFVFCiR+hfVrP4l2m+dCmQg== - dependencies: - "@jest/types" "^29.4.2" - "@types/node" "*" - chalk "^4.0.0" - jest-message-util "^29.4.2" - jest-util "^29.4.2" - slash "^3.0.0" - -"@jest/core@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-29.4.2.tgz#6e999b67bdc2df9d96ba9b142465bda71ee472c2" - integrity sha512-KGuoQah0P3vGNlaS/l9/wQENZGNKGoWb+OPxh3gz+YzG7/XExvYu34MzikRndQCdM2S0tzExN4+FL37i6gZmCQ== - dependencies: - "@jest/console" "^29.4.2" - "@jest/reporters" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - ci-info "^3.2.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - jest-changed-files "^29.4.2" - jest-config "^29.4.2" - jest-haste-map "^29.4.2" - jest-message-util "^29.4.2" - jest-regex-util "^29.4.2" - jest-resolve "^29.4.2" - jest-resolve-dependencies "^29.4.2" - jest-runner "^29.4.2" - jest-runtime "^29.4.2" - jest-snapshot "^29.4.2" - jest-util "^29.4.2" - jest-validate "^29.4.2" - jest-watcher "^29.4.2" - micromatch "^4.0.4" - pretty-format "^29.4.2" - slash "^3.0.0" - strip-ansi "^6.0.0" - -"@jest/environment@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-29.4.2.tgz#ee92c316ee2fbdf0bcd9d2db0ef42d64fea26b56" - integrity sha512-JKs3VUtse0vQfCaFGJRX1bir9yBdtasxziSyu+pIiEllAQOe4oQhdCYIf3+Lx+nGglFktSKToBnRJfD5QKp+NQ== - dependencies: - "@jest/fake-timers" "^29.4.2" - "@jest/types" "^29.4.2" - "@types/node" "*" - jest-mock "^29.4.2" - -"@jest/expect-utils@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/expect-utils/-/expect-utils-29.4.2.tgz#cd0065dfdd8e8a182aa350cc121db97b5eed7b3f" - integrity sha512-Dd3ilDJpBnqa0GiPN7QrudVs0cczMMHtehSo2CSTjm3zdHx0RcpmhFNVEltuEFeqfLIyWKFI224FsMSQ/nsJQA== - dependencies: - jest-get-type "^29.4.2" - -"@jest/expect@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/expect/-/expect-29.4.2.tgz#2d4a6a41b29380957c5094de19259f87f194578b" - integrity sha512-NUAeZVApzyaeLjfWIV/64zXjA2SS+NuUPHpAlO7IwVMGd5Vf9szTl9KEDlxY3B4liwLO31os88tYNHl6cpjtKQ== - dependencies: - expect "^29.4.2" - jest-snapshot "^29.4.2" - -"@jest/fake-timers@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-29.4.2.tgz#af43ee1a5720b987d0348f80df98f2cb17d45cd0" - integrity sha512-Ny1u0Wg6kCsHFWq7A/rW/tMhIedq2siiyHyLpHCmIhP7WmcAmd2cx95P+0xtTZlj5ZbJxIRQi4OPydZZUoiSQQ== - dependencies: - "@jest/types" "^29.4.2" - "@sinonjs/fake-timers" "^10.0.2" - "@types/node" "*" - jest-message-util "^29.4.2" - jest-mock "^29.4.2" - jest-util "^29.4.2" - -"@jest/globals@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-29.4.2.tgz#73f85f5db0e17642258b25fd0b9fc89ddedb50eb" - integrity sha512-zCk70YGPzKnz/I9BNFDPlK+EuJLk21ur/NozVh6JVM86/YYZtZHqxFFQ62O9MWq7uf3vIZnvNA0BzzrtxD9iyg== - dependencies: - "@jest/environment" "^29.4.2" - "@jest/expect" "^29.4.2" - "@jest/types" "^29.4.2" - jest-mock "^29.4.2" - -"@jest/reporters@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-29.4.2.tgz#6abfa923941daae0acc76a18830ee9e79a22042d" - integrity sha512-10yw6YQe75zCgYcXgEND9kw3UZZH5tJeLzWv4vTk/2mrS1aY50A37F+XT2hPO5OqQFFnUWizXD8k1BMiATNfUw== - dependencies: - "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" - "@jridgewell/trace-mapping" "^0.3.15" - "@types/node" "*" - chalk "^4.0.0" - collect-v8-coverage "^1.0.0" - exit "^0.1.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - istanbul-lib-coverage "^3.0.0" - istanbul-lib-instrument "^5.1.0" - istanbul-lib-report "^3.0.0" - istanbul-lib-source-maps "^4.0.0" - istanbul-reports "^3.1.3" - jest-message-util "^29.4.2" - jest-util "^29.4.2" - jest-worker "^29.4.2" - slash "^3.0.0" - string-length "^4.0.1" - strip-ansi "^6.0.0" - v8-to-istanbul "^9.0.1" - -"@jest/schemas@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/schemas/-/schemas-29.4.2.tgz#cf7cfe97c5649f518452b176c47ed07486270fc1" - integrity sha512-ZrGzGfh31NtdVH8tn0mgJw4khQuNHiKqdzJAFbCaERbyCP9tHlxWuL/mnMu8P7e/+k4puWjI1NOzi/sFsjce/g== - dependencies: - "@sinclair/typebox" "^0.25.16" - -"@jest/source-map@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-29.4.2.tgz#f9815d59e25cd3d6828e41489cd239271018d153" - integrity sha512-tIoqV5ZNgYI9XCKXMqbYe5JbumcvyTgNN+V5QW4My033lanijvCD0D4PI9tBw4pRTqWOc00/7X3KVvUh+qnF4Q== - dependencies: - "@jridgewell/trace-mapping" "^0.3.15" - callsites "^3.0.0" - graceful-fs "^4.2.9" - -"@jest/test-result@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-29.4.2.tgz#34b0ba069f2e3072261e4884c8fb6bd15ed6fb8d" - integrity sha512-HZsC3shhiHVvMtP+i55MGR5bPcc3obCFbA5bzIOb8pCjwBZf11cZliJncCgaVUbC5yoQNuGqCkC0Q3t6EItxZA== - dependencies: - "@jest/console" "^29.4.2" - "@jest/types" "^29.4.2" - "@types/istanbul-lib-coverage" "^2.0.0" - collect-v8-coverage "^1.0.0" - -"@jest/test-sequencer@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-29.4.2.tgz#8b48e5bc4af80b42edacaf2a733d4f295edf28fb" - integrity sha512-9Z2cVsD6CcObIVrWigHp2McRJhvCxL27xHtrZFgNC1RwnoSpDx6fZo8QYjJmziFlW9/hr78/3sxF54S8B6v8rg== - dependencies: - "@jest/test-result" "^29.4.2" - graceful-fs "^4.2.9" - jest-haste-map "^29.4.2" - slash "^3.0.0" - -"@jest/transform@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-29.4.2.tgz#b24b72dbab4c8675433a80e222d6a8ef4656fb81" - integrity sha512-kf1v5iTJHn7p9RbOsBuc/lcwyPtJaZJt5885C98omWz79NIeD3PfoiiaPSu7JyCyFzNOIzKhmMhQLUhlTL9BvQ== - dependencies: - "@babel/core" "^7.11.6" - "@jest/types" "^29.4.2" - "@jridgewell/trace-mapping" "^0.3.15" - babel-plugin-istanbul "^6.1.1" - chalk "^4.0.0" - convert-source-map "^2.0.0" - fast-json-stable-stringify "^2.1.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.4.2" - jest-regex-util "^29.4.2" - jest-util "^29.4.2" - micromatch "^4.0.4" - pirates "^4.0.4" - slash "^3.0.0" - write-file-atomic "^4.0.2" - -"@jest/types@^29.4.2": - version "29.4.2" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-29.4.2.tgz#8f724a414b1246b2bfd56ca5225d9e1f39540d82" - integrity sha512-CKlngyGP0fwlgC1BRUtPZSiWLBhyS9dKwKmyGxk8Z6M82LBEGB2aLQSg+U1MyLsU+M7UjnlLllBM2BLWKVm/Uw== - dependencies: - "@jest/schemas" "^29.4.2" - "@types/istanbul-lib-coverage" "^2.0.0" - "@types/istanbul-reports" "^3.0.0" - "@types/node" "*" - "@types/yargs" "^17.0.8" - chalk "^4.0.0" - -"@jridgewell/gen-mapping@^0.1.0": - version "0.1.1" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz#e5d2e450306a9491e3bd77e323e38d7aff315996" - integrity sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w== - dependencies: - "@jridgewell/set-array" "^1.0.0" - "@jridgewell/sourcemap-codec" "^1.4.10" - -"@jridgewell/gen-mapping@^0.3.0", "@jridgewell/gen-mapping@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz#c1aedc61e853f2bb9f5dfe6d4442d3b565b253b9" - integrity sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A== - dependencies: - "@jridgewell/set-array" "^1.0.1" - "@jridgewell/sourcemap-codec" "^1.4.10" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/resolve-uri@3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz#2203b118c157721addfe69d47b70465463066d78" - integrity sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w== - -"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1": - version "1.1.2" - resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72" - integrity sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw== - -"@jridgewell/source-map@^0.3.2": - version "0.3.2" - resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.2.tgz#f45351aaed4527a298512ec72f81040c998580fb" - integrity sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw== - dependencies: - "@jridgewell/gen-mapping" "^0.3.0" - "@jridgewell/trace-mapping" "^0.3.9" - -"@jridgewell/sourcemap-codec@1.4.14", "@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.13": - version "1.4.14" - resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz#add4c98d341472a289190b424efbdb096991bb24" - integrity sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw== - -"@jridgewell/trace-mapping@^0.3.12", "@jridgewell/trace-mapping@^0.3.15", "@jridgewell/trace-mapping@^0.3.9": - version "0.3.17" - resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz#793041277af9073b0951a7fe0f0d8c4c98c36985" - integrity sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g== - dependencies: - "@jridgewell/resolve-uri" "3.1.0" - "@jridgewell/sourcemap-codec" "1.4.14" - -"@mapbox/node-pre-gyp@^1.0.5": - version "1.0.10" - resolved "https://registry.yarnpkg.com/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.10.tgz#8e6735ccebbb1581e5a7e652244cadc8a844d03c" - integrity sha512-4ySo4CjzStuprMwk35H5pPbkymjv1SF3jGLj6rAHp/xT/RF7TL7bd9CTm1xDY49K2qF7jmR/g7k+SkLETP6opA== - dependencies: - detect-libc "^2.0.0" - https-proxy-agent "^5.0.0" - make-dir "^3.1.0" - node-fetch "^2.6.7" - nopt "^5.0.0" - npmlog "^5.0.1" - rimraf "^3.0.2" - semver "^7.3.5" - tar "^6.1.11" - -"@netlify/functions@^1.4.0": - version "1.4.0" - resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-1.4.0.tgz#027a2e5d54df5519ccbd14cf450231e97bbbf93a" - integrity sha512-gy7ULTIRroc2/jyFVGx1djCmmBMVisIwrvkqggq5B6iDcInRSy2Tpkm+V5C63hKJVkNRskKWtLQKm9ecCaQTjA== - dependencies: - is-promise "^4.0.0" - -"@nodelib/fs.scandir@2.1.5": - version "2.1.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" - integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== - dependencies: - "@nodelib/fs.stat" "2.0.5" - run-parallel "^1.1.9" - -"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": - version "2.0.5" - resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" - integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== - -"@nodelib/fs.walk@^1.2.3": - version "1.2.8" - resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" - integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== - dependencies: - "@nodelib/fs.scandir" "2.1.5" - fastq "^1.6.0" - -"@nuxt-themes/docus@^1.0.2": - version "1.8.1" - resolved "https://registry.yarnpkg.com/@nuxt-themes/docus/-/docus-1.8.1.tgz#96f9bab331e32743cf0d3fb6cdd09fdd3045448b" - integrity sha512-UrsqD24e5hewHKDvqfgDNV0I6dGpKieaZoUG7aaCPP0glAqNlWMw8SFHlM7KFBx9iREmWed1XxCkN7wAcJPIUQ== - dependencies: - "@nuxt-themes/elements" "^0.7.0" - "@nuxt-themes/tokens" "^1.7.3" - "@nuxt-themes/typography" "^0.8.0" - "@nuxt/content" "^2.4.3" - "@nuxthq/studio" "^0.7.0" - "@vueuse/nuxt" "^9.12.0" - -"@nuxt-themes/elements@^0.7.0": - version "0.7.2" - resolved "https://registry.yarnpkg.com/@nuxt-themes/elements/-/elements-0.7.2.tgz#fe0688cd4a2f7f557169c7a56fb95bb5db6ccd06" - integrity sha512-tix0nXchdMMyfSdp+x7VtQE0unNWrQ/xDxxHX/p8JzZMDs4jnXnfJnpdIljhZCyIlFe+9KM5wAZuzb4C0lsGrg== - dependencies: - "@nuxt-themes/tokens" "^1.7.3" - "@vueuse/core" "^9.12.0" - -"@nuxt-themes/tokens@^1.7.3": - version "1.7.3" - resolved "https://registry.yarnpkg.com/@nuxt-themes/tokens/-/tokens-1.7.3.tgz#f2a34a7077de717077ba6a39cc1d020c10a19fd1" - integrity sha512-BlCudbVaUTYfgM9Qi8SYC8KpRdHb1LiQ8PVPpi4Me/5QYq3jSWQ5C15PPQYHhIqOdpcBNFuVF3WPP1QFP+oHnw== - dependencies: - "@nuxtjs/color-mode" "^3.2.0" - "@vueuse/core" "^9.12.0" - pinceau "^0.13.8" - -"@nuxt-themes/typography@^0.8.0": - version "0.8.0" - resolved "https://registry.yarnpkg.com/@nuxt-themes/typography/-/typography-0.8.0.tgz#22e443a726fb9493faae0ec7f0538787595a06c4" - integrity sha512-aK1Bp9FnlTcg8sc3ZGGGx4dMlQrIRMxZhBwFEbh4JtLupcckJVt3FsJE+JGEmyRWSFbJrsagLdv6cZ9Vn7OCfQ== - dependencies: - "@nuxt-themes/tokens" "^1.7.3" - "@nuxtjs/color-mode" "^3.2.0" - nuxt-config-schema "^0.4.4" - nuxt-icon "^0.2.10" - ufo "^1.0.1" - -"@nuxt/content@^2.4.3": - version "2.4.3" - resolved "https://registry.yarnpkg.com/@nuxt/content/-/content-2.4.3.tgz#db3bbd35b87b9e23518eef19cc063a200d86062a" - integrity sha512-HRx4+9RK2bgtBObcgfrWg/MS0G+mgq87tAA6Q6vjusDpVGE4DhnPN/9nkEMEyfebPZ22iI7z51GN2Od/SnABHA== - dependencies: - "@nuxt/kit" "3.1.1" - consola "^2.15.3" - defu "^6.1.2" - destr "^1.2.2" - detab "^3.0.2" - json5 "^2.2.3" - knitwork "^1.0.0" - listhen "^1.0.2" - mdast-util-to-hast "^12.2.6" - mdurl "^1.0.1" - ohash "^1.0.0" - pathe "^1.1.0" - property-information "^6.2.0" - rehype-external-links "^2.0.1" - rehype-raw "^6.1.1" - rehype-slug "^5.1.0" - rehype-sort-attribute-values "^4.0.0" - rehype-sort-attributes "^4.0.0" - remark-emoji "3.0.2" - remark-gfm "^3.0.1" - remark-mdc "^1.1.3" - remark-parse "^10.0.1" - remark-rehype "^10.1.0" - remark-squeeze-paragraphs "^5.0.1" - scule "^1.0.0" - shiki-es "^0.2.0" - slugify "^1.6.5" - socket.io-client "^4.5.4" - ufo "^1.0.1" - unified "^10.1.2" - unist-builder "^3.0.1" - unist-util-position "^4.0.4" - unist-util-visit "^4.1.2" - unstorage "^1.0.1" - ws "^8.12.0" - -"@nuxt/devalue@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@nuxt/devalue/-/devalue-2.0.0.tgz#c7bd7e9a516514e612d5d2e511ffc399e0eac322" - integrity sha512-YBI/6o2EBz02tdEJRBK8xkt3zvOFOWlLBf7WKYGBsSYSRtjjgrqPe2skp6VLLmKx5WbHHDNcW+6oACaurxGzeA== - -"@nuxt/kit@3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@nuxt/kit/-/kit-3.1.1.tgz#7641afe192297db02d3585234e9d2a71caf164df" - integrity sha512-wmqVCIuD/te6BKf3YiqWyMumKI5JIpkiv0li/1Y3QHnTkoxyIhLkbFgNcQHuBxJ3eMlk2UjAjAqWiqBHTX54vQ== - dependencies: - "@nuxt/schema" "3.1.1" - c12 "^1.1.0" - consola "^2.15.3" - defu "^6.1.2" - globby "^13.1.3" - hash-sum "^2.0.0" - ignore "^5.2.4" - jiti "^1.16.2" - knitwork "^1.0.0" - lodash.template "^4.5.0" - mlly "^1.1.0" - pathe "^1.1.0" - pkg-types "^1.0.1" - scule "^1.0.0" - semver "^7.3.8" - unctx "^2.1.1" - unimport "^2.0.1" - untyped "^1.2.2" - -"@nuxt/kit@3.1.2", "@nuxt/kit@^3.0.0", "@nuxt/kit@^3.1.0", "@nuxt/kit@^3.1.1": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@nuxt/kit/-/kit-3.1.2.tgz#bf5f932b8a82f40bcfec4c1037021f0a75df4582" - integrity sha512-m8/AF8hBJiG7aTx2CpiDGeLYYz30fUoPbJ9XiSmHqRIXv1goAFWHSkzWfRNEsoAAbMHf76oB917wVUQ3VSSQHg== - dependencies: - "@nuxt/schema" "3.1.2" - c12 "^1.1.0" - consola "^2.15.3" - defu "^6.1.2" - globby "^13.1.3" - hash-sum "^2.0.0" - ignore "^5.2.4" - jiti "^1.16.2" - knitwork "^1.0.0" - lodash.template "^4.5.0" - mlly "^1.1.0" - pathe "^1.1.0" - pkg-types "^1.0.1" - scule "^1.0.0" - semver "^7.3.8" - unctx "^2.1.1" - unimport "^2.1.0" - untyped "^1.2.2" - -"@nuxt/schema@3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@nuxt/schema/-/schema-3.1.1.tgz#c45f67f245a117f99b028d98f259cce4c740ffa7" - integrity sha512-/KuoCDVGrLD9W7vwuYhu4HbdT/BpbrhA4Pm9dGn7Jah40kHDGqUnJxugvMjt+4suq53rLQyTA0LRDWfFxfxAOQ== - dependencies: - c12 "^1.1.0" - create-require "^1.1.1" - defu "^6.1.2" - hookable "^5.4.2" - jiti "^1.16.2" - pathe "^1.1.0" - pkg-types "^1.0.1" - postcss-import-resolver "^2.0.0" - scule "^1.0.0" - std-env "^3.3.1" - ufo "^1.0.1" - unimport "^2.0.1" - untyped "^1.2.2" - -"@nuxt/schema@3.1.2", "@nuxt/schema@^3.0.0": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@nuxt/schema/-/schema-3.1.2.tgz#a916ecfa96c04553403f7da8d1e7058f65cb3ad1" - integrity sha512-wru9LhRXTa6WQlx7c0oYrtvJY7TiVlkBKXY5Rsmfo0StJuWohgZiReu9fu6z6GU4MzZlX25TVjwvq9Q7bNVbSQ== - dependencies: - c12 "^1.1.0" - create-require "^1.1.1" - defu "^6.1.2" - hookable "^5.4.2" - jiti "^1.16.2" - pathe "^1.1.0" - pkg-types "^1.0.1" - postcss-import-resolver "^2.0.0" - scule "^1.0.0" - std-env "^3.3.2" - ufo "^1.0.1" - unimport "^2.1.0" - untyped "^1.2.2" - -"@nuxt/telemetry@^2.1.9": - version "2.1.9" - resolved "https://registry.yarnpkg.com/@nuxt/telemetry/-/telemetry-2.1.9.tgz#e1ccc39396ead5024082788d173097d32b58c5da" - integrity sha512-mUyDqmB8GUJwTHVnwxuapeUHDSsUycOt+ZsA7GB6F8MOBJiVhQl/EeEAWoO2TUs0BPp2SlY9uO6eQihvxyLRqQ== - dependencies: - "@nuxt/kit" "^3.0.0" - chalk "^5.2.0" - ci-info "^3.7.1" - consola "^2.15.3" - create-require "^1.1.1" - defu "^6.1.1" - destr "^1.2.2" - dotenv "^16.0.3" - fs-extra "^10.1.0" - git-url-parse "^13.1.0" - inquirer "^9.1.4" - is-docker "^3.0.0" - jiti "^1.16.2" - mri "^1.2.0" - nanoid "^4.0.0" - node-fetch "^3.3.0" - ofetch "^1.0.0" - parse-git-config "^3.0.0" - rc9 "^2.0.0" - std-env "^3.3.1" - -"@nuxt/ui-templates@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@nuxt/ui-templates/-/ui-templates-1.1.1.tgz#db3539e3c9391c217510def5242cf74739e685ea" - integrity sha512-PjVETP7+iZXAs5Q8O4ivl4t6qjWZMZqwiTVogUXHoHGZZcw7GZW3u3tzfYfE1HbzyYJfr236IXqQ02MeR8Fz2w== - -"@nuxt/vite-builder@3.1.2": - version "3.1.2" - resolved "https://registry.yarnpkg.com/@nuxt/vite-builder/-/vite-builder-3.1.2.tgz#bd82f1d9fd0b804674dea845fb6b324444cc753a" - integrity sha512-xKH71LG2xKAmCNlu1rqeL9YmGpJCr4NKg9py3yqmMN+CdivIU4kJ+O5gU0DxX9vo7ZSl7d72v+kyQa3KEM2Gyg== - dependencies: - "@nuxt/kit" "3.1.2" - "@rollup/plugin-replace" "^5.0.2" - "@vitejs/plugin-vue" "^4.0.0" - "@vitejs/plugin-vue-jsx" "^3.0.0" - autoprefixer "^10.4.13" - chokidar "^3.5.3" - cssnano "^5.1.14" - defu "^6.1.2" - esbuild "^0.17.5" - escape-string-regexp "^5.0.0" - estree-walker "^3.0.3" - externality "^1.0.0" - fs-extra "^11.1.0" - get-port-please "^3.0.1" - h3 "^1.1.0" - knitwork "^1.0.0" - magic-string "^0.27.0" - mlly "^1.1.0" - ohash "^1.0.0" - pathe "^1.1.0" - perfect-debounce "^0.1.3" - pkg-types "^1.0.1" - postcss "^8.4.21" - postcss-import "^15.1.0" - postcss-url "^10.1.3" - rollup "^3.12.1" - rollup-plugin-visualizer "^5.9.0" - ufo "^1.0.1" - unplugin "^1.0.1" - vite "~4.1.1" - vite-node "^0.28.3" - vite-plugin-checker "^0.5.5" - vue-bundle-renderer "^1.0.0" - -"@nuxthq/studio@^0.7.0": - version "0.7.2" - resolved "https://registry.yarnpkg.com/@nuxthq/studio/-/studio-0.7.2.tgz#1117a7a0863d0e046aae55765e2f2b9e0952b133" - integrity sha512-8TRUJra53nl+P3XyjcHUKOh/nFo+6MqCy+OXqscKG7MuY8DZ9GNKO8mIIMkyjwseJTGPhSDNqVAsHiHQ0SFfwQ== - dependencies: - "@nuxt/kit" "^3.0.0" - "@nuxt/schema" "^3.0.0" - defu "^6.1.1" - nuxt-component-meta "^0.4.3" - nuxt-config-schema "^0.4.2" - socket.io-client "^4.5.4" - ufo "^1.0.1" - -"@nuxtjs/algolia@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@nuxtjs/algolia/-/algolia-1.5.0.tgz#26f3a12af5f2b7c0b436f16ff57e72fd2d5abba8" - integrity sha512-XOcpUK5lcDXfwRmb2Oln4lgaDeDaE78nPmk3pYpev06vWOCLCXZDLjk/ct0PY+mONgwysW8+ZoSwMQQK89Rnkg== - dependencies: - "@algolia/cache-in-memory" "^4.14.2" - "@algolia/recommend" "^4.12.2" - "@nuxt/kit" "^3.0.0" - algoliasearch "^4.11.0" - instantsearch.css "^7.4.5" - metadata-scraper "^0.2.49" - rollup-plugin-node-polyfills "^0.2.1" - storyblok-algolia-indexer "^1.0.3" - vue-instantsearch "^4.3.2" - -"@nuxtjs/color-mode@^3.2.0": - version "3.2.0" - resolved "https://registry.yarnpkg.com/@nuxtjs/color-mode/-/color-mode-3.2.0.tgz#b5b6a3931a6ddd9646c3aad121d357c635792eb7" - integrity sha512-isDR01yfadopiHQ/VEVUpyNSPrk5PCjUHS4t1qYRZwuRGefU4s9Iaxf6H9nmr1QFzoMgTm+3T0r/54jLwtpZbA== - dependencies: - "@nuxt/kit" "^3.0.0" - lodash.template "^4.5.0" - pathe "^1.0.0" - -"@planetscale/database@^1.5.0": - version "1.5.0" - resolved "https://registry.yarnpkg.com/@planetscale/database/-/database-1.5.0.tgz#073d9ca9841ad62896a6e31f610e89112e6264ef" - integrity sha512-Qwh7Or1W5dB5mZ9EQqDkgvkDKhBBmQe58KIVUy0SGocNtr5fP4JAWtvZ6EdLAV6C6hVpzNlCA2xIg9lKTswm1Q== - -"@rollup/plugin-alias@^4.0.2", "@rollup/plugin-alias@^4.0.3": - version "4.0.3" - resolved "https://registry.yarnpkg.com/@rollup/plugin-alias/-/plugin-alias-4.0.3.tgz#571f6fb26387df91d0363905a7fd835757727ae2" - integrity sha512-ZuDWE1q4PQDhvm/zc5Prun8sBpLJy41DMptYrS6MhAy9s9kL/doN1613BWfEchGVfKxzliJ3BjbOPizXX38DbQ== - dependencies: - slash "^4.0.0" - -"@rollup/plugin-commonjs@^24.0.0", "@rollup/plugin-commonjs@^24.0.1": - version "24.0.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-commonjs/-/plugin-commonjs-24.0.1.tgz#d54ba26a3e3c495dc332bd27a81f7e9e2df46f90" - integrity sha512-15LsiWRZk4eOGqvrJyu3z3DaBu5BhXIMeWnijSRvd8irrrg9SHpQ1pH+BUK4H6Z9wL9yOxZJMTLU+Au86XHxow== - dependencies: - "@rollup/pluginutils" "^5.0.1" - commondir "^1.0.1" - estree-walker "^2.0.2" - glob "^8.0.3" - is-reference "1.2.1" - magic-string "^0.27.0" - -"@rollup/plugin-inject@^5.0.3": - version "5.0.3" - resolved "https://registry.yarnpkg.com/@rollup/plugin-inject/-/plugin-inject-5.0.3.tgz#0783711efd93a9547d52971db73b2fb6140a67b1" - integrity sha512-411QlbL+z2yXpRWFXSmw/teQRMkXcAAC8aYTemc15gwJRpvEVDQwoe+N/HTFD8RFG8+88Bme9DK2V9CVm7hJdA== - dependencies: - "@rollup/pluginutils" "^5.0.1" - estree-walker "^2.0.2" - magic-string "^0.27.0" - -"@rollup/plugin-json@^6.0.0": - version "6.0.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-json/-/plugin-json-6.0.0.tgz#199fea6670fd4dfb1f4932250569b14719db234a" - integrity sha512-i/4C5Jrdr1XUarRhVu27EEwjt4GObltD7c+MkCIpO2QIbojw8MUs+CCTqOphQi3Qtg1FLmYt+l+6YeoIf51J7w== - dependencies: - "@rollup/pluginutils" "^5.0.1" - -"@rollup/plugin-node-resolve@^15.0.1": - version "15.0.1" - resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.0.1.tgz#72be449b8e06f6367168d5b3cd5e2802e0248971" - integrity sha512-ReY88T7JhJjeRVbfCyNj+NXAG3IIsVMsX9b5/9jC98dRP8/yxlZdz7mHZbHk5zHr24wZZICS5AcXsFZAXYUQEg== - dependencies: - "@rollup/pluginutils" "^5.0.1" - "@types/resolve" "1.20.2" - deepmerge "^4.2.2" - is-builtin-module "^3.2.0" - is-module "^1.0.0" - resolve "^1.22.1" - -"@rollup/plugin-replace@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-5.0.2.tgz#45f53501b16311feded2485e98419acb8448c61d" - integrity sha512-M9YXNekv/C/iHHK+cvORzfRYfPbq0RDD8r0G+bMiTXjNGKulPnCT9O3Ss46WfhI6ZOCgApOP7xAdmCQJ+U2LAA== - dependencies: - "@rollup/pluginutils" "^5.0.1" - magic-string "^0.27.0" - -"@rollup/plugin-terser@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@rollup/plugin-terser/-/plugin-terser-0.4.0.tgz#4c76249ad337f3eb04ab409332f23717af2c1fbf" - integrity sha512-Ipcf3LPNerey1q9ZMjiaWHlNPEHNU/B5/uh9zXLltfEQ1lVSLLeZSgAtTPWGyw8Ip1guOeq+mDtdOlEj/wNxQw== - dependencies: - serialize-javascript "^6.0.0" - smob "^0.0.6" - terser "^5.15.1" - -"@rollup/plugin-wasm@^6.1.2": - version "6.1.2" - resolved "https://registry.yarnpkg.com/@rollup/plugin-wasm/-/plugin-wasm-6.1.2.tgz#faf57f8e2ed12b9e0e898ba67963c52e1cd5f4c3" - integrity sha512-YdrQ7zfnZ54Y+6raCev3tR1PrhQGxYKSTajGylhyP0oBacouuNo6KcNCk+pYKw9M98jxRWLFFca/udi76IDXzg== - -"@rollup/pluginutils@^4.0.0": - version "4.2.1" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.2.1.tgz#e6c6c3aba0744edce3fb2074922d3776c0af2a6d" - integrity sha512-iKnFXr7NkdZAIHiIWE+BX5ULi/ucVFYWD6TbAV+rZctiRTY2PL6tsIKhoIOaoskiWAkgu+VsbXgUVDNLHf+InQ== - dependencies: - estree-walker "^2.0.1" - picomatch "^2.2.2" - -"@rollup/pluginutils@^5.0.1", "@rollup/pluginutils@^5.0.2": - version "5.0.2" - resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.0.2.tgz#012b8f53c71e4f6f9cb317e311df1404f56e7a33" - integrity sha512-pTd9rIsP92h+B6wWwFbW8RkZv4hiR/xKsqre4SIuAOaOEQRxi0lqLke9k2/7WegC85GgUs9pjmOjCUi3In4vwA== - dependencies: - "@types/estree" "^1.0.0" - estree-walker "^2.0.2" - picomatch "^2.3.1" - -"@sinclair/typebox@^0.25.16": - version "0.25.21" - resolved "https://registry.yarnpkg.com/@sinclair/typebox/-/typebox-0.25.21.tgz#763b05a4b472c93a8db29b2c3e359d55b29ce272" - integrity sha512-gFukHN4t8K4+wVC+ECqeqwzBDeFeTzBXroBTqE6vcWrQGbEUpHO7LYdG0f4xnvYq4VOEwITSlHlp0JBAIFMS/g== - -"@sindresorhus/is@^4.0.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.6.0.tgz#3c7c9c46e678feefe7a2e5bb609d3dbd665ffb3f" - integrity sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw== - -"@sinonjs/commons@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-2.0.0.tgz#fd4ca5b063554307e8327b4564bd56d3b73924a3" - integrity sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg== - dependencies: - type-detect "4.0.8" - -"@sinonjs/fake-timers@^10.0.2": - version "10.0.2" - resolved "https://registry.yarnpkg.com/@sinonjs/fake-timers/-/fake-timers-10.0.2.tgz#d10549ed1f423d80639c528b6c7f5a1017747d0c" - integrity sha512-SwUDyjWnah1AaNl7kxsa7cfLhlTYoiyhDAIgyh+El30YvXs/o7OLXpYH88Zdhyx9JExKrmHDJ+10bwIcY80Jmw== - dependencies: - "@sinonjs/commons" "^2.0.0" - -"@socket.io/component-emitter@~3.1.0": - version "3.1.0" - resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" - integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== - -"@szmarczak/http-timer@^4.0.5": - version "4.0.6" - resolved "https://registry.yarnpkg.com/@szmarczak/http-timer/-/http-timer-4.0.6.tgz#b4a914bb62e7c272d4e5989fe4440f812ab1d807" - integrity sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w== - dependencies: - defer-to-connect "^2.0.0" - -"@trysound/sax@0.2.0": - version "0.2.0" - resolved "https://registry.yarnpkg.com/@trysound/sax/-/sax-0.2.0.tgz#cccaab758af56761eb7bf37af6f03f326dd798ad" - integrity sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA== - -"@types/babel__core@^7.1.14": - version "7.20.0" - resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.20.0.tgz#61bc5a4cae505ce98e1e36c5445e4bee060d8891" - integrity sha512-+n8dL/9GWblDO0iU6eZAwEIJVr5DWigtle+Q6HLOrh/pdbXOhOtqzq8VPPE2zvNJzSKY4vH/z3iT3tn0A3ypiQ== - dependencies: - "@babel/parser" "^7.20.7" - "@babel/types" "^7.20.7" - "@types/babel__generator" "*" - "@types/babel__template" "*" - "@types/babel__traverse" "*" - -"@types/babel__generator@*": - version "7.6.4" - resolved "https://registry.yarnpkg.com/@types/babel__generator/-/babel__generator-7.6.4.tgz#1f20ce4c5b1990b37900b63f050182d28c2439b7" - integrity sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg== - dependencies: - "@babel/types" "^7.0.0" - -"@types/babel__template@*": - version "7.4.1" - resolved "https://registry.yarnpkg.com/@types/babel__template/-/babel__template-7.4.1.tgz#3d1a48fd9d6c0edfd56f2ff578daed48f36c8969" - integrity sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g== - dependencies: - "@babel/parser" "^7.1.0" - "@babel/types" "^7.0.0" - -"@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.18.3" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.18.3.tgz#dfc508a85781e5698d5b33443416b6268c4b3e8d" - integrity sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w== - dependencies: - "@babel/types" "^7.3.0" - -"@types/cacheable-request@^6.0.1": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@types/cacheable-request/-/cacheable-request-6.0.3.tgz#a430b3260466ca7b5ca5bfd735693b36e7a9d183" - integrity sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw== - dependencies: - "@types/http-cache-semantics" "*" - "@types/keyv" "^3.1.4" - "@types/node" "*" - "@types/responselike" "^1.0.0" - -"@types/chai@^4.3.4": - version "4.3.4" - resolved "https://registry.yarnpkg.com/@types/chai/-/chai-4.3.4.tgz#e913e8175db8307d78b4e8fa690408ba6b65dee4" - integrity sha512-KnRanxnpfpjUTqTCXslZSEdLfXExwgNxYPdiO2WGUj8+HDjFi8R3k5RVKPeSCzLjCcshCAtVO2QBbVuAV4kTnw== - -"@types/debug@^4.0.0": - version "4.1.7" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.7.tgz#7cc0ea761509124709b8b2d1090d8f6c17aadb82" - integrity sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg== - dependencies: - "@types/ms" "*" - -"@types/dom-speech-recognition@^0.0.1": - version "0.0.1" - resolved "https://registry.yarnpkg.com/@types/dom-speech-recognition/-/dom-speech-recognition-0.0.1.tgz#e326761a04b4a49c0eec2ac7948afc1c6aa12baa" - integrity sha512-udCxb8DvjcDKfk1WTBzDsxFbLgYxmQGKrE/ricoMqHRNjSlSUCcamVTA5lIQqzY10mY5qCY0QDwBfFEwhfoDPw== - -"@types/estree@*", "@types/estree@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.0.tgz#5fb2e536c1ae9bf35366eed879e827fa59ca41c2" - integrity sha512-WulqXMDUTYAXCjZnk6JtIHPigp55cVtDgDrO2gHRwhyJto21+1zbVCtOYB2L1F9w4qCQ0rOGWBnBe0FNTiEJIQ== - -"@types/google.maps@^3.45.3": - version "3.51.1" - resolved "https://registry.yarnpkg.com/@types/google.maps/-/google.maps-3.51.1.tgz#7cd2007d231d49b9642d84e752d25f41827f7c2b" - integrity sha512-Wtl6PUL26jEbC1NBqJi7uoyYZo1/I3EDCd9pZk9EN6ZDvKaO28M5+nIQGyYomzvkMpMHnfywpTzalhwr76/oAg== - -"@types/graceful-fs@^4.1.3": - version "4.1.6" - resolved "https://registry.yarnpkg.com/@types/graceful-fs/-/graceful-fs-4.1.6.tgz#e14b2576a1c25026b7f02ede1de3b84c3a1efeae" - integrity sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw== - dependencies: - "@types/node" "*" - -"@types/hast@^2.0.0": - version "2.3.4" - resolved "https://registry.yarnpkg.com/@types/hast/-/hast-2.3.4.tgz#8aa5ef92c117d20d974a82bdfb6a648b08c0bafc" - integrity sha512-wLEm0QvaoawEDoTRwzTXp4b4jpwiJDvR5KMnFnVodm3scufTlBOWRD6N1OBf9TZMhjlNsSfcO5V+7AF4+Vy+9g== - dependencies: - "@types/unist" "*" - -"@types/hogan.js@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/hogan.js/-/hogan.js-3.0.1.tgz#64c54407b30da359763e14877f5702b8ae85d61c" - integrity sha512-D03i/2OY7kGyMq9wdQ7oD8roE49z/ZCZThe/nbahtvuqCNZY9T2MfedOWyeBdbEpY2W8Gnh/dyJLdFtUCOkYbg== - -"@types/http-cache-semantics@*": - version "4.0.1" - resolved "https://registry.yarnpkg.com/@types/http-cache-semantics/-/http-cache-semantics-4.0.1.tgz#0ea7b61496902b95890dc4c3a116b60cb8dae812" - integrity sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ== - -"@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0", "@types/istanbul-lib-coverage@^2.0.1": - version "2.0.4" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz#8467d4b3c087805d63580480890791277ce35c44" - integrity sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g== - -"@types/istanbul-lib-report@*": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#c14c24f18ea8190c118ee7562b7ff99a36552686" - integrity sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg== - dependencies: - "@types/istanbul-lib-coverage" "*" - -"@types/istanbul-reports@^3.0.0": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz#9153fe98bba2bd565a63add9436d6f0d7f8468ff" - integrity sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw== - dependencies: - "@types/istanbul-lib-report" "*" - -"@types/jest@^29.2.4": - version "29.4.0" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-29.4.0.tgz#a8444ad1704493e84dbf07bb05990b275b3b9206" - integrity sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ== - dependencies: - expect "^29.0.0" - pretty-format "^29.0.0" - -"@types/keyv@^3.1.4": - version "3.1.4" - resolved "https://registry.yarnpkg.com/@types/keyv/-/keyv-3.1.4.tgz#3ccdb1c6751b0c7e52300bcdacd5bcbf8faa75b6" - integrity sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg== - dependencies: - "@types/node" "*" - -"@types/mdast@^3.0.0": - version "3.0.10" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.10.tgz#4724244a82a4598884cbbe9bcfd73dff927ee8af" - integrity sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA== - dependencies: - "@types/unist" "*" - -"@types/ms@*": - version "0.7.31" - resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" - integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== - -"@types/node@*": - version "18.13.0" - resolved "https://registry.yarnpkg.com/@types/node/-/node-18.13.0.tgz#0400d1e6ce87e9d3032c19eb6c58205b0d3f7850" - integrity sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg== - -"@types/parse5@^6.0.0": - version "6.0.3" - resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-6.0.3.tgz#705bb349e789efa06f43f128cef51240753424cb" - integrity sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g== - -"@types/prettier@^2.1.5": - version "2.7.2" - resolved "https://registry.yarnpkg.com/@types/prettier/-/prettier-2.7.2.tgz#6c2324641cc4ba050a8c710b2b251b377581fbf0" - integrity sha512-KufADq8uQqo1pYKVIYzfKbJfBAc0sOeXqGbFaSpv8MRmC/zXgowNZmFcbngndGk922QDmOASEXUZCaY48gs4cg== - -"@types/qs@^6.5.3": - version "6.9.7" - resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.7.tgz#63bb7d067db107cc1e457c303bc25d511febf6cb" - integrity sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw== - -"@types/resolve@1.20.2": - version "1.20.2" - resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" - integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== - -"@types/responselike@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/responselike/-/responselike-1.0.0.tgz#251f4fe7d154d2bad125abe1b429b23afd262e29" - integrity sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== - dependencies: - "@types/node" "*" - -"@types/stack-utils@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" - integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== - -"@types/unist@*", "@types/unist@^2.0.0": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" - integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== - -"@types/web-bluetooth@^0.0.16": - version "0.0.16" - resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz#1d12873a8e49567371f2a75fe3e7f7edca6662d8" - integrity sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ== - -"@types/yargs-parser@*": - version "21.0.0" - resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.0.tgz#0c60e537fa790f5f9472ed2776c2b71ec117351b" - integrity sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA== - -"@types/yargs@^17.0.8": - version "17.0.22" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-17.0.22.tgz#7dd37697691b5f17d020f3c63e7a45971ff71e9a" - integrity sha512-pet5WJ9U8yPVRhkwuEIp5ktAeAqRZOq4UdAyWLWzxbtpyXnzbtLdKiXAjJzi/KLmPGS9wk86lUFWZFN6sISo4g== - dependencies: - "@types/yargs-parser" "*" - -"@unhead/dom@1.0.21", "@unhead/dom@^1.0.21": - version "1.0.21" - resolved "https://registry.yarnpkg.com/@unhead/dom/-/dom-1.0.21.tgz#f42c004bd26046f4b2a870c3bac3282e88015a73" - integrity sha512-rwVz7NWMdQ8kSTXv/WOhB0eTWYFD2SQwQ/J109IEqNUN9X3pIwcvdvlXMCG+qhJGFyiIgOl2X+W0cE+u/IiLVA== - dependencies: - "@unhead/schema" "1.0.21" - -"@unhead/schema@1.0.21", "@unhead/schema@^1.0.21": - version "1.0.21" - resolved "https://registry.yarnpkg.com/@unhead/schema/-/schema-1.0.21.tgz#7b4b3ad118241682ad297bd100632ff239732184" - integrity sha512-amYg6vJ37xUhnL6bvL4S3lz6yDs5lWeqJu63/3a5bxH3Dq0WPJ+kdhpUXI+4enoNaWvLvm860WXUOtKr5D+DMg== - dependencies: - "@zhead/schema" "^1.1.0" - hookable "^5.4.2" - -"@unhead/ssr@^1.0.20", "@unhead/ssr@^1.0.21": - version "1.0.21" - resolved "https://registry.yarnpkg.com/@unhead/ssr/-/ssr-1.0.21.tgz#f9b9828c74b05666c08b8b32366f4a21cec10bd7" - integrity sha512-QWy+vKZWVb+XfHl/B/rEoniMGFpDjXiYBkjJZyuf+9By8DzQUscMaTv14neW1ZR6pq56c4B7Tp1N3Lve8SW+rA== - dependencies: - "@unhead/schema" "1.0.21" - -"@unhead/vue@^1.0.21": - version "1.0.21" - resolved "https://registry.yarnpkg.com/@unhead/vue/-/vue-1.0.21.tgz#7d43301e682dec3fa41e883dc2517b6673d40808" - integrity sha512-UCwgY4MbQEnFUo+/xmzBPK3PjC+oeCCzSsgK6eLk3vUC8Cuarrvw06wy8s0cO94DkpAi56Ih9oRWA16a/tih1A== - dependencies: - "@unhead/schema" "1.0.21" - hookable "^5.4.2" - -"@unocss/reset@^0.49.4": - version "0.49.4" - resolved "https://registry.yarnpkg.com/@unocss/reset/-/reset-0.49.4.tgz#bac937c96fef3ed85d2d39b7e013578a0a950c0a" - integrity sha512-+9j4bN4cWlsWr3HGlFk+bAb7+1DdwTxQM3UbHjd9QsKVAVV1gE0VHHxU207NOYsIdeBFAOFVkxqFYCyhnfQpnQ== - -"@vercel/nft@^0.22.6": - version "0.22.6" - resolved "https://registry.yarnpkg.com/@vercel/nft/-/nft-0.22.6.tgz#edb30d300bb809c0945ea4c7b87e56f634885541" - integrity sha512-gTsFnnT4mGxodr4AUlW3/urY+8JKKB452LwF3m477RFUJTAaDmcz2JqFuInzvdybYIeyIv1sSONEJxsxnbQ5JQ== - dependencies: - "@mapbox/node-pre-gyp" "^1.0.5" - "@rollup/pluginutils" "^4.0.0" - acorn "^8.6.0" - async-sema "^3.1.1" - bindings "^1.4.0" - estree-walker "2.0.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - micromatch "^4.0.2" - node-gyp-build "^4.2.2" - resolve-from "^5.0.0" - -"@vitejs/plugin-vue-jsx@^3.0.0": - version "3.0.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue-jsx/-/plugin-vue-jsx-3.0.0.tgz#42e89d6d9eb89604d109ff9a615d77c3c080dd25" - integrity sha512-vurkuzgac5SYuxd2HUZqAFAWGTF10diKBwJNbCvnWijNZfXd+7jMtqjPFbGt7idOJUn584fP1Ar9j/GN2jQ3Ew== - dependencies: - "@babel/core" "^7.20.5" - "@babel/plugin-transform-typescript" "^7.20.2" - "@vue/babel-plugin-jsx" "^1.1.1" - -"@vitejs/plugin-vue@^4.0.0": - version "4.0.0" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-4.0.0.tgz#93815beffd23db46288c787352a8ea31a0c03e5e" - integrity sha512-e0X4jErIxAB5oLtDqbHvHpJe/uWNkdpYV83AOG2xo2tEVSzCzewgJMtREZM30wXnM5ls90hxiOtAuVU6H5JgbA== - -"@volar/language-core@1.0.24": - version "1.0.24" - resolved "https://registry.yarnpkg.com/@volar/language-core/-/language-core-1.0.24.tgz#5d767571e77728464635e61af1debca944811fe0" - integrity sha512-vTN+alJiWwK0Pax6POqrmevbtFW2dXhjwWiW/MW4f48eDYPLdyURWcr8TixO7EN/nHsUBj2udT7igFKPtjyAKg== - dependencies: - "@volar/source-map" "1.0.24" - muggle-string "^0.1.0" - -"@volar/source-map@1.0.24": - version "1.0.24" - resolved "https://registry.yarnpkg.com/@volar/source-map/-/source-map-1.0.24.tgz#ad4c827fea5c26b4bf38a86d983e7deb65b1c61e" - integrity sha512-Qsv/tkplx18pgBr8lKAbM1vcDqgkGKQzbChg6NW+v0CZc3G7FLmK+WrqEPzKlN7Cwdc6XVL559Nod8WKAfKr4A== - dependencies: - muggle-string "^0.1.0" - -"@volar/vue-language-core@1.0.24", "@volar/vue-language-core@^1.0.24": - version "1.0.24" - resolved "https://registry.yarnpkg.com/@volar/vue-language-core/-/vue-language-core-1.0.24.tgz#81d180a8e09a53cb575e83acb79a31493891a1a4" - integrity sha512-2NTJzSgrwKu6uYwPqLiTMuAzi7fAY3yFy5PJ255bGJc82If0Xr+cW8pC80vpjG0D/aVLmlwAdO4+Ya2BI8GdDg== - dependencies: - "@volar/language-core" "1.0.24" - "@volar/source-map" "1.0.24" - "@vue/compiler-dom" "^3.2.45" - "@vue/compiler-sfc" "^3.2.45" - "@vue/reactivity" "^3.2.45" - "@vue/shared" "^3.2.45" - minimatch "^5.1.1" - vue-template-compiler "^2.7.14" - -"@vue/babel-helper-vue-transform-on@^1.0.2": - version "1.0.2" - resolved "https://registry.yarnpkg.com/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz#9b9c691cd06fc855221a2475c3cc831d774bc7dc" - integrity sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA== - -"@vue/babel-plugin-jsx@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz#0c5bac27880d23f89894cd036a37b55ef61ddfc1" - integrity sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w== - dependencies: - "@babel/helper-module-imports" "^7.0.0" - "@babel/plugin-syntax-jsx" "^7.0.0" - "@babel/template" "^7.0.0" - "@babel/traverse" "^7.0.0" - "@babel/types" "^7.0.0" - "@vue/babel-helper-vue-transform-on" "^1.0.2" - camelcase "^6.0.0" - html-tags "^3.1.0" - svg-tags "^1.0.0" - -"@vue/compiler-core@3.2.47": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.2.47.tgz#3e07c684d74897ac9aa5922c520741f3029267f8" - integrity sha512-p4D7FDnQb7+YJmO2iPEv0SQNeNzcbHdGByJDsT4lynf63AFkOTFN07HsiRSvjGo0QrxR/o3d0hUyNCUnBU2Tig== - dependencies: - "@babel/parser" "^7.16.4" - "@vue/shared" "3.2.47" - estree-walker "^2.0.2" - source-map "^0.6.1" - -"@vue/compiler-dom@3.2.47", "@vue/compiler-dom@^3.2.45": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.2.47.tgz#a0b06caf7ef7056939e563dcaa9cbde30794f305" - integrity sha512-dBBnEHEPoftUiS03a4ggEig74J2YBZ2UIeyfpcRM2tavgMWo4bsEfgCGsu+uJIL/vax9S+JztH8NmQerUo7shQ== - dependencies: - "@vue/compiler-core" "3.2.47" - "@vue/shared" "3.2.47" - -"@vue/compiler-sfc@3.2.47", "@vue/compiler-sfc@^3.2.45": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.2.47.tgz#1bdc36f6cdc1643f72e2c397eb1a398f5004ad3d" - integrity sha512-rog05W+2IFfxjMcFw10tM9+f7i/+FFpZJJ5XHX72NP9eC2uRD+42M3pYcQqDXVYoj74kHMSEdQ/WmCjt8JFksQ== - dependencies: - "@babel/parser" "^7.16.4" - "@vue/compiler-core" "3.2.47" - "@vue/compiler-dom" "3.2.47" - "@vue/compiler-ssr" "3.2.47" - "@vue/reactivity-transform" "3.2.47" - "@vue/shared" "3.2.47" - estree-walker "^2.0.2" - magic-string "^0.25.7" - postcss "^8.1.10" - source-map "^0.6.1" - -"@vue/compiler-ssr@3.2.47": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.2.47.tgz#35872c01a273aac4d6070ab9d8da918ab13057ee" - integrity sha512-wVXC+gszhulcMD8wpxMsqSOpvDZ6xKXSVWkf50Guf/S+28hTAXPDYRTbLQ3EDkOP5Xz/+SY37YiwDquKbJOgZw== - dependencies: - "@vue/compiler-dom" "3.2.47" - "@vue/shared" "3.2.47" - -"@vue/devtools-api@^6.4.5": - version "6.5.0" - resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.5.0.tgz#98b99425edee70b4c992692628fa1ea2c1e57d07" - integrity sha512-o9KfBeaBmCKl10usN4crU53fYtC1r7jJwdGKjPT24t348rHxgfpZ0xL3Xm/gLUYnc0oTp8LAmrxOeLyu6tbk2Q== - -"@vue/reactivity-transform@3.2.47": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/reactivity-transform/-/reactivity-transform-3.2.47.tgz#e45df4d06370f8abf29081a16afd25cffba6d84e" - integrity sha512-m8lGXw8rdnPVVIdIFhf0LeQ/ixyHkH5plYuS83yop5n7ggVJU+z5v0zecwEnX7fa7HNLBhh2qngJJkxpwEEmYA== - dependencies: - "@babel/parser" "^7.16.4" - "@vue/compiler-core" "3.2.47" - "@vue/shared" "3.2.47" - estree-walker "^2.0.2" - magic-string "^0.25.7" - -"@vue/reactivity@3.2.47", "@vue/reactivity@^3.2.45", "@vue/reactivity@^3.2.47": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.2.47.tgz#1d6399074eadfc3ed35c727e2fd707d6881140b6" - integrity sha512-7khqQ/75oyyg+N/e+iwV6lpy1f5wq759NdlS1fpAhFXa8VeAIKGgk2E/C4VF59lx5b+Ezs5fpp/5WsRYXQiKxQ== - dependencies: - "@vue/shared" "3.2.47" - -"@vue/runtime-core@3.2.47": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.2.47.tgz#406ebade3d5551c00fc6409bbc1eeb10f32e121d" - integrity sha512-RZxbLQIRB/K0ev0K9FXhNbBzT32H9iRtYbaXb0ZIz2usLms/D55dJR2t6cIEUn6vyhS3ALNvNthI+Q95C+NOpA== - dependencies: - "@vue/reactivity" "3.2.47" - "@vue/shared" "3.2.47" - -"@vue/runtime-dom@3.2.47": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.2.47.tgz#93e760eeaeab84dedfb7c3eaf3ed58d776299382" - integrity sha512-ArXrFTjS6TsDei4qwNvgrdmHtD930KgSKGhS5M+j8QxXrDJYLqYw4RRcDy1bz1m1wMmb6j+zGLifdVHtkXA7gA== - dependencies: - "@vue/runtime-core" "3.2.47" - "@vue/shared" "3.2.47" - csstype "^2.6.8" - -"@vue/server-renderer@3.2.47": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.2.47.tgz#8aa1d1871fc4eb5a7851aa7f741f8f700e6de3c0" - integrity sha512-dN9gc1i8EvmP9RCzvneONXsKfBRgqFeFZLurmHOveL7oH6HiFXJw5OGu294n1nHc/HMgTy6LulU/tv5/A7f/LA== - dependencies: - "@vue/compiler-ssr" "3.2.47" - "@vue/shared" "3.2.47" - -"@vue/shared@3.2.47", "@vue/shared@^3.2.45", "@vue/shared@^3.2.47": - version "3.2.47" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.2.47.tgz#e597ef75086c6e896ff5478a6bfc0a7aa4bbd14c" - integrity sha512-BHGyyGN3Q97EZx0taMQ+OLNuZcW3d37ZEVmEAyeoA9ERdGvm9Irc/0Fua8SNyOtV1w6BS4q25wbMzJujO9HIfQ== - -"@vueuse/core@9.12.0", "@vueuse/core@^9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-9.12.0.tgz#e5b20f901e081c7ae5fe0e5f3af217929034eefe" - integrity sha512-h/Di8Bvf6xRcvS/PvUVheiMYYz3U0tH3X25YxONSaAUBa841ayMwxkuzx/DGUMCW/wHWzD8tRy2zYmOC36r4sg== - dependencies: - "@types/web-bluetooth" "^0.0.16" - "@vueuse/metadata" "9.12.0" - "@vueuse/shared" "9.12.0" - vue-demi "*" - -"@vueuse/head@^1.0.24": - version "1.0.25" - resolved "https://registry.yarnpkg.com/@vueuse/head/-/head-1.0.25.tgz#5eec15535b07fea13d9e94dd16a4a52862de129a" - integrity sha512-ACfRqD3bbh92cIzDDR1CmqShXCXhQv/EUUcaDMYaexA4ulorYHd+2Yo5/ljoS4jDoMgsqBSP0XJZT3nySMB5gw== - dependencies: - "@unhead/dom" "^1.0.21" - "@unhead/schema" "^1.0.21" - "@unhead/ssr" "^1.0.21" - "@unhead/vue" "^1.0.21" - -"@vueuse/metadata@9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-9.12.0.tgz#19a0fefcba6a66a2382af10a7a67ebad6eec1f27" - integrity sha512-9oJ9MM9lFLlmvxXUqsR1wLt1uF7EVbP5iYaHJYqk+G2PbMjY6EXvZeTjbdO89HgoF5cI6z49o2zT/jD9SVoNpQ== - -"@vueuse/nuxt@^9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@vueuse/nuxt/-/nuxt-9.12.0.tgz#0c09fa3beec4a91f38c0c0fab9ff3963a600c2c1" - integrity sha512-zT7ieMmJgyB+hpQ6aG2HiyvNFHm5D/s2Z7fQjWcxuDI9Xawr7ECWBkSinxwrmtZ7+0lacXak+VMFpbx/z/zp2Q== - dependencies: - "@nuxt/kit" "^3.1.1" - "@vueuse/core" "9.12.0" - "@vueuse/metadata" "9.12.0" - local-pkg "^0.4.3" - vue-demi "*" - -"@vueuse/shared@9.12.0": - version "9.12.0" - resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-9.12.0.tgz#e6597da80084cba8fc3d6545f4c2fa9817b80428" - integrity sha512-TWuJLACQ0BVithVTRbex4Wf1a1VaRuSpVeyEd4vMUWl54PzlE0ciFUshKCXnlLuD0lxIaLK4Ypj3NXYzZh4+SQ== - dependencies: - vue-demi "*" - -"@zhead/schema@^1.1.0": - version "1.1.0" - resolved "https://registry.yarnpkg.com/@zhead/schema/-/schema-1.1.0.tgz#5c500382ad0f63e347bfe3cf83b95fb19f5a3d6d" - integrity sha512-hEtK+hUAKS3w1+F++m6EeZ6bWeLDXraqN2nCyRVIP5vvR3bWjXVP9OM9x7Pmn7Hp6T7FKmsG2C8rvouQU2806w== - -abbrev@1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" - integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q== - -acorn-node@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/acorn-node/-/acorn-node-1.8.2.tgz#114c95d64539e53dede23de8b9d96df7c7ae2af8" - integrity sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A== - dependencies: - acorn "^7.0.0" - acorn-walk "^7.0.0" - xtend "^4.0.2" - -acorn-walk@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" - integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== - -acorn@^7.0.0: - version "7.4.1" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" - integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== - -acorn@^8.5.0, acorn@^8.6.0, acorn@^8.8.1, acorn@^8.8.2: - version "8.8.2" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.2.tgz#1b2f25db02af965399b9776b0c2c391276d37c4a" - integrity sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw== - -agent-base@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" - integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== - dependencies: - debug "4" - -algoliasearch-helper@^3.11.3: - version "3.11.3" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.11.3.tgz#6e7af8afe6f9a9e55186abffb7b6cf7ca8de3301" - integrity sha512-TbaEvLwiuGygHQIB8y+OsJKQQ40+JKUua5B91X66tMUHyyhbNHvqyr0lqd3wCoyKx7WybyQrC0WJvzoIeh24Aw== - dependencies: - "@algolia/events" "^4.0.1" - -algoliasearch@^4.0.0, algoliasearch@^4.11.0, algoliasearch@^4.12.0: - version "4.14.3" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-4.14.3.tgz#f02a77a4db17de2f676018938847494b692035e7" - integrity sha512-GZTEuxzfWbP/vr7ZJfGzIl8fOsoxN916Z6FY2Egc9q2TmZ6hvq5KfAxY89pPW01oW/2HDEKA8d30f9iAH9eXYg== - dependencies: - "@algolia/cache-browser-local-storage" "4.14.3" - "@algolia/cache-common" "4.14.3" - "@algolia/cache-in-memory" "4.14.3" - "@algolia/client-account" "4.14.3" - "@algolia/client-analytics" "4.14.3" - "@algolia/client-common" "4.14.3" - "@algolia/client-personalization" "4.14.3" - "@algolia/client-search" "4.14.3" - "@algolia/logger-common" "4.14.3" - "@algolia/logger-console" "4.14.3" - "@algolia/requester-browser-xhr" "4.14.3" - "@algolia/requester-common" "4.14.3" - "@algolia/requester-node-http" "4.14.3" - "@algolia/transporter" "4.14.3" - -ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: - version "4.3.2" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" - integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== - dependencies: - type-fest "^0.21.3" - -ansi-escapes@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-6.0.0.tgz#68c580e87a489f6df3d761028bb93093fde6bd8a" - integrity sha512-IG23inYII3dWlU2EyiAiGj6Bwal5GzsgPMwjYGvc1HPE2dgbj4ZB5ToWBKSquKw74nB3TIuOwaI6/jSULzfgrw== - dependencies: - type-fest "^3.0.0" - -ansi-regex@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" - integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== - -ansi-regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.0.1.tgz#3183e38fae9a65d7cb5e53945cd5897d0260a06a" - integrity sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA== - -ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -ansi-styles@^4.0.0, ansi-styles@^4.1.0: - version "4.3.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" - integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== - dependencies: - color-convert "^2.0.1" - -ansi-styles@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-5.2.0.tgz#07449690ad45777d1924ac2abb2fc8895dba836b" - integrity sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== - -ansi-styles@^6.1.0: - version "6.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" - integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== - -anymatch@^3.0.3, anymatch@^3.1.3, anymatch@~3.1.2: - version "3.1.3" - resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" - integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== - dependencies: - normalize-path "^3.0.0" - picomatch "^2.0.4" - -"aproba@^1.0.3 || ^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/aproba/-/aproba-2.0.0.tgz#52520b8ae5b569215b354efc0caa3fe1e45a8adc" - integrity sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ== - -arch@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/arch/-/arch-2.2.0.tgz#1bc47818f305764f23ab3306b0bfc086c5a29d11" - integrity sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ== - -archiver-utils@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/archiver-utils/-/archiver-utils-2.1.0.tgz#e8a460e94b693c3e3da182a098ca6285ba9249e2" - integrity sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw== - dependencies: - glob "^7.1.4" - graceful-fs "^4.2.0" - lazystream "^1.0.0" - lodash.defaults "^4.2.0" - lodash.difference "^4.5.0" - lodash.flatten "^4.4.0" - lodash.isplainobject "^4.0.6" - lodash.union "^4.6.0" - normalize-path "^3.0.0" - readable-stream "^2.0.0" - -archiver@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/archiver/-/archiver-5.3.1.tgz#21e92811d6f09ecfce649fbefefe8c79e57cbbb6" - integrity sha512-8KyabkmbYrH+9ibcTScQ1xCJC/CGcugdVIwB+53f5sZziXgwUh3iXlAlANMxcZyDEfTHMe6+Z5FofV8nopXP7w== - dependencies: - archiver-utils "^2.1.0" - async "^3.2.3" - buffer-crc32 "^0.2.1" - readable-stream "^3.6.0" - readdir-glob "^1.0.0" - tar-stream "^2.2.0" - zip-stream "^4.1.0" - -are-we-there-yet@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" - integrity sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw== - dependencies: - delegates "^1.0.0" - readable-stream "^3.6.0" - -arg@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/arg/-/arg-5.0.2.tgz#c81433cc427c92c4dcf4865142dbca6f15acd59c" - integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -argparse@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" - integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== - -assert@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/assert/-/assert-2.0.0.tgz#95fc1c616d48713510680f2eaf2d10dd22e02d32" - integrity sha512-se5Cd+js9dXJnu6Ag2JFc00t+HmHOen+8Q+L7O9zI0PqQXr20uk2J0XQqMxZEeo5U50o8Nvmmx7dZrl+Ufr35A== - dependencies: - es6-object-assign "^1.1.0" - is-nan "^1.2.1" - object-is "^1.0.1" - util "^0.12.0" - -assertion-error@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/assertion-error/-/assertion-error-1.1.0.tgz#e60b6b0e8f301bd97e5375215bda406c85118c0b" - integrity sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw== - -ast-types@0.14.2: - version "0.14.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.14.2.tgz#600b882df8583e3cd4f2df5fa20fa83759d4bdfd" - integrity sha512-O0yuUDnZeQDL+ncNGlJ78BiO4jnYI3bvMsD5prT0/nsgijG/LpNBIr63gTjVTNsiGkgQhiyCShTgxt8oXOrklA== - dependencies: - tslib "^2.0.1" - -ast-types@0.15.2: - version "0.15.2" - resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.15.2.tgz#39ae4809393c4b16df751ee563411423e85fb49d" - integrity sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg== - dependencies: - tslib "^2.0.1" - -async-sema@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/async-sema/-/async-sema-3.1.1.tgz#e527c08758a0f8f6f9f15f799a173ff3c40ea808" - integrity sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg== - -async@^3.2.3: - version "3.2.4" - resolved "https://registry.yarnpkg.com/async/-/async-3.2.4.tgz#2d22e00f8cddeb5fde5dd33522b56d1cf569a81c" - integrity sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ== - -autoprefixer@^10.4.13: - version "10.4.13" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-10.4.13.tgz#b5136b59930209a321e9fa3dca2e7c4d223e83a8" - integrity sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg== - dependencies: - browserslist "^4.21.4" - caniuse-lite "^1.0.30001426" - fraction.js "^4.2.0" - normalize-range "^0.1.2" - picocolors "^1.0.0" - postcss-value-parser "^4.2.0" - -available-typed-arrays@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz#92f95616501069d07d10edb2fc37d3e1c65123b7" - integrity sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw== - -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== - dependencies: - follow-redirects "^1.14.7" - -babel-jest@^29.3.1, babel-jest@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-29.4.2.tgz#b17b9f64be288040877cbe2649f91ac3b63b2ba6" - integrity sha512-vcghSqhtowXPG84posYkkkzcZsdayFkubUgbE3/1tuGbX7AQtwCkkNA/wIbB0BMjuCPoqTkiDyKN7Ty7d3uwNQ== - dependencies: - "@jest/transform" "^29.4.2" - "@types/babel__core" "^7.1.14" - babel-plugin-istanbul "^6.1.1" - babel-preset-jest "^29.4.2" - chalk "^4.0.0" - graceful-fs "^4.2.9" - slash "^3.0.0" - -babel-plugin-istanbul@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz#fa88ec59232fd9b4e36dbbc540a8ec9a9b47da73" - integrity sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA== - dependencies: - "@babel/helper-plugin-utils" "^7.0.0" - "@istanbuljs/load-nyc-config" "^1.0.0" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-instrument "^5.0.4" - test-exclude "^6.0.0" - -babel-plugin-jest-hoist@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.4.2.tgz#22aa43e255230f02371ffef1cac7eedef58f60bc" - integrity sha512-5HZRCfMeWypFEonRbEkwWXtNS1sQK159LhRVyRuLzyfVBxDy/34Tr/rg4YVi0SScSJ4fqeaR/OIeceJ/LaQ0pQ== - dependencies: - "@babel/template" "^7.3.3" - "@babel/types" "^7.3.3" - "@types/babel__core" "^7.1.14" - "@types/babel__traverse" "^7.0.6" - -babel-preset-current-node-syntax@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz#b4399239b89b2a011f9ddbe3e4f401fc40cff73b" - integrity sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ== - dependencies: - "@babel/plugin-syntax-async-generators" "^7.8.4" - "@babel/plugin-syntax-bigint" "^7.8.3" - "@babel/plugin-syntax-class-properties" "^7.8.3" - "@babel/plugin-syntax-import-meta" "^7.8.3" - "@babel/plugin-syntax-json-strings" "^7.8.3" - "@babel/plugin-syntax-logical-assignment-operators" "^7.8.3" - "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.3" - "@babel/plugin-syntax-numeric-separator" "^7.8.3" - "@babel/plugin-syntax-object-rest-spread" "^7.8.3" - "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" - "@babel/plugin-syntax-optional-chaining" "^7.8.3" - "@babel/plugin-syntax-top-level-await" "^7.8.3" - -babel-preset-jest@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-29.4.2.tgz#f0b20c6a79a9f155515e72a2d4f537fe002a4e38" - integrity sha512-ecWdaLY/8JyfUDr0oELBMpj3R5I1L6ZqG+kRJmwqfHtLWuPrJStR0LUkvUhfykJWTsXXMnohsayN/twltBbDrQ== - dependencies: - babel-plugin-jest-hoist "^29.4.2" - babel-preset-current-node-syntax "^1.0.0" - -bail@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/bail/-/bail-2.0.2.tgz#d26f5cd8fe5d6f832a31517b9f7c356040ba6d5d" - integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw== - -balanced-match@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" - integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== - -base64-js@^1.3.1: - version "1.5.1" - resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" - integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== - -binary-extensions@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" - integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== - -bindings@^1.4.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" - integrity sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ== - dependencies: - file-uri-to-path "1.0.0" - -bl@^4.0.3: - version "4.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" - integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== - dependencies: - buffer "^5.5.0" - inherits "^2.0.4" - readable-stream "^3.4.0" - -bl@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/bl/-/bl-5.1.0.tgz#183715f678c7188ecef9fe475d90209400624273" - integrity sha512-tv1ZJHLfTDnXE6tMHv73YgSJaWR2AFuPwMntBe7XL/GBFHnT0CLnsHMogfk5+GzCDC5ZWarSCYaIGATZt9dNsQ== - dependencies: - buffer "^6.0.3" - inherits "^2.0.4" - readable-stream "^3.4.0" - -boolbase@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e" - integrity sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww== - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -brace-expansion@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" - integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== - dependencies: - balanced-match "^1.0.0" - -braces@^3.0.2, braces@~3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - -browserslist@^4.0.0, browserslist@^4.16.6, browserslist@^4.21.3, browserslist@^4.21.4: - version "4.21.5" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.21.5.tgz#75c5dae60063ee641f977e00edd3cfb2fb7af6a7" - integrity sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w== - dependencies: - caniuse-lite "^1.0.30001449" - electron-to-chromium "^1.4.284" - node-releases "^2.0.8" - update-browserslist-db "^1.0.10" - -bs-logger@0.x: - version "0.2.6" - resolved "https://registry.yarnpkg.com/bs-logger/-/bs-logger-0.2.6.tgz#eb7d365307a72cf974cc6cda76b68354ad336bd8" - integrity sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog== - dependencies: - fast-json-stable-stringify "2.x" - -bser@2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/bser/-/bser-2.1.1.tgz#e6787da20ece9d07998533cfd9de6f5c38f4bc05" - integrity sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ== - dependencies: - node-int64 "^0.4.0" - -buffer-crc32@^0.2.1, buffer-crc32@^0.2.13: - version "0.2.13" - resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" - integrity sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ== - -buffer-from@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" - integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== - -buffer@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" - integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.1.13" - -buffer@^6.0.3: - version "6.0.3" - resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" - integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== - dependencies: - base64-js "^1.3.1" - ieee754 "^1.2.1" - -builtin-modules@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" - integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== - -c12@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/c12/-/c12-1.1.0.tgz#2d73596c885f0b990dcd91244b15e3c0405ebbeb" - integrity sha512-9KRFWEng+TH8sGST4NNdiKzZGw1Z1CHnPGAmNqAyVP7suluROmBjD8hsiR34f94DdlrvtGvvmiGDsoFXlCBWIw== - dependencies: - defu "^6.1.1" - dotenv "^16.0.3" - giget "^1.0.0" - jiti "^1.16.0" - mlly "^1.0.0" - pathe "^1.0.0" - pkg-types "^1.0.1" - rc9 "^2.0.0" - -cac@^6.7.14: - version "6.7.14" - resolved "https://registry.yarnpkg.com/cac/-/cac-6.7.14.tgz#804e1e6f506ee363cb0e3ccbb09cad5dd9870959" - integrity sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== - -cacheable-lookup@^5.0.3: - version "5.0.4" - resolved "https://registry.yarnpkg.com/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz#5a6b865b2c44357be3d5ebc2a467b032719a7005" - integrity sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA== - -cacheable-request@^7.0.2: - version "7.0.2" - resolved "https://registry.yarnpkg.com/cacheable-request/-/cacheable-request-7.0.2.tgz#ea0d0b889364a25854757301ca12b2da77f91d27" - integrity sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew== - dependencies: - clone-response "^1.0.2" - get-stream "^5.1.0" - http-cache-semantics "^4.0.0" - keyv "^4.0.0" - lowercase-keys "^2.0.0" - normalize-url "^6.0.1" - responselike "^2.0.0" - -call-bind@^1.0.0, call-bind@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" - integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== - dependencies: - function-bind "^1.1.1" - get-intrinsic "^1.0.2" - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camel-case@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/camel-case/-/camel-case-4.1.2.tgz#9728072a954f805228225a6deea6b38461e1bd5a" - integrity sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw== - dependencies: - pascal-case "^3.1.2" - tslib "^2.0.3" - -camelcase-css@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" - integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== - -camelcase@^5.3.1: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -camelcase@^6.0.0, camelcase@^6.2.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" - integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== - -caniuse-api@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" - integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== - dependencies: - browserslist "^4.0.0" - caniuse-lite "^1.0.0" - lodash.memoize "^4.1.2" - lodash.uniq "^4.5.0" - -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001426, caniuse-lite@^1.0.30001449: - version "1.0.30001451" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz#2e197c698fc1373d63e1406d6607ea4617c613f1" - integrity sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w== - -capital-case@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/capital-case/-/capital-case-1.0.4.tgz#9d130292353c9249f6b00fa5852bee38a717e669" - integrity sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - upper-case-first "^2.0.2" - -ccount@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" - integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== - -chai@^4.3.7: - version "4.3.7" - resolved "https://registry.yarnpkg.com/chai/-/chai-4.3.7.tgz#ec63f6df01829088e8bf55fca839bcd464a8ec51" - integrity sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A== - dependencies: - assertion-error "^1.1.0" - check-error "^1.0.2" - deep-eql "^4.1.2" - get-func-name "^2.0.0" - loupe "^2.3.1" - pathval "^1.1.1" - type-detect "^4.0.5" - -chalk@^2.0.0: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chalk@^4.0.0, chalk@^4.1.1, chalk@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" - integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== - dependencies: - ansi-styles "^4.1.0" - supports-color "^7.1.0" - -chalk@^5.0.0, chalk@^5.1.2, chalk@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-5.2.0.tgz#249623b7d66869c673699fb66d65723e54dfcfb3" - integrity sha512-ree3Gqw/nazQAPuJJEy+avdl7QfZMcUvmHIKgEZkGL+xOBzRvup5Hxo6LHuMceSxOabuJLJm5Yp/92R9eMmMvA== - -change-case@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/change-case/-/change-case-4.1.2.tgz#fedfc5f136045e2398c0410ee441f95704641e12" - integrity sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A== - dependencies: - camel-case "^4.1.2" - capital-case "^1.0.4" - constant-case "^3.0.4" - dot-case "^3.0.4" - header-case "^2.0.4" - no-case "^3.0.4" - param-case "^3.0.4" - pascal-case "^3.1.2" - path-case "^3.0.4" - sentence-case "^3.0.4" - snake-case "^3.0.4" - tslib "^2.0.3" - -changelogen@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/changelogen/-/changelogen-0.4.1.tgz#41361aed69ea963154f92dc28d85071d7d8ed461" - integrity sha512-p1dJO1Z995odIxdypzAykHIaUu+XnEvwYPSTyKJsbpL82o99sxN1G24tbecoMxTsV4PI+ZId82GJXRL2hhOeJA== - dependencies: - c12 "^1.1.0" - consola "^2.15.3" - convert-gitmoji "^0.1.3" - execa "^6.1.0" - mri "^1.2.0" - node-fetch-native "^1.0.1" - pkg-types "^1.0.1" - scule "^1.0.0" - semver "^7.3.8" - -char-regex@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" - integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== - -character-entities-html4@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/character-entities-html4/-/character-entities-html4-2.1.0.tgz#1f1adb940c971a4b22ba39ddca6b618dc6e56b2b" - integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA== - -character-entities-legacy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz#76bc83a90738901d7bc223a9e93759fdd560125b" - integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ== - -character-entities@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" - integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== - -character-reference-invalid@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz#85c66b041e43b47210faf401278abf808ac45cb9" - integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw== - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -check-error@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/check-error/-/check-error-1.0.2.tgz#574d312edd88bb5dd8912e9286dd6c0aed4aac82" - integrity sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA== - -chokidar@^3.5.1, chokidar@^3.5.3: - version "3.5.3" - resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" - integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== - dependencies: - anymatch "~3.1.2" - braces "~3.0.2" - glob-parent "~5.1.2" - is-binary-path "~2.1.0" - is-glob "~4.0.1" - normalize-path "~3.0.0" - readdirp "~3.6.0" - optionalDependencies: - fsevents "~2.3.2" - -chownr@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/chownr/-/chownr-2.0.0.tgz#15bfbe53d2eab4cf70f18a8cd68ebe5b3cb1dece" - integrity sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ== - -chroma-js@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chroma-js/-/chroma-js-2.4.2.tgz#dffc214ed0c11fa8eefca2c36651d8e57cbfb2b0" - integrity sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A== - -ci-info@^3.2.0, ci-info@^3.7.1: - version "3.7.1" - resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-3.7.1.tgz#708a6cdae38915d597afdf3b145f2f8e1ff55f3f" - integrity sha512-4jYS4MOAaCIStSRwiuxc4B8MYhIe676yO1sYGzARnjXkWpmzZMMYxY6zu8WYWDhSuth5zhrQ1rhNSibyyvv4/w== - -cjs-module-lexer@^1.0.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz#9f84ba3244a512f3a54e5277e8eef4c489864e40" - integrity sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA== - -cli-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-4.0.0.tgz#3cecfe3734bf4fe02a8361cbdc0f6fe28c6a57ea" - integrity sha512-VGtlMu3x/4DOtIUwEkRezxUZ2lBacNJCHash0N0WeZDBS+7Ux1dm3XWAgWYxLJFMMdOeXMHXorshEFhbMSGelg== - dependencies: - restore-cursor "^4.0.0" - -cli-spinners@^2.6.1: - version "2.7.0" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.7.0.tgz#f815fd30b5f9eaac02db604c7a231ed7cb2f797a" - integrity sha512-qu3pN8Y3qHNgE2AFweciB1IfMnmZ/fsNTEE+NOFjmGB2F/7rLhnhzppvpCnN4FovtP26k8lHyy9ptEbNwWFLzw== - -cli-width@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-4.0.0.tgz#a5622f6a3b0a9e3e711a25f099bf2399f608caf6" - integrity sha512-ZksGS2xpa/bYkNzN3BAw1wEjsLV/ZKOf/CCrJ/QOBsxx6fOARIkwTutxp1XIOIohi6HKmOFjMoK/XaqDVUpEEw== - -clipboardy@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/clipboardy/-/clipboardy-3.0.0.tgz#f3876247404d334c9ed01b6f269c11d09a5e3092" - integrity sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg== - dependencies: - arch "^2.2.0" - execa "^5.1.1" - is-wsl "^2.2.0" - -cliui@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" - integrity sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== - dependencies: - string-width "^4.2.0" - strip-ansi "^6.0.1" - wrap-ansi "^7.0.0" - -clone-response@^1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/clone-response/-/clone-response-1.0.3.tgz#af2032aa47816399cf5f0a1d0db902f517abb8c3" - integrity sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA== - dependencies: - mimic-response "^1.0.0" - -clone@^1.0.2: - version "1.0.4" - resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" - integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== - -cluster-key-slot@^1.1.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz#88ddaa46906e303b5de30d3153b7d9fe0a0c19ac" - integrity sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA== - -co@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" - integrity sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ== - -collect-v8-coverage@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" - integrity sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg== - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-convert@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" - integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== - dependencies: - color-name "~1.1.4" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw== - -color-name@^1.1.4, color-name@~1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" - integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== - -color-support@^1.1.2: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2" - integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg== - -colord@^2.9.1: - version "2.9.3" - resolved "https://registry.yarnpkg.com/colord/-/colord-2.9.3.tgz#4f8ce919de456f1d5c1c368c307fe20f3e59fb43" - integrity sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw== - -colorette@^2.0.19: - version "2.0.19" - resolved "https://registry.yarnpkg.com/colorette/-/colorette-2.0.19.tgz#cdf044f47ad41a0f4b56b3a0d5b4e6e1a2d5a798" - integrity sha512-3tlv/dIP7FWvj3BsbHrGLJ6l/oKh1O3TcgBqMn+yyCagOxc23fyzDS6HypQbgxWbkpDnf52p1LuR4eWDQ/K9WQ== - -comma-separated-tokens@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" - integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== - -commander@7, commander@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" - integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== - -commander@^2.20.0: - version "2.20.3" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" - integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== - -commander@^8.0.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" - integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== - -commander@^9.5.0: - version "9.5.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-9.5.0.tgz#bc08d1eb5cedf7ccb797a96199d41c7bc3e60d30" - integrity sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ== - -commondir@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" - integrity sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg== - -compress-commons@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/compress-commons/-/compress-commons-4.1.1.tgz#df2a09a7ed17447642bad10a85cc9a19e5c42a7d" - integrity sha512-QLdDLCKNV2dtoTorqgxngQCMA+gWXkM/Nwu7FpeBhk/RdkzimqC3jueb/FDmaZeXh+uby1jkBqE3xArsLBE5wQ== - dependencies: - buffer-crc32 "^0.2.13" - crc32-stream "^4.0.2" - normalize-path "^3.0.0" - readable-stream "^3.6.0" - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== - -consola@^2.15.3: - version "2.15.3" - resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" - integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== - -console-control-strings@^1.0.0, console-control-strings@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/console-control-strings/-/console-control-strings-1.1.0.tgz#3d7cf4464db6446ea644bf4b39507f9851008e8e" - integrity sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ== - -constant-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/constant-case/-/constant-case-3.0.4.tgz#3b84a9aeaf4cf31ec45e6bf5de91bdfb0589faf1" - integrity sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - upper-case "^2.0.2" - -convert-gitmoji@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/convert-gitmoji/-/convert-gitmoji-0.1.3.tgz#0263f1e201920cd0ccff391bdb492b640314fddb" - integrity sha512-t5yxPyI8h8KPvRwrS/sRrfIpT2gJbmBAY0TFokyUBy3PM44RuFRpZwHdACz+GTSPLRLo3s4qsscOMLjHiXBwzw== - -convert-source-map@^1.6.0, convert-source-map@^1.7.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.9.0.tgz#7faae62353fb4213366d0ca98358d22e8368b05f" - integrity sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A== - -convert-source-map@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" - integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== - -cookie-es@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/cookie-es/-/cookie-es-0.5.0.tgz#a6ad89923e68c542fc9e760b07aefa5ab020d719" - integrity sha512-RyZrFi6PNpBFbIaQjXDlFIhFVqV42QeKSZX1yQIl6ihImq6vcHNGMtqQ/QzY3RMPuYSkvsRwtnt5M9NeYxKt0g== - -core-util-is@~1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" - integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== - -crc-32@^1.2.0: - version "1.2.2" - resolved "https://registry.yarnpkg.com/crc-32/-/crc-32-1.2.2.tgz#3cad35a934b8bf71f25ca524b6da51fb7eace2ff" - integrity sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ== - -crc32-stream@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/crc32-stream/-/crc32-stream-4.0.2.tgz#c922ad22b38395abe9d3870f02fa8134ed709007" - integrity sha512-DxFZ/Hk473b/muq1VJ///PMNLj0ZMnzye9thBpmjpJKCc5eMgB95aK8zCGrGfQ90cWo561Te6HK9D+j4KPdM6w== - dependencies: - crc-32 "^1.2.0" - readable-stream "^3.4.0" - -create-require@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" - integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== - -cross-spawn@^7.0.3: - version "7.0.3" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" - integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== - dependencies: - path-key "^3.1.0" - shebang-command "^2.0.0" - which "^2.0.1" - -css-declaration-sorter@^6.3.1: - version "6.3.1" - resolved "https://registry.yarnpkg.com/css-declaration-sorter/-/css-declaration-sorter-6.3.1.tgz#be5e1d71b7a992433fb1c542c7a1b835e45682ec" - integrity sha512-fBffmak0bPAnyqc/HO8C3n2sHrp9wcqQz6ES9koRF2/mLOVAx9zIQ3Y7R29sYCteTPqMCwns4WYQoCX91Xl3+w== - -css-select@^4.1.3: - version "4.3.0" - resolved "https://registry.yarnpkg.com/css-select/-/css-select-4.3.0.tgz#db7129b2846662fd8628cfc496abb2b59e41529b" - integrity sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ== - dependencies: - boolbase "^1.0.0" - css-what "^6.0.1" - domhandler "^4.3.1" - domutils "^2.8.0" - nth-check "^2.0.1" - -css-tree@^1.1.2, css-tree@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/css-tree/-/css-tree-1.1.3.tgz#eb4870fb6fd7707327ec95c2ff2ab09b5e8db91d" - integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q== - dependencies: - mdn-data "2.0.14" - source-map "^0.6.1" - -css-what@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/css-what/-/css-what-6.1.0.tgz#fb5effcf76f1ddea2c81bdfaa4de44e79bac70f4" - integrity sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw== - -cssesc@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee" - integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg== - -cssnano-preset-default@^5.2.13: - version "5.2.13" - resolved "https://registry.yarnpkg.com/cssnano-preset-default/-/cssnano-preset-default-5.2.13.tgz#e7353b0c57975d1bdd97ac96e68e5c1b8c68e990" - integrity sha512-PX7sQ4Pb+UtOWuz8A1d+Rbi+WimBIxJTRyBdgGp1J75VU0r/HFQeLnMYgHiCAp6AR4rqrc7Y4R+1Rjk3KJz6DQ== - dependencies: - css-declaration-sorter "^6.3.1" - cssnano-utils "^3.1.0" - postcss-calc "^8.2.3" - postcss-colormin "^5.3.0" - postcss-convert-values "^5.1.3" - postcss-discard-comments "^5.1.2" - postcss-discard-duplicates "^5.1.0" - postcss-discard-empty "^5.1.1" - postcss-discard-overridden "^5.1.0" - postcss-merge-longhand "^5.1.7" - postcss-merge-rules "^5.1.3" - postcss-minify-font-values "^5.1.0" - postcss-minify-gradients "^5.1.1" - postcss-minify-params "^5.1.4" - postcss-minify-selectors "^5.2.1" - postcss-normalize-charset "^5.1.0" - postcss-normalize-display-values "^5.1.0" - postcss-normalize-positions "^5.1.1" - postcss-normalize-repeat-style "^5.1.1" - postcss-normalize-string "^5.1.0" - postcss-normalize-timing-functions "^5.1.0" - postcss-normalize-unicode "^5.1.1" - postcss-normalize-url "^5.1.0" - postcss-normalize-whitespace "^5.1.1" - postcss-ordered-values "^5.1.3" - postcss-reduce-initial "^5.1.1" - postcss-reduce-transforms "^5.1.0" - postcss-svgo "^5.1.0" - postcss-unique-selectors "^5.1.1" - -cssnano-utils@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/cssnano-utils/-/cssnano-utils-3.1.0.tgz#95684d08c91511edfc70d2636338ca37ef3a6861" - integrity sha512-JQNR19/YZhz4psLX/rQ9M83e3z2Wf/HdJbryzte4a3NSuafyp9w/I4U+hx5C2S9g41qlstH7DEWnZaaj83OuEA== - -cssnano@^5.1.14: - version "5.1.14" - resolved "https://registry.yarnpkg.com/cssnano/-/cssnano-5.1.14.tgz#07b0af6da73641276fe5a6d45757702ebae2eb05" - integrity sha512-Oou7ihiTocbKqi0J1bB+TRJIQX5RMR3JghA8hcWSw9mjBLQ5Y3RWqEDoYG3sRNlAbCIXpqMoZGbq5KDR3vdzgw== - dependencies: - cssnano-preset-default "^5.2.13" - lilconfig "^2.0.3" - yaml "^1.10.2" - -csso@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/csso/-/csso-4.2.0.tgz#ea3a561346e8dc9f546d6febedd50187cf389529" - integrity sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA== - dependencies: - css-tree "^1.1.2" - -csstype@^2.6.8: - version "2.6.21" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-2.6.21.tgz#2efb85b7cc55c80017c66a5ad7cbd931fda3a90e" - integrity sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w== - -csstype@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.1.tgz#841b532c45c758ee546a11d5bd7b7b473c8c30b9" - integrity sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw== - -cuint@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/cuint/-/cuint-0.2.2.tgz#408086d409550c2631155619e9fa7bcadc3b991b" - integrity sha512-d4ZVpCW31eWwCMe1YT3ur7mUDnTXbgwyzaL320DrcRT45rfjYxkt5QWLrmOJ+/UEAI2+fQgKe/fCjR8l4TpRgw== - -"d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3, d3-array@^3.2.0: - version "3.2.2" - resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-3.2.2.tgz#f8ac4705c5b06914a7e0025bbf8d5f1513f6a86e" - integrity sha512-yEEyEAbDrF8C6Ob2myOBLjwBLck1Z89jMGFee0oPsn95GqjerpaOA4ch+vc2l0FNFFwMD5N7OCSEN5eAlsUbgQ== - dependencies: - internmap "1 - 2" - -d3-axis@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-3.0.0.tgz#c42a4a13e8131d637b745fc2973824cfeaf93322" - integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw== - -d3-brush@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-3.0.0.tgz#6f767c4ed8dcb79de7ede3e1c0f89e63ef64d31c" - integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ== - dependencies: - d3-dispatch "1 - 3" - d3-drag "2 - 3" - d3-interpolate "1 - 3" - d3-selection "3" - d3-transition "3" - -d3-chord@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-chord/-/d3-chord-3.0.1.tgz#d156d61f485fce8327e6abf339cb41d8cbba6966" - integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g== - dependencies: - d3-path "1 - 3" - -"d3-color@1 - 3", d3-color@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-3.1.0.tgz#395b2833dfac71507f12ac2f7af23bf819de24e2" - integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA== - -d3-contour@4: - version "4.0.2" - resolved "https://registry.yarnpkg.com/d3-contour/-/d3-contour-4.0.2.tgz#bb92063bc8c5663acb2422f99c73cbb6c6ae3bcc" - integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA== - dependencies: - d3-array "^3.2.0" - -d3-delaunay@6: - version "6.0.2" - resolved "https://registry.yarnpkg.com/d3-delaunay/-/d3-delaunay-6.0.2.tgz#7fd3717ad0eade2fc9939f4260acfb503f984e92" - integrity sha512-IMLNldruDQScrcfT+MWnazhHbDJhcRJyOEBAJfwQnHle1RPh6WDuLvxNArUju2VSMSUuKlY5BGHRJ2cYyoFLQQ== - dependencies: - delaunator "5" - -"d3-dispatch@1 - 3", d3-dispatch@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz#5fc75284e9c2375c36c839411a0cf550cbfc4d5e" - integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg== - -"d3-drag@2 - 3", d3-drag@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-3.0.0.tgz#994aae9cd23c719f53b5e10e3a0a6108c69607ba" - integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg== - dependencies: - d3-dispatch "1 - 3" - d3-selection "3" - -"d3-dsv@1 - 3", d3-dsv@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-dsv/-/d3-dsv-3.0.1.tgz#c63af978f4d6a0d084a52a673922be2160789b73" - integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q== - dependencies: - commander "7" - iconv-lite "0.6" - rw "1" - -"d3-ease@1 - 3", d3-ease@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-3.0.1.tgz#9658ac38a2140d59d346160f1f6c30fda0bd12f4" - integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w== - -d3-fetch@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-fetch/-/d3-fetch-3.0.1.tgz#83141bff9856a0edb5e38de89cdcfe63d0a60a22" - integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw== - dependencies: - d3-dsv "1 - 3" - -d3-force@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-force/-/d3-force-3.0.0.tgz#3e2ba1a61e70888fe3d9194e30d6d14eece155c4" - integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg== - dependencies: - d3-dispatch "1 - 3" - d3-quadtree "1 - 3" - d3-timer "1 - 3" - -"d3-format@1 - 3", d3-format@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-3.1.0.tgz#9260e23a28ea5cb109e93b21a06e24e2ebd55641" - integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA== - -d3-geo@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-geo/-/d3-geo-3.1.0.tgz#74fd54e1f4cebd5185ac2039217a98d39b0a4c0e" - integrity sha512-JEo5HxXDdDYXCaWdwLRt79y7giK8SbhZJbFWXqbRTolCHFI5jRqteLzCsq51NKbUoX0PjBVSohxrx+NoOUujYA== - dependencies: - d3-array "2.5.0 - 3" - -d3-hierarchy@3: - version "3.1.2" - resolved "https://registry.yarnpkg.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz#b01cd42c1eed3d46db77a5966cf726f8c09160c6" - integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA== - -"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz#3c47aa5b32c5b3dfb56ef3fd4342078a632b400d" - integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g== - dependencies: - d3-color "1 - 3" - -"d3-path@1 - 3", d3-path@3, d3-path@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-3.1.0.tgz#22df939032fb5a71ae8b1800d61ddb7851c42526" - integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ== - -d3-polygon@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-polygon/-/d3-polygon-3.0.1.tgz#0b45d3dd1c48a29c8e057e6135693ec80bf16398" - integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg== - -"d3-quadtree@1 - 3", d3-quadtree@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz#6dca3e8be2b393c9a9d514dabbd80a92deef1a4f" - integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw== - -d3-random@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-random/-/d3-random-3.0.1.tgz#d4926378d333d9c0bfd1e6fa0194d30aebaa20f4" - integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ== - -d3-scale-chromatic@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-scale-chromatic/-/d3-scale-chromatic-3.0.0.tgz#15b4ceb8ca2bb0dcb6d1a641ee03d59c3b62376a" - integrity sha512-Lx9thtxAKrO2Pq6OO2Ua474opeziKr279P/TKZsMAhYyNDD3EnCffdbgeSYN5O7m2ByQsxtuP2CSDczNUIZ22g== - dependencies: - d3-color "1 - 3" - d3-interpolate "1 - 3" - -d3-scale@4: - version "4.0.2" - resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-4.0.2.tgz#82b38e8e8ff7080764f8dcec77bd4be393689396" - integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ== - dependencies: - d3-array "2.10.0 - 3" - d3-format "1 - 3" - d3-interpolate "1.2.0 - 3" - d3-time "2.1.1 - 3" - d3-time-format "2 - 4" - -"d3-selection@2 - 3", d3-selection@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-3.0.0.tgz#c25338207efa72cc5b9bd1458a1a41901f1e1b31" - integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ== - -d3-shape@3: - version "3.2.0" - resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-3.2.0.tgz#a1a839cbd9ba45f28674c69d7f855bcf91dfc6a5" - integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA== - dependencies: - d3-path "^3.1.0" - -"d3-time-format@2 - 4", d3-time-format@4: - version "4.1.0" - resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-4.1.0.tgz#7ab5257a5041d11ecb4fe70a5c7d16a195bb408a" - integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg== - dependencies: - d3-time "1 - 3" - -"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-3.1.0.tgz#9310db56e992e3c0175e1ef385e545e48a9bb5c7" - integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q== - dependencies: - d3-array "2 - 3" - -"d3-timer@1 - 3", d3-timer@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-3.0.1.tgz#6284d2a2708285b1abb7e201eda4380af35e63b0" - integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== - -"d3-transition@2 - 3", d3-transition@3: - version "3.0.1" - resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-3.0.1.tgz#6869fdde1448868077fdd5989200cb61b2a1645f" - integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w== - dependencies: - d3-color "1 - 3" - d3-dispatch "1 - 3" - d3-ease "1 - 3" - d3-interpolate "1 - 3" - d3-timer "1 - 3" - -d3-zoom@3: - version "3.0.0" - resolved "https://registry.yarnpkg.com/d3-zoom/-/d3-zoom-3.0.0.tgz#d13f4165c73217ffeaa54295cd6969b3e7aee8f3" - integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw== - dependencies: - d3-dispatch "1 - 3" - d3-drag "2 - 3" - d3-interpolate "1 - 3" - d3-selection "2 - 3" - d3-transition "2 - 3" - -d3@^7.0.0, d3@^7.7.0: - version "7.8.2" - resolved "https://registry.yarnpkg.com/d3/-/d3-7.8.2.tgz#2bdb3c178d095ae03b107a18837ae049838e372d" - integrity sha512-WXty7qOGSHb7HR7CfOzwN1Gw04MUOzN8qh9ZUsvwycIMb4DYMpY9xczZ6jUorGtO6bR9BPMPaueIKwiDxu9uiQ== - dependencies: - d3-array "3" - d3-axis "3" - d3-brush "3" - d3-chord "3" - d3-color "3" - d3-contour "4" - d3-delaunay "6" - d3-dispatch "3" - d3-drag "3" - d3-dsv "3" - d3-ease "3" - d3-fetch "3" - d3-force "3" - d3-format "3" - d3-geo "3" - d3-hierarchy "3" - d3-interpolate "3" - d3-path "3" - d3-polygon "3" - d3-quadtree "3" - d3-random "3" - d3-scale "4" - d3-scale-chromatic "3" - d3-selection "3" - d3-shape "3" - d3-time "3" - d3-time-format "4" - d3-timer "3" - d3-transition "3" - d3-zoom "3" - -dagre-d3-es@7.0.6: - version "7.0.6" - resolved "https://registry.yarnpkg.com/dagre-d3-es/-/dagre-d3-es-7.0.6.tgz#8cab465ff95aca8a1ca2292d07e1fb31b5db83f2" - integrity sha512-CaaE/nZh205ix+Up4xsnlGmpog5GGm81Upi2+/SBHxwNwrccBb3K51LzjZ1U6hgvOlAEUsVWf1xSTzCyKpJ6+Q== - dependencies: - d3 "^7.7.0" - lodash-es "^4.17.21" - -data-uri-to-buffer@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz#d8feb2b2881e6a4f58c2e08acfd0e2834e26222e" - integrity sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A== - -de-indent@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/de-indent/-/de-indent-1.0.2.tgz#b2038e846dc33baa5796128d0804b455b8c1e21d" - integrity sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg== - -debug@2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.4, debug@~4.3.1, debug@~4.3.2: - version "4.3.4" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" - integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== - dependencies: - ms "2.1.2" - -decode-named-character-reference@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" - integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== - dependencies: - character-entities "^2.0.0" - -decompress-response@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/decompress-response/-/decompress-response-6.0.0.tgz#ca387612ddb7e104bd16d85aab00d5ecf09c66fc" - integrity sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ== - dependencies: - mimic-response "^3.1.0" - -dedent@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" - integrity sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA== - -deep-eql@^4.1.2: - version "4.1.3" - resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-4.1.3.tgz#7c7775513092f7df98d8df9996dd085eb668cc6d" - integrity sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw== - dependencies: - type-detect "^4.0.0" - -deepmerge@^4.2.2: - version "4.3.0" - resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.0.tgz#65491893ec47756d44719ae520e0e2609233b59b" - integrity sha512-z2wJZXrmeHdvYJp/Ux55wIjqo81G5Bp4c+oELTW+7ar6SogWHajt5a9gO3s3IDaGSAXjDk0vlQKN3rms8ab3og== - -defaults@^1.0.3: - version "1.0.4" - resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.4.tgz#b0b02062c1e2aa62ff5d9528f0f98baa90978d7a" - integrity sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A== - dependencies: - clone "^1.0.2" - -defer-to-connect@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/defer-to-connect/-/defer-to-connect-2.0.1.tgz#8016bdb4143e4632b77a3449c6236277de520587" - integrity sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg== - -define-lazy-prop@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz#3f7ae421129bcaaac9bc74905c98a0009ec9ee7f" - integrity sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og== - -define-properties@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.4.tgz#0b14d7bd7fbeb2f3572c3a7eda80ea5d57fb05b1" - integrity sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA== - dependencies: - has-property-descriptors "^1.0.0" - object-keys "^1.1.1" - -defined@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.1.tgz#c0b9db27bfaffd95d6f61399419b893df0f91ebf" - integrity sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q== - -defu@^6.0.0, defu@^6.1.1, defu@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/defu/-/defu-6.1.2.tgz#1217cba167410a1765ba93893c6dbac9ed9d9e5c" - integrity sha512-+uO4+qr7msjNNWKYPHqN/3+Dx3NFkmIzayk2L1MyZQlvgZb/J1A0fo410dpKrN2SnqFjt8n4JL8fDJE0wIgjFQ== - -delaunator@5: - version "5.0.0" - resolved "https://registry.yarnpkg.com/delaunator/-/delaunator-5.0.0.tgz#60f052b28bd91c9b4566850ebf7756efe821d81b" - integrity sha512-AyLvtyJdbv/U1GkiS6gUUzclRoAY4Gs75qkMygJJhU75LW4DNuSF2RMzpxs9jw9Oz1BobHjTdkG3zdP55VxAqw== - dependencies: - robust-predicates "^3.0.0" - -delegates@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" - integrity sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ== - -denque@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/denque/-/denque-2.1.0.tgz#e93e1a6569fb5e66f16a3c2a2964617d349d6ab1" - integrity sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw== - -depd@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" - integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== - -dequal@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - -destr@^1.2.1, destr@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/destr/-/destr-1.2.2.tgz#7ba9befcafb645a50e76b260449c63927b51e22f" - integrity sha512-lrbCJwD9saUQrqUfXvl6qoM+QN3W7tLV5pAOs+OqOmopCCz/JkE05MHedJR1xfk4IAnZuJXPVuN5+7jNA2ZCiA== - -destroy@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" - integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg== - -detab@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/detab/-/detab-3.0.2.tgz#b9909b52881badd598f653c5e4fcc7c94b158474" - integrity sha512-7Bp16Bk8sk0Y6gdXiCtnpGbghn8atnTJdd/82aWvS5ESnlcNvgUc10U2NYS0PAiDSGjWiI8qs/Cv1b2uSGdQ8w== - -detect-libc@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd" - integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w== - -detect-newline@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651" - integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== - -detective@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/detective/-/detective-5.2.1.tgz#6af01eeda11015acb0e73f933242b70f24f91034" - integrity sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw== - dependencies: - acorn-node "^1.8.2" - defined "^1.0.0" - minimist "^1.2.6" - -didyoumean@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" - integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw== - -diff-sequences@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-29.4.2.tgz#711fe6bd8a5869fe2539cee4a5152425ff671fda" - integrity sha512-R6P0Y6PrsH3n4hUXxL3nns0rbRk6Q33js3ygJBeEpbzLzgcNuJ61+u0RXasFpTKISw99TxUzFnumSnRLsjhLaw== - -diff@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" - integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== - -dir-glob@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" - integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== - dependencies: - path-type "^4.0.0" - -dlv@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/dlv/-/dlv-1.1.3.tgz#5c198a8a11453596e751494d49874bc7732f2e79" - integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA== - -dom-serializer@^1.0.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-1.4.1.tgz#de5d41b1aea290215dc45a6dae8adcf1d32e2d30" - integrity sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag== - dependencies: - domelementtype "^2.0.1" - domhandler "^4.2.0" - entities "^2.0.0" - -domelementtype@^2.0.1, domelementtype@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/domelementtype/-/domelementtype-2.3.0.tgz#5c45e8e869952626331d7aab326d01daf65d589d" - integrity sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw== - -domhandler@^4.2.0, domhandler@^4.3.1: - version "4.3.1" - resolved "https://registry.yarnpkg.com/domhandler/-/domhandler-4.3.1.tgz#8d792033416f59d68bc03a5aa7b018c1ca89279c" - integrity sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ== - dependencies: - domelementtype "^2.2.0" - -domino@^2.1.6: - version "2.1.6" - resolved "https://registry.yarnpkg.com/domino/-/domino-2.1.6.tgz#fe4ace4310526e5e7b9d12c7de01b7f485a57ffe" - integrity sha512-3VdM/SXBZX2omc9JF9nOPCtDaYQ67BGp5CoLpIQlO2KCAPETs8TcDHacF26jXadGbvUteZzRTeos2fhID5+ucQ== - -dompurify@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.4.1.tgz#f9cb1a275fde9af6f2d0a2644ef648dd6847b631" - integrity sha512-ewwFzHzrrneRjxzmK6oVz/rZn9VWspGFRDb4/rRtIsM1n36t9AKma/ye8syCpcw+XJ25kOK/hOG7t1j2I2yBqA== - -domutils@^2.8.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/domutils/-/domutils-2.8.0.tgz#4437def5db6e2d1f5d6ee859bd95ca7d02048135" - integrity sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A== - dependencies: - dom-serializer "^1.0.1" - domelementtype "^2.2.0" - domhandler "^4.2.0" - -dot-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/dot-case/-/dot-case-3.0.4.tgz#9b2b670d00a431667a8a75ba29cd1b98809ce751" - integrity sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -dot-prop@^7.2.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/dot-prop/-/dot-prop-7.2.0.tgz#468172a3529779814d21a779c1ba2f6d76609809" - integrity sha512-Ol/IPXUARn9CSbkrdV4VJo7uCy1I3VuSiWCaFSg+8BdUOzF9n3jefIpcgAydvUZbTdEBZs2vEiTiS9m61ssiDA== - dependencies: - type-fest "^2.11.2" - -dotenv@^16.0.3: - version "16.0.3" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-16.0.3.tgz#115aec42bac5053db3c456db30cc243a5a836a07" - integrity sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ== - -duplexer@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" - integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== - -eastasianwidth@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" - integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== - -ee-first@1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" - integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow== - -electron-to-chromium@^1.4.284: - version "1.4.290" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.290.tgz#62ddde1ea2db0dc31b6fe5898911fc85b83977fb" - integrity sha512-3uIkNYprKqAixFqANvs7K3zacGz6iVIMcGG1M/7hMn6Gvzxe52Xg//wRUds7GPv2uouF7EZ4Sh51fLDw+aBkHw== - -emittery@^0.13.1: - version "0.13.1" - resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" - integrity sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ== - -emoji-regex@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" - integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== - -emoji-regex@^9.2.2: - version "9.2.2" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" - integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== - -emoticon@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/emoticon/-/emoticon-4.0.1.tgz#2d2bbbf231ce3a5909e185bbb64a9da703a1e749" - integrity sha512-dqx7eA9YaqyvYtUhJwT4rC1HIp82j5ybS1/vQ42ur+jBe17dJMwZE4+gvL1XadSFfxaPFFGt3Xsw+Y8akThDlw== - -encodeurl@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" - integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w== - -end-of-stream@^1.1.0, end-of-stream@^1.4.1: - version "1.4.4" - resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" - integrity sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q== - dependencies: - once "^1.4.0" - -engine.io-client@~6.4.0: - version "6.4.0" - resolved "https://registry.yarnpkg.com/engine.io-client/-/engine.io-client-6.4.0.tgz#88cd3082609ca86d7d3c12f0e746d12db4f47c91" - integrity sha512-GyKPDyoEha+XZ7iEqam49vz6auPnNJ9ZBfy89f+rMMas8AuiMWOZ9PVzu8xb9ZC6rafUqiGHSCfu22ih66E+1g== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - engine.io-parser "~5.0.3" - ws "~8.11.0" - xmlhttprequest-ssl "~2.0.0" - -engine.io-parser@~5.0.3: - version "5.0.6" - resolved "https://registry.yarnpkg.com/engine.io-parser/-/engine.io-parser-5.0.6.tgz#7811244af173e157295dec9b2718dfe42a64ef45" - integrity sha512-tjuoZDMAdEhVnSFleYPCtdL2GXwVTGtNjoeJd9IhIG3C1xs9uwxqRNEu5WpnDZCaozwVlK/nuQhpodhXSIMaxw== - -enhanced-resolve@^4.1.1: - version "4.5.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz#2f3cfd84dbe3b487f18f2db2ef1e064a571ca5ec" - integrity sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg== - dependencies: - graceful-fs "^4.1.2" - memory-fs "^0.5.0" - tapable "^1.0.0" - -enhanced-resolve@^5.10.0: - version "5.12.0" - resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz#300e1c90228f5b570c4d35babf263f6da7155634" - integrity sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ== - dependencies: - graceful-fs "^4.2.4" - tapable "^2.2.0" - -entities@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.2.0.tgz#098dc90ebb83d8dffa089d55256b351d34c4da55" - integrity sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A== - -errno@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f" - integrity sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A== - dependencies: - prr "~1.0.1" - -error-ex@^1.3.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es6-object-assign@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/es6-object-assign/-/es6-object-assign-1.1.0.tgz#c2c3582656247c39ea107cb1e6652b6f9f24523c" - integrity sha512-MEl9uirslVwqQU369iHNWZXsI8yaZYGg/D65aOgZkeyFJwHYSxilf7rQzXKI7DdDuBPrBXbfk3sl9hJhmd5AUw== - -esbuild@^0.16.14, esbuild@^0.16.17: - version "0.16.17" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.16.17.tgz#fc2c3914c57ee750635fee71b89f615f25065259" - integrity sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg== - optionalDependencies: - "@esbuild/android-arm" "0.16.17" - "@esbuild/android-arm64" "0.16.17" - "@esbuild/android-x64" "0.16.17" - "@esbuild/darwin-arm64" "0.16.17" - "@esbuild/darwin-x64" "0.16.17" - "@esbuild/freebsd-arm64" "0.16.17" - "@esbuild/freebsd-x64" "0.16.17" - "@esbuild/linux-arm" "0.16.17" - "@esbuild/linux-arm64" "0.16.17" - "@esbuild/linux-ia32" "0.16.17" - "@esbuild/linux-loong64" "0.16.17" - "@esbuild/linux-mips64el" "0.16.17" - "@esbuild/linux-ppc64" "0.16.17" - "@esbuild/linux-riscv64" "0.16.17" - "@esbuild/linux-s390x" "0.16.17" - "@esbuild/linux-x64" "0.16.17" - "@esbuild/netbsd-x64" "0.16.17" - "@esbuild/openbsd-x64" "0.16.17" - "@esbuild/sunos-x64" "0.16.17" - "@esbuild/win32-arm64" "0.16.17" - "@esbuild/win32-ia32" "0.16.17" - "@esbuild/win32-x64" "0.16.17" - -esbuild@^0.17.5, esbuild@^0.17.6: - version "0.17.6" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.6.tgz#bbccd4433629deb6e0a83860b3b61da120ba4e01" - integrity sha512-TKFRp9TxrJDdRWfSsSERKEovm6v30iHnrjlcGhLBOtReE28Yp1VSBRfO3GTaOFMoxsNerx4TjrhzSuma9ha83Q== - optionalDependencies: - "@esbuild/android-arm" "0.17.6" - "@esbuild/android-arm64" "0.17.6" - "@esbuild/android-x64" "0.17.6" - "@esbuild/darwin-arm64" "0.17.6" - "@esbuild/darwin-x64" "0.17.6" - "@esbuild/freebsd-arm64" "0.17.6" - "@esbuild/freebsd-x64" "0.17.6" - "@esbuild/linux-arm" "0.17.6" - "@esbuild/linux-arm64" "0.17.6" - "@esbuild/linux-ia32" "0.17.6" - "@esbuild/linux-loong64" "0.17.6" - "@esbuild/linux-mips64el" "0.17.6" - "@esbuild/linux-ppc64" "0.17.6" - "@esbuild/linux-riscv64" "0.17.6" - "@esbuild/linux-s390x" "0.17.6" - "@esbuild/linux-x64" "0.17.6" - "@esbuild/netbsd-x64" "0.17.6" - "@esbuild/openbsd-x64" "0.17.6" - "@esbuild/sunos-x64" "0.17.6" - "@esbuild/win32-arm64" "0.17.6" - "@esbuild/win32-ia32" "0.17.6" - "@esbuild/win32-x64" "0.17.6" - -escalade@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" - integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== - -escape-html@~1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" - integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow== - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg== - -escape-string-regexp@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz#a30304e99daa32e23b2fd20f51babd07cffca344" - integrity sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w== - -escape-string-regexp@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" - integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== - -esprima@^4.0.0, esprima@~4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -estree-walker@2.0.2, estree-walker@^2.0.1, estree-walker@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" - integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== - -estree-walker@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-0.6.1.tgz#53049143f40c6eb918b23671d1fe3219f3a1b362" - integrity sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w== - -estree-walker@^3.0.1, estree-walker@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-3.0.3.tgz#67c3e549ec402a487b4fc193d1953a524752340d" - integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== - dependencies: - "@types/estree" "^1.0.0" - -etag@^1.8.1, etag@~1.8.1: - version "1.8.1" - resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" - integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg== - -eventemitter3@^4.0.0: - version "4.0.7" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== - -execa@^5.0.0, execa@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/execa/-/execa-5.1.1.tgz#f80ad9cbf4298f7bd1d4c9555c21e93741c411dd" - integrity sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.0" - human-signals "^2.1.0" - is-stream "^2.0.0" - merge-stream "^2.0.0" - npm-run-path "^4.0.1" - onetime "^5.1.2" - signal-exit "^3.0.3" - strip-final-newline "^2.0.0" - -execa@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-6.1.0.tgz#cea16dee211ff011246556388effa0818394fb20" - integrity sha512-QVWlX2e50heYJcCPG0iWtf8r0xjEYfz/OYLGDYH+IyjWezzPNxz63qNFOu0l4YftGWuizFVZHHs8PrLU5p2IDA== - dependencies: - cross-spawn "^7.0.3" - get-stream "^6.0.1" - human-signals "^3.0.1" - is-stream "^3.0.0" - merge-stream "^2.0.0" - npm-run-path "^5.1.0" - onetime "^6.0.0" - signal-exit "^3.0.7" - strip-final-newline "^3.0.0" - -exit@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/exit/-/exit-0.1.2.tgz#0632638f8d877cc82107d30a0fff1a17cba1cd0c" - integrity sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ== - -expect@^29.0.0, expect@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/expect/-/expect-29.4.2.tgz#2ae34eb88de797c64a1541ad0f1e2ea8a7a7b492" - integrity sha512-+JHYg9O3hd3RlICG90OPVjRkPBoiUH7PxvDVMnRiaq1g6JUgZStX514erMl0v2Dc5SkfVbm7ztqbd6qHHPn+mQ== - dependencies: - "@jest/expect-utils" "^29.4.2" - jest-get-type "^29.4.2" - jest-matcher-utils "^29.4.2" - jest-message-util "^29.4.2" - jest-util "^29.4.2" - -extend@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa" - integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -externality@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/externality/-/externality-1.0.0.tgz#8e116aab414a4e11ff5e9aaf31617dd5e56cb069" - integrity sha512-MAU9ci3XdpqOX1aoIoyL2DMzW97P8LYeJxIUkfXhOfsrkH4KLHFaYDwKN0B2l6tqedVJWiTIJtWmxmZfa05vOQ== - dependencies: - enhanced-resolve "^5.10.0" - mlly "^1.0.0" - pathe "^1.0.0" - ufo "^1.0.0" - -fast-glob@^3.2.11, fast-glob@^3.2.12, fast-glob@^3.2.7: - version "3.2.12" - resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.12.tgz#7f39ec99c2e6ab030337142da9e0c18f37afae80" - integrity sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w== - dependencies: - "@nodelib/fs.stat" "^2.0.2" - "@nodelib/fs.walk" "^1.2.3" - glob-parent "^5.1.2" - merge2 "^1.3.0" - micromatch "^4.0.4" - -fast-json-stable-stringify@2.x, fast-json-stable-stringify@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" - integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== - -fastq@^1.6.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.15.0.tgz#d04d07c6a2a68fe4599fea8d2e103a937fae6b3a" - integrity sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw== - dependencies: - reusify "^1.0.4" - -fb-watchman@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/fb-watchman/-/fb-watchman-2.0.2.tgz#e9524ee6b5c77e9e5001af0f85f3adbb8623255c" - integrity sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA== - dependencies: - bser "2.1.1" - -fetch-blob@^3.1.2, fetch-blob@^3.1.4: - version "3.2.0" - resolved "https://registry.yarnpkg.com/fetch-blob/-/fetch-blob-3.2.0.tgz#f09b8d4bbd45adc6f0c20b7e787e793e309dcce9" - integrity sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ== - dependencies: - node-domexception "^1.0.0" - web-streams-polyfill "^3.0.3" - -figures@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-5.0.0.tgz#126cd055052dea699f8a54e8c9450e6ecfc44d5f" - integrity sha512-ej8ksPF4x6e5wvK9yevct0UCXh8TTFlWGVLlgjZuoBH1HwjIfKE/IdL5mq89sFA7zELi1VhKpmtDnrs7zWyeyg== - dependencies: - escape-string-regexp "^5.0.0" - is-unicode-supported "^1.2.0" - -file-uri-to-path@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz#553a7b8446ff6f684359c445f1e37a05dacc33dd" - integrity sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw== - -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - -find-up@^4.0.0, find-up@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19" - integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw== - dependencies: - locate-path "^5.0.0" - path-exists "^4.0.0" - -flat@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/flat/-/flat-5.0.2.tgz#8ca6fe332069ffa9d324c327198c598259ceb241" - integrity sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ== - -follow-redirects@^1.0.0, follow-redirects@^1.14.7: - version "1.15.2" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" - integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== - -for-each@^0.3.3: - version "0.3.3" - resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.3.tgz#69b447e88a0a5d32c3e7084f3f1710034b21376e" - integrity sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw== - dependencies: - is-callable "^1.1.3" - -formdata-polyfill@^4.0.10: - version "4.0.10" - resolved "https://registry.yarnpkg.com/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz#24807c31c9d402e002ab3d8c720144ceb8848423" - integrity sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g== - dependencies: - fetch-blob "^3.1.2" - -fraction.js@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.2.0.tgz#448e5109a313a3527f5a3ab2119ec4cf0e0e2950" - integrity sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA== - -fresh@0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" - integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q== - -fs-constants@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" - integrity sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow== - -fs-extra@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf" - integrity sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-extra@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-11.1.0.tgz#5784b102104433bb0e090f48bfc4a30742c357ed" - integrity sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw== - dependencies: - graceful-fs "^4.2.0" - jsonfile "^6.0.1" - universalify "^2.0.0" - -fs-minipass@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" - integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== - dependencies: - minipass "^3.0.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== - -fsevents@^2.3.2, fsevents@~2.3.2: - version "2.3.2" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" - integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -gauge@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/gauge/-/gauge-3.0.2.tgz#03bf4441c044383908bcfa0656ad91803259b395" - integrity sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q== - dependencies: - aproba "^1.0.3 || ^2.0.0" - color-support "^1.1.2" - console-control-strings "^1.0.0" - has-unicode "^2.0.1" - object-assign "^4.1.1" - signal-exit "^3.0.0" - string-width "^4.2.3" - strip-ansi "^6.0.1" - wide-align "^1.1.2" - -gensync@^1.0.0-beta.2: - version "1.0.0-beta.2" - resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" - integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== - -get-caller-file@^2.0.5: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-func-name@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/get-func-name/-/get-func-name-2.0.0.tgz#ead774abee72e20409433a066366023dd6887a41" - integrity sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig== - -get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3: - version "1.2.0" - resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.2.0.tgz#7ad1dc0535f3a2904bba075772763e5051f6d05f" - integrity sha512-L049y6nFOuom5wGyRc3/gdTLO94dySVKRACj1RmJZBQXlbTMhtNIgkWkUHq+jYmZvKf14EW1EoJnnjbmoHij0Q== - dependencies: - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.3" - -get-package-type@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/get-package-type/-/get-package-type-0.1.0.tgz#8de2d803cff44df3bc6c456e6668b36c3926e11a" - integrity sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q== - -get-port-please@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/get-port-please/-/get-port-please-3.0.1.tgz#a24953a41dc249f76869ac25e81d6623e61ab010" - integrity sha512-R5pcVO8Z1+pVDu8Ml3xaJCEkBiiy1VQN9za0YqH8GIi1nIqD4IzQhzY6dDzMRtdS1lyiGlucRzm8IN8wtLIXng== - -get-stream@^5.1.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-5.2.0.tgz#4966a1795ee5ace65e706c4b7beb71257d6e22d3" - integrity sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA== - dependencies: - pump "^3.0.0" - -get-stream@^6.0.0, get-stream@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-6.0.1.tgz#a262d8eef67aced57c2852ad6167526a43cbf7b7" - integrity sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg== - -giget@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/giget/-/giget-1.0.0.tgz#fdd7e61a84996b19e00d2d4a6a65c60cc1f61c3d" - integrity sha512-KWELZn3Nxq5+0So485poHrFriK9Bn3V/x9y+wgqrHkbmnGbjfLmZ685/SVA/ovW+ewoqW0gVI47pI4yW/VNobQ== - dependencies: - colorette "^2.0.19" - defu "^6.1.1" - https-proxy-agent "^5.0.1" - mri "^1.2.0" - node-fetch-native "^1.0.1" - pathe "^1.0.0" - tar "^6.1.12" - -git-config-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/git-config-path/-/git-config-path-2.0.0.tgz#62633d61af63af4405a5024efd325762f58a181b" - integrity sha512-qc8h1KIQbJpp+241id3GuAtkdyJ+IK+LIVtkiFTRKRrmddDzs3SI9CvP1QYmWBFvm1I/PWRwj//of8bgAc0ltA== - -git-up@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/git-up/-/git-up-7.0.0.tgz#bace30786e36f56ea341b6f69adfd83286337467" - integrity sha512-ONdIrbBCFusq1Oy0sC71F5azx8bVkvtZtMJAsv+a6lz5YAmbNnLD6HAB4gptHZVLPR8S2/kVN6Gab7lryq5+lQ== - dependencies: - is-ssh "^1.4.0" - parse-url "^8.1.0" - -git-url-parse@^13.1.0: - version "13.1.0" - resolved "https://registry.yarnpkg.com/git-url-parse/-/git-url-parse-13.1.0.tgz#07e136b5baa08d59fabdf0e33170de425adf07b4" - integrity sha512-5FvPJP/70WkIprlUZ33bm4UAaFdjcLkJLpWft1BeZKqwR0uhhNGoKwlUaPtVb4LxCSQ++erHapRak9kWGj+FCA== - dependencies: - git-up "^7.0.0" - -github-slugger@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-2.0.0.tgz#52cf2f9279a21eb6c59dd385b410f0c0adda8f1a" - integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw== - -glob-parent@^5.1.2, glob-parent@~5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" - integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== - dependencies: - is-glob "^4.0.1" - -glob-parent@^6.0.2: - version "6.0.2" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" - integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== - dependencies: - is-glob "^4.0.3" - -glob@^7.1.3, glob@^7.1.4: - version "7.2.3" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" - integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.1.1" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^8.0.3: - version "8.1.0" - resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e" - integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^5.0.1" - once "^1.3.0" - -globals@^11.1.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -globby@^13.1.3: - version "13.1.3" - resolved "https://registry.yarnpkg.com/globby/-/globby-13.1.3.tgz#f62baf5720bcb2c1330c8d4ef222ee12318563ff" - integrity sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw== - dependencies: - dir-glob "^3.0.1" - fast-glob "^3.2.11" - ignore "^5.2.0" - merge2 "^1.4.1" - slash "^4.0.0" - -gopd@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c" - integrity sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA== - dependencies: - get-intrinsic "^1.1.3" - -got@^11.8.1: - version "11.8.6" - resolved "https://registry.yarnpkg.com/got/-/got-11.8.6.tgz#276e827ead8772eddbcfc97170590b841823233a" - integrity sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g== - dependencies: - "@sindresorhus/is" "^4.0.0" - "@szmarczak/http-timer" "^4.0.5" - "@types/cacheable-request" "^6.0.1" - "@types/responselike" "^1.0.0" - cacheable-lookup "^5.0.3" - cacheable-request "^7.0.2" - decompress-response "^6.0.0" - http2-wrapper "^1.0.0-beta.5.2" - lowercase-keys "^2.0.0" - p-cancelable "^2.0.0" - responselike "^2.0.0" - -graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.2.0, graceful-fs@^4.2.4, graceful-fs@^4.2.9: - version "4.2.10" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c" - integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA== - -gzip-size@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-7.0.0.tgz#9f9644251f15bc78460fccef4055ae5a5562ac60" - integrity sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA== - dependencies: - duplexer "^0.1.2" - -h3@^1.1.0, h3@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/h3/-/h3-1.4.0.tgz#9ee3eada42fdb01a5452f9e52f5d2e0542a37b52" - integrity sha512-FWG+FUdW6XQnf/54L4AXzZs1KUYwSJk5cbdFvTM4EG96bEQiWDJ5003xW4S3UGgXI0VJJgyY6KCaDmAL75kjbA== - dependencies: - cookie-es "^0.5.0" - destr "^1.2.2" - iron-webcrypto "^0.4.0" - radix3 "^1.0.0" - ufo "^1.0.1" - uncrypto "^0.1.2" - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw== - -has-flag@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" - integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== - -has-property-descriptors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz#610708600606d36961ed04c196193b6a607fa861" - integrity sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ== - dependencies: - get-intrinsic "^1.1.1" - -has-symbols@^1.0.2, has-symbols@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.3.tgz#bb7b2c4349251dce87b125f7bdf874aa7c8b39f8" - integrity sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A== - -has-tostringtag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" - integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== - dependencies: - has-symbols "^1.0.2" - -has-unicode@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/has-unicode/-/has-unicode-2.0.1.tgz#e0e6fe6a28cf51138855e086d1691e771de2a8b9" - integrity sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ== - -has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hash-sum@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hash-sum/-/hash-sum-2.0.0.tgz#81d01bb5de8ea4a214ad5d6ead1b523460b0b45a" - integrity sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg== - -hast-util-from-parse5@^7.0.0: - version "7.1.1" - resolved "https://registry.yarnpkg.com/hast-util-from-parse5/-/hast-util-from-parse5-7.1.1.tgz#1887b4dd4e19f29d9c48c2e1c8bfeaac13a1f3a0" - integrity sha512-R6PoNcUs89ZxLJmMWsVbwSWuz95/9OriyQZ3e2ybwqGsRXzhA6gv49rgGmQvLbZuSNDv9fCg7vV7gXUsvtUFaA== - dependencies: - "@types/hast" "^2.0.0" - "@types/unist" "^2.0.0" - hastscript "^7.0.0" - property-information "^6.0.0" - vfile "^5.0.0" - vfile-location "^4.0.0" - web-namespaces "^2.0.0" - -hast-util-has-property@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/hast-util-has-property/-/hast-util-has-property-2.0.1.tgz#8ec99c3e8f02626304ee438cdb9f0528b017e083" - integrity sha512-X2+RwZIMTMKpXUzlotatPzWj8bspCymtXH3cfG3iQKV+wPF53Vgaqxi/eLqGck0wKq1kS9nvoB1wchbCPEL8sg== - -hast-util-heading-rank@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/hast-util-heading-rank/-/hast-util-heading-rank-2.1.1.tgz#063b43b9cfb56a1a8ded84dd68d8af69e8864545" - integrity sha512-iAuRp+ESgJoRFJbSyaqsfvJDY6zzmFoEnL1gtz1+U8gKtGGj1p0CVlysuUAUjq95qlZESHINLThwJzNGmgGZxA== - dependencies: - "@types/hast" "^2.0.0" - -hast-util-is-element@^2.0.0: - version "2.1.3" - resolved "https://registry.yarnpkg.com/hast-util-is-element/-/hast-util-is-element-2.1.3.tgz#cd3279cfefb70da6d45496068f020742256fc471" - integrity sha512-O1bKah6mhgEq2WtVMk+Ta5K7pPMqsBBlmzysLdcwKVrqzZQ0CHqUPiIVspNhAG1rvxpvJjtGee17XfauZYKqVA== - dependencies: - "@types/hast" "^2.0.0" - "@types/unist" "^2.0.0" - -hast-util-parse-selector@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/hast-util-parse-selector/-/hast-util-parse-selector-3.1.1.tgz#25ab00ae9e75cbc62cf7a901f68a247eade659e2" - integrity sha512-jdlwBjEexy1oGz0aJ2f4GKMaVKkA9jwjr4MjAAI22E5fM/TXVZHuS5OpONtdeIkRKqAaryQ2E9xNQxijoThSZA== - dependencies: - "@types/hast" "^2.0.0" - -hast-util-raw@^7.2.0: - version "7.2.3" - resolved "https://registry.yarnpkg.com/hast-util-raw/-/hast-util-raw-7.2.3.tgz#dcb5b22a22073436dbdc4aa09660a644f4991d99" - integrity sha512-RujVQfVsOrxzPOPSzZFiwofMArbQke6DJjnFfceiEbFh7S05CbPt0cYN+A5YeD3pso0JQk6O1aHBnx9+Pm2uqg== - dependencies: - "@types/hast" "^2.0.0" - "@types/parse5" "^6.0.0" - hast-util-from-parse5 "^7.0.0" - hast-util-to-parse5 "^7.0.0" - html-void-elements "^2.0.0" - parse5 "^6.0.0" - unist-util-position "^4.0.0" - unist-util-visit "^4.0.0" - vfile "^5.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-to-parse5@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/hast-util-to-parse5/-/hast-util-to-parse5-7.1.0.tgz#c49391bf8f151973e0c9adcd116b561e8daf29f3" - integrity sha512-YNRgAJkH2Jky5ySkIqFXTQiaqcAtJyVE+D5lkN6CdtOqrnkLfGYYrEcKuHOJZlp+MwjSwuD3fZuawI+sic/RBw== - dependencies: - "@types/hast" "^2.0.0" - comma-separated-tokens "^2.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - web-namespaces "^2.0.0" - zwitch "^2.0.0" - -hast-util-to-string@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/hast-util-to-string/-/hast-util-to-string-2.0.0.tgz#b008b0a4ea472bf34dd390b7eea1018726ae152a" - integrity sha512-02AQ3vLhuH3FisaMM+i/9sm4OXGSq1UhOOCpTLLQtHdL3tZt7qil69r8M8iDkZYyC0HCFylcYoP+8IO7ddta1A== - dependencies: - "@types/hast" "^2.0.0" - -hastscript@^7.0.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/hastscript/-/hastscript-7.2.0.tgz#0eafb7afb153d047077fa2a833dc9b7ec604d10b" - integrity sha512-TtYPq24IldU8iKoJQqvZOuhi5CyCQRAbvDOX0x1eW6rsHSxa/1i2CCiptNTotGHJ3VoHRGmqiv6/D3q113ikkw== - dependencies: - "@types/hast" "^2.0.0" - comma-separated-tokens "^2.0.0" - hast-util-parse-selector "^3.0.0" - property-information "^6.0.0" - space-separated-tokens "^2.0.0" - -he@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f" - integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== - -header-case@^2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/header-case/-/header-case-2.0.4.tgz#5a42e63b55177349cf405beb8d775acabb92c063" - integrity sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q== - dependencies: - capital-case "^1.0.4" - tslib "^2.0.3" - -hogan.js@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/hogan.js/-/hogan.js-3.0.2.tgz#4cd9e1abd4294146e7679e41d7898732b02c7bfd" - integrity sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg== - dependencies: - mkdirp "0.3.0" - nopt "1.0.10" - -hookable@^5.4.2: - version "5.4.2" - resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.4.2.tgz#6a1d3c4b3cb5b4262f99b3070ce0ee92c9c78049" - integrity sha512-6rOvaUiNKy9lET1X0ECnyZ5O5kSV0PJbtA5yZUgdEF7fGJEVwSLSislltyt7nFwVVALYHQJtfGeAR2Y0A0uJkg== - -htm@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/htm/-/htm-3.1.1.tgz#49266582be0dc66ed2235d5ea892307cc0c24b78" - integrity sha512-983Vyg8NwUE7JkZ6NmOqpCZ+sh1bKv2iYTlUkzlWmA5JD2acKoxd4KVxbMmxX/85mtfdnDmTFoNKcg5DGAvxNQ== - -html-escaper@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453" - integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== - -html-tags@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" - integrity sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg== - -html-void-elements@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-2.0.1.tgz#29459b8b05c200b6c5ee98743c41b979d577549f" - integrity sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A== - -http-cache-semantics@^4.0.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" - integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== - -http-errors@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" - integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== - dependencies: - depd "2.0.0" - inherits "2.0.4" - setprototypeof "1.2.0" - statuses "2.0.1" - toidentifier "1.0.1" - -http-proxy@^1.18.1: - version "1.18.1" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" - integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== - dependencies: - eventemitter3 "^4.0.0" - follow-redirects "^1.0.0" - requires-port "^1.0.0" - -http-shutdown@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/http-shutdown/-/http-shutdown-1.2.2.tgz#41bc78fc767637c4c95179bc492f312c0ae64c5f" - integrity sha512-S9wWkJ/VSY9/k4qcjG318bqJNruzE4HySUhFYknwmu6LBP97KLLfwNf+n4V1BHurvFNkSKLFnK/RsuUnRTf9Vw== - -http2-wrapper@^1.0.0-beta.5.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/http2-wrapper/-/http2-wrapper-1.0.3.tgz#b8f55e0c1f25d4ebd08b3b0c2c079f9590800b3d" - integrity sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg== - dependencies: - quick-lru "^5.1.1" - resolve-alpn "^1.0.0" - -https-proxy-agent@^5.0.0, https-proxy-agent@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz#c59ef224a04fe8b754f3db0063a25ea30d0005d6" - integrity sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA== - dependencies: - agent-base "6" - debug "4" - -human-signals@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" - integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== - -human-signals@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-3.0.1.tgz#c740920859dafa50e5a3222da9d3bf4bb0e5eef5" - integrity sha512-rQLskxnM/5OCldHo+wNXbpVgDn5A17CUoKX+7Sokwaknlq7CdSnphy0W39GU8dw59XiCXmFXDg4fRuckQRKewQ== - -iconv-lite@0.6: - version "0.6.3" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" - integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== - dependencies: - safer-buffer ">= 2.1.2 < 3.0.0" - -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ieee754@^1.1.13, ieee754@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" - integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== - -ignore@^5.2.0, ignore@^5.2.4: - version "5.2.4" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.4.tgz#a291c0c6178ff1b960befe47fcdec301674a6324" - integrity sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ== - -import-local@^3.0.2: - version "3.1.0" - resolved "https://registry.yarnpkg.com/import-local/-/import-local-3.1.0.tgz#b4479df8a5fd44f6cdce24070675676063c95cb4" - integrity sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg== - dependencies: - pkg-dir "^4.2.0" - resolve-cwd "^3.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.3: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -ini@^1.3.5: - version "1.3.8" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" - integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== - -inquirer@^9.1.4: - version "9.1.4" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-9.1.4.tgz#482da8803670a64bd942bc5166a9547a19d41474" - integrity sha512-9hiJxE5gkK/cM2d1mTEnuurGTAoHebbkX0BYl3h7iEg7FYfuNIom+nDfBCSWtvSnoSrWCeBxqqBZu26xdlJlXA== - dependencies: - ansi-escapes "^6.0.0" - chalk "^5.1.2" - cli-cursor "^4.0.0" - cli-width "^4.0.0" - external-editor "^3.0.3" - figures "^5.0.0" - lodash "^4.17.21" - mute-stream "0.0.8" - ora "^6.1.2" - run-async "^2.4.0" - rxjs "^7.5.7" - string-width "^5.1.2" - strip-ansi "^7.0.1" - through "^2.3.6" - wrap-ansi "^8.0.1" - -instantsearch.css@^7.4.5: - version "7.4.5" - resolved "https://registry.yarnpkg.com/instantsearch.css/-/instantsearch.css-7.4.5.tgz#2a521aa634329bf1680f79adf87c79d67669ec8d" - integrity sha512-iIGBYjCokU93DDB8kbeztKtlu4qVEyTg1xvS6iSO1YvqRwkIZgf0tmsl/GytsLdZhuw8j4wEaeYsCzNbeJ/zEQ== - -instantsearch.js@4.50.3: - version "4.50.3" - resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-4.50.3.tgz#3b810fed3b52f0b887c3ae458cd9054433872742" - integrity sha512-xfVKe7/uAzxnSJeUI2M4RQZycnggx+jtKB6ZCp10Q2FGsPn0pwf2kHO1r0oy05SFYj/UmRf6NXV6h7GjR+ctKg== - dependencies: - "@algolia/events" "^4.0.1" - "@algolia/ui-components-highlight-vdom" "^1.2.1" - "@algolia/ui-components-shared" "^1.2.1" - "@types/dom-speech-recognition" "^0.0.1" - "@types/google.maps" "^3.45.3" - "@types/hogan.js" "^3.0.0" - "@types/qs" "^6.5.3" - algoliasearch-helper "^3.11.3" - hogan.js "^3.0.2" - htm "^3.0.0" - preact "^10.10.0" - qs "^6.5.1 < 6.10" - search-insights "^2.1.0" - -"internmap@1 - 2": - version "2.0.3" - resolved "https://registry.yarnpkg.com/internmap/-/internmap-2.0.3.tgz#6685f23755e43c524e251d29cbc97248e3061009" - integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg== - -ioredis@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/ioredis/-/ioredis-5.3.0.tgz#b5469f0fd374648ef074840c00c1d8eed42fca3f" - integrity sha512-Id9jKHhsILuIZpHc61QkagfVdUj2Rag5GzG1TGEvRNeM7dtTOjICgjC+tvqYxi//PuX2wjQ+Xjva2ONBuf92Pw== - dependencies: - "@ioredis/commands" "^1.1.1" - cluster-key-slot "^1.1.0" - debug "^4.3.4" - denque "^2.1.0" - lodash.defaults "^4.2.0" - lodash.isarguments "^3.1.0" - redis-errors "^1.2.0" - redis-parser "^3.0.0" - standard-as-callback "^2.1.0" - -ip-regex@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/ip-regex/-/ip-regex-5.0.0.tgz#cd313b2ae9c80c07bd3851e12bf4fa4dc5480632" - integrity sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw== - -iron-webcrypto@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/iron-webcrypto/-/iron-webcrypto-0.4.0.tgz#8e23931ea0649c9c5cefb5e43c8375e60cd7952d" - integrity sha512-5OG53gJ4dBTq4y3IJqK7MEG9CPZRsYn9EP9J4jjgH4TcP/ywdsSMAmqj9VTSzdXu0/xfUrqjGHU7WLUme2+k5Q== - -is-absolute-url@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-4.0.1.tgz#16e4d487d4fded05cfe0685e53ec86804a5e94dc" - integrity sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A== - -is-alphabetical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphabetical/-/is-alphabetical-2.0.1.tgz#01072053ea7c1036df3c7d19a6daaec7f19e789b" - integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ== - -is-alphanumerical@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz#7c03fbe96e3e931113e57f964b0a368cc2dfd875" - integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw== - dependencies: - is-alphabetical "^2.0.0" - is-decimal "^2.0.0" - -is-arguments@^1.0.4: - version "1.1.1" - resolved "https://registry.yarnpkg.com/is-arguments/-/is-arguments-1.1.1.tgz#15b3f88fda01f2a97fec84ca761a560f123efa9b" - integrity sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA== - dependencies: - call-bind "^1.0.2" - has-tostringtag "^1.0.0" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg== - -is-binary-path@~2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" - integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== - dependencies: - binary-extensions "^2.0.0" - -is-buffer@^2.0.0: - version "2.0.5" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.5.tgz#ebc252e400d22ff8d77fa09888821a24a658c191" - integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ== - -is-builtin-module@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" - integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== - dependencies: - builtin-modules "^3.3.0" - -is-callable@^1.1.3: - version "1.2.7" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" - integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== - -is-core-module@^2.9.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.11.0.tgz#ad4cb3e3863e814523c96f3f58d26cc570ff0144" - integrity sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw== - dependencies: - has "^1.0.3" - -is-decimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-decimal/-/is-decimal-2.0.1.tgz#9469d2dc190d0214fd87d78b78caecc0cc14eef7" - integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A== - -is-docker@^2.0.0, is-docker@^2.1.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa" - integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ== - -is-docker@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200" - integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ== - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== - -is-fullwidth-code-point@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" - integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== - -is-generator-fn@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" - integrity sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ== - -is-generator-function@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.0.10.tgz#f1558baf1ac17e0deea7c0415c438351ff2b3c72" - integrity sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A== - dependencies: - has-tostringtag "^1.0.0" - -is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" - integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== - dependencies: - is-extglob "^2.1.1" - -is-hexadecimal@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz#86b5bf668fca307498d319dfc03289d781a90027" - integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg== - -is-interactive@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-2.0.0.tgz#40c57614593826da1100ade6059778d597f16e90" - integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ== - -is-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" - integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== - -is-nan@^1.2.1: - version "1.3.2" - resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d" - integrity sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w== - dependencies: - call-bind "^1.0.0" - define-properties "^1.1.3" - -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - -is-plain-obj@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz#d65025edec3657ce032fd7db63c97883eaed71f0" - integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg== - -is-primitive@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-3.0.1.tgz#98c4db1abff185485a657fc2905052b940524d05" - integrity sha512-GljRxhWvlCNRfZyORiH77FwdFwGcMO620o37EOYC0ORWdq+WYNVqW0w2Juzew4M+L81l6/QS3t5gkkihyRqv9w== - -is-promise@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3" - integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ== - -is-reference@1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/is-reference/-/is-reference-1.2.1.tgz#8b2dac0b371f4bc994fdeaba9eb542d03002d0b7" - integrity sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ== - dependencies: - "@types/estree" "*" - -is-ssh@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/is-ssh/-/is-ssh-1.4.0.tgz#4f8220601d2839d8fa624b3106f8e8884f01b8b2" - integrity sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ== - dependencies: - protocols "^2.0.1" - -is-stream@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" - integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== - -is-stream@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-3.0.0.tgz#e6bfd7aa6bef69f4f472ce9bb681e3e57b4319ac" - integrity sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA== - -is-typed-array@^1.1.10, is-typed-array@^1.1.3: - version "1.1.10" - resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.10.tgz#36a5b5cb4189b575d1a3e4b08536bfb485801e3f" - integrity sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" - -is-unicode-supported@^1.1.0, is-unicode-supported@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz#d824984b616c292a2e198207d4a609983842f714" - integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ== - -is-wsl@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-2.2.0.tgz#74a4c76e77ca9fd3f932f290c17ea326cd157271" - integrity sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww== - dependencies: - is-docker "^2.0.0" - -isarray@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== - -istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz#189e7909d0a39fa5a3dfad5b03f71947770191d3" - integrity sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw== - -istanbul-lib-instrument@^5.0.4, istanbul-lib-instrument@^5.1.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz#d10c8885c2125574e1c231cacadf955675e1ce3d" - integrity sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg== - dependencies: - "@babel/core" "^7.12.3" - "@babel/parser" "^7.14.7" - "@istanbuljs/schema" "^0.1.2" - istanbul-lib-coverage "^3.2.0" - semver "^6.3.0" - -istanbul-lib-report@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz#7518fe52ea44de372f460a76b5ecda9ffb73d8a6" - integrity sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw== - dependencies: - istanbul-lib-coverage "^3.0.0" - make-dir "^3.0.0" - supports-color "^7.1.0" - -istanbul-lib-source-maps@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz#895f3a709fcfba34c6de5a42939022f3e4358551" - integrity sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw== - dependencies: - debug "^4.1.1" - istanbul-lib-coverage "^3.0.0" - source-map "^0.6.1" - -istanbul-reports@^3.1.3: - version "3.1.5" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-3.1.5.tgz#cc9a6ab25cb25659810e4785ed9d9fb742578bae" - integrity sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w== - dependencies: - html-escaper "^2.0.0" - istanbul-lib-report "^3.0.0" - -jest-changed-files@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-29.4.2.tgz#bee1fafc8b620d6251423d1978a0080546bc4376" - integrity sha512-Qdd+AXdqD16PQa+VsWJpxR3kN0JyOCX1iugQfx5nUgAsI4gwsKviXkpclxOK9ZnwaY2IQVHz+771eAvqeOlfuw== - dependencies: - execa "^5.0.0" - p-limit "^3.1.0" - -jest-circus@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-circus/-/jest-circus-29.4.2.tgz#2d00c04baefd0ee2a277014cd494d4b5970663ed" - integrity sha512-wW3ztp6a2P5c1yOc1Cfrt5ozJ7neWmqeXm/4SYiqcSriyisgq63bwFj1NuRdSR5iqS0CMEYwSZd89ZA47W9zUg== - dependencies: - "@jest/environment" "^29.4.2" - "@jest/expect" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/types" "^29.4.2" - "@types/node" "*" - chalk "^4.0.0" - co "^4.6.0" - dedent "^0.7.0" - is-generator-fn "^2.0.0" - jest-each "^29.4.2" - jest-matcher-utils "^29.4.2" - jest-message-util "^29.4.2" - jest-runtime "^29.4.2" - jest-snapshot "^29.4.2" - jest-util "^29.4.2" - p-limit "^3.1.0" - pretty-format "^29.4.2" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-cli@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-29.4.2.tgz#94a2f913a0a7a49d11bee98ad88bf48baae941f4" - integrity sha512-b+eGUtXq/K2v7SH3QcJvFvaUaCDS1/YAZBYz0m28Q/Ppyr+1qNaHmVYikOrbHVbZqYQs2IeI3p76uy6BWbXq8Q== - dependencies: - "@jest/core" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/types" "^29.4.2" - chalk "^4.0.0" - exit "^0.1.2" - graceful-fs "^4.2.9" - import-local "^3.0.2" - jest-config "^29.4.2" - jest-util "^29.4.2" - jest-validate "^29.4.2" - prompts "^2.0.1" - yargs "^17.3.1" - -jest-config@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-29.4.2.tgz#15386dd9ed2f7059516915515f786b8836a98f07" - integrity sha512-919CtnXic52YM0zW4C1QxjG6aNueX1kBGthuMtvFtRTAxhKfJmiXC9qwHmi6o2josjbDz8QlWyY55F1SIVmCWA== - dependencies: - "@babel/core" "^7.11.6" - "@jest/test-sequencer" "^29.4.2" - "@jest/types" "^29.4.2" - babel-jest "^29.4.2" - chalk "^4.0.0" - ci-info "^3.2.0" - deepmerge "^4.2.2" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-circus "^29.4.2" - jest-environment-node "^29.4.2" - jest-get-type "^29.4.2" - jest-regex-util "^29.4.2" - jest-resolve "^29.4.2" - jest-runner "^29.4.2" - jest-util "^29.4.2" - jest-validate "^29.4.2" - micromatch "^4.0.4" - parse-json "^5.2.0" - pretty-format "^29.4.2" - slash "^3.0.0" - strip-json-comments "^3.1.1" - -jest-diff@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-29.4.2.tgz#b88502d5dc02d97f6512d73c37da8b36f49b4871" - integrity sha512-EK8DSajVtnjx9sa1BkjZq3mqChm2Cd8rIzdXkQMA8e0wuXq53ypz6s5o5V8HRZkoEt2ywJ3eeNWFKWeYr8HK4g== - dependencies: - chalk "^4.0.0" - diff-sequences "^29.4.2" - jest-get-type "^29.4.2" - pretty-format "^29.4.2" - -jest-docblock@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-docblock/-/jest-docblock-29.4.2.tgz#c78a95eedf9a24c0a6cc16cf2abdc4b8b0f2531b" - integrity sha512-dV2JdahgClL34Y5vLrAHde3nF3yo2jKRH+GIYJuCpfqwEJZcikzeafVTGAjbOfKPG17ez9iWXwUYp7yefeCRag== - dependencies: - detect-newline "^3.0.0" - -jest-each@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-29.4.2.tgz#e1347aff1303f4c35470827a62c029d389c5d44a" - integrity sha512-trvKZb0JYiCndc55V1Yh0Luqi7AsAdDWpV+mKT/5vkpnnFQfuQACV72IoRV161aAr6kAVIBpmYzwhBzm34vQkA== - dependencies: - "@jest/types" "^29.4.2" - chalk "^4.0.0" - jest-get-type "^29.4.2" - jest-util "^29.4.2" - pretty-format "^29.4.2" - -jest-environment-node@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-29.4.2.tgz#0eab835b41e25fd0c1a72f62665fc8db08762ad2" - integrity sha512-MLPrqUcOnNBc8zTOfqBbxtoa8/Ee8tZ7UFW7hRDQSUT+NGsvS96wlbHGTf+EFAT9KC3VNb7fWEM6oyvmxtE/9w== - dependencies: - "@jest/environment" "^29.4.2" - "@jest/fake-timers" "^29.4.2" - "@jest/types" "^29.4.2" - "@types/node" "*" - jest-mock "^29.4.2" - jest-util "^29.4.2" - -jest-get-type@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-29.4.2.tgz#7cb63f154bca8d8f57364d01614477d466fa43fe" - integrity sha512-vERN30V5i2N6lqlFu4ljdTqQAgrkTFMC9xaIIfOPYBw04pufjXRty5RuXBiB1d72tGbURa/UgoiHB90ruOSivg== - -jest-haste-map@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-29.4.2.tgz#9112df3f5121e643f1b2dcbaa86ab11b0b90b49a" - integrity sha512-WkUgo26LN5UHPknkezrBzr7lUtV1OpGsp+NfXbBwHztsFruS3gz+AMTTBcEklvi8uPzpISzYjdKXYZQJXBnfvw== - dependencies: - "@jest/types" "^29.4.2" - "@types/graceful-fs" "^4.1.3" - "@types/node" "*" - anymatch "^3.0.3" - fb-watchman "^2.0.0" - graceful-fs "^4.2.9" - jest-regex-util "^29.4.2" - jest-util "^29.4.2" - jest-worker "^29.4.2" - micromatch "^4.0.4" - walker "^1.0.8" - optionalDependencies: - fsevents "^2.3.2" - -jest-leak-detector@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-29.4.2.tgz#8f05c6680e0cb46a1d577c0d3da9793bed3ea97b" - integrity sha512-Wa62HuRJmWXtX9F00nUpWlrbaH5axeYCdyRsOs/+Rb1Vb6+qWTlB5rKwCCRKtorM7owNwKsyJ8NRDUcZ8ghYUA== - dependencies: - jest-get-type "^29.4.2" - pretty-format "^29.4.2" - -jest-matcher-utils@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-29.4.2.tgz#08d0bf5abf242e3834bec92c7ef5071732839e85" - integrity sha512-EZaAQy2je6Uqkrm6frnxBIdaWtSYFoR8SVb2sNLAtldswlR/29JAgx+hy67llT3+hXBaLB0zAm5UfeqerioZyg== - dependencies: - chalk "^4.0.0" - jest-diff "^29.4.2" - jest-get-type "^29.4.2" - pretty-format "^29.4.2" - -jest-message-util@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-29.4.2.tgz#309a2924eae6ca67cf7f25781a2af1902deee717" - integrity sha512-SElcuN4s6PNKpOEtTInjOAA8QvItu0iugkXqhYyguRvQoXapg5gN+9RQxLAkakChZA7Y26j6yUCsFWN+hlKD6g== - dependencies: - "@babel/code-frame" "^7.12.13" - "@jest/types" "^29.4.2" - "@types/stack-utils" "^2.0.0" - chalk "^4.0.0" - graceful-fs "^4.2.9" - micromatch "^4.0.4" - pretty-format "^29.4.2" - slash "^3.0.0" - stack-utils "^2.0.3" - -jest-mock@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-29.4.2.tgz#e1054be66fb3e975d26d4528fcde6979e4759de8" - integrity sha512-x1FSd4Gvx2yIahdaIKoBjwji6XpboDunSJ95RpntGrYulI1ByuYQCKN/P7hvk09JB74IonU3IPLdkutEWYt++g== - dependencies: - "@jest/types" "^29.4.2" - "@types/node" "*" - jest-util "^29.4.2" - -jest-pnp-resolver@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz#930b1546164d4ad5937d5540e711d4d38d4cad2e" - integrity sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w== - -jest-regex-util@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-29.4.2.tgz#19187cca35d301f8126cf7a021dd4dcb7b58a1ca" - integrity sha512-XYZXOqUl1y31H6VLMrrUL1ZhXuiymLKPz0BO1kEeR5xER9Tv86RZrjTm74g5l9bPJQXA/hyLdaVPN/sdqfteig== - -jest-resolve-dependencies@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-29.4.2.tgz#6359db606f5967b68ca8bbe9dbc07a4306c12bf7" - integrity sha512-6pL4ptFw62rjdrPk7rRpzJYgcRqRZNsZTF1VxVTZMishbO6ObyWvX57yHOaNGgKoADtAHRFYdHQUEvYMJATbDg== - dependencies: - jest-regex-util "^29.4.2" - jest-snapshot "^29.4.2" - -jest-resolve@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-29.4.2.tgz#8831f449671d08d161fe493003f61dc9b55b808e" - integrity sha512-RtKWW0mbR3I4UdkOrW7552IFGLYQ5AF9YrzD0FnIOkDu0rAMlA5/Y1+r7lhCAP4nXSBTaE7ueeqj6IOwZpgoqw== - dependencies: - chalk "^4.0.0" - graceful-fs "^4.2.9" - jest-haste-map "^29.4.2" - jest-pnp-resolver "^1.2.2" - jest-util "^29.4.2" - jest-validate "^29.4.2" - resolve "^1.20.0" - resolve.exports "^2.0.0" - slash "^3.0.0" - -jest-runner@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-29.4.2.tgz#2bcecf72303369df4ef1e6e983c22a89870d5125" - integrity sha512-wqwt0drm7JGjwdH+x1XgAl+TFPH7poowMguPQINYxaukCqlczAcNLJiK+OLxUxQAEWMdy+e6nHZlFHO5s7EuRg== - dependencies: - "@jest/console" "^29.4.2" - "@jest/environment" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" - "@types/node" "*" - chalk "^4.0.0" - emittery "^0.13.1" - graceful-fs "^4.2.9" - jest-docblock "^29.4.2" - jest-environment-node "^29.4.2" - jest-haste-map "^29.4.2" - jest-leak-detector "^29.4.2" - jest-message-util "^29.4.2" - jest-resolve "^29.4.2" - jest-runtime "^29.4.2" - jest-util "^29.4.2" - jest-watcher "^29.4.2" - jest-worker "^29.4.2" - p-limit "^3.1.0" - source-map-support "0.5.13" - -jest-runtime@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-29.4.2.tgz#d86b764c5b95d76cb26ed1f32644e99de5d5c134" - integrity sha512-3fque9vtpLzGuxT9eZqhxi+9EylKK/ESfhClv4P7Y9sqJPs58LjVhTt8jaMp/pRO38agll1CkSu9z9ieTQeRrw== - dependencies: - "@jest/environment" "^29.4.2" - "@jest/fake-timers" "^29.4.2" - "@jest/globals" "^29.4.2" - "@jest/source-map" "^29.4.2" - "@jest/test-result" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" - "@types/node" "*" - chalk "^4.0.0" - cjs-module-lexer "^1.0.0" - collect-v8-coverage "^1.0.0" - glob "^7.1.3" - graceful-fs "^4.2.9" - jest-haste-map "^29.4.2" - jest-message-util "^29.4.2" - jest-mock "^29.4.2" - jest-regex-util "^29.4.2" - jest-resolve "^29.4.2" - jest-snapshot "^29.4.2" - jest-util "^29.4.2" - semver "^7.3.5" - slash "^3.0.0" - strip-bom "^4.0.0" - -jest-snapshot@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-29.4.2.tgz#ba1fb9abb279fd2c85109ff1757bc56b503bbb3a" - integrity sha512-PdfubrSNN5KwroyMH158R23tWcAXJyx4pvSvWls1dHoLCaUhGul9rsL3uVjtqzRpkxlkMavQjGuWG1newPgmkw== - dependencies: - "@babel/core" "^7.11.6" - "@babel/generator" "^7.7.2" - "@babel/plugin-syntax-jsx" "^7.7.2" - "@babel/plugin-syntax-typescript" "^7.7.2" - "@babel/traverse" "^7.7.2" - "@babel/types" "^7.3.3" - "@jest/expect-utils" "^29.4.2" - "@jest/transform" "^29.4.2" - "@jest/types" "^29.4.2" - "@types/babel__traverse" "^7.0.6" - "@types/prettier" "^2.1.5" - babel-preset-current-node-syntax "^1.0.0" - chalk "^4.0.0" - expect "^29.4.2" - graceful-fs "^4.2.9" - jest-diff "^29.4.2" - jest-get-type "^29.4.2" - jest-haste-map "^29.4.2" - jest-matcher-utils "^29.4.2" - jest-message-util "^29.4.2" - jest-util "^29.4.2" - natural-compare "^1.4.0" - pretty-format "^29.4.2" - semver "^7.3.5" - -jest-util@^29.0.0, jest-util@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-29.4.2.tgz#3db8580b295df453a97de4a1b42dd2578dabd2c2" - integrity sha512-wKnm6XpJgzMUSRFB7YF48CuwdzuDIHenVuoIb1PLuJ6F+uErZsuDkU+EiExkChf6473XcawBrSfDSnXl+/YG4g== - dependencies: - "@jest/types" "^29.4.2" - "@types/node" "*" - chalk "^4.0.0" - ci-info "^3.2.0" - graceful-fs "^4.2.9" - picomatch "^2.2.3" - -jest-validate@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-29.4.2.tgz#3b3f8c4910ab9a3442d2512e2175df6b3f77b915" - integrity sha512-tto7YKGPJyFbhcKhIDFq8B5od+eVWD/ySZ9Tvcp/NGCvYA4RQbuzhbwYWtIjMT5W5zA2W0eBJwu4HVw34d5G6Q== - dependencies: - "@jest/types" "^29.4.2" - camelcase "^6.2.0" - chalk "^4.0.0" - jest-get-type "^29.4.2" - leven "^3.1.0" - pretty-format "^29.4.2" - -jest-watcher@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-29.4.2.tgz#09c0f4c9a9c7c0807fcefb1445b821c6f7953b7c" - integrity sha512-onddLujSoGiMJt+tKutehIidABa175i/Ays+QvKxCqBwp7fvxP3ZhKsrIdOodt71dKxqk4sc0LN41mWLGIK44w== - dependencies: - "@jest/test-result" "^29.4.2" - "@jest/types" "^29.4.2" - "@types/node" "*" - ansi-escapes "^4.2.1" - chalk "^4.0.0" - emittery "^0.13.1" - jest-util "^29.4.2" - string-length "^4.0.1" - -jest-worker@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-29.4.2.tgz#d9b2c3bafc69311d84d94e7fb45677fc8976296f" - integrity sha512-VIuZA2hZmFyRbchsUCHEehoSf2HEl0YVF8SDJqtPnKorAaBuh42V8QsLnde0XP5F6TyCynGPEGgBOn3Fc+wZGw== - dependencies: - "@types/node" "*" - jest-util "^29.4.2" - merge-stream "^2.0.0" - supports-color "^8.0.0" - -jest@^29.3.1: - version "29.4.2" - resolved "https://registry.yarnpkg.com/jest/-/jest-29.4.2.tgz#4c2127d03a71dc187f386156ef155dbf323fb7be" - integrity sha512-+5hLd260vNIHu+7ZgMIooSpKl7Jp5pHKb51e73AJU3owd5dEo/RfVwHbA/na3C/eozrt3hJOLGf96c7EWwIAzg== - dependencies: - "@jest/core" "^29.4.2" - "@jest/types" "^29.4.2" - import-local "^3.0.2" - jest-cli "^29.4.2" - -jiti@^1.16.0, jiti@^1.16.2, jiti@^1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.17.0.tgz#9a4e1787b9d83e594a5ad27cdf9c9ab555112ac1" - integrity sha512-CByzPgFqYoB9odEeef7GNmQ3S5THIBOtzRYoSCya2Sv27AuQxy2jgoFjQ6VTF53xsq1MXRm+YWNvOoDHUAteOw== - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.14.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.1.tgz#dae812fdb3825fa306609a8717383c50c36a0537" - integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -js-yaml@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" - integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== - dependencies: - argparse "^2.0.1" - -jsesc@^2.5.1: - version "2.5.2" - resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" - integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== - -json-buffer@3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" - integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== - -json-parse-even-better-errors@^2.3.0: - version "2.3.1" - resolved "https://registry.yarnpkg.com/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" - integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== - -json5@^2.2.2, json5@^2.2.3: - version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" - integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== - -jsonc-parser@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/jsonc-parser/-/jsonc-parser-3.2.0.tgz#31ff3f4c2b9793f89c67212627c51c6394f88e76" - integrity sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w== - -jsonfile@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" - integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== - dependencies: - universalify "^2.0.0" - optionalDependencies: - graceful-fs "^4.1.6" - -keyv@^4.0.0: - version "4.5.2" - resolved "https://registry.yarnpkg.com/keyv/-/keyv-4.5.2.tgz#0e310ce73bf7851ec702f2eaf46ec4e3805cce56" - integrity sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g== - dependencies: - json-buffer "3.0.1" - -khroma@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/khroma/-/khroma-2.0.0.tgz#7577de98aed9f36c7a474c4d453d94c0d6c6588b" - integrity sha512-2J8rDNlQWbtiNYThZRvmMv5yt44ZakX+Tz5ZIp/mN1pt4snn+m030Va5Z4v8xA0cQFDXBwO/8i42xL4QPsVk3g== - -kleur@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" - integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== - -kleur@^4.0.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" - integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== - -klona@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22" - integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA== - -knitwork@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/knitwork/-/knitwork-1.0.0.tgz#38d124dead875bee5feea1733632295af58a49d2" - integrity sha512-dWl0Dbjm6Xm+kDxhPQJsCBTxrJzuGl0aP9rhr+TG8D3l+GL90N8O8lYUi7dTSAN2uuDqCtNgb6aEuQH5wsiV8Q== - -lazystream@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.1.tgz#494c831062f1f9408251ec44db1cba29242a2638" - integrity sha512-b94GiNHQNy6JNTrt5w6zNyffMrNkXZb3KTkCZJb2V1xaEGCk093vkZ2jk3tpaeP33/OiXC+WvK9AxUebnf5nbw== - dependencies: - readable-stream "^2.0.5" - -leven@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" - integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== - -lilconfig@^2.0.3, lilconfig@^2.0.5, lilconfig@^2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/lilconfig/-/lilconfig-2.0.6.tgz#32a384558bd58af3d4c6e077dd1ad1d397bc69d4" - integrity sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg== - -lines-and-columns@^1.1.6: - version "1.2.4" - resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632" - integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg== - -listhen@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/listhen/-/listhen-1.0.2.tgz#3332af0cf77dd914e12d125c70a9c6aed9537033" - integrity sha512-yXz0NIYfVJDBQK2vlCpD/OjSzYkur2mR44boUtlg0eES4holn7oYZf439y5JxP55EOzFtClZ8eZlMJ8a++FwlQ== - dependencies: - clipboardy "^3.0.0" - colorette "^2.0.19" - defu "^6.1.2" - get-port-please "^3.0.1" - http-shutdown "^1.2.2" - ip-regex "^5.0.0" - node-forge "^1.3.1" - ufo "^1.0.1" - -local-pkg@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-0.4.3.tgz#0ff361ab3ae7f1c19113d9bb97b98b905dbc4963" - integrity sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g== - -locate-path@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0" - integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g== - dependencies: - p-locate "^4.1.0" - -lodash-es@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.21.tgz#43e626c46e6591b7750beb2b50117390c609e3ee" - integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== - -lodash._reinterpolate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" - integrity sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA== - -lodash.debounce@^4.0.8: - version "4.0.8" - resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" - integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== - -lodash.defaults@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.defaults/-/lodash.defaults-4.2.0.tgz#d09178716ffea4dde9e5fb7b37f6f0802274580c" - integrity sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ== - -lodash.difference@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.difference/-/lodash.difference-4.5.0.tgz#9ccb4e505d486b91651345772885a2df27fd017c" - integrity sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA== - -lodash.flatten@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.flatten/-/lodash.flatten-4.4.0.tgz#f31c22225a9632d2bbf8e4addbef240aa765a61f" - integrity sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g== - -lodash.isarguments@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz#2f573d85c6a24289ff00663b491c1d338ff3458a" - integrity sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg== - -lodash.isplainobject@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" - integrity sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA== - -lodash.memoize@4.x, lodash.memoize@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" - integrity sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag== - -lodash.pick@^4.4.0: - version "4.4.0" - resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" - integrity sha512-hXt6Ul/5yWjfklSGvLQl8vM//l3FtyHZeuelpzK6mm99pNvN9yTDruNZPEJZD1oWrqo+izBmB7oUfWgcCX7s4Q== - -lodash.template@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" - integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== - dependencies: - lodash._reinterpolate "^3.0.0" - lodash.templatesettings "^4.0.0" - -lodash.templatesettings@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz#e481310f049d3cf6d47e912ad09313b154f0fb33" - integrity sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ== - dependencies: - lodash._reinterpolate "^3.0.0" - -lodash.union@^4.6.0: - version "4.6.0" - resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" - integrity sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw== - -lodash.uniq@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" - integrity sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ== - -lodash@^4.17.21: - version "4.17.21" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" - integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== - -log-symbols@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-5.1.0.tgz#a20e3b9a5f53fac6aeb8e2bb22c07cf2c8f16d93" - integrity sha512-l0x2DvrW294C9uDCoQe1VSU4gf529FkSZ6leBl4TiqZH/e+0R7hSfHQBNut2mNygDgHwvYHfFLn6Oxb3VWj2rA== - dependencies: - chalk "^5.0.0" - is-unicode-supported "^1.1.0" - -longest-streak@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" - integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== - -loupe@^2.3.1: - version "2.3.6" - resolved "https://registry.yarnpkg.com/loupe/-/loupe-2.3.6.tgz#76e4af498103c532d1ecc9be102036a21f787b53" - integrity sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA== - dependencies: - get-func-name "^2.0.0" - -lower-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-2.0.2.tgz#6fa237c63dbdc4a82ca0fd882e4722dc5e634e28" - integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg== - dependencies: - tslib "^2.0.3" - -lowercase-keys@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" - integrity sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA== - -lru-cache@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" - integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== - dependencies: - yallist "^3.0.2" - -lru-cache@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" - integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== - dependencies: - yallist "^4.0.0" - -lru-cache@^7.14.1: - version "7.14.1" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.14.1.tgz#8da8d2f5f59827edb388e63e459ac23d6d408fea" - integrity sha512-ysxwsnTKdAx96aTRdhDOCQfDgbHnt8SK0KY8SEjO0wHinhWOFTESbjVCMPbU1uGXg/ch4lifqx0wfjOawU2+WA== - -magic-string@^0.25.3, magic-string@^0.25.7: - version "0.25.9" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" - integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== - dependencies: - sourcemap-codec "^1.4.8" - -magic-string@^0.26.7: - version "0.26.7" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.7.tgz#caf7daf61b34e9982f8228c4527474dac8981d6f" - integrity sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow== - dependencies: - sourcemap-codec "^1.4.8" - -magic-string@^0.27.0: - version "0.27.0" - resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.27.0.tgz#e4a3413b4bab6d98d2becffd48b4a257effdbbf3" - integrity sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA== - dependencies: - "@jridgewell/sourcemap-codec" "^1.4.13" - -make-dir@^3.0.0, make-dir@^3.1.0, make-dir@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" - integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== - dependencies: - semver "^6.0.0" - -make-error@1.x: - version "1.3.6" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" - integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== - -makeerror@1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" - integrity sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg== - dependencies: - tmpl "1.0.5" - -markdown-table@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" - integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== - -mdast-squeeze-paragraphs@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/mdast-squeeze-paragraphs/-/mdast-squeeze-paragraphs-5.2.1.tgz#4f687231c9caf1073c4511557adae35414615d9e" - integrity sha512-npINYQrt0E5AvSvM7ZxIIyrG/7DX+g8jKWcJMudrcjI+b1eNOKbbu+wTo6cKvy5IzH159IPfpWoRVH7kwEmnug== - dependencies: - "@types/mdast" "^3.0.0" - unist-util-visit "^4.0.0" - -mdast-util-definitions@^5.0.0: - version "5.1.2" - resolved "https://registry.yarnpkg.com/mdast-util-definitions/-/mdast-util-definitions-5.1.2.tgz#9910abb60ac5d7115d6819b57ae0bcef07a3f7a7" - integrity sha512-8SVPMuHqlPME/z3gqVwWY4zVXn8lqKv/pAhC57FuJ40ImXyBpmO5ukh98zB2v7Blql2FiHjHv9LVztSIqjY+MA== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - unist-util-visit "^4.0.0" - -mdast-util-find-and-replace@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" - integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== - dependencies: - "@types/mdast" "^3.0.0" - escape-string-regexp "^5.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.0.0" - -mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.2.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.0.tgz#0214124154f26154a2b3f9d401155509be45e894" - integrity sha512-HN3W1gRIuN/ZW295c7zi7g9lVBllMgZE40RxCX37wrTPWXCWtpvOZdfnuK+1WNpvZje6XuJeI3Wnb4TJEUem+g== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - decode-named-character-reference "^1.0.0" - mdast-util-to-string "^3.1.0" - micromark "^3.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-decode-string "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-stringify-position "^3.0.0" - uvu "^0.5.0" - -mdast-util-gfm-autolink-literal@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" - integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== - dependencies: - "@types/mdast" "^3.0.0" - ccount "^2.0.0" - mdast-util-find-and-replace "^2.0.0" - micromark-util-character "^1.0.0" - -mdast-util-gfm-footnote@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" - integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - micromark-util-normalize-identifier "^1.0.0" - -mdast-util-gfm-strikethrough@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" - integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - -mdast-util-gfm-table@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" - integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== - dependencies: - "@types/mdast" "^3.0.0" - markdown-table "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.3.0" - -mdast-util-gfm-task-list-item@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" - integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - -mdast-util-gfm@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" - integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== - dependencies: - mdast-util-from-markdown "^1.0.0" - mdast-util-gfm-autolink-literal "^1.0.0" - mdast-util-gfm-footnote "^1.0.0" - mdast-util-gfm-strikethrough "^1.0.0" - mdast-util-gfm-table "^1.0.0" - mdast-util-gfm-task-list-item "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-phrasing@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" - integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== - dependencies: - "@types/mdast" "^3.0.0" - unist-util-is "^5.0.0" - -mdast-util-to-hast@^12.1.0, mdast-util-to-hast@^12.2.6: - version "12.3.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-hast/-/mdast-util-to-hast-12.3.0.tgz#045d2825fb04374e59970f5b3f279b5700f6fb49" - integrity sha512-pits93r8PhnIoU4Vy9bjW39M2jJ6/tdHyja9rrot9uujkN7UTU9SDnE6WNJz/IGyQk3XHX6yNNtrBH6cQzm8Hw== - dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-definitions "^5.0.0" - micromark-util-sanitize-uri "^1.1.0" - trim-lines "^3.0.0" - unist-util-generated "^2.0.0" - unist-util-position "^4.0.0" - unist-util-visit "^4.0.0" - -mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" - integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - longest-streak "^3.0.0" - mdast-util-phrasing "^3.0.0" - mdast-util-to-string "^3.0.0" - micromark-util-decode-string "^1.0.0" - unist-util-visit "^4.0.0" - zwitch "^2.0.0" - -mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.1.1.tgz#db859050d79d48cf9896d294de06f3ede7474d16" - integrity sha512-tGvhT94e+cVnQt8JWE9/b3cUQZWS732TJxXHktvP+BYo62PpYD53Ls/6cC60rW21dW+txxiM4zMdc6abASvZKA== - dependencies: - "@types/mdast" "^3.0.0" - -mdn-data@2.0.14: - version "2.0.14" - resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50" - integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow== - -mdurl@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" - integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g== - -memory-fs@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/memory-fs/-/memory-fs-0.5.0.tgz#324c01288b88652966d161db77838720845a8e3c" - integrity sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA== - dependencies: - errno "^0.1.3" - readable-stream "^2.0.1" - -merge-stream@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60" - integrity sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w== - -merge2@^1.3.0, merge2@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" - integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== - -mermaid@^9.2.2: - version "9.3.0" - resolved "https://registry.yarnpkg.com/mermaid/-/mermaid-9.3.0.tgz#8bd7c4a44b53e4e85c53a0a474442e9c273494ae" - integrity sha512-mGl0BM19TD/HbU/LmlaZbjBi//tojelg8P/mxD6pPZTAYaI+VawcyBdqRsoUHSc7j71PrMdJ3HBadoQNdvP5cg== - dependencies: - "@braintree/sanitize-url" "^6.0.0" - d3 "^7.0.0" - dagre-d3-es "7.0.6" - dompurify "2.4.1" - khroma "^2.0.0" - lodash-es "^4.17.21" - moment-mini "^2.24.0" - non-layered-tidy-tree-layout "^2.0.2" - stylis "^4.1.2" - uuid "^9.0.0" - -metadata-scraper@^0.2.49: - version "0.2.61" - resolved "https://registry.yarnpkg.com/metadata-scraper/-/metadata-scraper-0.2.61.tgz#3ca7f613519a849a8f28a700d7557be012a3c70d" - integrity sha512-ECV8r10nIVgn7Y5vY8lnlvi9vF1YgYBJjn2R1zrOcKRe47ra9Yg25ZE1ejL3Equqi8u2Mp346KHqIcR4PLdyTA== - dependencies: - domino "^2.1.6" - got "^11.8.1" - -micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1, micromark-core-commonmark@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.0.6.tgz#edff4c72e5993d93724a3c206970f5a15b0585ad" - integrity sha512-K+PkJTxqjFfSNkfAhp4GB+cZPfQd6dxtTXnf+RjZOV7T4EEXnvgzOcnp+eSTmpGk9d1S9sL6/lqrgSNn/s0HZA== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-factory-destination "^1.0.0" - micromark-factory-label "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-factory-title "^1.0.0" - micromark-factory-whitespace "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-html-tag-name "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - -micromark-extension-gfm-autolink-literal@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.3.tgz#dc589f9c37eaff31a175bab49f12290edcf96058" - integrity sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-footnote@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.0.4.tgz#cbfd8873b983e820c494498c6dac0105920818d5" - integrity sha512-E/fmPmDqLiMUP8mLJ8NbJWJ4bTw6tS+FEQS8CcuDtZpILuOb2kjLqPEeAePF1djXROHXChM/wPJw0iS4kHCcIg== - dependencies: - micromark-core-commonmark "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-strikethrough@^1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.4.tgz#162232c284ffbedd8c74e59c1525bda217295e18" - integrity sha512-/vjHU/lalmjZCT5xt7CcHVJGq8sYRm80z24qAKXzaHzem/xsDYb2yLL+NNVbYvmpLx3O7SYPuGL5pzusL9CLIQ== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-table@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.5.tgz#7b708b728f8dc4d95d486b9e7a2262f9cddbcbb4" - integrity sha512-xAZ8J1X9W9K3JTJTUL7G6wSKhp2ZYHrFk5qJgY/4B33scJzE2kpfRL6oiw/veJTbt7jiM/1rngLlOKPWr1G+vg== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-tagfilter@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.1.tgz#fb2e303f7daf616db428bb6a26e18fda14a90a4d" - integrity sha512-Ty6psLAcAjboRa/UKUbbUcwjVAv5plxmpUTy2XC/3nJFL37eHej8jrHrRzkqcpipJliuBH30DTs7+3wqNcQUVA== - dependencies: - micromark-util-types "^1.0.0" - -micromark-extension-gfm-task-list-item@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.3.tgz#7683641df5d4a09795f353574d7f7f66e47b7fc4" - integrity sha512-PpysK2S1Q/5VXi72IIapbi/jliaiOFzv7THH4amwXeYXLq3l1uo8/2Be0Ac1rEwK20MQEsGH2ltAZLNY2KI/0Q== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.1.tgz#40f3209216127a96297c54c67f5edc7ef2d1a2a2" - integrity sha512-p2sGjajLa0iYiGQdT0oelahRYtMWvLjy8J9LOCxzIQsllMCGLbsLW+Nc+N4vi02jcRJvedVJ68cjelKIO6bpDA== - dependencies: - micromark-extension-gfm-autolink-literal "^1.0.0" - micromark-extension-gfm-footnote "^1.0.0" - micromark-extension-gfm-strikethrough "^1.0.0" - micromark-extension-gfm-table "^1.0.0" - micromark-extension-gfm-tagfilter "^1.0.0" - micromark-extension-gfm-task-list-item "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-destination@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.0.0.tgz#fef1cb59ad4997c496f887b6977aa3034a5a277e" - integrity sha512-eUBA7Rs1/xtTVun9TmV3gjfPz2wEwgK5R5xcbIM5ZYAtvGF6JkyaDsj0agx8urXnO31tEO6Ug83iVH3tdedLnw== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-label@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.0.2.tgz#6be2551fa8d13542fcbbac478258fb7a20047137" - integrity sha512-CTIwxlOnU7dEshXDQ+dsr2n+yxpP0+fn271pu0bwDIS8uqfFcumXpj5mLn3hSC8iw2MUr6Gx8EcKng1dD7i6hg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-factory-space@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.0.0.tgz#cebff49968f2b9616c0fcb239e96685cb9497633" - integrity sha512-qUmqs4kj9a5yBnk3JMLyjtWYN6Mzfcx8uJfi5XAveBniDevmZasdGBba5b4QsvRcAkmvGo5ACmSUmyGiKTLZew== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-title@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.0.2.tgz#7e09287c3748ff1693930f176e1c4a328382494f" - integrity sha512-zily+Nr4yFqgMGRKLpTVsNl5L4PMu485fGFDOQJQBl2NFpjGte1e86zC0da93wf97jrc4+2G2GQudFMHn3IX+A== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-factory-whitespace@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.0.0.tgz#e991e043ad376c1ba52f4e49858ce0794678621c" - integrity sha512-Qx7uEyahU1lt1RnsECBiuEbfr9INjQTGa6Err+gF3g0Tx4YEviPbqqGKNv/NrBaE7dVHdn1bVZKM/n5I/Bak7A== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-character@^1.0.0, micromark-util-character@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.1.0.tgz#d97c54d5742a0d9611a68ca0cd4124331f264d86" - integrity sha512-agJ5B3unGNJ9rJvADMJ5ZiYjBRyDpzKAOk01Kpi1TKhlT1APx3XZk6eN7RtSz1erbWHC2L8T3xLZ81wdtGRZzg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-chunked@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.0.0.tgz#5b40d83f3d53b84c4c6bce30ed4257e9a4c79d06" - integrity sha512-5e8xTis5tEZKgesfbQMKRCyzvffRRUX+lK/y+DvsMFdabAicPkkZV6gO+FEWi9RfuKKoxxPwNL+dFF0SMImc1g== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-classify-character@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.0.0.tgz#cbd7b447cb79ee6997dd274a46fc4eb806460a20" - integrity sha512-F8oW2KKrQRb3vS5ud5HIqBVkCqQi224Nm55o5wYLzY/9PwHGXC01tr3d7+TqHHz6zrKQ72Okwtvm/xQm6OVNZA== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-combine-extensions@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.0.0.tgz#91418e1e74fb893e3628b8d496085639124ff3d5" - integrity sha512-J8H058vFBdo/6+AsjHp2NF7AJ02SZtWaVUjsayNFeAiydTxUwViQPxN0Hf8dp4FmCQi0UUFovFsEyRSUmFH3MA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-decode-numeric-character-reference@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.0.0.tgz#dcc85f13b5bd93ff8d2868c3dba28039d490b946" - integrity sha512-OzO9AI5VUtrTD7KSdagf4MWgHMtET17Ua1fIpXTpuhclCqD8egFWo85GxSGvxgkGS74bEahvtM0WP0HjvV0e4w== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-decode-string@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.0.2.tgz#942252ab7a76dec2dbf089cc32505ee2bc3acf02" - integrity sha512-DLT5Ho02qr6QWVNYbRZ3RYOSSWWFuH3tJexd3dgN1odEuPNxCngTCXJum7+ViRAd9BbdxCvMToPOD/IvVhzG6Q== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-symbol "^1.0.0" - -micromark-util-encode@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.0.1.tgz#2c1c22d3800870ad770ece5686ebca5920353383" - integrity sha512-U2s5YdnAYexjKDel31SVMPbfi+eF8y1U4pfiRW/Y8EFVCy/vgxk/2wWTxzcqE71LHtCuCzlBDRU2a5CQ5j+mQA== - -micromark-util-html-tag-name@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.1.0.tgz#eb227118befd51f48858e879b7a419fc0df20497" - integrity sha512-BKlClMmYROy9UiV03SwNmckkjn8QHVaWkqoAqzivabvdGcwNGMMMH/5szAnywmsTBUzDsU57/mFi0sp4BQO6dA== - -micromark-util-normalize-identifier@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.0.0.tgz#4a3539cb8db954bbec5203952bfe8cedadae7828" - integrity sha512-yg+zrL14bBTFrQ7n35CmByWUTFsgst5JhA4gJYoty4Dqzj4Z4Fr/DHekSS5aLfH9bdlfnSvKAWsAgJhIbogyBg== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-resolve-all@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.0.0.tgz#a7c363f49a0162e931960c44f3127ab58f031d88" - integrity sha512-CB/AGk98u50k42kvgaMM94wzBqozSzDDaonKU7P7jwQIuH2RU0TeBqGYJz2WY1UdihhjweivStrJ2JdkdEmcfw== - dependencies: - micromark-util-types "^1.0.0" - -micromark-util-sanitize-uri@^1.0.0, micromark-util-sanitize-uri@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.1.0.tgz#f12e07a85106b902645e0364feb07cf253a85aee" - integrity sha512-RoxtuSCX6sUNtxhbmsEFQfWzs8VN7cTctmBPvYivo98xb/kDEoTCtJQX5wyzIYEmk/lvNFTat4hL8oW0KndFpg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-symbol "^1.0.0" - -micromark-util-subtokenize@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.0.2.tgz#ff6f1af6ac836f8bfdbf9b02f40431760ad89105" - integrity sha512-d90uqCnXp/cy4G881Ub4psE57Sf8YD0pim9QdjCRNjfas2M1u6Lbt+XZK9gnHL2XFhnozZiEdCa9CNfXSfQ6xA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-util-symbol@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.0.1.tgz#b90344db62042ce454f351cf0bebcc0a6da4920e" - integrity sha512-oKDEMK2u5qqAptasDAwWDXq0tG9AssVwAx3E9bBF3t/shRIGsWIRG+cGafs2p/SnDSOecnt6hZPCE2o6lHfFmQ== - -micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.0.2.tgz#f4220fdb319205812f99c40f8c87a9be83eded20" - integrity sha512-DCfg/T8fcrhrRKTPjRrw/5LLvdGV7BHySf/1LOZx7TzWZdYRjogNtyNq885z3nNallwr3QUKARjqvHqX1/7t+w== - -micromark@^3.0.0, micromark@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.1.0.tgz#eeba0fe0ac1c9aaef675157b52c166f125e89f62" - integrity sha512-6Mj0yHLdUZjHnOPgr5xfWIMqMWS12zDN6iws9SLuSz76W8jTtAv24MN4/CL7gJrl5vtxGInkkqDv/JIoRsQOvA== - dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - decode-named-character-reference "^1.0.0" - micromark-core-commonmark "^1.0.1" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - -micromatch@^4.0.2, micromatch@^4.0.4, micromatch@^4.0.5: - version "4.0.5" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.5.tgz#bc8999a7cbbf77cdc89f132f6e467051b49090c6" - integrity sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA== - dependencies: - braces "^3.0.2" - picomatch "^2.3.1" - -mime@1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== - -mime@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-3.0.0.tgz#b374550dca3a0c18443b0c950a6a58f1931cf7a7" - integrity sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A== - -mime@~2.5.2: - version "2.5.2" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.5.2.tgz#6e3dc6cc2b9510643830e5f19d5cb753da5eeabe" - integrity sha512-tqkh47FzKeCPD2PUiPB6pkbMzsCasjxAfC62/Wap5qrUWcb+sFasXUC5I3gYM5iBM8v/Qpn4UK0x+j0iHyFPDg== - -mimic-fn@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" - integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== - -mimic-fn@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-4.0.0.tgz#60a90550d5cb0b239cca65d893b1a53b29871ecc" - integrity sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw== - -mimic-response@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-1.0.1.tgz#4923538878eef42063cb8a3e3b0798781487ab1b" - integrity sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ== - -mimic-response@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/mimic-response/-/mimic-response-3.1.0.tgz#2d1d59af9c1b129815accc2c46a022a5ce1fa3c9" - integrity sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ== - -minimatch@^3.0.4, minimatch@^3.1.1: - version "3.1.2" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" - integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== - dependencies: - brace-expansion "^1.1.7" - -minimatch@^5.0.1, minimatch@^5.1.0, minimatch@^5.1.1: - version "5.1.6" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" - integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== - dependencies: - brace-expansion "^2.0.1" - -minimatch@~3.0.4: - version "3.0.8" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.8.tgz#5e6a59bd11e2ab0de1cfb843eb2d82e546c321c1" - integrity sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q== - dependencies: - brace-expansion "^1.1.7" - -minimist@^1.2.6: - version "1.2.7" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18" - integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g== - -minipass@^3.0.0: - version "3.3.6" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a" - integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw== - dependencies: - yallist "^4.0.0" - -minipass@^4.0.0: - version "4.0.3" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-4.0.3.tgz#00bfbaf1e16e35e804f4aa31a7c1f6b8d9f0ee72" - integrity sha512-OW2r4sQ0sI+z5ckEt5c1Tri4xTgZwYDxpE54eqWlQloQRoWtXjqt9udJ5Z4dSv7wK+nfFI7FRXyCpBSft+gpFw== - -minizlib@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" - integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== - dependencies: - minipass "^3.0.0" - yallist "^4.0.0" - -mitt@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/mitt/-/mitt-2.1.0.tgz#f740577c23176c6205b121b2973514eade1b2230" - integrity sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg== - -mkdir@^0.0.2: - version "0.0.2" - resolved "https://registry.yarnpkg.com/mkdir/-/mkdir-0.0.2.tgz#3b9da7a4e5b13004ebc636581b160e1e04776851" - integrity sha512-98OnjcWaNEIRUJJe9rFoWlbkQ5n9z8F86wIPCrI961YEViiVybTuJln919WuuSHSnlrqXy0ELKCntoPy8C7lqg== - -mkdirp@0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.0.tgz#1bbf5ab1ba827af23575143490426455f481fe1e" - integrity sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew== - -mkdirp@^1.0.3, mkdirp@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" - integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== - -mkdist@^1.1.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/mkdist/-/mkdist-1.1.1.tgz#1492cd37174f43e7e2855d445af1b591bf1c4fa9" - integrity sha512-9cEzCsBD0qpybR/lJMB0vRIDZiHP7hJHTN2mQtFU2qt0vr7lFnghxersOJbKLshaDsl4GlnY2OBzmRRUTfuaDg== - dependencies: - defu "^6.1.2" - esbuild "^0.17.5" - fs-extra "^11.1.0" - globby "^13.1.3" - jiti "^1.16.2" - mri "^1.2.0" - pathe "^1.1.0" - -mlly@^1.0.0, mlly@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.1.0.tgz#9e23c5e675ef7b10cc47ee6281795cb1a7aa3aa2" - integrity sha512-cwzBrBfwGC1gYJyfcy8TcZU1f+dbH/T+TuOhtYP2wLv/Fb51/uV7HJQfBPtEupZ2ORLRU1EKFS/QfS3eo9+kBQ== - dependencies: - acorn "^8.8.1" - pathe "^1.0.0" - pkg-types "^1.0.1" - ufo "^1.0.1" - -moment-mini@^2.24.0: - version "2.29.4" - resolved "https://registry.yarnpkg.com/moment-mini/-/moment-mini-2.29.4.tgz#cbbcdc58ce1b267506f28ea6668dbe060a32758f" - integrity sha512-uhXpYwHFeiTbY9KSgPPRoo1nt8OxNVdMVoTBYHfSEKeRkIkwGpO+gERmhuhBtzfaeOyTkykSrm2+noJBgqt3Hg== - -mri@^1.1.0, mri@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" - integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== - -ms@2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -ms@2.1.3: - version "2.1.3" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" - integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== - -muggle-string@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/muggle-string/-/muggle-string-0.1.0.tgz#1fda8a281c8b27bb8b70466dbc9f27586a8baa6c" - integrity sha512-Tr1knR3d2mKvvWthlk7202rywKbiOm4rVFLsfAaSIhJ6dt9o47W4S+JMtWhd/PW9Wrdew2/S2fSvhz3E2gkfEg== - -mute-stream@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" - integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== - -nanoid@^3.3.4: - version "3.3.4" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.4.tgz#730b67e3cd09e2deacf03c027c81c9d9dbc5e8ab" - integrity sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw== - -nanoid@^4.0.0, nanoid@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-4.0.1.tgz#398d7ccfdbf9faf2231b2ca7e8fff5dbca6a509b" - integrity sha512-udKGtCCUafD3nQtJg9wBhRP3KMbPglUsgV5JVsXhvyBs/oefqb4sqMEhKBBgqZncYowu58p1prsZQBYvAj/Gww== - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== - -nitropack@^2.1.1: - version "2.2.0" - resolved "https://registry.yarnpkg.com/nitropack/-/nitropack-2.2.0.tgz#f5def0ab1c9c12f8c4314bd5ba9f6f6f05ad852d" - integrity sha512-aNJdupXKA3OiYHDvlCnrcJMKrVGoW7RGnRjm8sCUrgICRC9bbVdsZU/EQh3pLIc7DQW/rej3eRxceqfnlKsOkg== - dependencies: - "@cloudflare/kv-asset-handler" "^0.3.0" - "@netlify/functions" "^1.4.0" - "@rollup/plugin-alias" "^4.0.3" - "@rollup/plugin-commonjs" "^24.0.1" - "@rollup/plugin-inject" "^5.0.3" - "@rollup/plugin-json" "^6.0.0" - "@rollup/plugin-node-resolve" "^15.0.1" - "@rollup/plugin-replace" "^5.0.2" - "@rollup/plugin-terser" "^0.4.0" - "@rollup/plugin-wasm" "^6.1.2" - "@rollup/pluginutils" "^5.0.2" - "@vercel/nft" "^0.22.6" - archiver "^5.3.1" - c12 "^1.1.0" - chalk "^5.2.0" - chokidar "^3.5.3" - consola "^2.15.3" - cookie-es "^0.5.0" - defu "^6.1.2" - destr "^1.2.2" - dot-prop "^7.2.0" - esbuild "^0.17.6" - escape-string-regexp "^5.0.0" - etag "^1.8.1" - fs-extra "^11.1.0" - globby "^13.1.3" - gzip-size "^7.0.0" - h3 "^1.4.0" - hookable "^5.4.2" - http-proxy "^1.18.1" - is-primitive "^3.0.1" - jiti "^1.17.0" - klona "^2.0.6" - knitwork "^1.0.0" - listhen "^1.0.2" - mime "^3.0.0" - mlly "^1.1.0" - mri "^1.2.0" - node-fetch-native "^1.0.1" - ofetch "^1.0.0" - ohash "^1.0.0" - pathe "^1.1.0" - perfect-debounce "^0.1.3" - pkg-types "^1.0.1" - pretty-bytes "^6.1.0" - radix3 "^1.0.0" - rollup "^3.14.0" - rollup-plugin-visualizer "^5.9.0" - scule "^1.0.0" - semver "^7.3.8" - serve-placeholder "^2.0.1" - serve-static "^1.15.0" - source-map-support "^0.5.21" - std-env "^3.3.2" - ufo "^1.0.1" - unenv "^1.1.1" - unimport "^2.2.4" - unstorage "^1.1.4" - -no-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/no-case/-/no-case-3.0.4.tgz#d361fd5c9800f558551a8369fc0dcd4662b6124d" - integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg== - dependencies: - lower-case "^2.0.2" - tslib "^2.0.3" - -node-domexception@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/node-domexception/-/node-domexception-1.0.0.tgz#6888db46a1f71c0b76b3f7555016b63fe64766e5" - integrity sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ== - -node-emoji@^1.11.0: - version "1.11.0" - resolved "https://registry.yarnpkg.com/node-emoji/-/node-emoji-1.11.0.tgz#69a0150e6946e2f115e9d7ea4df7971e2628301c" - integrity sha512-wo2DpQkQp7Sjm2A0cq+sN7EHKO6Sl0ctXeBdFZrL9T9+UywORbufTcTZxom8YqpLQt/FqNMUkOpkZrJVYSKD3A== - dependencies: - lodash "^4.17.21" - -node-fetch-native@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/node-fetch-native/-/node-fetch-native-1.0.1.tgz#1dfe78f57545d07e07016b7df4c0cb9d2ff416c7" - integrity sha512-VzW+TAk2wE4X9maiKMlT+GsPU4OMmR1U9CrHSmd3DFLn2IcZ9VJ6M6BBugGfYUnPCLSYxXdZy17M0BEJyhUTwg== - -node-fetch@^2.6.7: - version "2.6.9" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.9.tgz#7c7f744b5cc6eb5fd404e0c7a9fec630a55657e6" - integrity sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg== - dependencies: - whatwg-url "^5.0.0" - -node-fetch@^3.3.0: - version "3.3.0" - resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-3.3.0.tgz#37e71db4ecc257057af828d523a7243d651d91e4" - integrity sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA== - dependencies: - data-uri-to-buffer "^4.0.0" - fetch-blob "^3.1.4" - formdata-polyfill "^4.0.10" - -node-forge@^1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3" - integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA== - -node-gyp-build@^4.2.2: - version "4.6.0" - resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" - integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== - -node-int64@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" - integrity sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw== - -node-releases@^2.0.8: - version "2.0.10" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.10.tgz#c311ebae3b6a148c89b1813fd7c4d3c024ef537f" - integrity sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w== - -non-layered-tidy-tree-layout@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/non-layered-tidy-tree-layout/-/non-layered-tidy-tree-layout-2.0.2.tgz#57d35d13c356643fc296a55fb11ac15e74da7804" - integrity sha512-gkXMxRzUH+PB0ax9dUN0yYF0S25BqeAYqhgMaLUFmpXLEk7Fcu8f4emJuOAY0V8kjDICxROIKsTAKsV/v355xw== - -nopt@1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" - integrity sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg== - dependencies: - abbrev "1" - -nopt@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" - integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== - dependencies: - abbrev "1" - -normalize-path@^3.0.0, normalize-path@~3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" - integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== - -normalize-range@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" - integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA== - -normalize-url@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a" - integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A== - -npm-run-path@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" - integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== - dependencies: - path-key "^3.0.0" - -npm-run-path@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-5.1.0.tgz#bc62f7f3f6952d9894bd08944ba011a6ee7b7e00" - integrity sha512-sJOdmRGrY2sjNTRMbSvluQqg+8X7ZK61yvzBEIDhz4f8z1TZFYABsqjjCBd/0PUNE9M6QDgHJXQkGUEm7Q+l9Q== - dependencies: - path-key "^4.0.0" - -npmlog@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-5.0.1.tgz#f06678e80e29419ad67ab964e0fa69959c1eb8b0" - integrity sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw== - dependencies: - are-we-there-yet "^2.0.0" - console-control-strings "^1.1.0" - gauge "^3.0.0" - set-blocking "^2.0.0" - -nth-check@^2.0.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-2.1.1.tgz#c9eab428effce36cd6b92c924bdb000ef1f1ed1d" - integrity sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w== - dependencies: - boolbase "^1.0.0" - -nuxi@3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/nuxi/-/nuxi-3.1.2.tgz#94a67b05e685a2efc797b1741470a8d085271e85" - integrity sha512-AJsGATQ6+jQYjwlPqM3ST24t4et/GDeoePtrBLsJEU4MNwVAZJM9lv8UyIlY+UeQVx10ZCn76sksOX7a1ViFEw== - optionalDependencies: - fsevents "~2.3.2" - -nuxt-component-meta@^0.4.3: - version "0.4.3" - resolved "https://registry.yarnpkg.com/nuxt-component-meta/-/nuxt-component-meta-0.4.3.tgz#43fc498753d402d02d5b38d36f449123717542c3" - integrity sha512-40wsnbCh2neNdKVrwSiqV/ea7QshYjp3kpfk8JZaxSW/XcgNg2tzka4L+M8caOvQalyAKi6AaENPLaTYOZDbQg== - dependencies: - "@nuxt/kit" "^3.0.0" - scule "^1.0.0" - typescript "^4.9.4" - vue-component-meta "^1.0.18" - -nuxt-config-schema@^0.4.2, nuxt-config-schema@^0.4.4: - version "0.4.4" - resolved "https://registry.yarnpkg.com/nuxt-config-schema/-/nuxt-config-schema-0.4.4.tgz#473d5210e2fb2c049af63180e039b9c05548d544" - integrity sha512-5NnyyH2qSgraQo6kcW/8SWqBZ/pEY/PwyepODPWYYv4ZZ8BiqC850OTmyO2oTBL4O+Xg4fR7hAwSB4g5pIMpSg== - dependencies: - "@nuxt/kit" "^3.0.0" - changelogen "^0.4.1" - defu "^6.1.2" - jiti "^1.16.2" - pathe "^1.0.0" - untyped "^1.2.2" - -nuxt-icon@^0.2.10: - version "0.2.11" - resolved "https://registry.yarnpkg.com/nuxt-icon/-/nuxt-icon-0.2.11.tgz#12ca6f4b15934119b38b17bd79ebf51e43fddc58" - integrity sha512-mHFgTMrf+CzG7l6cdVQ7vF2XaXz8fs53QC6bk+zVZEHBCCwFtRoG0H+62y+y0iAo2+cD4VYmvsAiRMKIlIGxVA== - dependencies: - "@iconify/vue" "^4.0.2" - "@nuxt/kit" "^3.1.0" - nuxt-config-schema "^0.4.4" - -nuxt@^3.0.0: - version "3.1.2" - resolved "https://registry.yarnpkg.com/nuxt/-/nuxt-3.1.2.tgz#74c8140b295e6fc8ada7921d7810f2cb25e16201" - integrity sha512-mzEYvokFZAtiZRfNNj72m94nuMWzBgOCwuxlYt9paxH4Y9qcBr+Ki4ppnqD1gK719exccGqJAODJWL05aN3HFA== - dependencies: - "@nuxt/devalue" "^2.0.0" - "@nuxt/kit" "3.1.2" - "@nuxt/schema" "3.1.2" - "@nuxt/telemetry" "^2.1.9" - "@nuxt/ui-templates" "^1.1.1" - "@nuxt/vite-builder" "3.1.2" - "@unhead/ssr" "^1.0.20" - "@vue/reactivity" "^3.2.47" - "@vue/shared" "^3.2.47" - "@vueuse/head" "^1.0.24" - chokidar "^3.5.3" - cookie-es "^0.5.0" - defu "^6.1.2" - destr "^1.2.2" - escape-string-regexp "^5.0.0" - estree-walker "^3.0.3" - fs-extra "^11.1.0" - globby "^13.1.3" - h3 "^1.1.0" - hash-sum "^2.0.0" - hookable "^5.4.2" - jiti "^1.16.2" - knitwork "^1.0.0" - magic-string "^0.27.0" - mlly "^1.1.0" - nitropack "^2.1.1" - nuxi "3.1.2" - ofetch "^1.0.0" - ohash "^1.0.0" - pathe "^1.1.0" - perfect-debounce "^0.1.3" - scule "^1.0.0" - strip-literal "^1.0.0" - ufo "^1.0.1" - ultrahtml "^1.2.0" - unctx "^2.1.1" - unenv "^1.0.3" - unhead "^1.0.20" - unimport "^2.1.0" - unplugin "^1.0.1" - untyped "^1.2.2" - vue "^3.2.47" - vue-bundle-renderer "^1.0.0" - vue-devtools-stub "^0.1.0" - vue-router "^4.1.6" - -object-assign@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" - integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== - -object-hash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-3.0.0.tgz#73f97f753e7baffc0e2cc9d6e079079744ac82e9" - integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw== - -object-is@^1.0.1: - version "1.1.5" - resolved "https://registry.yarnpkg.com/object-is/-/object-is-1.1.5.tgz#b9deeaa5fc7f1846a0faecdceec138e5778f53ac" - integrity sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw== - dependencies: - call-bind "^1.0.2" - define-properties "^1.1.3" - -object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -ofetch@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ofetch/-/ofetch-1.0.0.tgz#5a2604cdcb33349900e4f73ffe44de449a61101a" - integrity sha512-d40aof8czZFSQKJa4+F7Ch3UC5D631cK1TTUoK+iNEut9NoiCL+u0vykl/puYVUS2df4tIQl5upQcolIcEzQjQ== - dependencies: - destr "^1.2.1" - node-fetch-native "^1.0.1" - ufo "^1.0.0" - -ohash@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/ohash/-/ohash-1.0.0.tgz#e6ab04851ef9a479beb6e8a2457ff0d3ccf77371" - integrity sha512-kxSyzq6tt+6EE/xCnD1XaFhCCjUNUaz3X30rJp6mnjGLXAAvuPFqohMdv0aScWzajR45C29HyBaXZ8jXBwnh9A== - -on-finished@2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f" - integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg== - dependencies: - ee-first "1.1.1" - -once@^1.3.0, once@^1.3.1, once@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== - dependencies: - wrappy "1" - -onetime@^5.1.0, onetime@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" - integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== - dependencies: - mimic-fn "^2.1.0" - -onetime@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-6.0.0.tgz#7c24c18ed1fd2e9bca4bd26806a33613c77d34b4" - integrity sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ== - dependencies: - mimic-fn "^4.0.0" - -open@^8.4.0: - version "8.4.1" - resolved "https://registry.yarnpkg.com/open/-/open-8.4.1.tgz#2ab3754c07f5d1f99a7a8d6a82737c95e3101cff" - integrity sha512-/4b7qZNhv6Uhd7jjnREh1NjnPxlTq+XNWPG88Ydkj5AILcA5m3ajvcg57pB24EQjKv0dK62XnDqk9c/hkIG5Kg== - dependencies: - define-lazy-prop "^2.0.0" - is-docker "^2.1.1" - is-wsl "^2.2.0" - -ora@^6.1.2: - version "6.1.2" - resolved "https://registry.yarnpkg.com/ora/-/ora-6.1.2.tgz#7b3c1356b42fd90fb1dad043d5dbe649388a0bf5" - integrity sha512-EJQ3NiP5Xo94wJXIzAyOtSb0QEIAUu7m8t6UZ9krbz0vAJqr92JpcK/lEXg91q6B9pEGqrykkd2EQplnifDSBw== - dependencies: - bl "^5.0.0" - chalk "^5.0.0" - cli-cursor "^4.0.0" - cli-spinners "^2.6.1" - is-interactive "^2.0.0" - is-unicode-supported "^1.1.0" - log-symbols "^5.1.0" - strip-ansi "^7.0.1" - wcwidth "^1.0.1" - -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g== - -p-cancelable@^2.0.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/p-cancelable/-/p-cancelable-2.1.1.tgz#aab7fbd416582fa32a3db49859c122487c5ed2cf" - integrity sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg== - -p-limit@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" - integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== - dependencies: - p-try "^2.0.0" - -p-limit@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-3.1.0.tgz#e1daccbe78d0d1388ca18c64fea38e3e57e3706b" - integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== - dependencies: - yocto-queue "^0.1.0" - -p-locate@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07" - integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A== - dependencies: - p-limit "^2.2.0" - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -paneer@^0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/paneer/-/paneer-0.0.1.tgz#559f143416f40e3e322fcd8c1fbf264a628230c8" - integrity sha512-rT0koOL1kQ0CqxIS12FYrEuj6BTfQUn3QMj9led4QVBNw7s1MoF6owTGqwYF2FVPXMqVOxe+i6Ye8FGI7SXQCQ== - dependencies: - recast "^0.20.4" - -param-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/param-case/-/param-case-3.0.4.tgz#7d17fe4aa12bde34d4a77d91acfb6219caad01c5" - integrity sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -parse-entities@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/parse-entities/-/parse-entities-4.0.0.tgz#f67c856d4e3fe19b1a445c3fabe78dcdc1053eeb" - integrity sha512-5nk9Fn03x3rEhGaX1FU6IDwG/k+GxLXlFAkgrbM1asuAFl3BhdQWvASaIsmwWypRNcZKHPYnIuOSfIWEyEQnPQ== - dependencies: - "@types/unist" "^2.0.0" - character-entities "^2.0.0" - character-entities-legacy "^3.0.0" - character-reference-invalid "^2.0.0" - decode-named-character-reference "^1.0.0" - is-alphanumerical "^2.0.0" - is-decimal "^2.0.0" - is-hexadecimal "^2.0.0" - -parse-git-config@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/parse-git-config/-/parse-git-config-3.0.0.tgz#4a2de08c7b74a2555efa5ae94d40cd44302a6132" - integrity sha512-wXoQGL1D+2COYWCD35/xbiKma1Z15xvZL8cI25wvxzled58V51SJM04Urt/uznS900iQor7QO04SgdfT/XlbuA== - dependencies: - git-config-path "^2.0.0" - ini "^1.3.5" - -parse-json@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-5.2.0.tgz#c76fc66dee54231c962b22bcc8a72cf2f99753cd" - integrity sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg== - dependencies: - "@babel/code-frame" "^7.0.0" - error-ex "^1.3.1" - json-parse-even-better-errors "^2.3.0" - lines-and-columns "^1.1.6" - -parse-path@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/parse-path/-/parse-path-7.0.0.tgz#605a2d58d0a749c8594405d8cc3a2bf76d16099b" - integrity sha512-Euf9GG8WT9CdqwuWJGdf3RkUcTBArppHABkO7Lm8IzRQp0e2r/kkFnmhu4TSK30Wcu5rVAZLmfPKSBBi9tWFog== - dependencies: - protocols "^2.0.0" - -parse-url@^8.1.0: - version "8.1.0" - resolved "https://registry.yarnpkg.com/parse-url/-/parse-url-8.1.0.tgz#972e0827ed4b57fc85f0ea6b0d839f0d8a57a57d" - integrity sha512-xDvOoLU5XRrcOZvnI6b8zA6n9O9ejNk/GExuz1yBuWUGn9KA97GI6HTs6u02wKara1CeVmZhH+0TZFdWScR89w== - dependencies: - parse-path "^7.0.0" - -parse5@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" - integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== - -parseurl@~1.3.3: - version "1.3.3" - resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" - integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ== - -pascal-case@^3.1.2: - version "3.1.2" - resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-3.1.2.tgz#b48e0ef2b98e205e7c1dae747d0b1508237660eb" - integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - -path-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/path-case/-/path-case-3.0.4.tgz#9168645334eb942658375c56f80b4c0cb5f82c6f" - integrity sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -path-exists@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-4.0.0.tgz#513bdbe2d3b95d7762e8c1137efa195c6c61b5b3" - integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== - -path-key@^3.0.0, path-key@^3.1.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" - integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== - -path-key@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-4.0.0.tgz#295588dc3aee64154f877adb9d780b81c554bf18" - integrity sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ== - -path-parse@^1.0.7: - version "1.0.7" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" - integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== - -path-type@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" - integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== - -pathe@^1.0.0, pathe@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/pathe/-/pathe-1.1.0.tgz#e2e13f6c62b31a3289af4ba19886c230f295ec03" - integrity sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w== - -pathval@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/pathval/-/pathval-1.1.1.tgz#8534e77a77ce7ac5a2512ea21e0fdb8fcf6c3d8d" - integrity sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ== - -perfect-debounce@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-0.1.3.tgz#ff6798ea543a3ba1f0efeeaf97c0340f5c8871ce" - integrity sha512-NOT9AcKiDGpnV/HBhI22Str++XWcErO/bALvHCuhv33owZW/CjH8KAFLZDCmu3727sihe0wTxpDhyGc6M8qacQ== - -picocolors@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" - integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== - -picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3, picomatch@^2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" - integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== - -pify@^2.3.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog== - -pinceau@^0.13.8: - version "0.13.9" - resolved "https://registry.yarnpkg.com/pinceau/-/pinceau-0.13.9.tgz#1a5d441f540f6161b06d26905ee039b4625ae63a" - integrity sha512-Z6JuQ1x9R5DYoH+PuwsHKPSKi62OGuERhZ4SmMfH/Lbmu3mLslz9Tj+61Fv8b2oKbmBaNbPTmF4ddg4+YNQigw== - dependencies: - "@unocss/reset" "^0.49.4" - "@volar/vue-language-core" "^1.0.24" - acorn "^8.8.2" - chroma-js "^2.4.2" - consola "^2.15.3" - csstype "^3.1.1" - defu "^6.1.2" - magic-string "^0.27.0" - nanoid "^4.0.1" - ohash "^1.0.0" - paneer "^0.0.1" - postcss-custom-properties "13.1.1" - postcss-dark-theme-class "0.7.3" - postcss-nested "^6.0.0" - recast "^0.22.0" - scule "^1.0.0" - style-dictionary-esm "^1.2.0" - unbuild "^1.1.1" - unplugin "^1.0.1" - -pirates@^4.0.4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.5.tgz#feec352ea5c3268fb23a37c702ab1699f35a5f3b" - integrity sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ== - -pkg-dir@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-4.2.0.tgz#f099133df7ede422e81d1d8448270eeb3e4261f3" - integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ== - dependencies: - find-up "^4.0.0" - -pkg-types@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.0.1.tgz#25234407f9dc63409af45ced9407625ff446a761" - integrity sha512-jHv9HB+Ho7dj6ItwppRDDl0iZRYBD0jsakHXtFgoLr+cHSF6xC+QL54sJmWxyGxOLYSHm0afhXhXcQDQqH9z8g== - dependencies: - jsonc-parser "^3.2.0" - mlly "^1.0.0" - pathe "^1.0.0" - -postcss-calc@^8.2.3: - version "8.2.4" - resolved "https://registry.yarnpkg.com/postcss-calc/-/postcss-calc-8.2.4.tgz#77b9c29bfcbe8a07ff6693dc87050828889739a5" - integrity sha512-SmWMSJmB8MRnnULldx0lQIyhSNvuDl9HfrZkaqqE/WHAhToYsAvDq+yAsA/kIyINDszOp3Rh0GFoNuH5Ypsm3Q== - dependencies: - postcss-selector-parser "^6.0.9" - postcss-value-parser "^4.2.0" - -postcss-colormin@^5.3.0: - version "5.3.0" - resolved "https://registry.yarnpkg.com/postcss-colormin/-/postcss-colormin-5.3.0.tgz#3cee9e5ca62b2c27e84fce63affc0cfb5901956a" - integrity sha512-WdDO4gOFG2Z8n4P8TWBpshnL3JpmNmJwdnfP2gbk2qBA8PWwOYcmjmI/t3CmMeL72a7Hkd+x/Mg9O2/0rD54Pg== - dependencies: - browserslist "^4.16.6" - caniuse-api "^3.0.0" - colord "^2.9.1" - postcss-value-parser "^4.2.0" - -postcss-convert-values@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-convert-values/-/postcss-convert-values-5.1.3.tgz#04998bb9ba6b65aa31035d669a6af342c5f9d393" - integrity sha512-82pC1xkJZtcJEfiLw6UXnXVXScgtBrjlO5CBmuDQc+dlb88ZYheFsjTn40+zBVi3DkfF7iezO0nJUPLcJK3pvA== - dependencies: - browserslist "^4.21.4" - postcss-value-parser "^4.2.0" - -postcss-custom-properties@13.1.1: - version "13.1.1" - resolved "https://registry.yarnpkg.com/postcss-custom-properties/-/postcss-custom-properties-13.1.1.tgz#1d7b7d589124c3f5dfe9d255aba5ac15e9bd017c" - integrity sha512-FK4dBiHdzWOosLu3kEAHaYpfcrnMfVV4nP6PT6EFIfWXrtHH9LY8idfTYnEDpq/vgE33mr8ykhs7BjlgcT9agg== - dependencies: - "@csstools/cascade-layer-name-parser" "^1.0.0" - "@csstools/css-parser-algorithms" "^2.0.0" - "@csstools/css-tokenizer" "^2.0.0" - postcss-value-parser "^4.2.0" - -postcss-dark-theme-class@0.7.3: - version "0.7.3" - resolved "https://registry.yarnpkg.com/postcss-dark-theme-class/-/postcss-dark-theme-class-0.7.3.tgz#b17e8ea6070645d748fb2ff1783c21ae792139b8" - integrity sha512-M9vtfh8ORzQsVdT9BWb+xpEDAzC7nHBn7wVc988/JkEVLPupKcUnV0jw7RZ8sSj0ovpqN1POf6PLdt19JCHfhQ== - -postcss-discard-comments@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/postcss-discard-comments/-/postcss-discard-comments-5.1.2.tgz#8df5e81d2925af2780075840c1526f0660e53696" - integrity sha512-+L8208OVbHVF2UQf1iDmRcbdjJkuBF6IS29yBDSiWUIzpYaAhtNl6JYnYm12FnkeCwQqF5LeklOu6rAqgfBZqQ== - -postcss-discard-duplicates@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-duplicates/-/postcss-discard-duplicates-5.1.0.tgz#9eb4fe8456706a4eebd6d3b7b777d07bad03e848" - integrity sha512-zmX3IoSI2aoenxHV6C7plngHWWhUOV3sP1T8y2ifzxzbtnuhk1EdPwm0S1bIUNaJ2eNbWeGLEwzw8huPD67aQw== - -postcss-discard-empty@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-discard-empty/-/postcss-discard-empty-5.1.1.tgz#e57762343ff7f503fe53fca553d18d7f0c369c6c" - integrity sha512-zPz4WljiSuLWsI0ir4Mcnr4qQQ5e1Ukc3i7UfE2XcrwKK2LIPIqE5jxMRxO6GbI3cv//ztXDsXwEWT3BHOGh3A== - -postcss-discard-overridden@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-discard-overridden/-/postcss-discard-overridden-5.1.0.tgz#7e8c5b53325747e9d90131bb88635282fb4a276e" - integrity sha512-21nOL7RqWR1kasIVdKs8HNqQJhFxLsyRfAnUDm4Fe4t4mCWL9OJiHvlHPjcd8zc5Myu89b/7wZDnOSjFgeWRtw== - -postcss-import-resolver@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/postcss-import-resolver/-/postcss-import-resolver-2.0.0.tgz#95c61ac5489047bd93ff42a9cd405cfe9041e2c0" - integrity sha512-y001XYgGvVwgxyxw9J1a5kqM/vtmIQGzx34g0A0Oy44MFcy/ZboZw1hu/iN3VYFjSTRzbvd7zZJJz0Kh0AGkTw== - dependencies: - enhanced-resolve "^4.1.1" - -postcss-import@^14.1.0: - version "14.1.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-14.1.0.tgz#a7333ffe32f0b8795303ee9e40215dac922781f0" - integrity sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-import@^15.1.0: - version "15.1.0" - resolved "https://registry.yarnpkg.com/postcss-import/-/postcss-import-15.1.0.tgz#41c64ed8cc0e23735a9698b3249ffdbf704adc70" - integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew== - dependencies: - postcss-value-parser "^4.0.0" - read-cache "^1.0.0" - resolve "^1.1.7" - -postcss-js@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/postcss-js/-/postcss-js-4.0.1.tgz#61598186f3703bab052f1c4f7d805f3991bee9d2" - integrity sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw== - dependencies: - camelcase-css "^2.0.1" - -postcss-load-config@^3.1.4: - version "3.1.4" - resolved "https://registry.yarnpkg.com/postcss-load-config/-/postcss-load-config-3.1.4.tgz#1ab2571faf84bb078877e1d07905eabe9ebda855" - integrity sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== - dependencies: - lilconfig "^2.0.5" - yaml "^1.10.2" - -postcss-merge-longhand@^5.1.7: - version "5.1.7" - resolved "https://registry.yarnpkg.com/postcss-merge-longhand/-/postcss-merge-longhand-5.1.7.tgz#24a1bdf402d9ef0e70f568f39bdc0344d568fb16" - integrity sha512-YCI9gZB+PLNskrK0BB3/2OzPnGhPkBEwmwhfYk1ilBHYVAZB7/tkTHFBAnCrvBBOmeYyMYw3DMjT55SyxMBzjQ== - dependencies: - postcss-value-parser "^4.2.0" - stylehacks "^5.1.1" - -postcss-merge-rules@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-merge-rules/-/postcss-merge-rules-5.1.3.tgz#8f97679e67cc8d08677a6519afca41edf2220894" - integrity sha512-LbLd7uFC00vpOuMvyZop8+vvhnfRGpp2S+IMQKeuOZZapPRY4SMq5ErjQeHbHsjCUgJkRNrlU+LmxsKIqPKQlA== - dependencies: - browserslist "^4.21.4" - caniuse-api "^3.0.0" - cssnano-utils "^3.1.0" - postcss-selector-parser "^6.0.5" - -postcss-minify-font-values@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-minify-font-values/-/postcss-minify-font-values-5.1.0.tgz#f1df0014a726083d260d3bd85d7385fb89d1f01b" - integrity sha512-el3mYTgx13ZAPPirSVsHqFzl+BBBDrXvbySvPGFnQcTI4iNslrPaFq4muTkLZmKlGk4gyFAYUBMH30+HurREyA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-minify-gradients@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-minify-gradients/-/postcss-minify-gradients-5.1.1.tgz#f1fe1b4f498134a5068240c2f25d46fcd236ba2c" - integrity sha512-VGvXMTpCEo4qHTNSa9A0a3D+dxGFZCYwR6Jokk+/3oB6flu2/PnPXAh2x7x52EkY5xlIHLm+Le8tJxe/7TNhzw== - dependencies: - colord "^2.9.1" - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-minify-params@^5.1.4: - version "5.1.4" - resolved "https://registry.yarnpkg.com/postcss-minify-params/-/postcss-minify-params-5.1.4.tgz#c06a6c787128b3208b38c9364cfc40c8aa5d7352" - integrity sha512-+mePA3MgdmVmv6g+30rn57USjOGSAyuxUmkfiWpzalZ8aiBkdPYjXWtHuwJGm1v5Ojy0Z0LaSYhHaLJQB0P8Jw== - dependencies: - browserslist "^4.21.4" - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-minify-selectors@^5.2.1: - version "5.2.1" - resolved "https://registry.yarnpkg.com/postcss-minify-selectors/-/postcss-minify-selectors-5.2.1.tgz#d4e7e6b46147b8117ea9325a915a801d5fe656c6" - integrity sha512-nPJu7OjZJTsVUmPdm2TcaiohIwxP+v8ha9NehQ2ye9szv4orirRU3SDdtUmKH+10nzn0bAyOXZ0UEr7OpvLehg== - dependencies: - postcss-selector-parser "^6.0.5" - -postcss-nested@6.0.0, postcss-nested@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/postcss-nested/-/postcss-nested-6.0.0.tgz#1572f1984736578f360cffc7eb7dca69e30d1735" - integrity sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w== - dependencies: - postcss-selector-parser "^6.0.10" - -postcss-normalize-charset@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-charset/-/postcss-normalize-charset-5.1.0.tgz#9302de0b29094b52c259e9b2cf8dc0879879f0ed" - integrity sha512-mSgUJ+pd/ldRGVx26p2wz9dNZ7ji6Pn8VWBajMXFf8jk7vUoSrZ2lt/wZR7DtlZYKesmZI680qjr2CeFF2fbUg== - -postcss-normalize-display-values@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-display-values/-/postcss-normalize-display-values-5.1.0.tgz#72abbae58081960e9edd7200fcf21ab8325c3da8" - integrity sha512-WP4KIM4o2dazQXWmFaqMmcvsKmhdINFblgSeRgn8BJ6vxaMyaJkwAzpPpuvSIoG/rmX3M+IrRZEz2H0glrQNEA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-positions@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-positions/-/postcss-normalize-positions-5.1.1.tgz#ef97279d894087b59325b45c47f1e863daefbb92" - integrity sha512-6UpCb0G4eofTCQLFVuI3EVNZzBNPiIKcA1AKVka+31fTVySphr3VUgAIULBhxZkKgwLImhzMR2Bw1ORK+37INg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-repeat-style@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-5.1.1.tgz#e9eb96805204f4766df66fd09ed2e13545420fb2" - integrity sha512-mFpLspGWkQtBcWIRFLmewo8aC3ImN2i/J3v8YCFUwDnPu3Xz4rLohDO26lGjwNsQxB3YF0KKRwspGzE2JEuS0g== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-string@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-string/-/postcss-normalize-string-5.1.0.tgz#411961169e07308c82c1f8c55f3e8a337757e228" - integrity sha512-oYiIJOf4T9T1N4i+abeIc7Vgm/xPCGih4bZz5Nm0/ARVJ7K6xrDlLwvwqOydvyL3RHNf8qZk6vo3aatiw/go3w== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-timing-functions@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-5.1.0.tgz#d5614410f8f0b2388e9f240aa6011ba6f52dafbb" - integrity sha512-DOEkzJ4SAXv5xkHl0Wa9cZLF3WCBhF3o1SKVxKQAa+0pYKlueTpCgvkFAHfk+Y64ezX9+nITGrDZeVGgITJXjg== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-normalize-unicode@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-unicode/-/postcss-normalize-unicode-5.1.1.tgz#f67297fca3fea7f17e0d2caa40769afc487aa030" - integrity sha512-qnCL5jzkNUmKVhZoENp1mJiGNPcsJCs1aaRmURmeJGES23Z/ajaln+EPTD+rBeNkSryI+2WTdW+lwcVdOikrpA== - dependencies: - browserslist "^4.21.4" - postcss-value-parser "^4.2.0" - -postcss-normalize-url@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-normalize-url/-/postcss-normalize-url-5.1.0.tgz#ed9d88ca82e21abef99f743457d3729a042adcdc" - integrity sha512-5upGeDO+PVthOxSmds43ZeMeZfKH+/DKgGRD7TElkkyS46JXAUhMzIKiCa7BabPeIy3AQcTkXwVVN7DbqsiCew== - dependencies: - normalize-url "^6.0.1" - postcss-value-parser "^4.2.0" - -postcss-normalize-whitespace@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-normalize-whitespace/-/postcss-normalize-whitespace-5.1.1.tgz#08a1a0d1ffa17a7cc6efe1e6c9da969cc4493cfa" - integrity sha512-83ZJ4t3NUDETIHTa3uEg6asWjSBYL5EdkVB0sDncx9ERzOKBVJIUeDO9RyA9Zwtig8El1d79HBp0JEi8wvGQnA== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-ordered-values@^5.1.3: - version "5.1.3" - resolved "https://registry.yarnpkg.com/postcss-ordered-values/-/postcss-ordered-values-5.1.3.tgz#b6fd2bd10f937b23d86bc829c69e7732ce76ea38" - integrity sha512-9UO79VUhPwEkzbb3RNpqqghc6lcYej1aveQteWY+4POIwlqkYE21HKWaLDF6lWNuqCobEAyTovVhtI32Rbv2RQ== - dependencies: - cssnano-utils "^3.1.0" - postcss-value-parser "^4.2.0" - -postcss-reduce-initial@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-reduce-initial/-/postcss-reduce-initial-5.1.1.tgz#c18b7dfb88aee24b1f8e4936541c29adbd35224e" - integrity sha512-//jeDqWcHPuXGZLoolFrUXBDyuEGbr9S2rMo19bkTIjBQ4PqkaO+oI8wua5BOUxpfi97i3PCoInsiFIEBfkm9w== - dependencies: - browserslist "^4.21.4" - caniuse-api "^3.0.0" - -postcss-reduce-transforms@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-reduce-transforms/-/postcss-reduce-transforms-5.1.0.tgz#333b70e7758b802f3dd0ddfe98bb1ccfef96b6e9" - integrity sha512-2fbdbmgir5AvpW9RLtdONx1QoYG2/EtqpNQbFASDlixBbAYuTcJ0dECwlqNqH7VbaUnEnh8SrxOe2sRIn24XyQ== - dependencies: - postcss-value-parser "^4.2.0" - -postcss-selector-parser@^6.0.10, postcss-selector-parser@^6.0.11, postcss-selector-parser@^6.0.4, postcss-selector-parser@^6.0.5, postcss-selector-parser@^6.0.9: - version "6.0.11" - resolved "https://registry.yarnpkg.com/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz#2e41dc39b7ad74046e1615185185cd0b17d0c8dc" - integrity sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g== - dependencies: - cssesc "^3.0.0" - util-deprecate "^1.0.2" - -postcss-svgo@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/postcss-svgo/-/postcss-svgo-5.1.0.tgz#0a317400ced789f233a28826e77523f15857d80d" - integrity sha512-D75KsH1zm5ZrHyxPakAxJWtkyXew5qwS70v56exwvw542d9CRtTo78K0WeFxZB4G7JXKKMbEZtZayTGdIky/eA== - dependencies: - postcss-value-parser "^4.2.0" - svgo "^2.7.0" - -postcss-unique-selectors@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/postcss-unique-selectors/-/postcss-unique-selectors-5.1.1.tgz#a9f273d1eacd09e9aa6088f4b0507b18b1b541b6" - integrity sha512-5JiODlELrz8L2HwxfPnhOWZYWDxVHWL83ufOv84NrcgipI7TaeRsatAhK4Tr2/ZiYldpK/wBvw5BD3qfaK96GA== - dependencies: - postcss-selector-parser "^6.0.5" - -postcss-url@^10.1.3: - version "10.1.3" - resolved "https://registry.yarnpkg.com/postcss-url/-/postcss-url-10.1.3.tgz#54120cc910309e2475ec05c2cfa8f8a2deafdf1e" - integrity sha512-FUzyxfI5l2tKmXdYc6VTu3TWZsInayEKPbiyW+P6vmmIrrb4I6CGX0BFoewgYHLK+oIL5FECEK02REYRpBvUCw== - dependencies: - make-dir "~3.1.0" - mime "~2.5.2" - minimatch "~3.0.4" - xxhashjs "~0.2.2" - -postcss-value-parser@^4.0.0, postcss-value-parser@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz#723c09920836ba6d3e5af019f92bc0971c02e514" - integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ== - -postcss@^8.0.9, postcss@^8.1.10, postcss@^8.4.20, postcss@^8.4.21: - version "8.4.21" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.21.tgz#c639b719a57efc3187b13a1d765675485f4134f4" - integrity sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg== - dependencies: - nanoid "^3.3.4" - picocolors "^1.0.0" - source-map-js "^1.0.2" - -preact@^10.0.0, preact@^10.10.0: - version "10.12.0" - resolved "https://registry.yarnpkg.com/preact/-/preact-10.12.0.tgz#5126a361365b20dbced92e8eea459bf094069909" - integrity sha512-+w8ix+huD8CNZemheC53IPjMUFk921i02o30u0K6h53spMX41y/QhVDnG/nU2k42/69tvqWmVsgNLIiwRAcmxg== - -pretty-bytes@^6.0.0, pretty-bytes@^6.1.0: - version "6.1.0" - resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.0.tgz#1d1cc9aae1939012c74180b679da6684616bf804" - integrity sha512-Rk753HI8f4uivXi4ZCIYdhmG1V+WKzvRMg/X+M42a6t7D07RcmopXJMDNk6N++7Bl75URRGsb40ruvg7Hcp2wQ== - -pretty-format@^29.0.0, pretty-format@^29.4.2: - version "29.4.2" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-29.4.2.tgz#64bf5ccc0d718c03027d94ac957bdd32b3fb2401" - integrity sha512-qKlHR8yFVCbcEWba0H0TOC8dnLlO4vPlyEjRPw31FZ2Rupy9nLa8ZLbYny8gWEl8CkEhJqAE6IzdNELTBVcBEg== - dependencies: - "@jest/schemas" "^29.4.2" - ansi-styles "^5.0.0" - react-is "^18.0.0" - -process-nextick-args@~2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" - integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== - -prompts@^2.0.1: - version "2.4.2" - resolved "https://registry.yarnpkg.com/prompts/-/prompts-2.4.2.tgz#7b57e73b3a48029ad10ebd44f74b01722a4cb069" - integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q== - dependencies: - kleur "^3.0.3" - sisteransi "^1.0.5" - -property-information@^6.0.0, property-information@^6.2.0: - version "6.2.0" - resolved "https://registry.yarnpkg.com/property-information/-/property-information-6.2.0.tgz#b74f522c31c097b5149e3c3cb8d7f3defd986a1d" - integrity sha512-kma4U7AFCTwpqq5twzC1YVIDXSqg6qQK6JN0smOw8fgRy1OkMi0CYSzFmsy6dnqSenamAtj0CyXMUJ1Mf6oROg== - -protocols@^2.0.0, protocols@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/protocols/-/protocols-2.0.1.tgz#8f155da3fc0f32644e83c5782c8e8212ccf70a86" - integrity sha512-/XJ368cyBJ7fzLMwLKv1e4vLxOju2MNAIokcr7meSaNcVbWz/CPcW22cP04mwxOErdA5mwjA8Q6w/cdAQxVn7Q== - -prr@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" - integrity sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw== - -pump@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/pump/-/pump-3.0.0.tgz#b4a2116815bde2f4e1ea602354e8c75565107a64" - integrity sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww== - dependencies: - end-of-stream "^1.1.0" - once "^1.3.1" - -"qs@^6.5.1 < 6.10": - version "6.9.7" - resolved "https://registry.yarnpkg.com/qs/-/qs-6.9.7.tgz#4610846871485e1e048f44ae3b94033f0e675afe" - integrity sha512-IhMFgUmuNpyRfxA90umL7ByLlgRXu6tIfKPpF5TmcfRLlLCckfP/g3IQmju6jjpu+Hh8rA+2p6A27ZSPOOHdKw== - -queue-microtask@^1.2.2: - version "1.2.3" - resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" - integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== - -quick-lru@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-5.1.1.tgz#366493e6b3e42a3a6885e2e99d18f80fb7a8c932" - integrity sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA== - -radix3@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/radix3/-/radix3-1.0.0.tgz#d1c760b850206a6bd5dfd26820c25903cb20eccc" - integrity sha512-6n3AEXth91ASapMVKiEh2wrbFJmI+NBilrWE0AbiGgfm0xet0QXC8+a3K19r1UVYjUjctUgB053c3V/J6V0kCQ== - -randombytes@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" - integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== - dependencies: - safe-buffer "^5.1.0" - -range-parser@~1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031" - integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg== - -rc9@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/rc9/-/rc9-2.0.1.tgz#51e0f556759ee434e20ed29ca506b4ce97e7c6c0" - integrity sha512-9EfjLgNmzP9255YX8bGnILQcmdtOXKtUlFTu8bOZPJVtaUDZ2imswcUdpK51tMjTRQyB7r5RebNijrzuyGXcVA== - dependencies: - defu "^6.1.2" - destr "^1.2.2" - flat "^5.0.2" - -react-is@^18.0.0: - version "18.2.0" - resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" - integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== - -read-cache@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/read-cache/-/read-cache-1.0.0.tgz#e664ef31161166c9751cdbe8dbcf86b5fb58f774" - integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA== - dependencies: - pify "^2.3.0" - -readable-stream@^2.0.0, readable-stream@^2.0.1, readable-stream@^2.0.5: - version "2.3.7" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.7.tgz#1eca1cf711aef814c04f62252a36a62f6cb23b57" - integrity sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw== - dependencies: - core-util-is "~1.0.0" - inherits "~2.0.3" - isarray "~1.0.0" - process-nextick-args "~2.0.0" - safe-buffer "~5.1.1" - string_decoder "~1.1.1" - util-deprecate "~1.0.1" - -readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" - integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== - dependencies: - inherits "^2.0.3" - string_decoder "^1.1.1" - util-deprecate "^1.0.1" - -readdir-glob@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/readdir-glob/-/readdir-glob-1.1.2.tgz#b185789b8e6a43491635b6953295c5c5e3fd224c" - integrity sha512-6RLVvwJtVwEDfPdn6X6Ille4/lxGl0ATOY4FN/B9nxQcgOazvvI0nodiD19ScKq0PvA/29VpaOQML36o5IzZWA== - dependencies: - minimatch "^5.1.0" - -readdirp@~3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" - integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== - dependencies: - picomatch "^2.2.1" - -recast@^0.20.4: - version "0.20.5" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.20.5.tgz#8e2c6c96827a1b339c634dd232957d230553ceae" - integrity sha512-E5qICoPoNL4yU0H0NoBDntNB0Q5oMSNh9usFctYniLBluTthi3RsQVBXIJNbApOlvSwW/RGxIuokPcAc59J5fQ== - dependencies: - ast-types "0.14.2" - esprima "~4.0.0" - source-map "~0.6.1" - tslib "^2.0.1" - -recast@^0.22.0: - version "0.22.0" - resolved "https://registry.yarnpkg.com/recast/-/recast-0.22.0.tgz#1dd3bf1b86e5eb810b044221a1a734234ed3e9c0" - integrity sha512-5AAx+mujtXijsEavc5lWXBPQqrM4+Dl5qNH96N2aNeuJFUzpiiToKPsxQD/zAIJHspz7zz0maX0PCtCTFVlixQ== - dependencies: - assert "^2.0.0" - ast-types "0.15.2" - esprima "~4.0.0" - source-map "~0.6.1" - tslib "^2.0.1" - -redis-errors@^1.0.0, redis-errors@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/redis-errors/-/redis-errors-1.2.0.tgz#eb62d2adb15e4eaf4610c04afe1529384250abad" - integrity sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w== - -redis-parser@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-3.0.0.tgz#b66d828cdcafe6b4b8a428a7def4c6bcac31c8b4" - integrity sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A== - dependencies: - redis-errors "^1.0.0" - -regenerator-runtime@^0.13.11: - version "0.13.11" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz#f6dca3e7ceec20590d07ada785636a90cdca17f9" - integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg== - -rehype-external-links@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/rehype-external-links/-/rehype-external-links-2.0.1.tgz#fe54f9f227a1a2f8f6afe442ac4c9ee748f08756" - integrity sha512-u2dNypma+ps12SJWlS23zvbqwNx0Hl24t0YHXSM/6FCZj/pqWETCO3WyyrvALv4JYvRtuPjhiv2Lpen15ESqbA== - dependencies: - "@types/hast" "^2.0.0" - extend "^3.0.0" - is-absolute-url "^4.0.0" - space-separated-tokens "^2.0.0" - unified "^10.0.0" - unist-util-visit "^4.0.0" - -rehype-raw@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/rehype-raw/-/rehype-raw-6.1.1.tgz#81bbef3793bd7abacc6bf8335879d1b6c868c9d4" - integrity sha512-d6AKtisSRtDRX4aSPsJGTfnzrX2ZkHQLE5kiUuGOeEoLpbEulFF4hj0mLPbsa+7vmguDKOVVEQdHKDSwoaIDsQ== - dependencies: - "@types/hast" "^2.0.0" - hast-util-raw "^7.2.0" - unified "^10.0.0" - -rehype-slug@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/rehype-slug/-/rehype-slug-5.1.0.tgz#1f7e69be7ea1a2067bcc4cfe58e74c881d5c047e" - integrity sha512-Gf91dJoXneiorNEnn+Phx97CO7oRMrpi+6r155tTxzGuLtm+QrI4cTwCa9e1rtePdL4i9tSO58PeSS6HWfgsiw== - dependencies: - "@types/hast" "^2.0.0" - github-slugger "^2.0.0" - hast-util-has-property "^2.0.0" - hast-util-heading-rank "^2.0.0" - hast-util-to-string "^2.0.0" - unified "^10.0.0" - unist-util-visit "^4.0.0" - -rehype-sort-attribute-values@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/rehype-sort-attribute-values/-/rehype-sort-attribute-values-4.0.0.tgz#6a1baaced2f984ebed9aa201145c85cbc1c76880" - integrity sha512-+Y3OWTbbxSIutbXMVY7+aWFmcRyEvdz6HkghXAyVPjee1Y8HUi+/vryBL1UdEI9VknVBiGvphXAf5n6MDNOXOA== - dependencies: - "@types/hast" "^2.0.0" - hast-util-is-element "^2.0.0" - unified "^10.0.0" - unist-util-visit "^4.0.0" - -rehype-sort-attributes@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/rehype-sort-attributes/-/rehype-sort-attributes-4.0.0.tgz#b7766c864a370a07dd8ffa93b02c98322c20fe67" - integrity sha512-sCT58e12F+fJL8ZmvpEP2vAK7cpYffUAf0cMQjNfLIewWjMHMGo0Io+H8eztJoI1S9dvEm2XZT5zzchqe8gYJw== - dependencies: - "@types/hast" "^2.0.0" - unified "^10.0.0" - unist-util-visit "^4.0.0" - -remark-emoji@3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/remark-emoji/-/remark-emoji-3.0.2.tgz#786e88af1ecae682d74d7e1219989f34708205da" - integrity sha512-hEgxEv2sBtvhT3tNG/tQeeFY3EbslftaOoG14dDZndLo25fWJ6Fbg4ukFbIotOWWrfXyASjXjyHT+6n366k3mg== - dependencies: - emoticon "^4.0.0" - node-emoji "^1.11.0" - unist-util-visit "^4.1.0" - -remark-gfm@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/remark-gfm/-/remark-gfm-3.0.1.tgz#0b180f095e3036545e9dddac0e8df3fa5cfee54f" - integrity sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-gfm "^2.0.0" - micromark-extension-gfm "^2.0.0" - unified "^10.0.0" - -remark-mdc@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/remark-mdc/-/remark-mdc-1.1.3.tgz#590d9b47b69d0a54b278f2c1c5618e4dfb84afc7" - integrity sha512-ilYSkkQJhu5cUCEE2CJEncoMDoarP32ugfJpFWghXbnv3sWI3j2HtJuArc9tZzxN4ID6fngio3d8N87QfQAnRQ== - dependencies: - flat "^5.0.2" - js-yaml "^4.1.0" - mdast-util-from-markdown "^1.2.0" - mdast-util-to-markdown "^1.3.0" - micromark "^3.1.0" - micromark-core-commonmark "^1.0.6" - micromark-factory-space "^1.0.0" - micromark-factory-whitespace "^1.0.0" - micromark-util-character "^1.1.0" - parse-entities "^4.0.0" - scule "^1.0.0" - stringify-entities "^4.0.3" - unist-util-visit "^4.1.1" - unist-util-visit-parents "^5.1.1" - -remark-parse@^10.0.1: - version "10.0.1" - resolved "https://registry.yarnpkg.com/remark-parse/-/remark-parse-10.0.1.tgz#6f60ae53edbf0cf38ea223fe643db64d112e0775" - integrity sha512-1fUyHr2jLsVOkhbvPRBJ5zTKZZyD6yZzYaWCS6BPBdQ8vEMBCH+9zNCDA6tET/zHCi/jLqjCWtlJZUPk+DbnFw== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-from-markdown "^1.0.0" - unified "^10.0.0" - -remark-rehype@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/remark-rehype/-/remark-rehype-10.1.0.tgz#32dc99d2034c27ecaf2e0150d22a6dcccd9a6279" - integrity sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw== - dependencies: - "@types/hast" "^2.0.0" - "@types/mdast" "^3.0.0" - mdast-util-to-hast "^12.1.0" - unified "^10.0.0" - -remark-squeeze-paragraphs@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/remark-squeeze-paragraphs/-/remark-squeeze-paragraphs-5.0.1.tgz#c15aae559c43cc6e1fe85e24d6ec3cca7ecc4fa9" - integrity sha512-VWPAoa1bAAtU/aQfSLRZ7vOrwH9I02RhZTSo+e0LT3fVO9RKNCq/bwobIEBhxvNCt00JoQ7GwR3sYGhmD2/y6Q== - dependencies: - "@types/mdast" "^3.0.0" - mdast-squeeze-paragraphs "^5.0.0" - unified "^10.0.0" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== - -requires-port@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" - integrity sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ== - -resolve-alpn@^1.0.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/resolve-alpn/-/resolve-alpn-1.2.1.tgz#b7adbdac3546aaaec20b45e7d8265927072726f9" - integrity sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g== - -resolve-cwd@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/resolve-cwd/-/resolve-cwd-3.0.0.tgz#0f0075f1bb2544766cf73ba6a6e2adfebcb13f2d" - integrity sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg== - dependencies: - resolve-from "^5.0.0" - -resolve-from@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" - integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== - -resolve.exports@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/resolve.exports/-/resolve.exports-2.0.0.tgz#c1a0028c2d166ec2fbf7d0644584927e76e7400e" - integrity sha512-6K/gDlqgQscOlg9fSRpWstA8sYe8rbELsSTNpx+3kTrsVCzvSl0zIvRErM7fdl9ERWDsKnrLnwB+Ne89918XOg== - -resolve@^1.1.7, resolve@^1.20.0, resolve@^1.22.1: - version "1.22.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.1.tgz#27cb2ebb53f91abb49470a928bba7558066ac177" - integrity sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw== - dependencies: - is-core-module "^2.9.0" - path-parse "^1.0.7" - supports-preserve-symlinks-flag "^1.0.0" - -responselike@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/responselike/-/responselike-2.0.1.tgz#9a0bc8fdc252f3fb1cca68b016591059ba1422bc" - integrity sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw== - dependencies: - lowercase-keys "^2.0.0" - -restore-cursor@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-4.0.0.tgz#519560a4318975096def6e609d44100edaa4ccb9" - integrity sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg== - dependencies: - onetime "^5.1.0" - signal-exit "^3.0.2" - -reusify@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" - integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== - -rimraf@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" - integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== - dependencies: - glob "^7.1.3" - -robust-predicates@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/robust-predicates/-/robust-predicates-3.0.1.tgz#ecde075044f7f30118682bd9fb3f123109577f9a" - integrity sha512-ndEIpszUHiG4HtDsQLeIuMvRsDnn8c8rYStabochtUeCvfuvNptb5TUbVD68LRAILPX7p9nqQGh4xJgn3EHS/g== - -rollup-plugin-dts@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/rollup-plugin-dts/-/rollup-plugin-dts-5.1.1.tgz#8cc36ab13135b77ef0cfd6107e4af561c5dffd04" - integrity sha512-zpgo52XmnLg8w4k3MScinFHZK1+ro6r7uVe34fJ0Ee8AM45FvgvTuvfWWaRgIpA4pQ1BHJuu2ospncZhkcJVeA== - dependencies: - magic-string "^0.27.0" - optionalDependencies: - "@babel/code-frame" "^7.18.6" - -rollup-plugin-inject@^3.0.0: - version "3.0.2" - resolved "https://registry.yarnpkg.com/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz#e4233855bfba6c0c12a312fd6649dff9a13ee9f4" - integrity sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w== - dependencies: - estree-walker "^0.6.1" - magic-string "^0.25.3" - rollup-pluginutils "^2.8.1" - -rollup-plugin-node-polyfills@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz#53092a2744837164d5b8a28812ba5f3ff61109fd" - integrity sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA== - dependencies: - rollup-plugin-inject "^3.0.0" - -rollup-plugin-visualizer@^5.9.0: - version "5.9.0" - resolved "https://registry.yarnpkg.com/rollup-plugin-visualizer/-/rollup-plugin-visualizer-5.9.0.tgz#013ac54fb6a9d7c9019e7eb77eced673399e5a0b" - integrity sha512-bbDOv47+Bw4C/cgs0czZqfm8L82xOZssk4ayZjG40y9zbXclNk7YikrZTDao6p7+HDiGxrN0b65SgZiVm9k1Cg== - dependencies: - open "^8.4.0" - picomatch "^2.3.1" - source-map "^0.7.4" - yargs "^17.5.1" - -rollup-pluginutils@^2.8.1: - version "2.8.2" - resolved "https://registry.yarnpkg.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz#72f2af0748b592364dbd3389e600e5a9444a351e" - integrity sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ== - dependencies: - estree-walker "^0.6.1" - -rollup@^3.10.0, rollup@^3.12.1, rollup@^3.14.0: - version "3.14.0" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-3.14.0.tgz#f5925255f3b6e8de1dba3916d7619c7da5708d95" - integrity sha512-o23sdgCLcLSe3zIplT9nQ1+r97okuaiR+vmAPZPTDYB7/f3tgWIYNyiQveMsZwshBT0is4eGax/HH83Q7CG+/Q== - optionalDependencies: - fsevents "~2.3.2" - -run-async@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" - integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== - -run-parallel@^1.1.9: - version "1.2.0" - resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" - integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== - dependencies: - queue-microtask "^1.2.2" - -rw@1: - version "1.3.3" - resolved "https://registry.yarnpkg.com/rw/-/rw-1.3.3.tgz#3f862dfa91ab766b14885ef4d01124bfda074fb4" - integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ== - -rxjs@^7.5.7: - version "7.8.0" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.8.0.tgz#90a938862a82888ff4c7359811a595e14e1e09a4" - integrity sha512-F2+gxDshqmIub1KdvZkaEfGDwLNpPvk9Fs6LD/MyQxNgMds/WH9OdDDXOmxUZpME+iSK3rQCctkL0DYyytUqMg== - dependencies: - tslib "^2.1.0" - -sade@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" - integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== - dependencies: - mri "^1.1.0" - -safe-buffer@^5.1.0, safe-buffer@~5.2.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" - integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== - -safe-buffer@~5.1.0, safe-buffer@~5.1.1: - version "5.1.2" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" - integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== - -"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -scule@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/scule/-/scule-1.0.0.tgz#895e6f4ba887e78d8b9b4111e23ae84fef82376d" - integrity sha512-4AsO/FrViE/iDNEPaAQlb77tf0csuq27EsVpy6ett584EcRTp6pTDLoGWVxCD77y5iU5FauOvhsI4o1APwPoSQ== - -search-insights@^2.1.0: - version "2.2.3" - resolved "https://registry.yarnpkg.com/search-insights/-/search-insights-2.2.3.tgz#3ef11cc622261887d0a95d91d84eebb5e63fe6b3" - integrity sha512-fXwC0QzkBGZuGTb6FoQG+iLS81wljYuBU4Sco4TGTgp5boVkiKZeFqPV0e5h5++5QncTU2FQrQ+G3ILnqEa3yA== - -semver@7.x, semver@^7.3.4, semver@^7.3.5, semver@^7.3.8: - version "7.3.8" - resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.8.tgz#07a78feafb3f7b32347d725e33de7e2a2df67798" - integrity sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A== - dependencies: - lru-cache "^6.0.0" - -semver@^6.0.0, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -send@0.18.0: - version "0.18.0" - resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be" - integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg== - dependencies: - debug "2.6.9" - depd "2.0.0" - destroy "1.2.0" - encodeurl "~1.0.2" - escape-html "~1.0.3" - etag "~1.8.1" - fresh "0.5.2" - http-errors "2.0.0" - mime "1.6.0" - ms "2.1.3" - on-finished "2.4.1" - range-parser "~1.2.1" - statuses "2.0.1" - -sentence-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-3.0.4.tgz#3645a7b8c117c787fde8702056225bb62a45131f" - integrity sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg== - dependencies: - no-case "^3.0.4" - tslib "^2.0.3" - upper-case-first "^2.0.2" - -serialize-javascript@^6.0.0: - version "6.0.1" - resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.1.tgz#b206efb27c3da0b0ab6b52f48d170b7996458e5c" - integrity sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w== - dependencies: - randombytes "^2.1.0" - -serve-placeholder@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/serve-placeholder/-/serve-placeholder-2.0.1.tgz#dfa741812f49dfea472a68c4f292dbc40d28389a" - integrity sha512-rUzLlXk4uPFnbEaIz3SW8VISTxMuONas88nYWjAWaM2W9VDbt9tyFOr3lq8RhVOFrT3XISoBw8vni5una8qMnQ== - dependencies: - defu "^6.0.0" - -serve-static@^1.15.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540" - integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g== - dependencies: - encodeurl "~1.0.2" - escape-html "~1.0.3" - parseurl "~1.3.3" - send "0.18.0" - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw== - -setprototypeof@1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" - integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== - -shebang-command@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" - integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== - dependencies: - shebang-regex "^3.0.0" - -shebang-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" - integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== - -shiki-es@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/shiki-es/-/shiki-es-0.2.0.tgz#ae5bced62dca0ba46ee81149e68d428565a3e6fb" - integrity sha512-RbRMD+IuJJseSZljDdne9ThrUYrwBwJR04FvN4VXpfsU3MNID5VJGHLAD5je/HGThCyEKNgH+nEkSFEWKD7C3Q== - -signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7: - version "3.0.7" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" - integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== - -sisteransi@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/sisteransi/-/sisteransi-1.0.5.tgz#134d681297756437cc05ca01370d3a7a571075ed" - integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg== - -slash@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" - integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== - -slash@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/slash/-/slash-4.0.0.tgz#2422372176c4c6c5addb5e2ada885af984b396a7" - integrity sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew== - -slugify@^1.6.5: - version "1.6.5" - resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.5.tgz#c8f5c072bf2135b80703589b39a3d41451fbe8c8" - integrity sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ== - -smob@^0.0.6: - version "0.0.6" - resolved "https://registry.yarnpkg.com/smob/-/smob-0.0.6.tgz#09b268fea916158a2781c152044c6155adbb8aa1" - integrity sha512-V21+XeNni+tTyiST1MHsa84AQhT1aFZipzPpOFAVB8DkHzwJyjjAmt9bgwnuZiZWnIbMo2duE29wybxv/7HWUw== - -snake-case@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/snake-case/-/snake-case-3.0.4.tgz#4f2bbd568e9935abdfd593f34c691dadb49c452c" - integrity sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg== - dependencies: - dot-case "^3.0.4" - tslib "^2.0.3" - -socket.io-client@^4.5.4: - version "4.6.0" - resolved "https://registry.yarnpkg.com/socket.io-client/-/socket.io-client-4.6.0.tgz#449255d2e0fe429f5ab47ecd3e3b1716b0039c13" - integrity sha512-2XOp18xnGghUICSd5ziUIS4rB0dhr6S8OvAps8y+HhOjFQlqGcf+FIh6fCIsKKZyWFxJeFPrZRNPGsHDTsz1Ug== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.2" - engine.io-client "~6.4.0" - socket.io-parser "~4.2.1" - -socket.io-parser@~4.2.1: - version "4.2.2" - resolved "https://registry.yarnpkg.com/socket.io-parser/-/socket.io-parser-4.2.2.tgz#1dd384019e25b7a3d374877f492ab34f2ad0d206" - integrity sha512-DJtziuKypFkMMHCm2uIshOYC7QaylbtzQwiMYDuCKy3OPkjLzu4B2vAhTlqipRHHzrI0NJeBAizTK7X+6m1jVw== - dependencies: - "@socket.io/component-emitter" "~3.1.0" - debug "~4.3.1" - -source-map-js@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" - integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== - -source-map-support@0.5.13: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map-support@^0.5.21, source-map-support@~0.5.20: - version "0.5.21" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" - integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0, source-map@^0.6.1, source-map@~0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -source-map@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.4.tgz#a9bbe705c9d8846f4e08ff6765acf0f1b0898656" - integrity sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA== - -sourcemap-codec@^1.4.8: - version "1.4.8" - resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" - integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== - -space-separated-tokens@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" - integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g== - -stable@^0.1.8: - version "0.1.8" - resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" - integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w== - -stack-utils@^2.0.3: - version "2.0.6" - resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-2.0.6.tgz#aaf0748169c02fc33c8232abccf933f54a1cc34f" - integrity sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ== - dependencies: - escape-string-regexp "^2.0.0" - -standard-as-callback@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/standard-as-callback/-/standard-as-callback-2.1.0.tgz#8953fc05359868a77b5b9739a665c5977bb7df45" - integrity sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A== - -statuses@2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" - integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== - -std-env@^3.3.1, std-env@^3.3.2: - version "3.3.2" - resolved "https://registry.yarnpkg.com/std-env/-/std-env-3.3.2.tgz#af27343b001616015534292178327b202b9ee955" - integrity sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA== - -storyblok-algolia-indexer@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/storyblok-algolia-indexer/-/storyblok-algolia-indexer-1.0.3.tgz#1da5becfa0dd9e556f714e14a5dc991994ec964b" - integrity sha512-mwqf1bt4/Dn+dj71ousuHWWOx+wprj21nOjsfB16322ptywdfxynru7MU0RQoKktSbzkMQhhEhGBuuUDcsJnqQ== - dependencies: - algoliasearch "^4.12.0" - axios "^0.25.0" - storyblok-js-client "^4.2.0" - -storyblok-js-client@^4.2.0: - version "4.5.8" - resolved "https://registry.yarnpkg.com/storyblok-js-client/-/storyblok-js-client-4.5.8.tgz#862f67d4f25b6c50334e6965bf27f6b6a755ccae" - integrity sha512-0Wp7Kn4RdKaKRgMmNDu8c6OC81x1mgeri4ziKDY9BsUZEIGLTNw8KFIWlHYACgMnJQn/6qZaV9kGo8IY4hv6GA== - -string-length@^4.0.1: - version "4.0.2" - resolved "https://registry.yarnpkg.com/string-length/-/string-length-4.0.2.tgz#a8a8dc7bd5c1a82b9b3c8b87e125f66871b6e57a" - integrity sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ== - dependencies: - char-regex "^1.0.2" - strip-ansi "^6.0.0" - -"string-width@^1.0.2 || 2 || 3 || 4", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^5.0.1, string-width@^5.1.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" - integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== - dependencies: - eastasianwidth "^0.2.0" - emoji-regex "^9.2.2" - strip-ansi "^7.0.1" - -string_decoder@^1.1.1: - version "1.3.0" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" - integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== - dependencies: - safe-buffer "~5.2.0" - -string_decoder@~1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" - integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== - dependencies: - safe-buffer "~5.1.0" - -stringify-entities@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.3.tgz#cfabd7039d22ad30f3cc435b0ca2c1574fc88ef8" - integrity sha512-BP9nNHMhhfcMbiuQKCqMjhDP5yBCAxsPu4pHFFzJ6Alo9dZgY4VLDPutXqIjpRiMoKdp7Av85Gr73Q5uH9k7+g== - dependencies: - character-entities-html4 "^2.0.0" - character-entities-legacy "^3.0.0" - -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.0.1.tgz#61740a08ce36b61e50e65653f07060d000975fb2" - integrity sha512-cXNxvT8dFNRVfhVME3JAe98mkXDYN2O1l7jmcwMnOslDeESg1rF/OZMtK0nRAhiari1unG5cD4jG3rapUAkLbw== - dependencies: - ansi-regex "^6.0.1" - -strip-bom@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-4.0.0.tgz#9c3505c1db45bcedca3d9cf7a16f5c5aa3901878" - integrity sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w== - -strip-final-newline@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" - integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== - -strip-final-newline@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz#52894c313fbff318835280aed60ff71ebf12b8fd" - integrity sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw== - -strip-json-comments@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" - integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== - -strip-literal@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-literal/-/strip-literal-1.0.1.tgz#0115a332710c849b4e46497891fb8d585e404bd2" - integrity sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q== - dependencies: - acorn "^8.8.2" - -style-dictionary-esm@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/style-dictionary-esm/-/style-dictionary-esm-1.2.0.tgz#30148c6507effc2b9e6b92e6293a9d57442694ec" - integrity sha512-kOMB90UCMlXfYPgp0rB0L0P1FWJHQvNR3FIhYYDpQJhSI6q7608DVqLJXQzQG7CADM4SpaP10hlvr1t90TgWmw== - dependencies: - chalk "^4.1.2" - change-case "^4.1.2" - commander "^9.5.0" - consola "^2.15.3" - fs-extra "^11.1.0" - glob "^8.0.3" - jiti "^1.16.2" - json5 "^2.2.3" - jsonc-parser "^3.2.0" - lodash.template "^4.5.0" - tinycolor2 "^1.5.2" - -stylehacks@^5.1.1: - version "5.1.1" - resolved "https://registry.yarnpkg.com/stylehacks/-/stylehacks-5.1.1.tgz#7934a34eb59d7152149fa69d6e9e56f2fc34bcc9" - integrity sha512-sBpcd5Hx7G6seo7b1LkpttvTz7ikD0LlH5RmdcBNb6fFR0Fl7LQwHDFr300q4cwUqi+IYrFGmsIHieMBfnN/Bw== - dependencies: - browserslist "^4.21.4" - postcss-selector-parser "^6.0.4" - -stylis@^4.1.2: - version "4.1.3" - resolved "https://registry.yarnpkg.com/stylis/-/stylis-4.1.3.tgz#fd2fbe79f5fed17c55269e16ed8da14c84d069f7" - integrity sha512-GP6WDNWf+o403jrEp9c5jibKavrtLW+/qYGhFxFrG8maXhwTBI7gLLhiBb0o7uFccWN+EOS9aMO6cGHWAO07OA== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -supports-color@^7.1.0: - version "7.2.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" - integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== - dependencies: - has-flag "^4.0.0" - -supports-color@^8.0.0: - version "8.1.1" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" - integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== - dependencies: - has-flag "^4.0.0" - -supports-preserve-symlinks-flag@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" - integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== - -svg-tags@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/svg-tags/-/svg-tags-1.0.0.tgz#58f71cee3bd519b59d4b2a843b6c7de64ac04764" - integrity sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA== - -svgo@^2.7.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/svgo/-/svgo-2.8.0.tgz#4ff80cce6710dc2795f0c7c74101e6764cfccd24" - integrity sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg== - dependencies: - "@trysound/sax" "0.2.0" - commander "^7.2.0" - css-select "^4.1.3" - css-tree "^1.1.3" - csso "^4.2.0" - picocolors "^1.0.0" - stable "^0.1.8" - -tailwindcss@^3.2.4: - version "3.2.6" - resolved "https://registry.yarnpkg.com/tailwindcss/-/tailwindcss-3.2.6.tgz#9bedbc744a4a85d6120ce0cc3db024c551a5c733" - integrity sha512-BfgQWZrtqowOQMC2bwaSNe7xcIjdDEgixWGYOd6AL0CbKHJlvhfdbINeAW76l1sO+1ov/MJ93ODJ9yluRituIw== - dependencies: - arg "^5.0.2" - chokidar "^3.5.3" - color-name "^1.1.4" - detective "^5.2.1" - didyoumean "^1.2.2" - dlv "^1.1.3" - fast-glob "^3.2.12" - glob-parent "^6.0.2" - is-glob "^4.0.3" - lilconfig "^2.0.6" - micromatch "^4.0.5" - normalize-path "^3.0.0" - object-hash "^3.0.0" - picocolors "^1.0.0" - postcss "^8.0.9" - postcss-import "^14.1.0" - postcss-js "^4.0.0" - postcss-load-config "^3.1.4" - postcss-nested "6.0.0" - postcss-selector-parser "^6.0.11" - postcss-value-parser "^4.2.0" - quick-lru "^5.1.1" - resolve "^1.22.1" - -tapable@^1.0.0: - version "1.1.3" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-1.1.3.tgz#a1fccc06b58db61fd7a45da2da44f5f3a3e67ba2" - integrity sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA== - -tapable@^2.2.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0" - integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ== - -tar-stream@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287" - integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ== - dependencies: - bl "^4.0.3" - end-of-stream "^1.4.1" - fs-constants "^1.0.0" - inherits "^2.0.3" - readable-stream "^3.1.1" - -tar@^6.1.11, tar@^6.1.12: - version "6.1.13" - resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b" - integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw== - dependencies: - chownr "^2.0.0" - fs-minipass "^2.0.0" - minipass "^4.0.0" - minizlib "^2.1.1" - mkdirp "^1.0.3" - yallist "^4.0.0" - -terser@^5.15.1: - version "5.16.3" - resolved "https://registry.yarnpkg.com/terser/-/terser-5.16.3.tgz#3266017a9b682edfe019b8ecddd2abaae7b39c6b" - integrity sha512-v8wWLaS/xt3nE9dgKEWhNUFP6q4kngO5B8eYFUuebsu7Dw/UNAnpUod6UHo04jSSkv8TzKHjZDSd7EXdDQAl8Q== - dependencies: - "@jridgewell/source-map" "^0.3.2" - acorn "^8.5.0" - commander "^2.20.0" - source-map-support "~0.5.20" - -test-exclude@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" - integrity sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w== - dependencies: - "@istanbuljs/schema" "^0.1.2" - glob "^7.1.4" - minimatch "^3.0.4" - -through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg== - -tiny-invariant@^1.1.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.3.1.tgz#8560808c916ef02ecfd55e66090df23a4b7aa642" - integrity sha512-AD5ih2NlSssTCwsMznbvwMZpJ1cbhkGd2uueNxzv2jDlEeZdU04JQfRnggJQ8DrcVBGjAsCKwFBbDlVNtEMlzw== - -tinycolor2@^1.5.2: - version "1.6.0" - resolved "https://registry.yarnpkg.com/tinycolor2/-/tinycolor2-1.6.0.tgz#f98007460169b0263b97072c5ae92484ce02d09e" - integrity sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw== - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -tmpl@1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/tmpl/-/tmpl-1.0.5.tgz#8683e0b902bb9c20c4f726e3c0b69f36518c07cc" - integrity sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw== - -to-fast-properties@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz#dc5e698cbd079265bc73e0377681a4e4e83f616e" - integrity sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog== - -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - -toidentifier@1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" - integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== - -tr46@~0.0.3: - version "0.0.3" - resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" - integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== - -trim-lines@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/trim-lines/-/trim-lines-3.0.1.tgz#d802e332a07df861c48802c04321017b1bd87338" - integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg== - -trough@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" - integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== - -ts-jest@^29.0.3: - version "29.0.5" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-29.0.5.tgz#c5557dcec8fe434fcb8b70c3e21c6b143bfce066" - integrity sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA== - dependencies: - bs-logger "0.x" - fast-json-stable-stringify "2.x" - jest-util "^29.0.0" - json5 "^2.2.3" - lodash.memoize "4.x" - make-error "1.x" - semver "7.x" - yargs-parser "^21.0.1" - -tslib@^2.0.1, tslib@^2.0.3, tslib@^2.1.0: - version "2.5.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.5.0.tgz#42bfed86f5787aeb41d031866c8f402429e0fddf" - integrity sha512-336iVw3rtn2BUK7ORdIAHTyxHGRIHVReokCR3XjbckJMK7ms8FysBfhLR8IXnAgy7T0PTPNBWKiH514FOW/WSg== - -type-detect@4.0.8, type-detect@^4.0.0, type-detect@^4.0.5: - version "4.0.8" - resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" - integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== - -type-fest@^0.21.3: - version "0.21.3" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" - integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== - -type-fest@^2.11.2: - version "2.19.0" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-2.19.0.tgz#88068015bb33036a598b952e55e9311a60fd3a9b" - integrity sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA== - -type-fest@^3.0.0: - version "3.5.6" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-3.5.6.tgz#f8f3a630c185fb5d66ca6950c7cbc2893deb6b84" - integrity sha512-6bd2bflx8ed7c99tc6zSTIzHr1/QG29bQoK4Qh8MYGnlPbODUzGxklLShjwc/xWQQFHgIci+y5Arv7Rbb0LjXw== - -typesafe-path@^0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/typesafe-path/-/typesafe-path-0.2.2.tgz#91a436681b2f514badb114061b6a5e5c2b8943b1" - integrity sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA== - -typescript@^4.9.4: - version "4.9.5" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" - integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== - -ufo@^1.0.0, ufo@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.0.1.tgz#64ed43b530706bda2e4892f911f568cf4cf67d29" - integrity sha512-boAm74ubXHY7KJQZLlXrtMz52qFvpsbOxDcZOnw/Wf+LS4Mmyu7JxmzD4tDLtUQtmZECypJ0FrCz4QIe6dvKRA== - -ultrahtml@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/ultrahtml/-/ultrahtml-1.2.0.tgz#169583f2a069d6de3b9686bd994c19811d95a0d6" - integrity sha512-vxZM2yNvajRmCj/SknRYGNXk2tqiy6kRNvZjJLaleG3zJbSh/aNkOqD1/CVzypw8tyHyhpzYuwQgMMhUB4ZVNQ== - -unbuild@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unbuild/-/unbuild-1.1.1.tgz#c283f1bdb1a209f5446d29336887dd265facfab0" - integrity sha512-HlhHj6cUPBQJmhoczQoU6dzdTFO0Jr9EiGWEZ1EwHGXlGRR6LXcKyfX3PMrkM48uWJjBWiCgTQdkFOAk3tlK6Q== - dependencies: - "@rollup/plugin-alias" "^4.0.2" - "@rollup/plugin-commonjs" "^24.0.0" - "@rollup/plugin-json" "^6.0.0" - "@rollup/plugin-node-resolve" "^15.0.1" - "@rollup/plugin-replace" "^5.0.2" - "@rollup/pluginutils" "^5.0.2" - chalk "^5.2.0" - consola "^2.15.3" - defu "^6.1.1" - esbuild "^0.16.17" - globby "^13.1.3" - hookable "^5.4.2" - jiti "^1.16.2" - magic-string "^0.27.0" - mkdirp "^1.0.4" - mkdist "^1.1.0" - mlly "^1.1.0" - mri "^1.2.0" - pathe "^1.0.0" - pkg-types "^1.0.1" - pretty-bytes "^6.0.0" - rollup "^3.10.0" - rollup-plugin-dts "^5.1.1" - scule "^1.0.0" - typescript "^4.9.4" - untyped "^1.2.2" - -uncrypto@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/uncrypto/-/uncrypto-0.1.2.tgz#225aa7d41a13e4ad07ed837aedfa975a93afa924" - integrity sha512-kuZwRKV615lEw/Xx3Iz56FKk3nOeOVGaVmw0eg+x4Mne28lCotNFbBhDW7dEBCBKyKbRQiCadEZeNAFPVC5cgw== - -unctx@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/unctx/-/unctx-2.1.1.tgz#415b07cf6ce42fad59ae1e4fa42ace2e71f4372d" - integrity sha512-RffJlpvLOtolWsn0fxXsuSDfwiWcR6cyuykw2e0+zAggvGW1SesXt9WxIWlWpJhwVCZD/WlxxLqKLS50Q0CkWA== - dependencies: - acorn "^8.8.1" - estree-walker "^3.0.1" - magic-string "^0.26.7" - unplugin "^1.0.0" - -unenv@^1.0.3, unenv@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/unenv/-/unenv-1.1.1.tgz#e04ae9c5278ee9a2af7af54f9c37ba6043f38baf" - integrity sha512-AfQ+sKCdeSPX/rp0tL9LZz3cAu1Mt0i9UADuN1MtbsITKDS2PqSx8LQUBMf8lKuziitIWXXwU6JXrmzARFVSRw== - dependencies: - defu "^6.1.2" - mime "^3.0.0" - node-fetch-native "^1.0.1" - pathe "^1.1.0" - -unhead@^1.0.20: - version "1.0.21" - resolved "https://registry.yarnpkg.com/unhead/-/unhead-1.0.21.tgz#4f8d3de2c4ee7681c50e7e4090b19f8accd9ef83" - integrity sha512-vHXnozOkoSkCYIpGTWkW4JJbWMlY2I737sbBGxPj6maa9gEDMC50gwhCCVMnIvvMsJ6OxgNE5asEfSkSopfO+A== - dependencies: - "@unhead/dom" "1.0.21" - "@unhead/schema" "1.0.21" - hookable "^5.4.2" - -unified@^10.0.0, unified@^10.1.2: - version "10.1.2" - resolved "https://registry.yarnpkg.com/unified/-/unified-10.1.2.tgz#b1d64e55dafe1f0b98bb6c719881103ecf6c86df" - integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q== - dependencies: - "@types/unist" "^2.0.0" - bail "^2.0.0" - extend "^3.0.0" - is-buffer "^2.0.0" - is-plain-obj "^4.0.0" - trough "^2.0.0" - vfile "^5.0.0" - -unimport@^2.0.1, unimport@^2.1.0, unimport@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/unimport/-/unimport-2.2.4.tgz#3d0c7fb354e54ba277e58725aac73fbebabee0c7" - integrity sha512-qMgmeEGqqrrmEtm0dqxMG37J6xBtrriqxq9hILvDb+e6l2F0yTnJomLoCCp0eghLR7bYGeBsUU5Y0oyiUYhViw== - dependencies: - "@rollup/pluginutils" "^5.0.2" - escape-string-regexp "^5.0.0" - fast-glob "^3.2.12" - local-pkg "^0.4.3" - magic-string "^0.27.0" - mlly "^1.1.0" - pathe "^1.1.0" - pkg-types "^1.0.1" - scule "^1.0.0" - strip-literal "^1.0.0" - unplugin "^1.0.1" - -unist-builder@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/unist-builder/-/unist-builder-3.0.1.tgz#258b89dcadd3c973656b2327b347863556907f58" - integrity sha512-gnpOw7DIpCA0vpr6NqdPvTWnlPTApCTRzr+38E6hCWx3rz/cjo83SsKIlS1Z+L5ttScQ2AwutNnb8+tAvpb6qQ== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-generated@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/unist-util-generated/-/unist-util-generated-2.0.1.tgz#e37c50af35d3ed185ac6ceacb6ca0afb28a85cae" - integrity sha512-qF72kLmPxAw0oN2fwpWIqbXAVyEqUzDHMsbtPvOudIlUzXYFIeQIuxXQCRCFh22B7cixvU0MG7m3MW8FTq/S+A== - -unist-util-is@^5.0.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.0.tgz#37eed0617b76c114fd34d44c201aa96fd928b309" - integrity sha512-Glt17jWwZeyqrFqOK0pF1Ded5U3yzJnFr8CG1GMjCWTp9zDo2p+cmD6pWbZU8AgM5WU3IzRv6+rBwhzsGh6hBQ== - -unist-util-position@^4.0.0, unist-util-position@^4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/unist-util-position/-/unist-util-position-4.0.4.tgz#93f6d8c7d6b373d9b825844645877c127455f037" - integrity sha512-kUBE91efOWfIVBo8xzh/uZQ7p9ffYRtUbMRZBNFYwf0RK8koUMx6dGUfwylLOKmaT2cs4wSW96QoYUSXAyEtpg== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-stringify-position@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" - integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" - integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - -unist-util-visit@^4.0.0, unist-util-visit@^4.1.0, unist-util-visit@^4.1.1, unist-util-visit@^4.1.2: - version "4.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" - integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.1.1" - -universalify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" - integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== - -unplugin@^1.0.0, unplugin@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-1.0.1.tgz#83b528b981cdcea1cad422a12cd02e695195ef3f" - integrity sha512-aqrHaVBWW1JVKBHmGo33T5TxeL0qWzfvjWokObHA9bYmN7eNDkwOxmLjhioHl9878qDFMAaT51XNroRyuz7WxA== - dependencies: - acorn "^8.8.1" - chokidar "^3.5.3" - webpack-sources "^3.2.3" - webpack-virtual-modules "^0.5.0" - -unstorage@^1.0.1, unstorage@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/unstorage/-/unstorage-1.1.4.tgz#a69bd90e14b8e7aac5e468d6b0f193256d10309f" - integrity sha512-nrnCoWN8ewaZrwz5yf7QGkMn0FDoVer6yGIR56wvocNzAmZi1vXOnCaBxueB3Uu/SqNSH5N/ww41t6jNT8XccA== - dependencies: - anymatch "^3.1.3" - chokidar "^3.5.3" - destr "^1.2.2" - h3 "^1.1.0" - ioredis "^5.3.0" - listhen "^1.0.2" - lru-cache "^7.14.1" - mkdir "^0.0.2" - mri "^1.2.0" - node-fetch-native "^1.0.1" - ofetch "^1.0.0" - ufo "^1.0.1" - ws "^8.12.0" - optionalDependencies: - "@planetscale/database" "^1.5.0" - -untyped@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/untyped/-/untyped-1.2.2.tgz#d442a5d4b4281b5344cefed318736eb480b70c2f" - integrity sha512-EANYd5L6AdpgfldlgMcmvOOnj092nWhy0ybhc7uhEH12ipytDYz89EOegBQKj8qWL3u1wgYnmFjADhsuCJs5Aw== - dependencies: - "@babel/core" "^7.20.12" - "@babel/standalone" "^7.20.12" - "@babel/types" "^7.20.7" - scule "^1.0.0" - -update-browserslist-db@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz#0f54b876545726f17d00cd9a2561e6dade943ff3" - integrity sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ== - dependencies: - escalade "^3.1.1" - picocolors "^1.0.0" - -upper-case-first@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/upper-case-first/-/upper-case-first-2.0.2.tgz#992c3273f882abd19d1e02894cc147117f844324" - integrity sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg== - dependencies: - tslib "^2.0.3" - -upper-case@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/upper-case/-/upper-case-2.0.2.tgz#d89810823faab1df1549b7d97a76f8662bae6f7a" - integrity sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg== - dependencies: - tslib "^2.0.3" - -util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" - integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== - -util@^0.12.0: - version "0.12.5" - resolved "https://registry.yarnpkg.com/util/-/util-0.12.5.tgz#5f17a6059b73db61a875668781a1c2b136bd6fbc" - integrity sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA== - dependencies: - inherits "^2.0.3" - is-arguments "^1.0.4" - is-generator-function "^1.0.7" - is-typed-array "^1.1.3" - which-typed-array "^1.1.2" - -uuid@^9.0.0: - version "9.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" - integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== - -uvu@^0.5.0: - version "0.5.6" - resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" - integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== - dependencies: - dequal "^2.0.0" - diff "^5.0.0" - kleur "^4.0.3" - sade "^1.7.3" - -v8-to-istanbul@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz#b6f994b0b5d4ef255e17a0d17dc444a9f5132fa4" - integrity sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w== - dependencies: - "@jridgewell/trace-mapping" "^0.3.12" - "@types/istanbul-lib-coverage" "^2.0.1" - convert-source-map "^1.6.0" - -vfile-location@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-4.1.0.tgz#69df82fb9ef0a38d0d02b90dd84620e120050dd0" - integrity sha512-YF23YMyASIIJXpktBa4vIGLJ5Gs88UB/XePgqPmTa7cDA+JeO3yclbpheQYCHjVHBn/yePzrXuygIL+xbvRYHw== - dependencies: - "@types/unist" "^2.0.0" - vfile "^5.0.0" - -vfile-message@^3.0.0: - version "3.1.4" - resolved "https://registry.yarnpkg.com/vfile-message/-/vfile-message-3.1.4.tgz#15a50816ae7d7c2d1fa87090a7f9f96612b59dea" - integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw== - dependencies: - "@types/unist" "^2.0.0" - unist-util-stringify-position "^3.0.0" - -vfile@^5.0.0: - version "5.3.7" - resolved "https://registry.yarnpkg.com/vfile/-/vfile-5.3.7.tgz#de0677e6683e3380fafc46544cfe603118826ab7" - integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g== - dependencies: - "@types/unist" "^2.0.0" - is-buffer "^2.0.0" - unist-util-stringify-position "^3.0.0" - vfile-message "^3.0.0" - -vite-node@^0.28.3: - version "0.28.4" - resolved "https://registry.yarnpkg.com/vite-node/-/vite-node-0.28.4.tgz#ce709cde2200d86a2a45457fed65f453234b0261" - integrity sha512-KM0Q0uSG/xHHKOJvVHc5xDBabgt0l70y7/lWTR7Q0pR5/MrYxadT+y32cJOE65FfjGmJgxpVEEY+69btJgcXOQ== - dependencies: - cac "^6.7.14" - debug "^4.3.4" - mlly "^1.1.0" - pathe "^1.1.0" - picocolors "^1.0.0" - source-map "^0.6.1" - source-map-support "^0.5.21" - vite "^3.0.0 || ^4.0.0" - -vite-plugin-checker@^0.5.5: - version "0.5.5" - resolved "https://registry.yarnpkg.com/vite-plugin-checker/-/vite-plugin-checker-0.5.5.tgz#164159c129231e9d23507735f5b1cdce5fbda8da" - integrity sha512-BLaRlBmiVn3Fg/wR9A0+YNwgXVteFJaH8rCIiIgYQcQ50jc3oVe2m8i0xxG5geq36UttNJsAj7DpDelN7/KjOg== - dependencies: - "@babel/code-frame" "^7.12.13" - ansi-escapes "^4.3.0" - chalk "^4.1.1" - chokidar "^3.5.1" - commander "^8.0.0" - fast-glob "^3.2.7" - fs-extra "^11.1.0" - lodash.debounce "^4.0.8" - lodash.pick "^4.4.0" - npm-run-path "^4.0.1" - strip-ansi "^6.0.0" - tiny-invariant "^1.1.0" - vscode-languageclient "^7.0.0" - vscode-languageserver "^7.0.0" - vscode-languageserver-textdocument "^1.0.1" - vscode-uri "^3.0.2" - -"vite@^3.0.0 || ^4.0.0", vite@~4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.1.1.tgz#3b18b81a4e85ce3df5cbdbf4c687d93ebf402e6b" - integrity sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg== - dependencies: - esbuild "^0.16.14" - postcss "^8.4.21" - resolve "^1.22.1" - rollup "^3.10.0" - optionalDependencies: - fsevents "~2.3.2" - -vscode-jsonrpc@6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" - integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== - -vscode-languageclient@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" - integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== - dependencies: - minimatch "^3.0.4" - semver "^7.3.4" - vscode-languageserver-protocol "3.16.0" - -vscode-languageserver-protocol@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" - integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== - dependencies: - vscode-jsonrpc "6.0.0" - vscode-languageserver-types "3.16.0" - -vscode-languageserver-textdocument@^1.0.1: - version "1.0.8" - resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.8.tgz#9eae94509cbd945ea44bca8dcfe4bb0c15bb3ac0" - integrity sha512-1bonkGqQs5/fxGT5UchTgjGVnfysL0O8v1AYMBjqTbWQTFn721zaPGDYFkOKtfDgFiSgXM3KwaG3FMGfW4Ed9Q== - -vscode-languageserver-types@3.16.0: - version "3.16.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" - integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== - -vscode-languageserver@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" - integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== - dependencies: - vscode-languageserver-protocol "3.16.0" - -vscode-uri@^3.0.2: - version "3.0.7" - resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8" - integrity sha512-eOpPHogvorZRobNqJGhapa0JdwaxpjVvyBp0QIUMRMSf8ZAlqOdEquKuRmw9Qwu0qXtJIWqFtMkmvJjUZmMjVA== - -vue-bundle-renderer@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/vue-bundle-renderer/-/vue-bundle-renderer-1.0.1.tgz#989c96042d506070a41d86351c9287fb7d10bf9b" - integrity sha512-w1zRgff5lVJ5YAIkVSKuFjDyCgKdg/sPbcgZbosnMCoHblg0uThCKA2n/XWUGnw0Rh2+03UY/VtkwaYwMUSRyQ== - dependencies: - ufo "^1.0.1" - -vue-component-meta@^1.0.18: - version "1.0.24" - resolved "https://registry.yarnpkg.com/vue-component-meta/-/vue-component-meta-1.0.24.tgz#0b02499ab2503b39c655540ff644ccc63b4eeaa9" - integrity sha512-T2q8ptMjZA98wDYpyoTcrsvG8oPwyBKw73viLOCr4BSkdMDffyLhdxxVpn/XKangCki7bKY8TdK77w0YP5MCaw== - dependencies: - "@volar/language-core" "1.0.24" - "@volar/vue-language-core" "1.0.24" - typesafe-path "^0.2.2" - -vue-demi@*: - version "0.13.11" - resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.13.11.tgz#7d90369bdae8974d87b1973564ad390182410d99" - integrity sha512-IR8HoEEGM65YY3ZJYAjMlKygDQn25D5ajNFNoKh9RSDMQtlzCxtfQjdQgv9jjK+m3377SsJXY8ysq8kLCZL25A== - -vue-devtools-stub@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/vue-devtools-stub/-/vue-devtools-stub-0.1.0.tgz#a65b9485edecd4273cedcb8102c739b83add2c81" - integrity sha512-RutnB7X8c5hjq39NceArgXg28WZtZpGc3+J16ljMiYnFhKvd8hITxSWQSQ5bvldxMDU6gG5mkxl1MTQLXckVSQ== - -vue-gtag-next@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/vue-gtag-next/-/vue-gtag-next-1.14.0.tgz#793aef0b90dff4213b9f3a79827cd99b06e678dd" - integrity sha512-iJl+cOG2GU5NuxqzSSIpt03WVOvZqyKB9TOy7d55KiuvRklcnb2nlqxW5B/a3/sbIt7fla+XEkRyMCcoz0zAHw== - -vue-instantsearch@^4.3.2: - version "4.8.3" - resolved "https://registry.yarnpkg.com/vue-instantsearch/-/vue-instantsearch-4.8.3.tgz#20479213851397b5ef5027c7dba59f79c076183a" - integrity sha512-a7zPcuDlzr/kpeC1uETv/03tjhQ9liZq2PBIq5HGFOgov4oFbCB7AVRyHT5I/E9gmMlA+ltbz0+Ls/hA38i7kA== - dependencies: - instantsearch.js "4.50.3" - mitt "^2.1.0" - -vue-router@^4.1.6: - version "4.1.6" - resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.1.6.tgz#b70303737e12b4814578d21d68d21618469375a1" - integrity sha512-DYWYwsG6xNPmLq/FmZn8Ip+qrhFEzA14EI12MsMgVxvHFDYvlr4NXpVF5hrRH1wVcDP8fGi5F4rxuJSl8/r+EQ== - dependencies: - "@vue/devtools-api" "^6.4.5" - -vue-template-compiler@^2.7.14: - version "2.7.14" - resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz#4545b7dfb88090744c1577ae5ac3f964e61634b1" - integrity sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ== - dependencies: - de-indent "^1.0.2" - he "^1.2.0" - -vue@^3.2.47: - version "3.2.47" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.2.47.tgz#3eb736cbc606fc87038dbba6a154707c8a34cff0" - integrity sha512-60188y/9Dc9WVrAZeUVSDxRQOZ+z+y5nO2ts9jWXSTkMvayiWxCWOWtBQoYjLeccfXkiiPZWAHcV+WTPhkqJHQ== - dependencies: - "@vue/compiler-dom" "3.2.47" - "@vue/compiler-sfc" "3.2.47" - "@vue/runtime-dom" "3.2.47" - "@vue/server-renderer" "3.2.47" - "@vue/shared" "3.2.47" - -walker@^1.0.8: - version "1.0.8" - resolved "https://registry.yarnpkg.com/walker/-/walker-1.0.8.tgz#bd498db477afe573dc04185f011d3ab8a8d7653f" - integrity sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ== - dependencies: - makeerror "1.0.12" - -wcwidth@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" - integrity sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg== - dependencies: - defaults "^1.0.3" - -web-namespaces@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/web-namespaces/-/web-namespaces-2.0.1.tgz#1010ff7c650eccb2592cebeeaf9a1b253fd40692" - integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ== - -web-streams-polyfill@^3.0.3: - version "3.2.1" - resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz#71c2718c52b45fd49dbeee88634b3a60ceab42a6" - integrity sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q== - -webidl-conversions@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" - integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== - -webpack-sources@^3.2.3: - version "3.2.3" - resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-3.2.3.tgz#2d4daab8451fd4b240cc27055ff6a0c2ccea0cde" - integrity sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w== - -webpack-virtual-modules@^0.5.0: - version "0.5.0" - resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.5.0.tgz#362f14738a56dae107937ab98ea7062e8bdd3b6c" - integrity sha512-kyDivFZ7ZM0BVOUteVbDFhlRt7Ah/CSPwJdi8hBpkK7QLumUqdLtVfm/PX/hkcnrvr0i77fO5+TjZ94Pe+C9iw== - -whatwg-url@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" - integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== - dependencies: - tr46 "~0.0.3" - webidl-conversions "^3.0.0" - -which-typed-array@^1.1.2: - version "1.1.9" - resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.9.tgz#307cf898025848cf995e795e8423c7f337efbde6" - integrity sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA== - dependencies: - available-typed-arrays "^1.0.5" - call-bind "^1.0.2" - for-each "^0.3.3" - gopd "^1.0.1" - has-tostringtag "^1.0.0" - is-typed-array "^1.1.10" - -which@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" - integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== - dependencies: - isexe "^2.0.0" - -wide-align@^1.1.2: - version "1.1.5" - resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3" - integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg== - dependencies: - string-width "^1.0.2 || 2 || 3 || 4" - -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - -wrap-ansi@^8.0.1: - version "8.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" - integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== - dependencies: - ansi-styles "^6.1.0" - string-width "^5.0.1" - strip-ansi "^7.0.1" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== - -write-file-atomic@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/write-file-atomic/-/write-file-atomic-4.0.2.tgz#a9df01ae5b77858a027fd2e80768ee433555fcfd" - integrity sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg== - dependencies: - imurmurhash "^0.1.4" - signal-exit "^3.0.7" - -ws@^8.12.0: - version "8.12.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.12.0.tgz#485074cc392689da78e1828a9ff23585e06cddd8" - integrity sha512-kU62emKIdKVeEIOIKVegvqpXMSTAMLJozpHZaJNDYqBjzlSYXQGviYwN1osDLJ9av68qHd4a2oSjd7yD4pacig== - -ws@~8.11.0: - version "8.11.0" - resolved "https://registry.yarnpkg.com/ws/-/ws-8.11.0.tgz#6a0d36b8edfd9f96d8b25683db2f8d7de6e8e143" - integrity sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg== - -xmlhttprequest-ssl@~2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz#91360c86b914e67f44dce769180027c0da618c67" - integrity sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A== - -xtend@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" - integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== - -xxhashjs@~0.2.2: - version "0.2.2" - resolved "https://registry.yarnpkg.com/xxhashjs/-/xxhashjs-0.2.2.tgz#8a6251567621a1c46a5ae204da0249c7f8caa9d8" - integrity sha512-AkTuIuVTET12tpsVIQo+ZU6f/qDmKuRUcjaqR+OIvm+aCBsZ95i7UVY5WJ9TMsSaZ0DA2WxoZ4acu0sPH+OKAw== - dependencies: - cuint "^0.2.2" - -y18n@^5.0.5: - version "5.0.8" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" - integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== - -yallist@^3.0.2: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" - integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== - -yallist@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" - integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== - -yaml@^1.10.2: - version "1.10.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.2.tgz#2301c5ffbf12b467de8da2333a459e29e7920e4b" - integrity sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg== - -yargs-parser@^21.0.1, yargs-parser@^21.1.1: - version "21.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-21.1.1.tgz#9096bceebf990d21bb31fa9516e0ede294a77d35" - integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== - -yargs@^17.3.1, yargs@^17.5.1: - version "17.6.2" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.6.2.tgz#2e23f2944e976339a1ee00f18c77fedee8332541" - integrity sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw== - dependencies: - cliui "^8.0.1" - escalade "^3.1.1" - get-caller-file "^2.0.5" - require-directory "^2.1.1" - string-width "^4.2.3" - y18n "^5.0.5" - yargs-parser "^21.1.1" - -yocto-queue@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" - integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zip-stream@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-4.1.0.tgz#51dd326571544e36aa3f756430b313576dc8fc79" - integrity sha512-zshzwQW7gG7hjpBlgeQP9RuyPGNxvJdzR8SUM3QhxCnLjWN2E7j3dOvpeDcQoETfHx0urRS7EtmVToql7YpU4A== - dependencies: - archiver-utils "^2.1.0" - compress-commons "^4.1.0" - readable-stream "^3.6.0" - -zwitch@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" - integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A== diff --git a/docs/.vuepress/components/LaunchPad.vue b/docs/.vuepress/components/LaunchPad.vue deleted file mode 100644 index b7148222157..00000000000 --- a/docs/.vuepress/components/LaunchPad.vue +++ /dev/null @@ -1,39 +0,0 @@ - - - - - \ No newline at end of file diff --git a/docs/.vuepress/components/Wow.vue b/docs/.vuepress/components/Wow.vue deleted file mode 100644 index 403fb541e4b..00000000000 --- a/docs/.vuepress/components/Wow.vue +++ /dev/null @@ -1,188 +0,0 @@ - - - - - \ No newline at end of file diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js deleted file mode 100644 index cbf21f5a337..00000000000 --- a/docs/.vuepress/config.js +++ /dev/null @@ -1,313 +0,0 @@ -module.exports = { - title: 'MassTransit', - description: 'A free, open-source distributed application framework for .NET.', - head: [ - ['link', { rel: "apple-touch-icon", sizes: "180x180", href: "/apple-touch-icon.png"}], - ['link', { rel: "icon", type: "image/png", sizes: "32x32", href: "/favicon-32x32.png"}], - ['link', { rel: "icon", type: "image/png", sizes: "16x16", href: "/favicon-16x16.png"}], - ['link', { rel: "manifest", href: "/site.webmanifest"}], - ['link', { rel: "mask-icon", href: "/safari-pinned-tab.svg", color: "#3a0839"}], - ['link', { rel: "shortcut icon", href: "/favicon.ico"}], - ['meta', { name: "msapplication-TileColor", content: "#3a0839"}], - ['meta', { name: "msapplication-config", content: "/browserconfig.xml"}], - ['meta', { name: "theme-color", content: "#ffffff"}], - ['meta', { name: "viewport", content: "width=device-width, initial-scale=1"}], - ], - plugins: [ - '@vuepress/back-to-top', - [ - '@vuepress/google-analytics', { - 'ga': 'UA-156512132-1' - } - ] - ], - themeConfig: { - logo: '/mt-logo-small.png', - algolia: { - apiKey: 'e458b7be70837c0e85b6b229c4e26664', - indexName: 'masstransit' - }, - nav: [ - { text: "Discord", link: "/discord" }, - { text: 'NuGet', link: 'https://nuget.org/packages/MassTransit' } - ], - sidebarDepth: 1, - sidebar: [ - { - title: 'Getting Started', - path: '/getting-started/', - collapsable: false, - children: [ - { - title: 'Quick Starts', - path: '/quick-starts/', - collapsable: true, - children: [ - '/quick-starts/in-memory', - '/quick-starts/rabbitmq', - '/quick-starts/azure-service-bus', - '/quick-starts/sqs' - ] - }, - '/getting-started/upgrade-v6', - { - title: 'Release Notes', - path: '/releases/', - collapsable: true, - children: [ - '/releases/v8.0.0', - '/releases/v7.2.3', - '/releases/v7.2.0', - '/releases/v7.1.8', - '/releases/v7.1.7', - '/releases/v7.1.6', - '/releases/v7.1.5', - '/releases/v7.1.4', - '/releases/v7.1.3', - '/releases/v7.1.1', - '/releases/v7.1.0', - '/releases/v7.0.7', - '/releases/v7.0.6', - '/releases/v7.0.4' - ] - } - ] - }, - { - title: 'Using MassTransit', - path: '/usage/', - collapsable: false, - children: [ - '/usage/templates', - '/usage/configuration', - { - title: 'Transports', - path: '/usage/transports/', - collapsable: true, - children: [ - '/usage/transports/rabbitmq', - '/usage/transports/azure-sb', - '/usage/transports/activemq', - '/usage/transports/amazonsqs', - '/usage/transports/grpc', - '/usage/transports/in-memory' - ] - }, - { - title: 'Riders', - path: '/usage/riders/', - collapsable: true, - children: [ - '/usage/riders/kafka', - '/usage/riders/eventhub' - ] - }, - { - title: 'FaaS', - path: '/usage/faas/', - collapsable: true, - children: [ - '/usage/faas/azure-functions', - '/usage/faas/aws-lambda' - ] - }, - '/usage/guidance', - '/usage/mediator', - '/usage/messages', - '/usage/consumers', - '/usage/producers', - '/usage/exceptions', - '/usage/requests', - { - title: 'Sagas', - path: '/usage/sagas/', - collapsable: true, - children: [ - '/usage/sagas/automatonymous', - '/usage/sagas/consumer-saga', - { - title: 'Persistence', - path: '/usage/sagas/persistence', - collapsable: false, - children: [ - '/usage/sagas/azure-table', - '/usage/sagas/cosmos', - '/usage/sagas/dapper', - '/usage/sagas/ef', - '/usage/sagas/efcore', - '/usage/sagas/marten', - '/usage/sagas/mongodb', - '/usage/sagas/nhibernate', - '/usage/sagas/redis', - '/usage/sagas/session' - ] - } - ] - }, - { - title: 'Containers', - path: '/usage/containers/', - collapsable: true, - children: [ - ['/usage/containers/definitions', 'Definitions'], - ['/usage/containers/msdi', 'Microsoft'], - '/usage/containers/multibus' - ] - }, - ['/usage/testing', 'Testing'], - ['/usage/logging', 'Logging'], - { - title: 'Advanced', - collapsable: true, - sidebarDepth: 2, - children: [ - { - title: 'Scheduling', - path: '/advanced/scheduling/', - collapsable: true, - children: [ - '/advanced/scheduling/scheduling-api', - '/advanced/scheduling/activemq-delayed', - '/advanced/scheduling/amazonsqs-scheduler', - '/advanced/scheduling/azure-sb-scheduler', - '/advanced/scheduling/rabbitmq-delayed', - '/advanced/scheduling/hangfire' - ] - }, - { - title: 'Courier', - path: '/advanced/courier/', - collapsable: true, - children: [ - '/advanced/courier/activities', - '/advanced/courier/builder', - '/advanced/courier/execute', - '/advanced/courier/events', - '/advanced/courier/subscriptions' - ] - }, - { - title: 'Middleware', - path: '/advanced/middleware/', - collapsable: true, - children: [ - '/advanced/middleware/receive', - '/advanced/middleware/killswitch', - '/advanced/middleware/circuit-breaker', - '/advanced/middleware/rate-limiter', - '/advanced/middleware/transactions', - '/advanced/middleware/custom', - '/advanced/middleware/scoped' - ] - }, - '/advanced/transactional-outbox', - '/usage/message-data', - { - title: 'Monitoring', - collapsable: true, - children: [ - '/advanced/monitoring/diagnostic-source', - '/advanced/monitoring/prometheus', - '/advanced/monitoring/applications-insights', - '/advanced/monitoring/perfcounters' - ] - }, - '/advanced/connect-endpoint', - '/advanced/observers', - { - title: 'Topology', - path: '/advanced/topology/', - collapsable: true, - children: [ - '/advanced/topology/message', - '/advanced/topology/publish', - '/advanced/topology/send', - '/advanced/topology/consume', - '/advanced/topology/conventions', - '/advanced/topology/rabbitmq', - '/advanced/topology/servicebus', - '/advanced/topology/deploy' - ] - }, - { - title: 'SignalR', - path: '/advanced/signalr/', - collapsable: true, - children: [ - '/advanced/signalr/quickstart', - '/advanced/signalr/hub_endpoints', - '/advanced/signalr/interop', - '/advanced/signalr/sample', - '/advanced/signalr/considerations' - ] - }, - 'advanced/audit', - 'advanced/batching', - 'advanced/job-consumers' - ] - } - ] - }, - { - title: 'Support', - path: '/support' - }, - { - title: 'Getting Help', - path: '/learn/', - collapsable: true, - children: [ - '/troubleshooting/common-gotchas', - '/troubleshooting/show-config', - '/learn/analyzers', - '/learn/samples', - '/learn/videos', - '/learn/training', - '/learn/loving-the-community', - '/learn/contributing', - '/getting-started/live-coding' - ] - }, - { - title: 'Articles', - collapsable: true, - children: [ - '/articles/mediator', - '/articles/outbox', - '/articles/durable-futures', - '/articles/net5' - ] - }, - { - title: "Platform", - path: '/platform/', - collapsable: true, - children: [ - '/platform/configuration' - ] - }, - { - title: "Reference", - children: [ - '/architecture/packages', - '/architecture/interoperability', - '/architecture/nservicebus', - '/architecture/versioning', - '/architecture/newid', - '/architecture/encrypted-messages', - '/architecture/green-cache', - '/architecture/history' - ] - } - ], - searchPlaceholder: 'Search...', - lastUpdated: 'Last Updated', - repo: 'MassTransit/MassTransit', - - docsRepo: 'MassTransit/MassTransit', - docsDir: 'docs', - docsBranch: 'develop', - editLinks: true, - editLinkText: 'Help us by improving this page!' - } -} diff --git a/docs/.vuepress/public/ReceivePipeline.png b/docs/.vuepress/public/ReceivePipeline.png deleted file mode 100644 index 9e9049b6301..00000000000 Binary files a/docs/.vuepress/public/ReceivePipeline.png and /dev/null differ diff --git a/docs/.vuepress/public/android-chrome-192x192.png b/docs/.vuepress/public/android-chrome-192x192.png deleted file mode 100644 index 840139092a2..00000000000 Binary files a/docs/.vuepress/public/android-chrome-192x192.png and /dev/null differ diff --git a/docs/.vuepress/public/android-chrome-512x512.png b/docs/.vuepress/public/android-chrome-512x512.png deleted file mode 100644 index 01a5dd60e0d..00000000000 Binary files a/docs/.vuepress/public/android-chrome-512x512.png and /dev/null differ diff --git a/docs/.vuepress/public/apple-touch-icon.png b/docs/.vuepress/public/apple-touch-icon.png deleted file mode 100644 index bb4c385fd89..00000000000 Binary files a/docs/.vuepress/public/apple-touch-icon.png and /dev/null differ diff --git a/docs/.vuepress/public/azure-topology.png b/docs/.vuepress/public/azure-topology.png deleted file mode 100644 index e73eba7ad9b..00000000000 Binary files a/docs/.vuepress/public/azure-topology.png and /dev/null differ diff --git a/docs/.vuepress/public/browserconfig.xml b/docs/.vuepress/public/browserconfig.xml deleted file mode 100644 index b3930d0f047..00000000000 --- a/docs/.vuepress/public/browserconfig.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - #da532c - - - diff --git a/docs/.vuepress/public/consumer-inbox.png b/docs/.vuepress/public/consumer-inbox.png deleted file mode 100644 index e83e5311302..00000000000 Binary files a/docs/.vuepress/public/consumer-inbox.png and /dev/null differ diff --git a/docs/.vuepress/public/consumer-regular.png b/docs/.vuepress/public/consumer-regular.png deleted file mode 100644 index d8c649430aa..00000000000 Binary files a/docs/.vuepress/public/consumer-regular.png and /dev/null differ diff --git a/docs/.vuepress/public/favicon-16x16.png b/docs/.vuepress/public/favicon-16x16.png deleted file mode 100644 index 9e83bd92898..00000000000 Binary files a/docs/.vuepress/public/favicon-16x16.png and /dev/null differ diff --git a/docs/.vuepress/public/favicon-32x32.png b/docs/.vuepress/public/favicon-32x32.png deleted file mode 100644 index 03d709ecc96..00000000000 Binary files a/docs/.vuepress/public/favicon-32x32.png and /dev/null differ diff --git a/docs/.vuepress/public/favicon.ico b/docs/.vuepress/public/favicon.ico deleted file mode 100644 index 5fb575bcba3..00000000000 Binary files a/docs/.vuepress/public/favicon.ico and /dev/null differ diff --git a/docs/.vuepress/public/improving-small.png b/docs/.vuepress/public/improving-small.png deleted file mode 100644 index 66168e92ee7..00000000000 Binary files a/docs/.vuepress/public/improving-small.png and /dev/null differ diff --git a/docs/.vuepress/public/improving.png b/docs/.vuepress/public/improving.png deleted file mode 100644 index 41bb6431ed4..00000000000 Binary files a/docs/.vuepress/public/improving.png and /dev/null differ diff --git a/docs/.vuepress/public/inbox-outbox-broker.png b/docs/.vuepress/public/inbox-outbox-broker.png deleted file mode 100644 index d5adecc792d..00000000000 Binary files a/docs/.vuepress/public/inbox-outbox-broker.png and /dev/null differ diff --git a/docs/.vuepress/public/inbox-outbox.png b/docs/.vuepress/public/inbox-outbox.png deleted file mode 100644 index d44d6d1d3be..00000000000 Binary files a/docs/.vuepress/public/inbox-outbox.png and /dev/null differ diff --git a/docs/.vuepress/public/mstile-150x150.png b/docs/.vuepress/public/mstile-150x150.png deleted file mode 100644 index eddd8b5f7c8..00000000000 Binary files a/docs/.vuepress/public/mstile-150x150.png and /dev/null differ diff --git a/docs/.vuepress/public/mt-logo-color.png b/docs/.vuepress/public/mt-logo-color.png deleted file mode 100644 index a59539b6ceb..00000000000 Binary files a/docs/.vuepress/public/mt-logo-color.png and /dev/null differ diff --git a/docs/.vuepress/public/mt-logo-small.png b/docs/.vuepress/public/mt-logo-small.png deleted file mode 100644 index 282eb322001..00000000000 Binary files a/docs/.vuepress/public/mt-logo-small.png and /dev/null differ diff --git a/docs/.vuepress/public/outbox-to-broker.png b/docs/.vuepress/public/outbox-to-broker.png deleted file mode 100644 index e796b415438..00000000000 Binary files a/docs/.vuepress/public/outbox-to-broker.png and /dev/null differ diff --git a/docs/.vuepress/public/rabbitmq-publish-topology.png b/docs/.vuepress/public/rabbitmq-publish-topology.png deleted file mode 100644 index db96b669bb7..00000000000 Binary files a/docs/.vuepress/public/rabbitmq-publish-topology.png and /dev/null differ diff --git a/docs/.vuepress/public/rabbitmq-send-topology.png b/docs/.vuepress/public/rabbitmq-send-topology.png deleted file mode 100644 index b5d3e89d501..00000000000 Binary files a/docs/.vuepress/public/rabbitmq-send-topology.png and /dev/null differ diff --git a/docs/.vuepress/public/register.png b/docs/.vuepress/public/register.png deleted file mode 100644 index 99487dbc33f..00000000000 Binary files a/docs/.vuepress/public/register.png and /dev/null differ diff --git a/docs/.vuepress/public/requestResponse.svg b/docs/.vuepress/public/requestResponse.svg deleted file mode 100644 index 74254041539..00000000000 --- a/docs/.vuepress/public/requestResponse.svg +++ /dev/null @@ -1 +0,0 @@ -Request / ResponseClient AHTTP EndpointClient QueueService QueueService EndpointSend RequestConnectionused for theresponseSend ResponseSend RequestConsume RequestResponseAddressheader used todeliver responseSend ResponseConsume ResponseRequestIdcorrelates torequest clientawait httpClient.PostAsyncawait requestClient.GetResponse \ No newline at end of file diff --git a/docs/.vuepress/public/safari-pinned-tab.svg b/docs/.vuepress/public/safari-pinned-tab.svg deleted file mode 100644 index ef890888105..00000000000 --- a/docs/.vuepress/public/safari-pinned-tab.svg +++ /dev/null @@ -1,344 +0,0 @@ - - - - -Created by potrace 1.11, written by Peter Selinger 2001-2013 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/.vuepress/public/site.webmanifest b/docs/.vuepress/public/site.webmanifest deleted file mode 100644 index b20abb7cbb2..00000000000 --- a/docs/.vuepress/public/site.webmanifest +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "", - "short_name": "", - "icons": [ - { - "src": "/android-chrome-192x192.png", - "sizes": "192x192", - "type": "image/png" - }, - { - "src": "/android-chrome-512x512.png", - "sizes": "512x512", - "type": "image/png" - } - ], - "theme_color": "#ffffff", - "background_color": "#ffffff", - "display": "standalone" -} diff --git a/docs/.vuepress/public/write-to-broker.png b/docs/.vuepress/public/write-to-broker.png deleted file mode 100644 index c154a76c3e6..00000000000 Binary files a/docs/.vuepress/public/write-to-broker.png and /dev/null differ diff --git a/docs/.vuepress/public/write-to-outbox.png b/docs/.vuepress/public/write-to-outbox.png deleted file mode 100644 index d94cf91e37f..00000000000 Binary files a/docs/.vuepress/public/write-to-outbox.png and /dev/null differ diff --git a/docs/.vuepress/styles/palette.styl b/docs/.vuepress/styles/palette.styl deleted file mode 100644 index dca2df3a57c..00000000000 --- a/docs/.vuepress/styles/palette.styl +++ /dev/null @@ -1 +0,0 @@ -$accentColor = #0ea5e9 \ No newline at end of file diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 65e218c3bb2..00000000000 --- a/docs/README.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -# home: true -layout: Wow -heroImage: /mt-logo-color.png -heroText: MassTransit -tagline: A free, open-source distributed application framework for .NET -actionText: Get Started → -actionLink: /getting-started/ -transports: -- text: RabbitMQ - link: /quick-starts/rabbitmq - description: RabbitMQ is a high performance, highly available freely available open source message broker -- text: Azure Service Bus - link: /quick-starts/azure-service-bus - description: Want to play in the Azure Cloud, use this transport to keep everything in Azure -- text: SQS - link: /quick-starts/sqs - description: Prefer the AWS cloud? Utilize the Serverless SNS + SQS model -features: -- title: Simple yet Sophisticated - details: Easy to use and understand, allowing you to focus on solving business problems -- title: Transport Liquidity - details: Deploy using RabbitMQ, Azure Service Bus, ActiveMQ, and Amazon SQS/SNS without having to rewrite it -- title: Powerful Message Patterns - details: Consumers, sagas, state machines, and choreography-based distributed transactions with compensation -- title: End-to-End Solution - details: Handles message serialization, headers, and routing, broker topology, exceptions, retries, concurrency, connection and consumer lifecycle management -- title: Unit Testable - details: Fast in-memory test harness to simplify unit testing, including sent, published, and consumed message observers -- title: Monitoring - details: Modern support for distributed tracing, service health and liveliness checks -footer: Apache 2.0 Licensed | Copyright © 2007-2022 Chris Patterson ---- - -MassTransit is an open-source distributed application framework for .NET. MassTransit makes it easy to create applications and services that leverage message-based, loosely-coupled asynchronous communication for higher availability, reliability, and scalability. - -MassTransit works with several well-supported message transports and provides an [extensive set of developer-friendly features](usage/transports) to build durable asynchronous services. diff --git a/docs/advanced/README.md b/docs/advanced/README.md deleted file mode 100644 index c072d1703e2..00000000000 --- a/docs/advanced/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Advanced Topics - -* [Sagas](sagas/README.md) - * [Automatonymous](sagas/automatonymous.md) - * [Persistence](sagas/persistence.md) -* [Courier](courier/README.md) - * [Activities](courier/activities.md) - * [Builder](courier/builder.md) - * [Execution](courier/execute.md) - * [Events](courier/events.md) - * [Subscriptions](courier/subscriptions.md) -* [Middleware](middleware/README.md) - * [Receive Pipeline](middleware/receive.md) - * [Circuit breaker](middleware/circuit-breaker.md) - * [Rate limiter](middleware/rate-limiter.md) - * [Latest](middleware/latest.md) - * [Custom](middleware/custom.md) -* [Topology](topology/README.md) -* [Transactions](transactions.md) -* [Interoperability](interoperability.md) - * [Encrypted Messages](interoperability/encrypted-messages.md) -* [SignalR](signalr/README.md) - * [Quick Start](signalr/quickstart.md) - * [Hub Endpoints](signalr/hub_endpoints.md) - * [Interop](signalr/interop.md) - * [Sample](signalr/sample.md) - * [Considerations](signalr/considerations.md) \ No newline at end of file diff --git a/docs/advanced/audit.md b/docs/advanced/audit.md deleted file mode 100644 index 18d91567ab5..00000000000 --- a/docs/advanced/audit.md +++ /dev/null @@ -1,119 +0,0 @@ -# Message Audit - -Due to asynchronous nature of messaging, it is not always easy to find out the message flow. -Step-into end-to-end debugging is almost impossible to use, especially if message processing -is done in parallel and consumers perform atomic operations. - -To enable better diagnostic and troubleshooting, the audit log, which contains all messages -that have been sent and consumed, would provide a great help. - -Also, certain systems require keeping the full log of operations, and if all operations are -done using messages, storing these messages will satisfy such requirement. - -MassTransit supports message audit by using special observers. - -## Principles - -Two main parts need to be saved for each message to provide complete audit: -* The message itself -* Metadata - -Message metadata includes: -* Message id -* Message type -* Context type (Send, Publish or Consume) -* Conversation id -* Correlation id -* Initiator id -* Request id (for request/response) -* Source address -* Destination address -* Response address (for request/response) -* Fault address - -The audit feature is generic and requires an implementation for the `IMessageAuditStore` interface. -This interface is very simple and has one method only: -```csharp -Task StoreMessage( message, MessageAuditMetadata metadata); -``` - -Some audit store implementations are included out of the box and described below. - -There are three observers that connect to the message pipeline and record them. -Two are for messages being sent - send and publish observers; one is for messages that -are being consumed. - -Consume observer is invoked before a message is consumed, so the message is stored to the audit -store even if a consumer fails. Send and publish observers are invoked after the message -has actually been sent. - -## Installation - -There are two extensions methods for `IBusControl` that enable sent and consumed messages audit. -Configuring both looks like this: - -```csharp -var busControl = ConfigureBus(); // all usual configuration -busControl.ConnectSendAuditObservers(auditStore); -busControl.ConnectConsumeAuditObserver(auditStore); -``` - -There, the `auditStore` is the audit persistent store. Currently available stores include: -* [Entity Framework](../usage/sagas/ef.md) -* [Entity Framework Core](../usage/sagas/efcore.md) -* [Azure Tables](../usage/audit/azuretable.md) - -Please remember that observers need to be configured before the bus starts. - -## Filters - -Sometimes there are messages in the system that are technical. These could be some monitoring and -health check messages, which are being sent often and could pollute the audit log. Another example -could be the `Fault` events. - -In order not to save these messages to the audit store, you can filter them out. You can configure -the observers to use message filters like this: - -```csharp -busControl.ConnectSendAuditObservers(auditStore, - c => c.Ignore()); -busControl.ConnectConsumeAuditObserver(auditStore, - c => c.Ignore(typeof(ServicePoll), typeof(RubbishEvent))); -``` - -## Metadata factory - -By default, the audit logging feature uses its own, quite complete, metadata collection mechanism. -However, you can implement your own metadata factories to collect more data or different data. - -There are two types of metadata factories: -* `DefaultConsumeMetadataFactory` that gets the `ConsumeObserver` -* `DefaultSendMetadataFactory` that gets the `SendObserver` (which is used for both send and publish) - -Factories are simple classes that implement `IConsumeMetadataFactory` or `ISendMetadataFactory` -interfaces and return a new `MessageAuditMetadata` object from a given context. -For example, the default consume audit metadata factory implementation looks like this: - -```csharp -return new MessageAuditMetadata -{ - ContextType = contextType, - ConversationId = context.ConversationId, - CorrelationId = context.CorrelationId, - InitiatorId = context.InitiatorId, - MessageId = context.MessageId, - RequestId = context.RequestId, - DestinationAddress = context.DestinationAddress?.AbsoluteUri, - SourceAddress = context.SourceAddress?.AbsoluteUri, - FaultAddress = context.FaultAddress?.AbsoluteUri, - ResponseAddress = context.ResponseAddress?.AbsoluteUri, - Headers = context.Headers?.GetAll()?.ToDictionary(k => k.Key, v => v.Value.ToString()) -}; -``` - -To use your own factory, you can use the third parameter when configuring the observers: - -```csharp -busControl.ConnectSendAuditObservers(auditStore, filter, new MySendContextMetadataFactory()); -busControl.ConnectConsumeAuditObserver(auditStore, filter, new MySendContextMetadataFactory()); -``` diff --git a/docs/advanced/batching.md b/docs/advanced/batching.md deleted file mode 100644 index c8e9553d0bb..00000000000 --- a/docs/advanced/batching.md +++ /dev/null @@ -1,33 +0,0 @@ -# Batching - -In some scenarios, high message volume can lead to consumer resource bottlenecks. If a system is publishing thousands of messages per second, and has a consumer that is writing the content of those messages to some type of storage, the storage system might not be optimized for thousands of individual writes per second. It may, however, perform better if writes are performed in batches. For example, receiving one hundred messages and then writing the content of those messages using a single storage operation may be significantly more efficient (and faster). - -MassTransit supports receiving messages and delivering those messages to a consumer as a `Batch`. - -To create a batch consumer, consume the `Batch` interface, where `T` is the message type. That consumer can then be configured using the container integration, with the batch options specified in a consumer definition. The example below consumes a batch of _OrderAudit_ events, up to 100 at a time, and up to 10 concurrent batches. - -<<< @/docs/code/advanced/BatchingConsumer.cs - -Once the consumer has been created, configure the consumer on a receive endpoint (in this case, using the default convention). - -<<< @/docs/code/advanced/BatchingConsumerBus.cs - -If automatic receive endpoint configuration is not used, the receive endpoint can be configured explicitly. - -<<< @/docs/code/advanced/BatchingConsumerExplicit.cs - -::: warning PrefetchCount -Every transport has its own limitations that may constrain the batch size. For instance, Amazon SQS fetches ten messages at a time, making it an optimal batch size. It is best to experiment and see what works best in your environment. - -If the _PrefetchCount_ is lower than the batch limit, performance will be limited by the time limit as the batch size will never be reached. -::: - -For instance, when using Azure Service Bus, there are two settings which must be configured as shown below. - -<<< @/docs/code/advanced/BatchingConsumerAzure.cs - -### Batch Interface - -The `Batch` interface, shown below, also includes the first message receipt time, the last message receipt time, and the completion mode of the batch (message limit or time limit was reached). - -<<< @/src/MassTransit.Abstractions/Contracts/Batch.cs diff --git a/docs/advanced/connect-endpoint.md b/docs/advanced/connect-endpoint.md deleted file mode 100644 index 7c9642c5147..00000000000 --- a/docs/advanced/connect-endpoint.md +++ /dev/null @@ -1,33 +0,0 @@ -# Connect Endpoint - -Receive endpoints should be configured with the bus, so that they are all started at the same time. In some situations, however, a receive endpoint may be needed after the bus has been started. For instance, a request client may want to use a separate queue from responses instead of using the bus's queue. Or it may be that a new consumer is now available, and needs to be connected to the bus. - -To connect a new receive endpoint to the bus, use the `ConnectReceiveEndpoint` method. - -```cs -var handle = bus.ConnectReceiveEndpoint("secondary-queue", x => -{ - x.Consumer(); -}) - -// wait for the receive endpoint to be ready, throws an exception if a fault occurs -var ready = await handle.Ready; -``` - -When connecting a receive endpoint, consumers will configure the broker topology so that messages types are subscribed to topics/exchanges. If the receive endpoint is temporary, the configuration should be skipped, and the `ConnectConsumer` style methods should be used after the receive endpoint is ready. - -### Disconnect an Endpoint - -Connected endpoints will be stopped when the bus is stopped. If the endpoint needs to be stopped before the bus, the handle can be used. - -```csharp -await handle.StopAsync(); -``` - -### Container Integration - -When using `AddMassTransit` with your dependency injection container of choice, receive endpoints can be connected using the `IReceiveEndpointConnector` interface. When using this interface, consumers which were added during configuration can be configured on the receive endpoint. - -To connect a receive endpoint and configure a consumer: - -<<< @/docs/code/containers/MicrosoftConnect.cs diff --git a/docs/advanced/courier/README.md b/docs/advanced/courier/README.md deleted file mode 100644 index 9cd7eba1c9f..00000000000 --- a/docs/advanced/courier/README.md +++ /dev/null @@ -1,27 +0,0 @@ -# Using Courier - -Developing applications using a distributed, message-based architecture significantly increases the complexity of performing transactions, where an end-to-end set of steps must be completed entirely, or not at all. In an application using an ACID database, this is typically done using SQL transactions, where partial operations are rolled back if the transaction cannot be completed. However, this doesn't scale when the steps being to include dependencies outside of a single database. And in the distributed, *microservices* based architectures, the use of a single ACID database is shrinking to completely non-existent. - -MassTransit Courier is a mechanism for creating and executing distributed transactions with fault compensation that can be used to meet the requirements previously within the domain of database transactions, but built to scale across a large system of distributed services. Courier also works well with MassTransit sagas, which add transaction monitoring and recoverability. - -## Using a Routing Slip - -A routing slip specifies a sequence of processing steps called *activities* that are combined into a single transaction. As each activity completes, the routing slip is forwarded to the next activity in the itinerary. When all activities have completed, the routing slip is completed and the transaction is complete. - -A key advantage to using a routing slip is it allows the activities to vary for each transaction. Depending upon the requirements for each transaction, which may differ based on things like payment methods, billing or shipping address, or customer preference ratings, the routing slip builder can selectively add activities to the routing slip. This dynamic behavior is in contrast to a more explicit behavior defined by a state machine or sequential workflow that is statically defined (either through the use of code, a DSL, or something like Windows Workflow). - -## MassTransit Courier - -MassTransit Courier is a framework that implements the routing slip pattern. Leveraging a durable messaging transport and the advanced saga features of MassTransit, Courier provides a powerful set of components to simplify the use of routing slips in distributed applications. Combining the routing slip pattern with a state machine such as [Automatonymous][2]results in a reliable, recoverable, and supportable approach for coordinating and monitoring message processing across multiple services. - -In addition to the basic routing slip pattern, MassTransit Courier also supports [compensations][1] which allow activities to store execution data so that reversible operations can be undone, using either a traditional rollback mechanism or by applying an offsetting operation. For example, an activity that holds a seat for a patron could release the held seat when compensated. - -* [Activities](activities) -* [Builder](builder) -* [Execution](execute) -* [Events](events) -* [Subscriptions](subscriptions) - - -[1]: http://en.wikipedia.org/wiki/Compensation_%28engineering%29 -[2]: https://github.com/MassTransit/Automatonymous \ No newline at end of file diff --git a/docs/advanced/courier/activities.md b/docs/advanced/courier/activities.md deleted file mode 100644 index 842427b3bfd..00000000000 --- a/docs/advanced/courier/activities.md +++ /dev/null @@ -1,89 +0,0 @@ -# Activities - -In MassTransit Courier, an *Activity* refers to a processing step that can be added to a routing slip. - -To create an activity, create a class that implements the *IActivity* interface. - -```csharp -public class DownloadImageActivity : - IActivity -{ - Task Execute(ExecuteContext context); - Task Compensate(CompensateContext context); -} -``` - -The *IActivity* interface is generic with two arguments. The first parameter specifies the activity’s argument type and the second parameter specifies the activity’s log type. In the example shown above, *DownloadImageArguments* is the argument type and *DownloadImageLog* is the log type. Both parameters may be interface, class or record types. Where the type is a class or a record, the proper accessors should be specified (i.e. `{ get; set; }` or `{ get; init; }`). - -## Execute Activities - -An *Execute Activity* is an activity that only executes and does not support compensation. As such, the declaration of a log type is not required. - -```csharp -public class ValidateImageActivity : - IExecuteActivity -{ - Task Execute(ExecuteContext context); -} -``` - -## Implementing an activity - -An activity must implement two interface methods, *Execute* and *Compensate*. The *Execute* method is called while the routing slip is executing activities and the *Compensate* method is called when a routing slip faults and needs to be compensated. - -When the *Execute* method is called, an *execution* argument is passed containing the activity arguments, the routing slip *TrackingNumber*, and methods to mark the activity as completed or faulted. The actual routing slip message, as well as any details of the underlying infrastructure, are excluded from the *execution* argument to prevent coupling between the activity and the implementation. An example *Execute* method is shown below. - -```csharp -async Task Execute(ExecuteContext execution) -{ - DownloadImageArguments args = execution.Arguments; - string imageSavePath = Path.Combine(args.WorkPath, - execution.TrackingNumber.ToString()); - - await _httpClient.GetAndSave(args.ImageUri, imageSavePath); - - return execution.Completed(new {ImageSavePath = imageSavePath}); -} -``` - -## Completing an activity - -Once activity processing is complete, the activity returns an *ExecutionResult* to the host. If the activity executes successfully, the activity can elect to store compensation data in an activity log which is passed to the *Completed* method on the *execution* argument. If the activity chooses not to store any compensation data, the activity log argument is not required. In addition to compensation data, the activity can add or modify variables stored in the routing slip for use by subsequent activities. - -> In the example above, the activity specifies the *DownloadImageLog* interface and initializes the log using an anonymous object. The object is then passed to the *Completed* method for storage in the routing slip before sending the routing slip to the next activity. - -## Terminating a routing slip - -In some situations, it may make sense to terminate the routing slip without executing any of the subsequent activities in the itinerary. This might be due to a business rule, in which the routing slip shouldn't be faulted, but needs to end immediately. - -To terminate a routing slip, call _Terminate_ as shown. - -```csharp -// regular termination -return execution.Terminate(); - -// terminate and include additional variables in the event -return execution.Terminate(new { Reason = "Not a good time, dude."}); -``` - -## Faulting a routing slip - -By default, if an activity throws an exception, it will be _faulted_ and a `RoutingSlipFaulted` event will be published (unless a subscription changes the rules). An activity can also return _Faulted_ rather than throwing an exception. - -## Compensating an activity - -When an activity fails, the *Compensate* method is called for previously executed activities in the routing slip that stored compensation data. If an activity does not store any compensation data, the *Compensate* method is never called. The compensation method for the example above is shown below. - -```csharp -Task Compensate(CompensateContext compensation) -{ - DownloadImageLog log = compensation.Log; - File.Delete(log.ImageSavePath); - - return compensation.Compensated(); -} -``` - -Using the activity log data, the activity compensates by removing the downloaded image from the work directory. Once the activity has compensated the previous execution, it returns a *CompensationResult* by calling the *Compensated* method. If the compensating actions could not be performed (either via logic or an exception) and the inability to compensate results in a failure state, the *Failed* method can be used instead, optionally specifying an *Exception*. - - diff --git a/docs/advanced/courier/builder.md b/docs/advanced/courier/builder.md deleted file mode 100644 index 9595c047a5a..00000000000 --- a/docs/advanced/courier/builder.md +++ /dev/null @@ -1,89 +0,0 @@ -# Building a routing slip - -A routing slip contains an itinerary, variables, and activity/compensation logs. It is defined by a message contract, which is used by the underlying Courier components to execute and compensate the transaction. The routing slip contract includes: - -- A tracking number, which should be unique for each routing slip -- An itinerary, which is an ordered list of activities -- An activity log, containing an ordered list of previously executed activities -- A compensation log, containing an order list of previous executed activities which may be compensated if the routing slip faults -- A collection of variables, which can be mapped to activity arguments -- A collection of subscriptions, which can be added to notify consumers of routing slip events -- A collection of exceptions which may have occurred during routing slip execution - -Developers are discouraged from directly implementing the *RoutingSlip* message type and should instead use a *RoutingSlipBuilder* to create a routing slip. The *RoutingSlipBuilder* encapsulates the creation of the routing slip and includes methods to add activities (and their arguments), activity logs, and variables to the routing slip. For example, to create a routing slip with two activities and an additional variable, a developer would write: - -```csharp -var builder = new RoutingSlipBuilder(NewId.NextGuid()); -builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage"), - new - { - ImageUri = new Uri("http://images.google.com/someImage.jpg") - }); -builder.AddActivity("FilterImage", new Uri("rabbitmq://localhost/execute_filterimage")); -builder.AddVariable("WorkPath", @"\dfs\work"); - -var routingSlip = builder.Build(); -``` - -Each activity requires a name for display purposes and a URI specifying the execution address. The execution address is where the routing slip should be sent to execute the activity. For each activity, arguments can be specified that are stored and presented to the activity via the activity arguments interface type specify by the first argument of the *IActivity* interface. The activities added to the routing slip are combined into an *Itinerary*, which is the list of activities to be executed, and stored in the routing slip. - -> Managing the inventory of available activities, as well as their names and execution addresses, is the responsibility of the application and is not part of the MassTransit Courier. Since activities are application specific, and the business logic to determine which activities to execute and in what order is part of the application domain, the details are left to the application developer. - -## Activity Arguments - -Each activity declares an activity argument type, which must be an interface. When the routing slip is received by an activity host, the argument type is used to read data from the routing slip and deliver it to the activity. - -The argument properties are mapped, by name, to the argument type from the routing slip using: - -- Explicitly declared arguments, added to the itinerary with the activity -- Implicitly mapped arguments, added as variables to the routing slip - -To specify an explicit activity argument, specify the argument value while adding the activity using the routing slip builder. - -```csharp -var builder = new RoutingSlipBuilder(NewId.NextGuid()); -builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage"), new - { - ImageUri = new Uri("http://images.google.com/someImage.jpg") - }); -``` - -To specify an implicit activity argument, add a variable to the routing slip with the same name/type as the activity argument. - -```csharp -var builder = new RoutingSlipBuilder(NewId.NextGuid()); -builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage")); -builder.AddVariable("ImageUri", "http://images.google.com/someImage.jpg"); -``` - -If an activity argument is not specified when the routing slip is created, it may be added by an activity that executes prior to the activity that requires the argument. For instance, if the _DownloadImage_ activity stored the image in a local cache, that address could be added and used by another activity to access the cached image. - -First, the routing slip would be built without the argument value. - -```csharp -var builder = new RoutingSlipBuilder(NewId.NextGuid()); -builder.AddActivity("DownloadImage", new Uri("rabbitmq://localhost/execute_downloadimage")); -builder.AddActivity("ProcessImage", new Uri("rabbitmq://localhost/execute_processimage")); -builder.AddVariable("ImageUri", "http://images.google.com/someImage.jpg"); -``` - -Then, the first activity would add the variable to the routing slip on completion. - -```csharp -async Task Execute(ExecuteContext context) -{ - ... - return context.CompletedWithVariables(new { ImagePath = ...}); -} -``` - -The process image activity would then use that variable as an argument value. - -```csharp -async Task Execute(ExecuteContext context) -{ - var path = context.Arguments.ImagePath; -} -``` - - diff --git a/docs/advanced/courier/events.md b/docs/advanced/courier/events.md deleted file mode 100644 index 4d31e1af908..00000000000 --- a/docs/advanced/courier/events.md +++ /dev/null @@ -1,17 +0,0 @@ -# Monitoring routing slip execution - -During routing slip execution, events are published when the routing slip completes or faults. Every event message includes the *TrackingNumber* as well as a *Timestamp* (in UTC, of course) indicating when the event occurred: - - * RoutingSlipCompleted - * RoutingSlipFaulted - * RoutingSlipCompensationFailed - -Additional events are published for each activity, including: - - * RoutingSlipActivityCompleted - * RoutingSlipActivityFaulted - * RoutingSlipActivityCompensated - * RoutingSlipActivityCompensationFailed - -By observing these events, an application can monitor and track the state of a routing slip. To maintain the current state, an Automatonymous state machine could be created. To maintain history, events could be stored in a database and then queried using the *TrackingNumber* of the routing slip. - diff --git a/docs/advanced/courier/execute.md b/docs/advanced/courier/execute.md deleted file mode 100644 index 8a5582a599e..00000000000 --- a/docs/advanced/courier/execute.md +++ /dev/null @@ -1,9 +0,0 @@ -# Executing the routing slip - -Once built, the routing slip is executed, which sends it to the first activity’s execute URI. To make it easy and to ensure that source information is included, an extension method on *IBus* is available, the usage of which is shown below. - -```csharp -await bus.Execute(routingSlip); -``` - -It should be pointed out that if the address for the first activity is invalid or cannot be reached, an exception will be thrown by the *Execute* method. diff --git a/docs/advanced/courier/subscriptions.md b/docs/advanced/courier/subscriptions.md deleted file mode 100644 index ff4d879ba40..00000000000 --- a/docs/advanced/courier/subscriptions.md +++ /dev/null @@ -1,59 +0,0 @@ -# Subscriptions - -By default, routing slip events are published -- which means that any subscribed consumers will receive the events. While this is useful getting started, it can quickly get out of control as applications grow and multiple unrelated routing slips are used. To handle this, subscriptions were added (yes, added, because they weren't though of until we experienced this ourselves). - -Subscriptions are added to the routing slip at the time it is built using the `RoutingSlipBuilder`. - -```csharp -builder.AddSubscription(new Uri("rabbitmq://localhost/log-events"), - RoutingSlipEvents.All); -``` - -This subscription would send all routing slip events to the specified endpoint. If the application only wanted specified events, the events can be selected by specifying the enumeration values for those events. For example, to only get the `RoutingSlipCompleted` and `RoutingSlipFaulted` events, the following code would be used. - -```csharp -builder.AddSubscription(new Uri("rabbitmq://localhost/log-events"), - RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); -``` - -It is also possible to tweak the content of the events to cut down on message size. For instance, by default, the `RoutingSlipCompleted` event includes the variables from the routing slip. If the variables contained a large document, that document would be copied to the event. Eliminating the variables from the event would reduce the message size, thereby reducing the traffic on the message broker. To specify the contents of a routing slip event subscription, an additional argument is specified. - -```csharp -builder.AddSubscription(new Uri("rabbitmq://localhost/log-events"), - RoutingSlipEvents.Completed, RoutingSlipEventContents.None); -``` - -This would send the `RoutingSlipCompleted` event to the endpoint, without any of the variables be included (only the main properties of the event would be present). - -> Once a subscription is added to a routing slip, events are no longer published -- they are only sent to the addresses specified in the subscriptions. However, multiple subscriptions can be specified -- the endpoints just need to be known at the time the routing slip is built. - -## Custom events - -It is also possible to specify a subscription with a custom event, a message that is created by the application developer. This makes it possible to create your own event types and publish them in response to routing slip events occurring. And this includes having the full context of a regular endpoint `Send` so that any headers or context settings can be applied. - -To create a custom event subscription, use the overload shown below. - -```csharp -// first, define the event type in your assembly -public record OrderProcessingCompleted -{ - public Guid TrackingNumber { get; init; } - public DateTime Timestamp { get; init; } - - public string OrderId { get; init; } - public string OrderApproval { get; init; } -} - -// then, add the subscription with the custom properties -builder.AddSubscription(new Uri("rabbitmq://localhost/order-events"), - RoutingSlipEvents.Completed, - x => x.Send(new - { - OrderId = "BFG-9000", - OrderApproval = "ComeGetSome" - })); -``` - -In the message contract above, there are four properties, but only two of them are specified. By default, the base `RoutingSlipCompleted` event is created, and then the content of that event is *merged* into the message created in the subscription. This ensures that the dynamic values, such as the `TrackingNumber` and the `Timestamp`, which are present in the default event, are available in the custom event. - -Custom events can also select with contents are merged with the custom event, using an additional method overload. diff --git a/docs/advanced/job-consumers.md b/docs/advanced/job-consumers.md deleted file mode 100644 index 931dd4ebc6d..00000000000 --- a/docs/advanced/job-consumers.md +++ /dev/null @@ -1,58 +0,0 @@ -# Job Consumers - - - -When a message is delivered from the message broker to a consumer instance, the message is _locked_ by the broker. Once the consumer completes, MassTransit will acknowledge the message on the broker, removing it from the queue. While the message is locked, it will not be delivered to another consumer – on any bus instance reading from the same queue (competing consumer pattern). However, if the broker connection is lost the message will be unlocked and redelivered to a new consumer instance. The lock timeout is usually long enough for most message consumers, and this rarely is an issue in practice for consumers that complete quickly. - -However, there are plenty of use cases where consumers may run for a longer duration, from minutes to even hours. In these situations, a job consumer _may_ be used to decouple the consumer from the broker. A job consumer is a specialized consumer designed to execute _jobs_, defined by implementing the `IJobConsumer` interface where `T` is the job message type. Job consumers may be used for long-running tasks, such as converting a video file, but can really be used for any task. Job consumers have additional requirements, such as a database to store the job messages, manage concurrency and retry, and report job completion or failure. - -::: warning -MassTransit includes a job service that keeps track of each job, assigns jobs to service instances, and schedules job retries when necessary. The job service uses three saga state machines and the default configuration uses an in-memory saga repository, which is **not durable**. When using job consumers for production use cases, configuring durable saga repositories is _highly recommended_ to avoid possible message loss. - -Check out the [sample project](https://github.com/MassTransit/Sample-JobConsumers) on GitHub, which includes the Entity Framework configuration for the job service state machines. -::: - -To use job consumers, a _service instance_ must be configured (see below). - -### IJobConsumer - -A job consumer implements the `IJobConsumer` interface, shown below. - -<<< @/src/MassTransit.Abstractions/IJobConsumer.cs - -### Configuration - - -The example below configures a job consumer on a receive endpoint named using an _IEndpointNameFormatter_ passing the consumer type. - -<<< @/docs/code/turnout/JobSystemConsoleService.cs - -In this example, the job timeout as well as the number of concurrent jobs allowed is specified using `JobOptions` when configuring the consumer. The job options can also be specified using a consumer definition in the same way. - -### Client - -To submit jobs to the job consumer, use the service client to create a request client as shown, and send the request. The _JobId_ is assigned the _RequestId_ value. - -<<< @/docs/code/turnout/JobSystemClient.cs - -### Job Service Endpoints - -The job service saga state machines are configured on their own endpoints, using the configured endpoint name formatter. These endpoints are required on _at least one_ bus instance. Additionally, it is not necessary to configure them on _every_ bus instance. In the example above, the job service endpoint are configured. Another method, _ConfigureJobService_, is used to configure the job service without configuring the saga state machine endpoints. In situations where there are many bus instances with job consumers, it is suggested that only one or two instances host the job service endpoints to avoid concurrency issues with the sagas repositories – particularly when optimistic locking is used. - -To configure a service instance without the job service endpoints, replace _ConfigureJobServiceEndpoints_ with _ConfigureJobService_. - -```cs -x.UsingRabbitMq((context, cfg) => -{ - cfg.ServiceInstance(instance => - { - instance.ConfigureJobService(); - - instance.ConfigureEndpoints(context); - }); -}); -``` - -For a more detailed example of configuring the job service endpoints, including persistent storage, see the sample mentioned in the warning box above. diff --git a/docs/advanced/middleware/README.md b/docs/advanced/middleware/README.md deleted file mode 100644 index c699d7afc28..00000000000 --- a/docs/advanced/middleware/README.md +++ /dev/null @@ -1,165 +0,0 @@ -# Middleware - -MassTransit is built using a network of pipes and filters to dispatch messages. A pipe is composed of a series of filters, each of which is a key atom and are described below. - -A detailed view of MassTransit's [Receive Pipeline](receive.md) is a good example of the sophistication possible. - -Middleware components are configured using extension methods on any pipe configurator `IPipeConfigurator`, and the extension methods all begin with `Use` to separate them from other methods. - -To understand how middleware components are built, an understanding of filters and pipes is needed. - -## Filters - -A filter is a middleware component that performs a specific function, and should adhere to the single responsibility principal – do one thing, one thing only (and hopefully do it well). By sticking to this approach, developers are able to opt-in to each behavior without including unnecessary or unwatched functionality. - -There are many filters included with GreenPipes, and a whole lot more of them are included with MassTransit. In fact, the entire MassTransit message flow is built around pipes and filters. - -Developers can create their own filters. To create a filter, create a class that implements `IFilter`. - -```cs -public interface IFilter - where T : class, PipeContext -{ - void Probe(ProbeContext context); - - Task Send(T context, IPipe next); -} -``` - -The _Probe_ method is used to interrogate the filter about its behavior. This should describe the filter in a way that a developer would understand its role when looking at a network graph. For example, a transaction filter may add the following to the context. - -```cs -public void Probe(ProbeContext context) -{ - context.CreateFilterScope("transaction"); -} -``` - -The _Send_ method is used to send contexts through the pipe to each filter. _Context_ is the actual context, and _next_ is used to pass the context to the next filter in the pipe. Send returns a Task, and should always follow the .NET guidelines for asynchronous methods. - -### PipeContext - -The _context_ type has a `PipeContext` constraint, which is another core atom in _GreenPipes_. A pipe context can include _payloads_, which are kept in a last-in, first-out (LIFO) collection. Payloads are identified by _type_, and can be retrieved, added, and updated using the `PipeContext` methods: - -```cs -public interface PipeContext -{ - /// - /// Used to cancel the execution of the context - /// - CancellationToken CancellationToken { get; } - - /// - /// Checks if a payload is present in the context - /// - bool HasPayloadType(Type payloadType); - - /// - /// Retrieves a payload from the pipe context - /// - /// The payload type - /// The payload - /// - bool TryGetPayload(out T payload) - where T : class; - - /// - /// Returns an existing payload or creates the payload using the factory method provided - /// - /// The payload type - /// The payload factory is the payload is not present - /// The payload - T GetOrAddPayload(PayloadFactory payloadFactory) - where T : class; - - /// - /// Either adds a new payload, or updates an existing payload - /// - /// The payload factory called if the payload is not present - /// The payload factory called if the payload already exists - /// The payload type - /// - T AddOrUpdatePayload(PayloadFactory addFactory, UpdatePayloadFactory updateFactory) - where T : class; -``` - -The payload methods are also used to check if a pipe context is another type of context. For example, to see if the `SendContext` is a `RabbitMqSendContext`, the `TryGetPayload` method should be used instead of trying to pattern match or cast the _context_ parameter. - -```cs -public async Task Send(SendContext context, IPipe next) -{ - if(context.TryGetPayload(out var rabbitMqSendContext)) - rabbitMqSendContext.Priority = 3; - - return next.Send(context); -} -``` - -::: tip -It is entirely the filter's responsibility to call _Send_ on the _next_ parameter. This gives the filter ultimately control over the context and behavior. It is how the retry filter is able to retry – by controlling the context flow. -::: - -User-defined payloads are easily added, so that subsequent filters can use them. The following example adds a payload. - -```cs -public class SomePayload -{ - public int Value { get; set; } -} - -public async Task Send(SendContext context, IPipe next) -{ - var payload = context.GetOrAddPayload(() => new SomePayload{Value = 27}); - - return next.Send(context); -} -``` - -::: tip -Using interfaces for payload types is highly recommended. -::: - -## Pipes - -Filters are combined in sequence to form a pipe. A pipe configurator, along with a pipe builder, is used to configure and build a pipe. - -```cs -public interface CustomContext : - PipeContext -{ - string SomeThing { get; } -} - -IPipe pipe = Pipe.New(x => -{ - x.UseFilter(new CustomFilter(...)); -}) -``` - -The `IPipe` interface is similar to `IFilter`, but a pipe hides the _next_ parameter as it is part of the pipe's structure. It is the pipe's responsibility to pass the -appropriate _next_ parameter to the individual filters in the pipe. - -```cs -public interface IPipe - where T : class, PipeContext -{ - Task Send(T context); -} -``` - -Send can be called, passing a context instance as shown. - -```cs -public class BaseCustomContext : - BasePipeContext, - CustomContext -{ - public string SomeThing { get; set; } -} - -await pipe.Send(new BaseCustomContext { SomeThing = "Hello" }); -``` - - - - diff --git a/docs/advanced/middleware/circuit-breaker.md b/docs/advanced/middleware/circuit-breaker.md deleted file mode 100644 index a5db3107cf9..00000000000 --- a/docs/advanced/middleware/circuit-breaker.md +++ /dev/null @@ -1,46 +0,0 @@ -# Circuit Breaker - -A circuit breaker is used to protect resources (remote, local, or otherwise) from being overloaded when -in a failure state. For example, a remote web site may be unavailable and calling that web site in a -message consumer takes 30-60 seconds to time out. By continuing to call the failing service, the service -may be unable to recover. A circuit breaker detects the repeated failures and trips, preventing further -calls to the service and giving it time to recover. Once the reset interval expires, calls are slowly allowed -back to the service. If it is still failing, the breaker remains open, and the timeout interval resets. -Once the service returns to healthy, calls flow normally as the breaker closes. - -Read Martin Fowler's description of the pattern [here](http://martinfowler.com/bliki/CircuitBreaker.html). - -To add the circuit breaker to a receive endpoint: - -```csharp -cfg.ReceiveEndpoint("customer_update_queue", e => -{ - e.UseCircuitBreaker(cb => - { - cb.TrackingPeriod = TimeSpan.FromMinutes(1); - cb.TripThreshold = 15; - cb.ActiveThreshold = 10; - cb.ResetInterval = TimeSpan.FromMinutes(5); - }); - // other configuration -}); -``` - -There are four settings that can be adjusted on a circuit breaker. - -### TrackingPeriod -The window of time before the success/failure counts are reset to zero. This is typically set to around - one minute, but can be as high as necessary. More than ten seems really strange to me. - -### TripThreshold - This is a percentage, and is based on the ratio of successful to failed attempts. When set to 15, if the ratio - exceeds 15%, the circuit breaker opens and remains open until the `ResetInterval` expires. - -### ActiveThreshold - This is the number of messages that must reach the circuit breaker in a tracking period before the circuit breaker - can trip. If set to 10, the trip threshold is not evaluated until at least 10 messages have been received. - -### ResetInterval - The period of time between the circuit breaker trip and the first attempt to close the circuit breaker. Messages - that reach the circuit breaker during the open period will immediately fail with the same exception that tripped - the circuit breaker. diff --git a/docs/advanced/middleware/concurrency-limit.md b/docs/advanced/middleware/concurrency-limit.md deleted file mode 100644 index a0d00e32ea7..00000000000 --- a/docs/advanced/middleware/concurrency-limit.md +++ /dev/null @@ -1,88 +0,0 @@ -# Concurrency Limit - -By specifying a concurrent message limit, MassTransit limits the number of messages delivered to a consumer at the same time. At the same time, since a consumer factory is used to create consumers, it also limits the number of concurrent consumers that exist at the same time. - -::: tip -The concurrent message limit applies to the total of all message types consumed by the consumer. -::: - -::: tip -The `ConcurrentMessageLimit` is not initialized by default, and does not need to be specified. If no limit is specified, which is the default, it will equal the PrefetchCount. -::: - -### Consumer - -To add a concurrent message limit to a consumer: - -```csharp -cfg.ReceiveEndpoint("submit-order", e => -{ - e.Consumer(cc => - { - cc.UseConcurrentMessageLimit(2); - }); -}); -``` - -### Saga - -To add a concurrent message limit to a saga: - -```csharp -cfg.ReceiveEndpoint("order-status", e => -{ - e.Saga(cc => - { - cc.UseConcurrentMessageLimit(2); - }); -}); -``` - -## Dynamically adjusting the concurrent message limit - -The concurrent message limit can be dynamically adjusted using a management endpoint. - -To add a concurrent message limit to a consumer, and support dynamic adjustment: - -```csharp -var management = cfg.ManagementEndpoint(); - -cfg.ReceiveEndpoint("order-status", e => -{ - e.Saga(cc => - { - cc.UseConcurrentMessageLimit(2, management, "order-status"); - }); -}); -``` - -To adjust the concurrent message limit: - -```csharp -var client = bus.CreateRequestClient(); -var response = await client.GetResponse(new -{ - Id = "order-status", - ConcurrencyLimit = 4, - Timestamp = DateTime.UtcNow -}); -``` - -## Legacy Concurrency Limit - -The concurrency limit support built into GreenPipes supports any `PipeContext`, which means it can be used anywhere a filter can be added. It is still supported, however, the new syntax above is recommended. - -To use the Green Pipes concurrency limit: - -```csharp -cfg.ReceiveEndpoint("submit-order", e => -{ - e.UseConcurrencyLimit(4); - - e.Consumer(); -}); -``` - -Used this way, a single filter will be applied to each message type. - - diff --git a/docs/advanced/middleware/custom.md b/docs/advanced/middleware/custom.md deleted file mode 100644 index 6e05a9efebd..00000000000 --- a/docs/advanced/middleware/custom.md +++ /dev/null @@ -1,185 +0,0 @@ -# Custom - -Middleware components are configured using extension methods, to make them easy to discover. - -::: tip NOTE -By default, a middleware configuration method should start with `Use`. -::: - -An example middleware component that would log exceptions to the console is shown below. - -```csharp -Bus.Factory.CreateUsingInMemory(cfg => -{ - cfg.UseExceptionLogger(); -}); -``` - -The extension method creates the pipe specification for the middleware component, which can be added to any pipe. For a component on the message consumption pipeline, use `ConsumeContext` instead of any `PipeContext`. - -```csharp -public static class ExampleMiddlewareConfiguratorExtensions -{ - public static void UseExceptionLogger(this IPipeConfigurator configurator) - where T : class, PipeContext - { - configurator.AddPipeSpecification(new ExceptionLoggerSpecification()); - } -} -``` - -The pipe specification is a class that adds the filter to the pipeline. Additional logic can be included, such as configuring optional settings, etc. using a closure syntax similar to the other configuration classes in MassTransit. - -```csharp -public class ExceptionLoggerSpecification : - IPipeSpecification - where T : class, PipeContext -{ - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - - public void Apply(IPipeBuilder builder) - { - builder.AddFilter(new ExceptionLoggerFilter()); - } -} -``` - -Finally, the middleware component itself is a filter added to the pipeline. All filters have absolute and complete control of the execution context and flow of the message. Pipelines are entirely asynchronous, and expect that asynchronous operations will be performed. - -::: danger -Do not use legacy constructs such as .Wait, .Result, or .WaitAll() as these can cause blocking in the message pipeline. While they might work in same cases, you've been warned! -::: - - -```csharp -public class ExceptionLoggerFilter : - IFilter - where T : class, PipeContext -{ - long _exceptionCount; - long _successCount; - long _attemptCount; - - public void Probe(ProbeContext context) - { - var scope = context.CreateFilterScope("exceptionLogger"); - scope.Add("attempted", _attemptCount); - scope.Add("succeeded", _successCount); - scope.Add("faulted", _exceptionCount); - } - - /// - /// Send is called for each context that is sent through the pipeline - /// - /// The context sent through the pipeline - /// The next filter in the pipe, must be called or the pipe ends here - public async Task Send(T context, IPipe next) - { - try - { - Interlocked.Increment(ref _attemptCount); - - // here the next filter in the pipe is called - await next.Send(context); - - Interlocked.Increment(ref _successCount); - } - catch (Exception ex) - { - Interlocked.Increment(ref _exceptionCount); - - await Console.Out.WriteLineAsync($"An exception occurred: {ex.Message}"); - - // propagate the exception up the call stack - throw; - } - } -} -``` - -The example filter above is stateful. If the filter was stateless, the same filter instance could be used by multiple pipes — worth considering if the filter has high memory requirements. - -### Message Type Filters - -In many cases, the message type is used by a filter. To create an instance of a generic filter that includes the message type, use the configuration observer. - -```cs -public class MessageFilterConfigurationObserver : - ConfigurationObserver, - IMessageConfigurationObserver -{ - public MessageFilterConfigurationObserver(IConsumePipeConfigurator receiveEndpointConfigurator) - : base(receiveEndpointConfigurator) - { - Connect(this); - } - - public void MessageConfigured(IConsumePipeConfigurator configurator) - where TMessage : class - { - var specification = new MessageFilterPipeSpecification(); - - configurator.AddPipeSpecification(specification); - } -} -``` - -Then, in the specification, the appropriate filter can be created and added to the pipeline. - -```cs -public class MessageFilterPipeSpecification : - IPipeSpecification> - where T : class -{ - public void Apply(IPipeBuilder> builder) - { - var filter = new MessageFilter(); - - builder.AddFilter(filter); - } - - public IEnumerable Validate() - { - yield break; - } -} -``` - -The filter could then include the message type as a generic type parameter. - -```cs -public class MessageFilter : - IFilter> - where T : class -{ - public void Probe(ProbeContext context) - { - var scope = context.CreateFilterScope("messageFilter"); - } - - public async Task Send(ConsumeContext context, IPipe> next) - { - // do something - - await next.Send(context); - } -} -``` - -The extension method for the above is shown below (for completeness). - -```cs -public static class MessageFilterConfigurationExtensions -{ - public static void UseMessageFilter(this IConsumePipeConfigurator configurator) - { - if (configurator == null) - throw new ArgumentNullException(nameof(configurator)); - - var observer = new MessageFilterConfigurationObserver(configurator); - } -} -``` diff --git a/docs/advanced/middleware/killswitch.md b/docs/advanced/middleware/killswitch.md deleted file mode 100644 index 4f1fd7a3518..00000000000 --- a/docs/advanced/middleware/killswitch.md +++ /dev/null @@ -1,60 +0,0 @@ -# Kill Switch - -A Kill Switch is used to prevent failing consumers from moving all the messages from the input queue to the error queue. By monitoring message consumption and tracking message successes and failures, a Kill Switch stops the receive endpoint when a trip threshold has been reached. - -Typically, consumer exceptions are transient issues and suspending consumption until a later time when the transient issue may have been resolved. - -::: tip -A Kill Switch is the messaging analog of a Circuit Breaker, and operates in a similar manner. However, instead of inducing failure to reduce pressure on a backing service, the kill switch stops consuming messages instead thereby reducing pressure on backing services. - -> Read Martin Fowler's description of the pattern [here](http://martinfowler.com/bliki/CircuitBreaker.html). -::: - -### Configuration - -A Kill Switch can be configured on an individual receive endpoint or all receive endpoints on the bus. To configure a kill switch on all receive endpoints, add the _UseKillSwitch_ method as shown. - -```cs -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.UseKillSwitch(options => options - .SetActivationThreshold(10) - .SetTripThreshold(0.15) - .SetRestartTimeout(m: 1)); - - cfg.ReceiveEndpoint("some-queue", e => - { - e.Consumer(); - }); -}); -``` - -In the above example, the kill switch will activate after _10_ messages have been consumed. If the ratio of failures/attempts exceeds _15%_, the kill switch will trip and stop the receive endpoint. After _1_ minute, the receive endpoint will be restarted. Once restarted, if exceptions are still observed, the receive endpoint will be stopped again for _1_ minute. - -To configure the kill switch on a receive endpoint, the syntax is the same as shown. - -```cs -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.ReceiveEndpoint("some-queue", e => - { - e.UseKillSwitch(options => options - .SetActivationThreshold(10) - .SetTripThreshold(0.15) - .SetRestartTimeout(m: 1)); - - e.Consumer(); - }); -}); -``` - -### Options - -| Option | Description | -| ---------------------------- | --------------------------------------------------------- | -| `TrackingPeriod` | The time window for tracking exceptions | -| `TripThreshold` | The percentage of failed messages that triggers the kill switch. Should be 0-100, but seriously like 5-10. | -| `ActivationThreshold` | The number of messages that must be consumed before the kill switch activates. | -| `RestartTimeout` | The wait time before restarting the receive endpoint | -| `ExceptionFilter` | By default, all exceptions are tracked. An exception filter can be configured to only track specific exceptions. | - diff --git a/docs/advanced/middleware/latest.md b/docs/advanced/middleware/latest.md deleted file mode 100644 index 5d0b7c23a02..00000000000 --- a/docs/advanced/middleware/latest.md +++ /dev/null @@ -1,24 +0,0 @@ -# Latest - -The latest filter is pretty simple, it keeps track of the latest message received by the pipeline and makes that -value available. It seems pretty simple, and it is, but it is actually useful in metrics and monitoring scenarios. - -> This filter is actually usable to capture any context type on any pipe, so you know. - -To add a latest to a receive endpoint: - -```csharp -ILatestFilter> tempFilter; - -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Host("localhost"); - - cfg.ReceiveEndpoint("customer_update_queue", e => - { - e.Handler(context => Task.FromResult(true), x => - { - x.UseLatest(x => x.Created = filter => tempFilter = filter); - }) - }); -``` \ No newline at end of file diff --git a/docs/advanced/middleware/rate-limiter.md b/docs/advanced/middleware/rate-limiter.md deleted file mode 100644 index ce148cc5a50..00000000000 --- a/docs/advanced/middleware/rate-limiter.md +++ /dev/null @@ -1,31 +0,0 @@ -# Rate Limiter - -A rate limiter is used to restrict the number of messages processed within a time period. The reason may be -that an API or service only accepts a certain number of calls per minute, and will delay any subsequent attempts -until the rate limiting period has expired. - -::: warning -The rate limiter will delay message delivery until the rate limit expires, so it is best to avoid large time windows -and keep the rate limits sane. Something like 1000 over 10 minutes is a bad idea, versus 100 over a minute. Try to -adjust the values and see what works for you. -::: - -There are two modes that a rate limiter can operate, but only one of them is currently supported (the other may come later). - -To add a rate limiter to a receive endpoint: - -```csharp -cfg.ReceiveEndpoint("customer_update_queue", e => -{ - e.UseRateLimit(1000, TimeSpan.FromSeconds(5)); - // other configuration -}); -``` - -The two arguments supported by the rate limiter include: - -### RateLimit - The number of calls allowed in the time period. - -### Interval - The time interval before the message count is reset to zero. diff --git a/docs/advanced/middleware/receive.md b/docs/advanced/middleware/receive.md deleted file mode 100644 index 036460c0d03..00000000000 --- a/docs/advanced/middleware/receive.md +++ /dev/null @@ -1,5 +0,0 @@ -# Receive Pipeline - -To see how the middleware components (all the pipes and filters) are composed, the visualized receive pipeline is shown below. - -![Receive Pipeline](/ReceivePipeline.png) diff --git a/docs/advanced/middleware/scoped.md b/docs/advanced/middleware/scoped.md deleted file mode 100644 index 0bb5d866d8e..00000000000 --- a/docs/advanced/middleware/scoped.md +++ /dev/null @@ -1,184 +0,0 @@ -# Scoped Filters - -Most of the built-in filters are created and added to the pipeline during configuration. This approach is typically sufficient, however, there are scenarios where the filter needs access to other components at runtime. - -Using a scoped filter, combined with a supported dependency injection container (either MSDI or Autofac), allows a new filter instance to be resolved from the container for each message. If a current scope is not available, a new scope will be created using the root container. - -### Filter Classes - -Scoped filters must be generic classes with a single generic argument for the message type. For example, a scoped consume filter would be defined as shown below. - -```cs -public class TFilter : - IFilter> -``` - -### Supported Filter Contexts - -Scope filters are added using one of the following methods, which are specific to the filter context type. - -| Type | Usage | -| ---------------------------- | --------------------------------------------------------- | -| `ConsumeContext` | `UseConsumeFilter(typeof(TFilter<>), context)` | -| `SendContext` | `UseSendFilter(typeof(TFilter<>), context)` | -| `PublishContext` | `UsePublishFilter(typeof(TFilter<>), context)` | -| `ExecuteContext` | `UseExecuteActivityFilter(typeof(TFilter<>), context)` | -| `CompensateContext` | `UseCompensateActivityFilter(typeof(TFilter<>), context)` | - -More information could be found inside [middleware](README.md) section - -### Usage - -To create a `ConsumeContext` filter and add it to the receive endpoint: - -```cs -public class MyConsumeFilter : - IFilter> - where T : class -{ - public MyConsumeFilter(IMyDependency dependency) { } - - public async Task Send(ConsumeContext context, IPipe> next) { } - - public void Probe(ProbeContext context) { } -} - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - //other configuration - services.AddScoped(); //register dependency - - services.AddConsumer(); - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.ReceiveEndpoint("input-queue", e => - { - e.UseConsumeFilter(typeof(MyConsumeFilter<>), context); //generic filter - - e.ConfigureConsumer(); - }); - }); - }); - } -} -``` - -To create a `SendContext` filter and add it to the send pipeline: - -```cs -public class MySendFilter : - IFilter> - where T : class -{ - public MySendFilter(IMyDependency dependency) { } - - public async Task Send(SendContext context, IPipe> next) { } - - public void Probe(ProbeContext context) { } -} - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - //other configuration - services.AddScoped(); //register dependency - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.UseSendFilter(typeof(MySendFilter<>), context); //generic filter - }); - }); - } -} -``` - -### Combining Consume And Send/Publish Filters - -A common use case with scoped filters is transferring data between the consumer. This data may be extracted from headers, or could include context or authorization information that needs to be passed from a consumed message context to sent or published messages. In these situations, there _may_ be some special requirements to ensure everything works as expected. - -The following example has both consume and send filters, and utilize a shared dependency to communicate data to outbound messages. - -```cs -public class MyConsumeFilter : - IFilter> - where T : class -{ - public MyConsumeFilter(MyDependency dependency) { } - - public async Task Send(ConsumeContext context, IPipe> next) { } - - public void Probe(ProbeContext context) { } -} - -public class MySendFilter : - IFilter> - where T : class -{ - public MySendFilter(MyDependency dependency) { } - - public async Task Send(SendContext context, IPipe> next) { } - - public void Probe(ProbeContext context) { } -} - -public class MyDependency -{ - public string SomeValue { get; set; } -} - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddScoped(); - - services.AddMassTransit(x => - { - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseSendFilter(typeof(MySendFilter<>), context); - - cfg.ReceiveEndpoint("input-queue", e => - { - e.UseConsumeFilter(typeof(MyConsumeFilter<>), context); - e.ConfigureConsumer(context); - }); - }); - }); - } -} -``` - -::: warning -When using the InMemoryOutbox with scoped publish or send filters, `UseMessageScope` (for MSDI) or `UseMessageLifetimeScope` (for Autofac) must be configured _before_ the InMemoryOutbox. If `UseMessageRetry` is used, it must come _before_ either `UseMessageScope` or `UseMessageLifetimeScope`. -::: - -Because the InMemoryOutbox delays publishing and sending messages until after the consumer or saga completes, the created container scope will have been disposed. The `UseMessageScope` or `UseMessageLifetimeScope` filters create the scope before the InMemoryOutbox, which is then used by the consumer or saga and any scoped filters (consume, publish, or send). - -The updated receive endpoint configuration using the InMemoryOutbox is shown below. - -```cs - cfg.ReceiveEndpoint("input-queue", e => - { - e.UseMessageRetry(r => r.Intervals(100, 500, 1000, 2000)); - e.UseMessageScope(context); - e.UseInMemoryOutbox(); - - e.UseConsumeFilter(typeof(MyConsumeFilter<>), context); - e.ConfigureConsumer(context); - }); -``` - - - - diff --git a/docs/advanced/middleware/transactions.md b/docs/advanced/middleware/transactions.md deleted file mode 100644 index 78a0e6e3f57..00000000000 --- a/docs/advanced/middleware/transactions.md +++ /dev/null @@ -1,340 +0,0 @@ -# Transaction - -::: warning -Transactions, and using a shared transaction, is an advanced concept. Every scenario is different, so this is more of a guideline than a rule. -::: - -The message pipeline in MassTransit is asynchronous, leveraging the Task Parallel Library (TPL) extensively to maximum thread utilization. This means that receiving an individual message may involve several threads over the life cycle of the consumer. To prevent strange things from happening, developers should avoid using any *static* or *thread static* variables as these are one of the main causes of errors in asynchronous programming. - -The .NET `System.Transactions` namespace is a static hound, with many applications following the model of using a transaction scope to wrap a transactional operation. - -```cs -public class Repository -{ - public void Save(Entity entity) - { - using(var scope = new TransactionScope()) - { - SaveEntity(entity); - - scope.Complete(); - } - } -} -``` - -In this example, the creation of a `TransactionScope` actually sets a static variable, `Transaction.Current`, to the created or ambient transaction. That word *ambient* should be a big clue — it's using a static variable (in this case, it's actually a thread static, but anyway). - -It turns out that the above example is simple, and works, because there are no asynchronous methods. But that also means that the method blocks the thread while the database performs work (which takes an eternity in CPU time). Most databases support asynchronous operations (including Entity Framework), so it is logical to assume that using those methods would increase thread utilization. - -It is also often requested that a set of operations be managed as a *unit of work*. A single transaction is shared across multiple operations that are committed as a single unit. If the commit fails, everything is undone and the message is faulted (or retried, if the retry middleware is used). - -## Usage - -MassTransit includes transaction middleware to share a single committable transaction across any number consumers and any dependencies used by the those consumers. To use the middleware, it must be added to the bus or receive endpoint. - -```cs -Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.ReceiveEndpoint("event_queue", e => - { - e.UseTransaction(x => - { - Timeout = TimeSpan.FromSeconds(90); - IsolationLevel = IsolationLevel.ReadCommitted; - }); - - e.Consumer(); - }) -}); -``` - -For each message, a new `CommittableTransaction` is created. This transaction can be passed to classes that support transactional operations, such as `DbContext`, `SqlCommand`, and `SqlConnection`. It can also be used to create any `TransactionScope` that may be required to support a synchronous operation. - -To use the transaction directly in a consumer, the transaction can be pulled from the `ConsumeContext`. - -```csharp -public class TransactionalConsumer : - IConsumer -{ - readonly SqlConnection _connection; // ctor injected - - public async Task Consume(ConsumeContext context) - { - var transactionContext = context.GetPayload(); - - _connection.EnlistTransaction(transactionContext.Transaction); - - using (SqlCommand command = new SqlCommand(sql, _connection)) - { - using (var reader = await command.ExecuteReaderAsync()) - { - } - } - - // the connection lifetime should be managed by a container - // or perhaps another more specific middleware component. - } -} -``` - -The connection (and by use of the connection, the command) are enlisted in the transaction. Once the method completes, and control is returned to the transaction middleware, if no exceptions are thrown the transaction is committed (which should complete the database operation). If an exception is thrown, the transaction is rolled back. - -While not shown here, a class that provides the connection, and enlists the connection upon creation, should be added to the container to ensure that the transaction is not enlisted twice (not sure that's a bad thing though, it should be ignored). Also, as long as only a single connection string is enlisted, the DTC should not get involved. Using the same transaction across multiple connection strings is a bad thing, as it will make the DTC come into play which slows the world down significantly. - -## Unit of Work (Buffer) - -Sometimes you just have to integrate with Database first systems, but still want some of the perks that message buses have to offer. A good example is an API with your typical HTTP Requests. You want to manipulate your DB, commit, and then upon success, release the messages to the broker. This is NOT a distributed transaction. There's still a risk that you could have the DB up and the broker down, causing the messages to never be sent to the broker. So you've been warned! - -There are two options to provide this buffer: - -- Transactional Enlistment Bus -- Transactional Bus - -## Transactional Enlistment Bus - -Transports don't typically support transactions, so sending messages during a transaction only to encounter an exception resulting in a transaction rollback may lead to messages that were sent without the transaction being committed. - -::: tip In Memory Outbox -MassTransit has an in-memory outbox to deal with this problem, which can be used within a message consumer. It leverages the durable nature of a message transport to ensure that messages are ultimately sent. There is an extensive article and [video](https://youtu.be/P41IsVAc1nI) explaining the outbox behavior. This approach is preferred to performing transactional database writes outside of a consumer. -::: - -However, sometimes you are coming from the database first and can't get around it. For those situations, MassTransit has a _very simple_ transactional bus which enlists in the current transaction and defers outgoing messages until the transaction is being committed. There is still no rollback, once the messages are delivered to the broker, there is no pulling them back. - - -```cs -services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - }); - - x.AddTransactionalEnlistmentBus(); -}); -``` - -That is all that's needed. Now here's an example usage within an MVC Action. It's also important to use `TransactionScopeAsyncFlowOption.Enabled` as shown below. - -```cs -public class MyController : ControllerBase -{ - private readonly IPublishEndpoint _publishEndpoint; - private readonly MyDbContext _dbContext; - - public ValuesController(IPublishEndpoint publishEndpoint, MyDbContext dbContext) - { - _publishEndpoint = publishEndpoint; - _dbContext = dbContext; - } - - [HttpPost] - public async Task Post([FromBody] string value) - { - using(var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) - { - _dbContext.Posts.Add(new Post{...}); - await _dbContext.SaveChangesAsync(); - - await _publishEndpoint.Publish(new PostCreated{...}); - - transaction.Complete(); - } - - return Ok(); - } -} -``` - -Here's an example from within a Console App, with no Container: - -```cs -public class Program -{ - public static async Task Main() - { - var bus = Bus.Factory.CreateUsingRabbitMq(sbc => - { - sbc.Host("rabbitmq://localhost"); - }); - - await bus.StartAsync(); // This is important! - - var transactionalBus = new TransactionalEnlistmentBus(bus); - - while(/*some condition*/) - { - using(var transaction = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled)) - { - // Do whatever business logic you need. - - await transactionalBus.Publish(new ReportQueued{...}); - await transactionalBus.Send(new CalculateReport{...}); - - // Maybe other business logic - - transaction.Complete(); - } - } - - Console.WriteLine("Press any key to exit"); - await Task.Run(() => Console.ReadKey()); - - await bus.StopAsync(); - } -} -``` - -## Transactional Bus - -Here we are again, another option for holding onto the messages and releasing them as close to the database transaction Commit as possible. We made this alternative because using TransactionScope from the previous section, could [in certain cases](https://github.com/MassTransit/MassTransit/issues/2075) still cause a 2 phase commit escalation (not to mention that TransactionScope doesn't truely have async support, so we make [concessions by calling TaskUtil.Await](https://github.com/MassTransit/MassTransit/blob/develop/src/MassTransit/Transactions/TransactionalBusEnlistment.cs#L83)). So to offer an alternative to these drawbacks, MassTransit provides an Outbox Bus. - -::: warning -Never use the TransactionalBus or TransactionalEnlistmentBus when writing consumers. These tools are very specific and should be used only in the scenarios described. -::: - -The examples will show it's usage in an ASP.NET MVC application, which is where we most commonly use Scoped lifetime for our DbContext and therefore we want the same for our TransactionalBus. You could possibly use it in some console applications, but ones WITHOUT a MT Consumer. Once you have consumers you will ALWAYS use `ConsumeContext` to interact with the bus, and never the `IBus`. - -First Register the outbox bus. - -```cs -services.AddMassTransit(x => -{ - x.UsingRabbitMq((context, cfg) => - { - }); - - x.AddTransactionalBus(); -}); -``` - -Then use within your controller. - -```cs -public class MyController : ControllerBase -{ - private readonly ITransactionalBus _transactionalBus; - private readonly MyDbContext _dbContext; - - public ValuesController(ITransactionalBus transactionalBus, MyDbContext dbContext) - { - _transactionalBus = transactionalBus; - _dbContext = dbContext; - } - - [HttpPost] - public async Task Post([FromBody] string value) - { - using(var transaction = await _dbContext.Database.BeginTransactionAsync()) - { - try - { - _dbContext.Posts.Add(new Post{...}); - await _dbContext.SaveChangesAsync(); - - await _transactionalBus.Publish(new PostCreated{...}); - - await transaction.CommitAsync(); - await _transactionalBus.Release(); // Immediately after CommitAsync - } - catch (Exception) - { - transaction.Rollback(); - } - - } - - return Ok(); - } -} -``` - -One option to remove some of the boilerplate of opening a transaction each Action that writes to the DB is to make a Filter. You can then include all of the boilerplate code to begin the transaction, and release the outbox. - -```cs -public class DbContextTransactionFilter : TypeFilterAttribute -{ - public DbContextTransactionFilter() - : base(typeof(DbContextTransactionFilterImpl)) - { - } - - // This will be scoped per http request - private class DbContextTransactionFilterImpl : IAsyncActionFilter - { - private readonly MyDbContext _db; - private readonly ILogger _logger; - private readonly ITransactionalBus _transactionalBus; - - public DbContextTransactionFilterImpl( - MyDbContext db, - ILogger logger, - ITransactionalBus transactionalBus) - { - _db = db; - _logger = logger; - _transactionalBus = transactionalBus; - } - - public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) - { - using var transaction = await _db.Database.BeginTransactionAsync(); - - try - { - var actionExecuted = await next(); - if (actionExecuted.Exception != null && !actionExecuted.ExceptionHandled) - { - await transaction.RollbackAsync(); - } - else - { - await transaction.CommitAsync(); - await _transactionalBus.Release(); // Immediately after CommitAsync - } - } - catch (Exception) - { - try - { - await transaction.RollbackAsync(); - } - catch (Exception e) - { - // Swallow failed rollback - _logger.LogWarning(e, "Tried to rollback transaction but failed, swallow exception."); - } - - throw; - } - } - } -} -``` - -Now your Controller Action will look like: - -```cs -public class MyController : ControllerBase -{ - private readonly ITransactionalBus _transactionalBus; - private readonly MyDbContext _dbContext; - - public ValuesController(ITransactionalBus transactionalBus, MyDbContext dbContext) - { - _transactionalBus = transactionalBus; - _dbContext = dbContext; - } - - [HttpPost] - [DbContextTransactionFilter] - public async Task Post([FromBody] string value) - { - _dbContext.Posts.Add(new Post{...}); - await _dbContext.SaveChangesAsync(); - - await _transactionalBus.Publish(new PostCreated{...}); - - return Ok(); - } -} -``` diff --git a/docs/advanced/monitoring/applications-insights.md b/docs/advanced/monitoring/applications-insights.md deleted file mode 100644 index e64e3ba9035..00000000000 --- a/docs/advanced/monitoring/applications-insights.md +++ /dev/null @@ -1,86 +0,0 @@ -# Application Insights - -Application Insights (part of Azure Monitor) is able to capture and record metrics from MassTransit. It can also be configured as a log sink for logging. - -[Create an Application Insights resource](https://docs.microsoft.com/en-us/azure/application-insights/app-insights-create-new-resource#create-an-application-insights-resource-1) - -[Copy the instrumentation key](https://docs.microsoft.com/en-us/azure/application-insights/app-insights-create-new-resource#copy-the-instrumentation-key) - -To configure an application to use Application Insights with MassTransit: - -> Requires NuGets `MassTransit`, `Microsoft.ApplicationInsights.DependencyCollector` -> -> (for logging, add `Microsoft.Extensions.Logging.ApplicationInsights`) - -```csharp -using System; -using System.Reflection; -using System.Threading.Tasks; -using MassTransit; - -namespace Example -{ - public class MyMessageConsumerConsumer : - MassTransit.IConsumer - { - public async Task Consume(ConsumeContext context) - { - await Console.Out.WriteLineAsync($"Received: {context.Message.Value}"); - } - } - - // Message Definition - public class MyMessage - { - public string Value { get; set; } - } - - public class Program - { - public static async Task Main(string[] args) - { - var module = new DependencyTrackingTelemetryModule(); - module.IncludeDiagnosticSourceActivities.Add("MassTransit"); - - TelemetryConfiguration configuration = TelemetryConfiguration.CreateDefault(); - configuration.InstrumentationKey = ""; - configuration.TelemetryInitializers.Add(new HttpDependenciesParsingTelemetryInitializer()); - - var telemetryClient = new TelemetryClient(configuration); - module.Initialize(configuration); - - var loggerOptions = new ApplicationInsightsLoggerOptions(); - - var applicationInsightsLoggerProvider = new ApplicationInsightsLoggerProvider(Options.Create(configuration), - Options.Create(loggerOptions)); - - ILoggerFactory factory = new LoggerFactory(); - factory.AddProvider(applicationInsightsLoggerProvider); - - LogContext.ConfigureCurrentLogContext(factory); - - var busControl = Bus.Factory.CreateUsingInMemory(cfg => - { - cfg.ReceiveEndpoint("my_queue", ec => - { - ec.Consumer(); - }); - }); - - using(busControl.StartAsync()) - { - await busControl.Publish(new MyMessage{Value = "Hello, World."}); - - await Task.Run(() => Console.ReadLine()); - } - - module.Dispose(); - - telemetryClient.Flush(); - await Task.Delay(5000); - - configuration.Dispose(); - } - } -} -``` diff --git a/docs/advanced/monitoring/diagnostic-source.md b/docs/advanced/monitoring/diagnostic-source.md deleted file mode 100644 index ba4dbd4b010..00000000000 --- a/docs/advanced/monitoring/diagnostic-source.md +++ /dev/null @@ -1,86 +0,0 @@ -# DiagnosticSource - -MassTransit uses Microsoft's `System.Diagnostics.DiagnosticSource` to emit diagnostic events. This allows almost every metric trace provider to connect to MassTransit and monitor it. - -To connect, set the current log context prior to bus configuration using: - -```csharp -public static async Task Main(string[] args) -{ - var subscription = DiagnosticListener.AllListeners.Subscribe(new DiagnosticObserver()); - - var busControl = Bus.Factory.CreateUsingInMemory(cfg => - { - }); -} - -public class DiagnosticObserver : IObserver -{ - public void OnCompleted() { } - - public void OnError(Exception error) { } - - public void OnNext(DiagnosticListener value) - { - if (value.Name == "MassTransit") - { - // subscribe to the listener with your monitoring tool, etc. - } - } -} -``` - -That's it! Magic is done. Now you need to choose your Trace provider (for example: [Application Insights](https://docs.microsoft.com/en-us/azure/application-insights/app-insights-create-new-resource#create-an-application-insights-resource-1), [OpenTracing](https://github.com/opentracing-contrib/csharp-netcore)) and configure to read metrics from your `DiagnosticSource`. - -The `OpenTracing.Contrib.NetCore` library subscribes to every diagnostic source under the hood it doesn't require any interaction from your side, -however it neither propagates the remote context nor injects the current context to message headers, so the trace will be -limited to local operations. Also, it won't use `TraceId` and `SpanId` from the `Activity` even when you set the activity default id format to `W3C`, -those ids will be random and cannot be associated with `ActivityId`. - -To enable `Application Insights`, add it to the `Startup`: - -```csharp -public void ConfigureServices(IServiceCollection services) -{ - services.ConfigureTelemetryModule((m, o) => m.IncludeDiagnosticSourceActivities.Add("Listener.Name")); - - services.AddApplicationInsightsTelemetry("InstrumentationKey"); -} -``` - -### Available diagnostic events - -You can subscribe to different types of diagnostic events produced by MassTransit. -Below is the list of names of MassTransit diagnostic sources. Keep in mind that each -operation produces `Start` and `Stop` events using the `Activity`. So, when a message is -consumed, you get `MassTransit.Consumer.Consume.Start` event before the consumer is executed and -`MassTransit.Consumer.Consume.Stop` after the message is consumed. - -#### Transport - -- MassTransit.Transport.Send -- MassTransit.Transport.Receive - -#### Consumer - -- MassTransit.Consumer.Consume -- MassTransit.Consumer.Handle - -#### Saga - -- MassTransit.Saga.Send -- MassTransit.Saga.SendQuery -- MassTransit.Saga.Initiate -- MassTransit.Saga.Orchestrate -- MassTransit.Saga.Observe -- MassTransit.Saga.RaiseEvent - -#### Courier - -- MassTransit.Activity.Execute -- MassTransit.Activity.Compensate - -### Additional resources - -- [Activity User Guide](https://github.com/dotnet/runtime/blob/master/src/libraries/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md) -- [DiagnosticSource User Guide](https://github.com/dotnet/runtime/blob/master/src/libraries/System.Diagnostics.DiagnosticSource/src/DiagnosticSourceUsersGuide.md) diff --git a/docs/advanced/monitoring/perfcounters.md b/docs/advanced/monitoring/perfcounters.md deleted file mode 100644 index 202671c9ec0..00000000000 --- a/docs/advanced/monitoring/perfcounters.md +++ /dev/null @@ -1,78 +0,0 @@ -# Performance counters - -MassTransit has support for updating Windows performance counters. Chris has a post introducing them: -[Performance Counters Added to MassTransit][1]. - -### User permissions - -The user running your mass transit enabled application will need access to update the performance counters. - -```text -HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Perflib -``` - -If MassTransit does not detect the performance counters it wishes to write to it will attempt to create them. -If the user credentials do not have administrative access likely they will not have the ability to create the -performance counters and errors will be logged. - -### Windows installer - -When deploying your mass transit enabled application it is possible to have Windows Installer create your performance counters for you. Below is Xml used by Wix 3.0 to define the MassTransit performance counters. - -```xml - - -... - - - - - - - - - - - - - - - - - - - -``` - -[1]: http://lostechies.com/chrispatterson/2009/10/14/performance-counters-added-to-masstransit/ \ No newline at end of file diff --git a/docs/advanced/monitoring/prometheus.md b/docs/advanced/monitoring/prometheus.md deleted file mode 100644 index 63c060f93c9..00000000000 --- a/docs/advanced/monitoring/prometheus.md +++ /dev/null @@ -1,134 +0,0 @@ -# Prometheus Metrics - -[MassTransit.Prometheus](https://www.nuget.org/packages/MassTransit.Prometheus) - -MassTransit supports Prometheus metric capture, which provides useful observability into the bus, endpoints, consumers, and messages. - -> The `prometheus-net` library is used as the Prometheus client since it is mentioned on the Prometheus client list. - -### Installation - -```bash -$ dotnet add package prometheus-net.AspNetCore -$ dotnet add package MassTransit.Prometheus -``` - -### Configuration - -To configure the bus to capture metrics, add the `UsePrometheusMetrics()` method to your bus configuration. - -```cs -public void ConfigureServices(IServiceCollection services) -{ - // this registration is simplified - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.UsePrometheusMetrics(serviceName: "order_service"); - }); - }); -} -``` - -To then mount the metrics to `/metrics` go to your Startup.cs and add - -```cs -public void Configure(IApplicationBuilder app, IWebHostEnvironment env) -{ - // this registration is simplified - app.UseEndpoints(endpoints => - { - // add this line - endpoints.MapMetrics(); - endpoints.MapControllers(); - }); -} -``` - -> For more details, see the [Prometheus-Net Documentation](https://github.com/prometheus-net/prometheus-net#aspnet-core-exporter-middleware). - -### Metrics Captured - -The metrics captured by MassTransit are listed below. - -| Name | Description | -|:-----------|:------------| -| mt_receive_total | Total number of messages received -| mt_receive_fault_total | Total number of messages receive faults -| mt_receive_duration_seconds | Elapsed time spent receiving messages, in seconds -| mt_receive_in_progress | Number of messages being received -| mt_consume_total | Total number of messages consumed -| mt_consume_fault_total | Total number of message consume faults -| mt_consume_retry_total | Total number of message consume retries -| mt_consume_duration_seconds | Elapsed time spent consuming a message, in seconds -| mt_delivery_duration_seconds | Elapsed time between when the message was sent and when it was consumed, in seconds. -| mt_publish_total | Total number of messages published -| mt_publish_fault_total | Total number of message publish faults -| mt_send_total | Total number of messages sent -| mt_send_fault_total | Total number of message send faults -| mt_bus | Number of bus instances -| mt_endpoint | Number of receive endpoint instances -| mt_consumer_in_progress | Number of consumers in progress -| mt_handler_in_progress | Number of handlers in progress -| mt_saga_in_progress | Number of sagas in progress -| mt_activity_execute_in_progress | Number of activity executions in progress -| mt_activity_compensate_in_progress | Number of activity compensations in progress -| mt_activity_execute_total | Total number of activities executed -| mt_activity_execute_fault_total | Total number of activity executions faults -| mt_activity_execute_duration_seconds | Elapsed time spent executing an activity, in seconds -| mt_activity_compensate_total | Total number of activities compensated -| mt_activity_compensate_failure_total | Total number of activity compensation failures -| mt_activity_compensate_duration_seconds | Elapsed time spent compensating an activity, in seconds - - -### Labels - -For the metrics above, labels are specified where appropriate. - -| Name | Description | -|:-----------|:------------| -| service_name | The service name specified at bus configuration -| endpoint_address | The endpoint address -| message_type | The message type for the metric -| consumer_type | The consumer, saga, or activity type for the metric -| activity_name | The activity name -| argument_type | The activity execute argument type -| log_type | The activity compensate log type -| exception_type | The exception type for a fault metric - - -### Example Docker Compose - -```yaml -version: "3.7" - -services: - prometheus: - image: prom/prometheus - ports: - - "9090:9090" -``` - -**Example MassTransit Prometheus Config File** - -::: tip -You can use the domain `host.docker.internal` to access process -running on the host machine. -::: - -```yaml -global: - scrape_interval: 10s - -scrape_configs: - - job_name: masstransit - tls_config: - insecure_skip_verify: true - scheme: https - static_configs: - - targets: - - 'host.docker.internal:5001' - - -``` diff --git a/docs/advanced/observers.md b/docs/advanced/observers.md deleted file mode 100644 index 5cd0334c577..00000000000 --- a/docs/advanced/observers.md +++ /dev/null @@ -1,152 +0,0 @@ ---- -sidebarDepth: 1 ---- - -# Observers - -MassTransit supports several message observers allowing received, consumed, sent, and published messages to be monitored. There is a bus observer as well, so that the bus life cycle can be monitored. - -::: warning -Observers should not be used to modify or intercept messages. To intercept messages to add headers or modify message content, create a new or use an existing middleware component. -::: - -## Receive - -To observe messages as they are received by the transport, create a class that implements the `IReceiveObserver` interface, and connect it to the bus as shown below. - -<<< @/src/MassTransit.Abstractions/Observers/IReceiveObserver.cs - -To configure a receive observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. When a container is not being used, the `ConnectReceiveObserver` bus method can be used instead. - -```cs -services.AddReceiveObserver(); -``` - -```cs -services.AddReceiveObserver(provider => new ReceiveObserver()); -``` - -## Consume - -If the `ReceiveContext` isn't fascinating enough for you, perhaps the actual consumption of messages might float your boat. A consume observer implements the `IConsumeObserver` interface, as shown below. - -<<< @/src/MassTransit.Abstractions/Observers/IConsumeObserver.cs - -To configure a consume observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. When a container is not being used, the `ConnectConsumeObserver` bus method can be used instead. - -```cs -services.AddConsumeObserver(); -``` - -```cs -services.AddConsumeObserver(provider => new ConsumeObserver()); -``` - -### Consume Message - -Okay, so it's obvious that if you've read this far you want a more specific observer, one that only is called when a specific message type is consumed. We have you covered there too, as shown below. - -<<< @/src/MassTransit.Abstractions/Observers/IConsumeMessageObserver.cs - -To connect the observer, use the `ConnectConsumeMessageObserver` method before starting the bus. - -> The `ConsumeObserver` interface may be deprecated at some point, it's sort of a legacy observer that isn't recommended. - -## Send - -Okay, so, incoming messages are not your thing. We get it, you're all about what goes out. It's cool. It's better to send than to receive. Or is that give? Anyway, a send observer is also available. - -<<< @/src/MassTransit.Abstractions/Observers/ISendObserver.cs - -To configure a send observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer -creation. When a container is not being used, the `ConnectSendObserver` bus method can be used instead. - -```cs -services.AddSendObserver(); -``` - -```cs -services.AddSendObserver(provider => new SendObserver()); -``` - -## Publish - -In addition to send, publish is also observable. Because the semantics matter, absolutely. Using the MessageId to link them up as it's unique for each message. Remember that Publish and Send are two distinct operations so if you want to observe all messages that are leaving your service, you have to connect both Publish and Send observers. - -<<< @/src/MassTransit.Abstractions/Observers/IPublishObserver.cs - -To configure a public observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer -creation. When a container is not being used, the `ConnectPublishObserver` bus method can be used instead. - -```cs -services.AddPublishObserver(); -``` - -```cs -services.AddPublishObserver(provider => new PublishObserver()); -``` - -## Bus - -To observe bus life cycle events, create a class which implements `IBusObserver`, as shown below. - -<<< @/src/MassTransit.Abstractions/Observers/IBusObserver.cs - -To configure a bus observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. - -```cs -services.AddBusObserver(); -``` - -```cs -services.AddBusObserver(provider => new BusObserver()); -``` - -## Receive Endpoint - -<<< @/src/MassTransit.Abstractions/Observers/IReceiveEndpointObserver.cs - -To configure a receive endpoint observer, add it to the container using one of the methods shown below. The factory method version allows customization of the observer creation. - -```cs -services.AddReceiveEndpointObserver(); -``` - -```cs -services.AddReceiveEndpointObserver(provider => new ReceiveEndpointObserver()); -``` - -## State Machine Event - -To observe events consumed by a saga state machine, use an `IEventObserver` where `T` is the saga instance type. - -<<< @/src/MassTransit.Abstractions/SagaStateMachine/IEventObserver.cs - -To configure an event observer, add it to the container using one of the methods shown below. The factory method version allows customization of the -observer creation. - -```cs -services.AddEventObserver>(); -``` - -```cs -services.AddEventObserver(provider => new EventObserver()); -``` - -## State Machine State - -To observe state changes that happen in a saga state machine, use an `IStateObserver` where `T` is the saga instance type. - -<<< @/src/MassTransit.Abstractions/SagaStateMachine/IStateObserver.cs - -To configure a state observer, add it to the container using one of the methods shown below. The factory method version allows customization of the -observer creation. - -```cs -services.AddStateObserver>(); -``` - -```cs -services.AddStateObserver(provider => new StateObserver()); -``` - diff --git a/docs/advanced/scheduling/README.md b/docs/advanced/scheduling/README.md deleted file mode 100644 index e27d54b7332..00000000000 --- a/docs/advanced/scheduling/README.md +++ /dev/null @@ -1,82 +0,0 @@ -# Scheduling - -Time is important, particularly in distributed applications. Sophisticated systems need to schedule things, and MassTransit has extensive scheduling support. - -MassTransit supports two different methods of message scheduling: - -1. Scheduler-based, using either Quartz.NET or Hangfire, where the scheduler runs in a service and schedules messages using a queue. -1. Transport-based, using the transports built-in message scheduling/delay capabilities. In some cases, such as RabbitMQ, this requires an additional plug-in to be installed and configured. - -> Recurring schedules are only supported by scheduler-based solutions (Option 1). - -## Configuration - -Depending upon the scheduling method used, the bus must be configured to use the appropriate scheduler. - - - -<<< @/docs/code/scheduling/SchedulingEndpoint.cs - - - -<<< @/docs/code/scheduling/SchedulingRabbitMQ.cs - - - -<<< @/docs/code/scheduling/SchedulingAzure.cs - - - -<<< @/docs/code/scheduling/SchedulingActiveMQ.cs - - - -<<< @/docs/code/scheduling/SchedulingAmazonSQS.cs - - - -### Using the message scheduler - -To use the message scheduler (outside of a consumer), use _IMessageScheduler_ from the container. - -## Quartz.NET - -To use Quartz.NET, an instance of Quartz.NET must be running and configured to use the message broker. - -### Internal Quartz.NET instance - -MassTransit is able to connect to an existing Quartz.NET instance running in the same executable. - -<<< @/docs/code/scheduling/SchedulingInternalInstance.cs - -::: warning -The code above asumes Quartz.NET is already configured using dependency injection. -::: - -### External Quartz.NET instance - -MassTransit provides a [Docker Image](https://hub.docker.com/r/masstransit/quartz) with Quartz.NET ready-to-run using SQL Server. A complementary [SQL Server Image](https://hub.docker.com/r/masstransit/sqlserver-quartz) configured to run with Quartz.NET is also available. Combined, these images make getting started with Quartz easy. - -### Testing - -Quartz.NET can also be configured in-memory, which is great for unit testing. - -> Uses [MassTransit.Quartz](https://nuget.org/packages/MassTransit.Quartz) - -<<< @/docs/code/scheduling/SchedulingInMemory.cs - -The _UseInMemoryScheduler_ method initializes Quartz.NET for standalone in-memory operation, and configures a receive endpoint named `scheduler`. The _AddMessageScheduler_ adds _IMessageScheduler_ to the container, which will use the same scheduler endpoint. - -::: warning -Using the in-memory scheduler uses non-durable storage. If the process terminates, any scheduled messages will be lost, immediately, never to be found again. For any production system, using a standalone service is recommended with persistent storage. -::: - -## Transport-based - -To configure transport-based message scheduling, refer to the transport-specific section for details. - -* [ActiveMQ](activemq-delayed) -* [Amazon SQS](amazonsqs-scheduler) -* [Azure Service Bus](azure-sb-scheduler) -* [RabbitMQ](rabbitmq-delayed) - diff --git a/docs/advanced/scheduling/activemq-delayed.md b/docs/advanced/scheduling/activemq-delayed.md deleted file mode 100644 index b7c4e4aa52f..00000000000 --- a/docs/advanced/scheduling/activemq-delayed.md +++ /dev/null @@ -1,19 +0,0 @@ -# ActiveMQ - -MassTransit uses the built-in ActiveMQ scheduler to schedule messages. - -::: tip Quartz.NET Docker Image -MassTransit provides a [Docker Image](https://hub.docker.com/r/masstransit/activemq) with ActiveMQ ready to run, including scheduler support. -::: - -### Configuration - -To configure the ActiveMQ message scheduler, see the example below. - -<<< @/docs/code/scheduling/SchedulingActiveMQ.cs - -::: warning -Scheduled messages cannot be canceled when using the ActiveMQ message scheduler -::: - -[1]: https://activemq.apache.org/delay-and-schedule-message-delivery diff --git a/docs/advanced/scheduling/amazonsqs-scheduler.md b/docs/advanced/scheduling/amazonsqs-scheduler.md deleted file mode 100644 index 1bf6d815854..00000000000 --- a/docs/advanced/scheduling/amazonsqs-scheduler.md +++ /dev/null @@ -1,13 +0,0 @@ -# Amazon SQS - -Amazon SQS includes a _DelaySeconds_ property, which can be used to defer message delivery. MassTransit uses this feature to provide _scheduled_ message delivery. - -### Configuration - -To configure the Amazon SQS message scheduler, see the example below. - -<<< @/docs/code/scheduling/SchedulingAmazonSQS.cs - -::: warning -Scheduled messages cannot be canceled when using the Amazon SQS message scheduler -::: diff --git a/docs/advanced/scheduling/azure-sb-scheduler.md b/docs/advanced/scheduling/azure-sb-scheduler.md deleted file mode 100644 index 3acb02e9c16..00000000000 --- a/docs/advanced/scheduling/azure-sb-scheduler.md +++ /dev/null @@ -1,13 +0,0 @@ -# Azure Service Bus - -Azure Service Bus allows the enqueue time of a message to be specified, making it possible to schedule messages without the use of a separate message scheduler. MassTransit uses this feature to schedule messages. - -### Configuration - -To configure the Azure Service Bus message scheduler, see the example below. - -<<< @/docs/code/scheduling/SchedulingAzure.cs - -::: tip -Azure Service Bus supports message cancellation, unlike the other transports. -::: \ No newline at end of file diff --git a/docs/advanced/scheduling/hangfire.md b/docs/advanced/scheduling/hangfire.md deleted file mode 100644 index 0b90590ade3..00000000000 --- a/docs/advanced/scheduling/hangfire.md +++ /dev/null @@ -1,72 +0,0 @@ -# Hangfire Scheduler - -### Configuring Hangfire Scheduler - -::: warning -MassTransit will create own Hangfire Server which will be only listening to its related jobs. -::: - -By default MassTransit is using static Hangfire configuration - -```csharp -//your hangfire configuration -//NOTE: you need to configure hangfire before bus started - -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.UseHangfireScheduler("hangfire", options => { /*configure background server*/ }); -}); -``` - -### Configuring the Hangfire address - -The bus has an internal context that is used to make it so that consumers that need to schedule -messages do not have to be aware of the specific scheduler type being used, or the message scheduler -address. To configure the address, use the extension method shown below. - -```csharp -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.UseMessageScheduler(new Uri("queue:hangfire")); -}); -``` - -Once configured, messages may be scheduled. - -### Advance configuration - -If you are using Hangfire integration with ASP.NET Core or non static configuration you can provide your configuration for MassTransit implementing `IHangfireComponentResolver`. For example using `IServiceProvider`: - -```csharp -public class ServiceProviderHangfireComponentResolver : - IHangfireComponentResolver -{ - readonly IServiceProvider _serviceProvider; - - public ServiceProviderHangfireComponentResolver(IServiceProvider serviceProvider) - { - _serviceProvider = serviceProvider; - } - - public IBackgroundJobClient BackgroundJobClient => _serviceProvider.GetService(); - public IRecurringJobManager RecurringJobManager => _serviceProvider.GetService(); - public ITimeZoneResolver TimeZoneResolver => _serviceProvider.GetService(); - public IJobFilterProvider JobFilterProvider => _serviceProvider.GetService(); - public IEnumerable BackgroundProcesses => _serviceProvider.GetServices(); - public JobStorage JobStorage => _serviceProvider.GetService(); -} - -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Host(new Uri("rabbitmq://localhost/"), h => - { - h.Username("guest"); - h.Password("guest"); - }); - - cfg.UseHangfireScheduler(resolver, "hangfire", options => - { - /*configure background server*/ - }); -}); -``` diff --git a/docs/advanced/scheduling/rabbitmq-delayed.md b/docs/advanced/scheduling/rabbitmq-delayed.md deleted file mode 100644 index d456e94e997..00000000000 --- a/docs/advanced/scheduling/rabbitmq-delayed.md +++ /dev/null @@ -1,19 +0,0 @@ -# RabbitMQ - -MassTransit uses the [RabbitMQ delayed exchange][1] plug-in to schedule messages. - -::: tip RabbitMQ Docker Image -MassTransit provides a [Docker Image](https://hub.docker.com/r/masstransit/rabbitmq) with RabbitMQ ready to run, including the delayed exchange plug-in. -::: - -### Configuration - -To configure the delayed exchange message scheduler, see the example below. - -<<< @/docs/code/scheduling/SchedulingRabbitMQ.cs - -::: warning -Scheduled messages cannot be canceled when using the delayed exchange message scheduler. -::: - -[1]: https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/ diff --git a/docs/advanced/scheduling/redeliver.md b/docs/advanced/scheduling/redeliver.md deleted file mode 100644 index 4b1eba6dac9..00000000000 --- a/docs/advanced/scheduling/redeliver.md +++ /dev/null @@ -1,71 +0,0 @@ -# Scheduled Redelivery - -> This page may be removed in the future, for more detail on handling exceptions is available at the [Handling exceptions](/usage/exceptions) page. - -There are situations where a message cannot be processed, either due to an unavailable resource or a situation in which message ordering is important (you should try not to depend upon message order, but sometimes it is an easy workaround). In these situations, scheduling a message for redelivery is a powerful tool. - -### Retry using scheduled redelivery - -Handling exceptions (described in detail on the page linked above) includes the ability to use scheduled redelivery for longer retry delays when immediate or short delay retries fail. - -### Explicit message redelivery - -MassTransit makes it easy to schedule messages for redelivery. In the example below, the Quartz service is running as a separate service on the */quartz* queue. - -```csharp -public class UpdateCustomerAddressConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - try - { - // try to update the database - } - catch (CustomerNotFoundException exception) - { - // schedule redelivery in one minute - context.Redeliver(TimeSpan.FromMinutes(1)); - } - } -} -``` - -To enable the `Redeliver` method, the Quartz endpoint must be setup on the bus. - -```csharp -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.UseMessageScheduler(new Uri("queue:quartz")); -}); -``` - -### Redelivery with RabbitMQ - -It is also possible to use RabbitMQ Delayed Exchange to redeliver messages. You can even use it if the delayed exchange is not [configured](rabbitmq-delayed) as a default message scheduler. - -You can schedule a message to be redelivered via RabbitMQ delayed exchange from a consumer by using the `Defer` extension method like this: - -```csharp -public async Task Consume(ConsumeContext context) -{ - try - { - // try to update the database - } - catch (CustomerNotFoundException exception) - { - // schedule redelivery in one minute - context.Defer(TimeSpan.FromMinutes(1)); - } -} -``` - - -The redelivered message includes two additional message headers: - -#### MT-Redelivery-Count - The number of redelivery attempts the message has had. The first attempt is number 1. - -#### MT-Scheduling-DeliveredAddress - The address where the message was last delivered and subsequently scheduled for redelivery. diff --git a/docs/advanced/scheduling/scheduling-api.md b/docs/advanced/scheduling/scheduling-api.md deleted file mode 100644 index b59b4e2ce47..00000000000 --- a/docs/advanced/scheduling/scheduling-api.md +++ /dev/null @@ -1,53 +0,0 @@ -# Schedule Messages - -### From a Consumer - -To schedule messages from a consumer, use any of the _ConsumeContext_ extension methods, such as _ScheduleSend_, to schedule messages. - -<<< @/docs/code/scheduling/SchedulingConsumeContext.cs - -The message scheduler, specified during bus configuration, will be used to schedule the message. - -### From a Bus - -To schedule messages from a bus, use _IMessageScheduler_ from the container (or create a new one using the bus and appropriate scheduler). - -<<< @/docs/code/scheduling/SchedulingScheduler.cs - -### Recurring Messages - -You can also schedule a message to be send to you periodically. This functionality uses the Quartz.Net periodic -schedule feature and requires some knowledge of cron expressions. - -To request a recurring message, you need to use `ScheduleRecurringSend` extension method, which is available -for both `Context` and `SendEndpoint`. This message requires a schedule object as a parameter, which must -implement `RecurringSchedule` interface. Since this interface is rather broad, you can use the default -abstract implementation `DefaultRecurringSchedule` as the base class for your own schedule. - -```csharp -public class PollExternalSystemSchedule : DefaultRecurringSchedule -{ - public PollExternalSystemSchedule() - { - CronExpression = "0 0/1 * 1/1 * ? *"; // this means every minute - } -} - -public class PollExternalSystem {} -``` - -```csharp -var schedulerEndpoint = await bus.GetSendEndpoint(_schedulerAddress); - -var scheduledRecurringMessage = await schedulerEndpoint.ScheduleRecurringSend( - InputQueueAddress, new PollExternalSystemSchedule(), new PollExternalSystem()); -``` - -When you stop your service or just have any other need to tell Quartz service to stop sending you -these recurring messages, you can use the return value of `ScheduleRecurringSend` to cancel the recurring schedule. - -```csharp -await bus.CancelScheduledRecurringMessage(scheduledRecurringMessage); -``` - -You can also cancel using schedule id and schedule group values, which are part of the recurring schedule object. diff --git a/docs/advanced/signalr/README.md b/docs/advanced/signalr/README.md deleted file mode 100644 index b1d2b9ad797..00000000000 --- a/docs/advanced/signalr/README.md +++ /dev/null @@ -1,22 +0,0 @@ -# SignalR Backplane - -MassTransit offers a package which provides an easy option to get a SignalR Backplane up and running in with just a few lines of configuration. We won't go over the concept of a SignalR Backplane, more details can be found out about it [here](https://docs.microsoft.com/en-us/aspnet/signalr/overview/performance/scaleout-in-signalr). This page is old, and references the .NET Framework SignalR, but the concepts of scale out are the same for the newer .NET Core SignalR. - -**.NET Framework SignalR _(which MassTransit does not support)_ Backplane Options:** -* SQLServer -* Redis -* Azure Service Bus - -**.NET Core SignalR (which MassTransit _WILL_ work for) Backplane Options:** -* Redis (official) -* Azure SignalR Service (official) -* MassTransit (unofficial) - * RabbitMq - * ActiveMq - * Azure Service Bus - -* [Quick Start](quickstart.md) -* [Hub Endpoints](hub_endpoints.md) -* [Interop](interop.md) -* [Sample](sample.md) -* [Considerations](considerations.md) diff --git a/docs/advanced/signalr/considerations.md b/docs/advanced/signalr/considerations.md deleted file mode 100644 index 6b1ec9191f2..00000000000 --- a/docs/advanced/signalr/considerations.md +++ /dev/null @@ -1,6 +0,0 @@ -# Considerations - -* [Sticky Sessions is required, unless you force Websockets only](https://github.com/aspnet/SignalR/issues/2002#issuecomment-383622076) - * [Also a good read](https://rolandguijt.com/scaling-out-your-asp-net-core-signalr-application/) -* Although [this page](https://docs.microsoft.com/en-us/aspnet/signalr/overview/performance/scaleout-in-signalr) is written for the old SignalR, the scale out concepts still apply. -* Having a single hub is fine, but only use multiple hubs [for organization, not performance](https://stackoverflow.com/a/22151160). \ No newline at end of file diff --git a/docs/advanced/signalr/hub_endpoints.md b/docs/advanced/signalr/hub_endpoints.md deleted file mode 100644 index ef6f164dfa9..00000000000 --- a/docs/advanced/signalr/hub_endpoints.md +++ /dev/null @@ -1,15 +0,0 @@ -# Hub Endpoints - -The core of communication contracts between the client and server are hubs. Depending on your application and complexity you might have a few hubs as a separation of concern for your application. The backplanes work through 5 types of events **per hub**. - -So this translated well into MassTransit Events: - -* `All` - Invokes the method (with args) for each connection on the specified hub -* `Connection` - Invokes the method (with args) for the specific connection -* `Group` - Invokes the method (with args) for all connections belonging to the specified group -* `GroupManagement` - Adds or removes a connection to the group (on a remote server) -* `User` - Invokes the method (with args) for all connections belonging to the specific user id - -So each of these Messages has a corresponding consumer, and it will get a `HubLifetimeManager` through DI to perform the specific task. - -MassTransit's helper extension method will create an endpoint per consumer per hub, which follows the typical recommendation of one consumer per endpoint. Because of this, the number of endpoints can grow quickly if you have many hubs. It's best to also read some [SignalR Limitations](https://docs.microsoft.com/en-us/aspnet/signalr/overview/performance/scaleout-in-signalr#limitations), to understand what can become potential bottlenecks with SignalR and your backplane. SignalR recommends re-thinking your strategy for very high throughput, real-time applications (video games). diff --git a/docs/advanced/signalr/interop.md b/docs/advanced/signalr/interop.md deleted file mode 100644 index 991083ded5e..00000000000 --- a/docs/advanced/signalr/interop.md +++ /dev/null @@ -1,52 +0,0 @@ -# Interop - -The nice thing about using MassTransit as the back end is we can interact with the backplane by publishing the appropriate message (with hub). - - I can't think of a scenario you would ever publish `GroupManagement`. Only `All`, `Connection`, `Group`, and `User` should be used. - -To publish a message from a back end service (eg. console app, Topshelf): - -```csharp -await busControl.Publish>(new -{ - Messages = protocols.ToProtocolDictionary("broadcastMessage", new object[] { "backend-process", "Hello" }) -}); -``` -You are done! - -## Complex Hubs - -Your ASP.NET Core might have complex Hubs, with multiple interfaces injected. - -```csharp -public class ProductHub : Hub -{ - public ProductHub( - IService1 service1, - IService2 service2, - ICache cache, - IMapper mapper - ) - { - //... - } - - // Hub Methods... -} -``` - -Your back end service might exist in a separate project and namespace, with no knowledge of the hubs or injected services. Because MassTransit routes messages by namespace+message, I recommend to create a marker hub(s) within your back end service just for use of publishing. This saves you having to have all the hub(s) injected dependencies also within your back end service. - -```csharp -namespace YourNamespace.Should.Match.The.Hubs -{ - public class ProductHub : Hub - { - // That's it, nothing more needed. - } -} -``` - -## Protocol Dictionary - -SignalR supports multiple protocols for communicating with the Hub, the "serialized message" that is sent over the backplane is translated for each protocol method supported. The Extension method `.ToProtocolDictionary(...)` helps facilitate this translation into the protocol for communication. diff --git a/docs/advanced/signalr/quickstart.md b/docs/advanced/signalr/quickstart.md deleted file mode 100644 index fa6d98db16b..00000000000 --- a/docs/advanced/signalr/quickstart.md +++ /dev/null @@ -1,35 +0,0 @@ -# Quickstart - -In your ASP.NET Core Startup.cs file add the following - -```csharp -public void ConfigureServices(IServiceCollection services) -{ - // other config... - - services.AddSignalR(); - - // Other config perhaps... - - // creating the bus config - services.AddMassTransit(x => - { - // Add this for each Hub you have - x.AddSignalRHub(cfg => {/*Configure hub lifetime manager*/}); - - x.UsingRabbitMq((context, cfg) => - { - cfg.Host("localhost", "/", h => - { - h.Username("guest"); - h.Password("guest"); - }); - - // register consumer' and hub' endpoints - cfg.ConfigureEndpoints(context); - })); - }); -} -``` - -There you have it. All the consumers needed for the backplane are added to a temporary endpoint. ReceiveEndpoints without any queue name are considered Non Durable, and Auto Deleting. diff --git a/docs/advanced/signalr/sample.md b/docs/advanced/signalr/sample.md deleted file mode 100644 index f25db8da330..00000000000 --- a/docs/advanced/signalr/sample.md +++ /dev/null @@ -1,38 +0,0 @@ -# Sample - -We've included a sample ASP.NET Core project, and back end console application to show interoperability with the backplane. The only thing needed is RabbitMQ. I'd recommend using their [docker image](https://store.docker.com/community/images/library/rabbitmq) to spin up the broker. - -## Sample-SignalR - -You can view the [MassTransit Sample here](https://github.com/MassTransit/Sample-SignalR). The sample was based off of [Microsoft's chat sample](https://github.com/aspnet/SignalR-samples/tree/master/ChatSample), which is nearly identical to the [tutorial here](https://docs.microsoft.com/en-us/aspnet/core/tutorials/signalr?view=aspnetcore-2.2&tabs=visual-studio), except the only different is it's stripped down to the bare minimum (no razor Pages, bootstrap or JQuery libraries). - -The other difference is the Javascript client callback method name is "ReceiveMessage" versus "broadcastMessage", but both samples are nearly the same. and the hub route is /chat versus /chatHub. - -The other addition we added is in the Properties/launchSettings.json, which lets us start 2 profiles on different ports. Then helps simulate horizontal scaling. - -### Mvc Sample - -You can simulate scaleout by running the two profiles. - -``` -> cd (your cloned Sample-SignalR)\src\SampleSignalR.Mvc -> dotnet run --launch-profile sample1 -> dotnet run --launch-profile sample2 -``` - -Now in two browser tabs, open up in each: -http://localhost:5100 -http://localhost:5200 - -Then you can type a message in each, and see them show up in the other. The backplane works!! - -## Console Sample - -If you have some back end services (console apps, or Mt Topshelf consumers), you might want to notify users/groups of things that have happened in real time. You can do this by running this console app. - -``` -> cd (your cloned Sample-SignalR)\src\SampleSignalR.Service -> dotnet run -``` - -An type in a message to broadcast to all connections. You will see the message in your browsers chat messages diff --git a/docs/advanced/topology/README.md b/docs/advanced/topology/README.md deleted file mode 100644 index 5994c90b714..00000000000 --- a/docs/advanced/topology/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Topology - -In MassTransit, _Topology_ is how message types are used to configure broker topics (exchanges in RabbitMQ) and queues. Topology is also used to access specific broker capabilities, such as RabbitMQ direct exchanges and routing keys. - -Topology is separate from the send, publish, and consume pipelines which are focused more on middleware inside MassTransit. Topology allows conventions to be created that can create message-specific topology configuration at runtime as messages are published and sent. - -### Bus Topology - -Once the bus is created, access to topology is via the _Topology_ property on _IBus_. The _IBusTopology_ interface is shown below. - -<<< @/src/MassTransit.Abstractions/Topology/IBusTopology.cs - -The message, publish, and send topologies can be accessed using this interface. It is also possible to retrieve a message's publish address. The _Topology_ property may support other interfaces, such as a transport-specific host topology. Pattern matching can be used to check the host topology type as shown below. - -<<< @/docs/code/advanced/BusHostTopologyMatch.cs - diff --git a/docs/advanced/topology/consume.md b/docs/advanced/topology/consume.md deleted file mode 100644 index 218967cb168..00000000000 --- a/docs/advanced/topology/consume.md +++ /dev/null @@ -1,5 +0,0 @@ -# Consume Topology - -Each receive endpoint has a consume topology, which is configured as consumers are added. Depending upon the transport, additional methods may be available to support exchange bindings, topic subscriptions, etc. - -The consume topology uses the publish topology to ensure consistent naming of exchanges/topics for message types. diff --git a/docs/advanced/topology/conventions.md b/docs/advanced/topology/conventions.md deleted file mode 100644 index bb70e24dc46..00000000000 --- a/docs/advanced/topology/conventions.md +++ /dev/null @@ -1,40 +0,0 @@ -# Topology Conventions - -Conventions are used to apply topology to messages without requiring explicit configuration of every message type. - -A basic example of a convention is the default `CorrelationId` convention, which is automatically applied to all sent messages. As message types are sent, the convention is used to determine if the message contains a property that could be considered a CorrelationId, and uses that property to set the `CorrelationId` header on the message envelope. - -For example, the following message contains a property named `CorrelationId`, which is an obvious choice. Note that the `CorrelatedBy` interface is not part of the message contract. - -```csharp -public record OrderCreated -{ - public Guid CorrelationId { get; init; } -} -``` - -If there isn't a property named `CorrelationId`, the convention also checks for `CommandId` and `EventId` and uses that property to set the header value (the type must be a Guid, or a Guid?, no magic type conversion happening here). - -If the message implements the `CorrelatedBy` interface, that would be used before referencing any properties by name. - -During bus creation, it is possible to explicitly configure a message type (or any of the message type's inherited interfaces) to use a specific property for the `CorrelationId`. In the example below, the OrderId property is specified as the CorrelationId. - -```csharp -public record OrderSubmitted -{ - public Guid OrderId { get; init; } - public Guid CustomerId { get; init; } -} - -Bus.Factory.CreateUsingRabbitMq(..., cfg => -{ - cfg.Send(x => - { - x.UseCorrelationId(context => context.Message.OrderId); - }); -}); -``` - -The CorrelationId topology convention is [implemented here](https://github.com/MassTransit/MassTransit/tree/develop/src/MassTransit/Topology/Conventions/CorrelationId), which can be used as an example of how to create your own conventions, or add additional CorrelationId detectors to the existing convention. - -> Send topologies are applied to all outbound messages, regardless of whether they are _sent_ or _published_. diff --git a/docs/advanced/topology/deploy.md b/docs/advanced/topology/deploy.md deleted file mode 100644 index 6037660bee5..00000000000 --- a/docs/advanced/topology/deploy.md +++ /dev/null @@ -1,11 +0,0 @@ -# Deploy Topology - -There are some scenarios, such as when using Azure Functions, where it may be necessary to deploy the topology to the broker separately, without actually starting the service (and thereby consuming messages). To support this, MassTransit has a `DeployTopologyOnly` flag that can be specified when configuring the bus. When used with the `DeployAsync` method, a simple console application can be created that creates all the exchanges/topics, queues, and subscriptions/bindings. - -To deploy the broker topology using a console application, see the example below. - -<<< @/docs/code/containers/MicrosoftDeployTopology.cs - - - - diff --git a/docs/advanced/topology/message.md b/docs/advanced/topology/message.md deleted file mode 100644 index 10184c812c9..00000000000 --- a/docs/advanced/topology/message.md +++ /dev/null @@ -1,129 +0,0 @@ -# Message Topology - -Message types are extensively leveraged in MassTransit, so making it easy to configure how those message types are used by topology seemed obvious. - -## Entity Name Formatters - -### Message Type Entity Name Formatting - -MassTransit has built-in defaults for naming messaging entities (these are things like exchanges, topics, etc.). The defaults can be overridden as well. For instance, to change the topic name used by a message, just do it! - -```csharp -Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Message(x => - { - x.SetEntityName("omg-we-got-one"); - }); -}); -``` - -It's also possible to create a message-specific entity name formatter, by implementing `IMessageEntityNameFormatter` and specifying it during configuration. - -```csharp -class FancyNameFormatter : - IMessageEntityNameFormatter -{ - public string FormatEntityName() - { - // seriously, please don't do this, like, ever. - return type(T).Name.ToString(); - } -} - -Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Message(x => - { - x.SetEntityNameFormatter(new FancyNameFormatter()); - }); -}); -``` - -It's also possible to replace the entity name formatter for the entire topology. - -```csharp -class FancyNameFormatter : - IMessageEntityNameFormatter -{ - public string FormatEntityName() - { - // seriously, please don't do this, like, ever. - return type(T).Name.ToString(); - } -} - -class FancyNameFormatter : - IEntityNameFormatter -{ - public FancyNameFormatter(IEntityNameFormatter original) - { - _original = original; - } - - public string FormatEntityName() - { - if(T is OrderSubmitted) - return "we-got-one"; - - return _original.FormatEntityName(); - } -} - -Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Message(x => - { - x.SetEntityNameFormatter(new FancyNameFormatter(cfg.MessageTopology.EntityNameFormatter));; - }); -}); -``` - -## Attributes - -### EntityName Attribute - -_EntityName_ is an optional attribute used to override the default entity name for a message type. If present, the entity name will be used when creating the topic or exchange for the message. - -```cs -[EntityName("order-submitted")] -public record LegacyOrderSubmittedEvent -{ -} -``` - -### ConfigureConsumeTopology Attribute - -_ConfigureConsumeTopology_ is an optional attribute that may be specified on a message type to indicate whether the topic or exchange for the message type should be created and subscribed to the queue when consumed on a receive endpoint. - -```cs -[ConfigureConsumeTopology(false)] -public record DeleteRecord -{ -} -``` - -### ExcludeFromTopology Attribute - -_ExcludeFromTopology_ is an optional attribute that may be specified on a message type to indicate whether the topic or exchange for the message type should be created when publishing an implementing type or sub-type. In the example below, publishing the `ReformatHardDrive` command would not create the `ICommand` topic or exchange on the message broker. - -```cs -[ExcludeFromTopology] -public interface ICommand -{ -} - -public record ReformatHardDrive : - ICommand -{ -} -``` - -To avoid using the property, the publish topology can be configured along with the bus: - -```cs -...UsingRabbitMq((context,cfg) => -{ - cfg.Publish(p => p.Exclude = true); -}); -``` diff --git a/docs/advanced/topology/publish.md b/docs/advanced/topology/publish.md deleted file mode 100644 index a850955dc24..00000000000 --- a/docs/advanced/topology/publish.md +++ /dev/null @@ -1,15 +0,0 @@ -# Publish Topology - -Topology is a key part of publishing messages, and is responsible for how the broker's facilities are configured. - -The publish topology defines many aspects of broker configuration, including: - -- RabbitMQ Exchange names or Azure Service Bus Topic names - - Formatted, based upon the message type - - Explicit, based upon the configuration -- RabbitMQ Exchange Bindings or Azure Service Bus Topic Subscriptions - -When `Publish` is called, the topology is also used to: - -- Populate the `RoutingKey` of the message sent to the RabbitMQ exchange -- Populate the `PartitionId` or `SessionId` of the message sent to the Azure Service Bus topic \ No newline at end of file diff --git a/docs/advanced/topology/rabbitmq.md b/docs/advanced/topology/rabbitmq.md deleted file mode 100644 index bbe62cc1622..00000000000 --- a/docs/advanced/topology/rabbitmq.md +++ /dev/null @@ -1,247 +0,0 @@ -# RabbitMQ - -The send and publish topologies are extended to support RabbitMQ features, and make it possible to configure how exchanged are created. - -## Exchanges - -When a message is published, MassTransit sends it to an exchange that is named based upon the message type. Using topology, the exchange name, as well as the exchange properties can be configured to support a custom behavior. - -To configure the properties used when an exchange is created, the publish topology can be configured during bus creation: - -<<< @/docs/code/topology/TopologyRabbitMqPublish.cs - -### Exchange Layout - -In versions of MassTransit prior to 4.x, every implemented type was connected directly to the top-level exchange for the published message type. Starting with v4.0, the broker topology for inherited types can be configured to maintain the type hierarchy, which can significantly reduce the number of exchange bindings in some cases. To configure this new behavior, the publish topology is used to specify the broker topology option. - -```csharp -Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.PublishTopology.BrokerTopologyOptions = PublishBrokerTopologyOptions.MaintainHierarchy; -}); -``` - -### Exchange Binding - -To bind an exchange to a receive endpoint: - -```csharp -cfg.ReceiveEndpoint("input-queue", e => -{ - e.Bind("exchange-name"); - e.Bind(); -}) -``` - -The above will create two exchange bindings, one between the `exchange-name` exchange and the `input-queue` exchange and a second between the exchange name matching the `MessageType` and the same `input-queue` exchange. - -The properties of the exchange binding may also be configured: - -```csharp -cfg.ReceiveEndpoint("input-queue", e => -{ - e.Bind("exchange-name", x => - { - x.Durable = false; - x.AutoDelete = true; - x.ExchangeType = "direct"; - x.RoutingKey = "8675309"; - }); -}) -``` - -The above will create an exchange binding between the `exchange-name` and the `input-queue` exchange, using the configured properties. - -## RoutingKey - -The routing key on published/sent messages can be configured by convention, allowing the same method to be used for messages which implement a common interface type. If no common type is shared, each message type may be configured individually using various conventional selectors. Alternatively, developers may create their own convention to fit their needs. - -When configuring a bus, the send topology can be used to specify a routing key formatter for a particular message type. - -```csharp -public record SubmitOrder -{ - public string CustomerType { get; init; } - public Guid TransactionId { get; init; } - // ... -} - -Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Send(x => - { - // use customerType for the routing key - x.UseRoutingKeyFormatter(context => context.Message.CustomerType); - - // multiple conventions can be set, in this case also CorrelationId - x.UseCorrelationId(context => context.Message.TransactionId); - }); - //Keeping in mind that the default exchange config for your published type will be the full typename of your message - //we explicitly specify which exchange the message will be published to. So it lines up with the exchange we are binding our - //consumers too. - cfg.Message(x => x.SetEntityName("submitorder")); - //Also if your publishing your message: because publishing a message will, by default, send it to a fanout queue. - //We specify that we are sending it to a direct queue instead. In order for the routingkeys to take effect. - cfg.Publish(x => x.ExchangeType = ExchangeType.Direct); -}); -``` - -The consumer could then be created: - -```csharp -public class OrderConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - - } -} -``` - -And then connected to a receive endpoint: - -```csharp -Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.ReceiveEndpoint("priority-orders", x => - { - x.ConfigureConsumeTopology = false; - - x.Consumer(); - - x.Bind("submitorder", s => - { - s.RoutingKey = "PRIORITY"; - s.ExchangeType = ExchangeType.Direct; - }); - }); - - cfg.ReceiveEndpoint("regular-orders", x => - { - x.ConfigureConsumeTopology = false; - - x.Consumer(); - - x.Bind("submitorder", s => - { - s.RoutingKey = "REGULAR"; - s.ExchangeType = ExchangeType.Direct; - }); - }); -}); -``` - -This would split the messages sent to the exchange, by routing key, to the proper endpoint, using the CustomerType property. - -## Addressing - -Query string parameters supported: - -### RabbitMQ Query Parameters - -| Parameter | Type | Description | Implies | -| ------------- |-------|---------- |---------| -| temporary | bool | Temporary endpoint | durable = false, autodelete = true -| durable | bool | Save messages to disk | -| autodelete | bool | Delete when bus is stopped | -| bind | bool | Bind exchange to queue | -| queue | string| Bind to queue name | bind = true - - -## Broker Topology - -In this example topology, two commands and events are used. - -First, the event contracts that are supported by an endpoint that receives files from a customer. - -```csharp -public interface FileReceived -{ - Guid FileId { get; } - DateTime Timestamp { get; } - Uri Location { get; } -} - -public interface CustomerDataReceived -{ - DateTime Timestamp { get; } - string CustomerId { get; } - string SourceAddress { get; } - Uri Location { get; } -} -``` - -Second, the command contract for processing a file that was received. - -```csharp -public interface ProcessFile -{ - Guid FileId { get; } - Uri Location { get; } -} -``` - -The above contracts are used by the consumers to receive messages. From a publishing or sending perspective, two classes are created by the event producer and the command sender which implement these interfaces. - -```csharp -public record FileReceivedEvent : - FileReceived, - CustomerDataReceived -{ - public Guid FileId { get; init; } - public DateTime Timestamp { get; init; } - public Uri Location { get; init; } - public string CustomerId { get; init; } - public string SourceAddress { get; init; } -} -``` - -And the command class. - -```csharp -public record ProcessFileCommand : - ProcessFile -{ - public Guid FileId { get; init; } - public Uri Location { get; init; } -} -``` - -The consumers for these message contracts are as below. - -```csharp -class FileReceivedConsumer : - IConsumer -{ -} - -class CustomerAuditConsumer : - IConsumer -{ -} - -class ProcessFileConsumer : - IConsumer -{ -} -``` - -### Publish - -The exchanges and queues configures for the event example are as shown below. - -> MassTransit publishes messages to the message type exchange, and copies are routed to all the subscribers by RabbitMQ. This approach was [based on an article][2] on how to maximize routing performance in RabbitMQ. - -[2]: http://spring.io/blog/2011/04/01/routing-topologies-for-performance-and-scalability-with-rabbitmq/ - -![rabbitmq-publish-topology](/rabbitmq-publish-topology.png) - -### Send - -The exchanges and queues for the send example are shown. - -![rabbitmq-send-topology](/rabbitmq-send-topology.png) - -> Note that the broker topology can now be configured using the [topology](../topology/README.md) API. - diff --git a/docs/advanced/topology/send.md b/docs/advanced/topology/send.md deleted file mode 100644 index fd7c9ac7fda..00000000000 --- a/docs/advanced/topology/send.md +++ /dev/null @@ -1,7 +0,0 @@ -# Send Topology - -Topology does not cover sending messages beyond delivering messages to a queue. MassTransit sends messages via a _send endpoint_, which is retrieved using the endpoint's address only. - -The exception to this is when the transport supports additional capabilities on send, such as the partitioning of messages. With RabbitMQ this would include specifying the `RoutingKey`, and with Azure Service Bus this would include specifying the `PartitionId` or the `SessionId`. - -> Topology cannot alter the destination of a message, only the properties of the message delivery itself. Determining the path of a message is routing, which is handled separately. diff --git a/docs/advanced/topology/servicebus.md b/docs/advanced/topology/servicebus.md deleted file mode 100644 index 31b4ae6b6f2..00000000000 --- a/docs/advanced/topology/servicebus.md +++ /dev/null @@ -1,101 +0,0 @@ -# Azure Service Bus - -The send and publish topologies are extended to support the Azure Service Bus features, and make it possible to configure how topics are created. - -## Topics - -To specify properties used when a topic is created, the publish topology can be configured during bus creation: - -```csharp -Bus.Factory.CreateUsingAzureServiceBus(cfg => -{ - cfg.Publish(x => - { - x.EnablePartitioning = true; - }); -}); -``` - -## PartitionKey - -The PartitionKey on published/sent messages can be configured by convention, allowing the same method to be used for messages which implement a common interface type. If no common type is shared, each message type may be configured individually using various conventional selectors. Alternatively, developers may create their own convention to fit their needs. - -When configuring a bus, the send topology can be used to specify a routing key formatter for a particular message type. - -```csharp -public record SubmitOrder -{ - public string CustomerId { get; init; } - public Guid TransactionId { get; init; } -} - -Bus.Factory.CreateUsingAzureServiceBus(cfg => -{ - cfg.Send(x => - { - x.UsePartitionKeyFormatter(context => context.Message.CustomerId); - }); -}); -``` - -## SessionId - -The SessionId on published/sent messages can be configured by convention, allowing the same method to be used for messages which implement a common interface type. If no common type is shared, each message type may be configured individually using various conventional selectors. Alternatively, developers may create their own convention to fit their needs. - -When configuring a bus, the send topology can be used to specify a routing key formatter for a particular message type. - -```csharp -public record UpdateUserStatus -{ - public Guid UserId { get; init; } - public string Status { get; init; } -} - -Bus.Factory.CreateUsingAzureServiceBus(cfg => -{ - cfg.Send(x => - { - x.UseSessionIdFormatter(context => context.Message.UserId); - }); -}); -``` - -## Subscriptions - -In Azure, topics and topic subscriptions provide a mechanism for one-to-many communication (versus queues that are designed for one-to-one). A topic subscription acts as a virtual queue. To subscribe to a topic subscription directly the `SubscriptionEndpoint` should be used: - -```csharp -cfg.SubscriptionEndpoint("subscription-name", e => -{ - e.ConfigureConsumer(provider); -}) -``` - -Note that a topic subscription's messages can be forwarded to a receive endpoint (an Azure Service Bus queue), in the following way. Behind the scenes MassTransit is setting up [Service Bus Auto-forwarding](https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-auto-forwarding) between a topic subscription and a queue. - -```csharp -cfg.ReceiveEndpoint("input-queue", e => -{ - e.Subscribe("topic-name"); - e.Subscribe(); -}) -``` - -The properties of the topic subscription may also be configured: - -```csharp -cfg.ReceiveEndpoint("input-queue", e => -{ - e.Subscribe("topic-name", x => - { - x.AutoDeleteOnIdle = TimeSpan.FromMinutes(60); - }); -}) -``` - -### Broker Topology - -The topics, queues, and subscriptions configured on Azure Service Bus are shown below. - -![azure-topology](/azure-topology.png) - diff --git a/docs/advanced/transactional-outbox.md b/docs/advanced/transactional-outbox.md deleted file mode 100644 index 11f2df4957d..00000000000 --- a/docs/advanced/transactional-outbox.md +++ /dev/null @@ -1,157 +0,0 @@ -# Transactional Outbox - -It is common that a service may need to combine database writes with publishing events and/or sending commands. And in this scenario, it is usually desirable to do this atomically in a transaction. However, message brokers typically do not participate in transactions. Even if a message broker did support transactions, it would require two-phase commit (2PC) which should be avoided whenever possible. - -> While MassTransit has long provided an [in-memory outbox](/articles/outbox), there has often been criticism that it isn't a _real_ outbox. And while I have proven that it works, is reliable, and is extremely fast (broker message delivery speed), it does require care to ensure operations are idempotent and when an idempotent operation is detected events are republished. The in-memory outbox also does not function as an _inbox_, so exactly-once message delivery is not supported. - -The Transactional Outbox has two main components: - -- The **Bus Outbox** works within a container scope (such as the scope created for an ASP.NET Controller) and adds published and sent messages to the specified `DbContext`. Once the changes are saved, the messages are available to the delivery service which delivers them to the broker. - -- The **Consumer Outbox** is a combination of an _inbox_ and an _outbox_. The _inbox_ is used to keep track of received messages to guarantee exactly-once consumer behavior. The _outbox_ is used to store published and sent messages until the consumer completes successfully. Once completed, the stored messages are delivered to the broker after which the received message is acknowledged. The Consumer Outbox works with all consumer types, including Consumers, Sagas, and Courier Actvities. - -Either of these components can be used independently or both at the same time. - -### Bus Outbox Behavior - -Normally when messages are published or sent they are delivered directly to the message broker: - -![Delivery to Broker](/write-to-broker.png "Delivery to Broker") - -When the bus outbox is configured, the scoped interfaces are replaced with versions that write to the outbox. Since `ISendEndpointProvider` and `IPublishEndpoint` are registered as scoped in the container, they are able to share the same scope as the `DbContext` used by the application. - -![Delivery to Outbox](/write-to-outbox.png "Delivery to Outbox") - -Once the changes are saved in the `DbContext` (typically by the application calling `SaveChangesAsync`), the messages will be written to the database as part of the transaction and will be available to the delivery service. - -The delivery service queries the `OutboxMessage` table for messages published or sent via the Bus Outbox, and attempts to deliver any messages found to the message broker. - -![Delivery to Broker](/outbox-to-broker.png "Delivery to Broker") - -The delivery service uses the _OutboxState_ table to ensure that messages are delivered to the broker in the order they were published/sent. The _OutboxState_ table is also used to lock messages so that multiple instances of the delivery service can coexist without conflict. - -### Consumer Outbox Behavior - -Normally, when messages are published or sent by a consumer or one of its dependencies they are delivered directly to the message broker: - -![Regular Consumer Behavior](/consumer-regular.png "Regular Consumer Behavior") - -When the outbox is configured, the behavior changes. As a message is received, the _inbox_ is used to lock the message by `MessageId`. - -![Consumer Inbox](/consumer-inbox.png "Consumer Inbox") - -When the consumer publishes or sends a message, instead of being delivered to the broker it is stored in the _OutboxMessage_ table. - -![Inbox to Outbox](/inbox-outbox.png "Inbox to Outbox") - -Once the consumer completes and the messages are saved to the outbox, those messages are delivered to the message broker in the order they were produced. - -![Deliver Outbox to Broker](/inbox-outbox-broker.png "Deliver Outbox to Broker") - -If there are issues delivering the messages to the broker, message retry will continue to attempt message delivery. - -### Entity Framework Outbox - -The Transactional Outbox for Entity Framework Core uses three tables in the `DbContext` to store messages that are subsequently delivered to the message broker. - -| Table | Description | -| -----------------|-------| -| InboxState | Tracks received messages by `MessageId` for each endpoint | -| OutboxMessage | Stores messages published or sent using `ConsumeContext`, `IPublishEndpoint`, and `ISendEndpointProvider` | -| OutboxState | Tracks delivery of outbox messages by the delivery service (similar to _InboxState_ but for message sent outside of a consumer via the bus outbox) | - -### Configuration - -> The code below is based upon the [sample application](https://github.com/MassTransit/Sample-Outbox) - -The outbox components are included in the `MassTransit.EntityFrameworkCore` NuGet packages. The code below configures both the bus outbox and the consumer outbox using the default settings. In this case, PostgreSQL is the database engine. - -```cs -x.AddEntityFrameworkOutbox(o => -{ - // configure which database lock provider to use (Postgres, SqlServer, or MySql) - o.UsePostgres(); - - // enable the bus outbox - o.UseBusOutbox(); -}); -``` - -To configure the _DbContext_ with the appropriate tables, use the extension methods shown below: - -```cs -public class RegistrationDbContext : - DbContext -{ - public RegistrationDbContext(DbContextOptions options) - : base(options) - { - } - - protected override void OnModelCreating(ModelBuilder modelBuilder) - { - base.OnModelCreating(modelBuilder); - - modelBuilder.AddInboxStateEntity(); - modelBuilder.AddOutboxMessageEntity(); - modelBuilder.AddOutboxStateEntity(); - } -} -``` - -To configure the outbox on a receive endpoint, configure the receive endpoint as shown below. The configuration below uses a `SagaDefinition` to configure the receive endpoint, which is added to MassTransit along with the saga state machine. - -```cs -public class RegistrationStateDefinition : - SagaDefinition -{ - readonly IServiceProvider _provider; - - public RegistrationStateDefinition(IServiceProvider provider) - { - _provider = provider; - } - - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, - ISagaConfigurator consumerConfigurator) - { - endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 500, 1000, 1000, 1000, 1000, 1000)); - - endpointConfigurator.UseEntityFrameworkOutbox(_provider); - } -} -``` - -The definition is added with the saga state machine: - -```cs -x.AddSagaStateMachine() - .EntityFrameworkRepository(r => - { - r.ExistingDbContext(); - r.UsePostgres(); - }); -``` - -The Entity Framework outbox adds a hosted service which removes delivered _InboxState_ entries after the _DuplicateDetectionWindow_ has elapsed. The Bus Outbox includes an additional hosted service that delivers the outbox messages to the broker. - -### Configuration Options - -The available outbox settings are listed below. - -| Setting | Description | -| -----------------|-------| -| DuplicateDetectionWindow | The amount of time a message remains in the inbox for duplicate detection (based on MessageId) | -| IsolationLevel | The transaction isolation level to use (Serializable by default) | -| LockStatementProvider | The lock statement provider, needed to execute pessimistic locks. Is set via `UsePostgres`, `UseSqlServer` (the default), or `UseMySql` | -| QueryDelay | The delay between queries once messages are no longer available. When a query returns messages, subsequent queries are performed until no messages are returned after which the QueryDelay is used. | -| QueryMessageLimit | The maximum number of messages to query from the database at a time | -| QueryTimeout | The database query timeout | - -The bus outbox includes some additional settings: - -| Setting | Description | -| -----------------|-------| -| MessageDeliveryLimit | The number of messages to deliver at a time from the outbox to the broker | -| MessageDeliveryTimeout | Transport Send timeout when delivering messages to the transport | -| DisableDeliveryService() | Disable the outbox message delivery service, removing the hosted service from the service collection | diff --git a/docs/architecture/encrypted-messages.md b/docs/architecture/encrypted-messages.md deleted file mode 100644 index 1f47c273cfb..00000000000 --- a/docs/architecture/encrypted-messages.md +++ /dev/null @@ -1,92 +0,0 @@ -# Encrypted Messages - -If you use the encrypted message serializer, it uses BSON under the hood. The encryption format is AES-256. - -## BSON - -Before any encryption occurs the message is serialized in to BSON using the same `BsonMessageSerializer` that is used when just the BSON message format is used. - -## Initialization Vector (IV) - -The IV is rotated on every message to ensure that identical messages encrypted with same symmetric key do not end up with the same ciphertext. The IV itself is not a secret, it acts as a salt and is written to the first 16 bytes of the encrypted message so that it can be used to decrypt the message on the other side. - -## Symmetric Key - -MassTransit uses a default built in `ConstantSecureKeyProvider` allowing you to use a constant key for encrypting all your messages, this takes in a array of bytes to be used when encrypting the messages. - -However, if required you can implement your own `ISecureKeyProvider`, this is useful when you want to use a 3rd party key provider like AWS KMS or Azure Key Vault. Using a 3rd party like AWS KMS allows you to move the complexities of managing and rotating keys to someone else. - -### Generate a Key - -The best way to generate a key is via C# Interactive. Start by dropping in to the _Developer Command Prompt for VS_, this can be achieved via the Start Menu or by executing the following from the Run Dialog (`[Windows]`+`[R]`). - -```bash -cmd.exe /k "C:\Program Files (x86)\Microsoft Visual Studio\2017\Professional\Common7\Tools\VsDevCmd.bat" -``` - -Once in command prompt, run the C# Interactive (csi.exe) then run the following statements to generate a key. - -```csharp -> using System.Security.Cryptography; -> var aes = new AesCryptoServiceProvider(); -> aes.GenerateKey() -> aes.Key -byte[32] { 115, 171, 121, 43, 89, 24, 199, 205, 23, 221, 178, 104, 163, 32, 45, 84, 171, 86, 93, 13, 198, 132, 38, 65, 130, 192, 6, 159, 227, 104, 245, 222 } -``` - -If you want a text representation of the key, you can Base64 encode the key which can then be store in your `App.Config` or `appsettings.json`. - -```csharp -> var base64Key = Convert.ToBase64String(aes.Key); -> base64Key -"c6t5K1kYx80X3bJooyAtVKtWXQ3GhCZBgsAGn+No9d4=" -``` - -> Note that once you've generated the keys to keep them private! - -## Configuration - -The encrypted message serializer needs to be configured on the publisher and the consumer so that both sides can understand the message, this is simply done when configuring the bus. - -```csharp -var key = new []{ 1, 2, 3, 4, 5 }; - -var bus = Bus.Factory.CreateUsingRabbitMq(sbc => -{ - cfg.Host("localhost"); - - sbc.UseEncryption(key) - - sbc.ReceiveEndpoint("test_queue", ep => - { - ep.Consumer(); - }); -}); -``` - -You can also pass in your custom secure key provider in to the `UseEncryption` configuration method. - -```csharp -var keyProvider = new CustomKeyProvider(); - -var bus = Bus.Factory.CreateUsingRabbitMq(sbc => -{ - // ... - - sbc.UseEncryption(keyProvider) -}); -``` - -### Unencrypted Message - -After you've configured your encrypted message serializer, MassTransit will still process standard unencrypted messages. If this is undesirable then you can clear all other message deserializers on bus configuration. - -```csharp -var bus = Bus.Factory.CreateUsingRabbitMq(sbc => -{ - // ... - sbc.ClearMessageDeserializers(); - sbc.UseEncryption(key) -}); -``` - diff --git a/docs/architecture/green-cache.md b/docs/architecture/green-cache.md deleted file mode 100644 index 6cc3fa61641..00000000000 --- a/docs/architecture/green-cache.md +++ /dev/null @@ -1,83 +0,0 @@ -# Green Cache - -Caching has long been a requirement for building fast systems, including microprocessors, file systems, databases, the internet, and even developer applications. Typically, trading a little memory for increased performance is a good thing - applications respond more quickly which leads to happy users. - -MassTransit has tried a few different ways to cache things like send endpoints, to reduce the load on the broker and minimize application latency when sending messages. They've all sort of worked, but I really wanted to come up with a way to avoid duplicate work and also avoid cascade failures by caching failures. I also wanted to make sure highly dynamic systems that heavily use temporary queues don't run out of channels by keeping too many send endpoints cached that are only used once. - -To that end, I created _Green Cache_. - - -## Cache Architecture - -Green Cache is an in-memory, asynchronous, least-recently-used (LRU) cache optimized for indexed access to cached values. When using Green Cache, an index is used to read from the cache, with values added via a _missing value factory_ which can be provided to the `Get` operation. The cached values are held within the address of a single process, and are accessed directly by the code running in that process. - -> This type of cache is very fast, requires no serialization, and is well suited for creating connection pools, session pools, and for maintaining handles to resources. - -Green Cache is composed of a node tracker that keeps track of cached values which is then shared by the cache's indices. Each index has a key (that is strongly typed) which is a property on the cached value, that is the unique identity for the value. - - -## Asynchronous, for the, _await_ for it, win! - -Cached values are read using an index, and that read operation is fully asynchronous. The `Get` method shown below has two key aspects that make it a really powerful abstraction for this type of cache. - -```csharp -Task Get(TKey key, MissingValueFactory factory); -``` - -First the key, which should be obvious. If it were a `string`, a string would be provided to find the value using the index. The second argument is a factory method to create the value if it isn't found. This delegate is asynchronous as well, and is only called if the value is not found. - -```csharp -public delegate Task MissingValueFactory(TKey key); -``` - -The compositional nature of the TPL makes this a really strong abstraction -- the `Task` returned by the `Get` method above is not the same task that is returned from the missing value factory. In order to make cache access quick, the `Task` returned is a placeholder which is decoupled from the factory method. This placeholder reserves an index slot for the key specified, while the factory method is invoked asynchronously. - -Adding the key to the index allows subsequent reads for the same key to receive the same placeholder as the first read -- preventing duplicate factory methods being executed. The factory methods for these reads aren't lost, however, and are stored by the placeholder in the order they were received. This approach makes it possible for dozens of tasks to wait on the creation of the value for the same key to be returned. - -A subsequent reader for the same key may receive one of the following results: - -* The value created by the first reader, if the factory method ran to completion. -* The value created by a subsequent reader that called `Get` before this reader. -* The value created by the factory method provided by this reader. - - The value, if the factory method ran to completion - - A faulted task, _only_ if the factory method provided by this reader throws an exception. This fault would only be visible to this reader, any subsequent readers would not see this exception. - -If a subsequent reader does not provide a missing value factory, the reader will receive one of the following results: - -* The value created by a previous reader, if any previous reader's factory method ran to completion. -* A `KeyNotFoundException` if no previous reader's factory method ran to completion (either canceled or faulted). - -Once the value has been created, the placeholder is replaced with a cached node by the node tracker. If the cache has multiple indices, the value is then propagated to the other indices, making it available to readers. - -> An index by itself is consistent, you can read cached values and they'll exist once created, but there is no guarantee that another index will have the value until after it has been created and propagated. - - -## Recycling (staying Green) - -> A cache without an eviction policy is a memory leak. - -Green Cache uses two methods for managing memory usage, a capacity limit combined with an age limit. - -The capacity limit specifies how many values can be stored in the cache. The cache capacity is dynamic and doesn't represent a fixed limit how many values are in the cache at a point in time. Instead, capacity works in combination with the minimum age to make the cache useful for short-lived values, while keeping the size of the cache under control long term. - -The age limits define the minimum and maximum age of a value. The minimum age is a fixed lower limit specifying how long a value is cached, and a value will never be removed until it is of legal age. The maximum age is the longest an untouched value will remain in the cache before being removed. - -> Yes, if you add one hundred million values per minute to a cache with a capacity of 1000, you will have one hundred _meeellllion_ values in your cache for a minute. - - -### Under the hood - -To manage values, a node tracker determines the overall potential lifespan of a value (basically the difference between the maximum and minimum age) and splits that time period into buckets. The buckets are arranged in order by time, and accesses the buckets using a ring (which sounds really cool, but it's just using the `%` operator to avoid reusing a bucket index). - -As time passes, the active range of buckets moves forward. Once the cache capacity has been reached, older buckets are emptied to make room for new values. - -Values within a bucket can be touched, so that even if they are legally old enough to be removed from the cache they'll remain and be moved to the current bucket. This ensures that _lively_ values are kept in the cache longer than _cold fish_. - -The node tracker also have a maximum lifetime before it essentially _drops everything_ and restarts. The reason for this is to avoid weird things that happen when a cache lives forever and to allow the process memory structure a _reboot_ to avoid long-term garbage collection issues. The reason for this is simple -- years of building services that run 24x7 and seeing weird things magically fix themselves after restarting the service. - - -## Wrap up - -That's about it for now. For MassTransit developers, this doesn't change anything on the surface as it is completely internal. I just found it interesting enough to share, both to get feedback and to come to some mental conclusion after the time spent creating _Green Cache_. - - diff --git a/docs/architecture/history.md b/docs/architecture/history.md deleted file mode 100644 index 09e9fae763b..00000000000 --- a/docs/architecture/history.md +++ /dev/null @@ -1,23 +0,0 @@ -# History - -A commonly asked question is why was MassTransit created. Well, here is the story. - -In 2007, Chris Patterson \(@phatboyg\) and Dru Sellers \(@drusellers\) met at the first ALT.NET conference in Austin, TX. It was at this conference that Chris and Dru not only realized that they had a lot of the same problems to solve, but also how much the standard tooling provided by Microsoft just didn't fit their needs. Surrounded by the best and brightest in .NET, the energy was there to build better tooling that supported testable processes. Combined with an awareness of the latest advances in tooling, libraries, and coding practices; they decided that a better option must exist. After searching the .NET ecosystem for a tool that would help them achieve their goals, the only real option was the venerable NServiceBus. After reviewing NServiceBus, it was determined that the only real dependency injection container supported was Spring.NET. It also became obvious that NServiceBus wasn't quite ready for external contributors to come on board. For these reasons, they decided to embark on the quixotic trek of building their own service bus \(seriously, how hard could it be?? LOL\). - -Initially the goals were as much about learning distributed message based systems, as well as building something both of their companies could use. The first commit was pushed to GoogleCode on 12/26/2007, and shortly there after both Dru and Chris went to production with MassTransit and both of their companies have had success in getting value out of their efforts. - -After four years of continued success, Chris and Dru continued to push forward on their Journey, and were joined by Travis Smith \(@TravisTheTechie\). The near future should bring much for the MassTransit community as RabbitMQ became the broker of choice, lessening the focus on MSMQ. - -In early 2014, after a few years of research and design, work was started on an entirely new MassTransit. In order to embrace the world of asynchronous programming, as well as leveraging the power of advanced messaging platforms like RabbitMQ, a foundational rewrite was required. Much of the code in MassTransit was written prior to the introduction of the Task Parallel Library \(or TPL\), and even the .NET 4.0 support was before async and await were added to the language. - -To eliminate a ton of extremely complex code, support for MSMQ was completely ripped out, including all of the routing support that had to be built because of MSMQ’s lack of message routing. The remaining code was rewritten from bottom to top, resulting in an entirely new, completely asynchronous, and highly optimized framework for message processing. - -## The philosophy - -First and foremost, we are not an Enterprise Service Bus \(ESB\). While MassTransit is used in several enterprises it isn’t a swiss army knife, we are not driven by sales to be a million features wide, and an inch deep. We focus on a few key concepts and try to make them as robust as possible. - -We don’t do doodleware, you won’t find a designer, we are all about the keyboard samurai, the true in-the-trenches coder. That’s who we are, and those are our friends. If you want to draw, use a white board. - -We don’t do FTP->WS-deathstar->BS \(not that you can’t, it just isn't in the box\). We focus on the experience of using one transport in a given environment, and we try to make it as smooth as possible. - -MassTransit is built to be used inside the firewall, it isn't built to be used as a means to communicate with external vendors \(it can be, again it just isn't in the box\), it's meant to be used for getting your corporate services talking to each other and making building internal software easier. diff --git a/docs/architecture/interoperability.md b/docs/architecture/interoperability.md deleted file mode 100644 index 2bb5e0f99d6..00000000000 --- a/docs/architecture/interoperability.md +++ /dev/null @@ -1,96 +0,0 @@ -# Interoperability - -In MassTransit, developers specify types for messages. MassTransit's serializers then perform the hard work of converting the types to the serializer format (such as JSON, XML, BSON, etc.) and then back again. - -To interoperate with other languages and platforms, the message structure is important. - -### Content type - -To support custom message types, MassTransit uses a transport-level header to specify the message format. MassTransit simultaneously supports the following message formats on a single transport. - -- json (application/vnd.masstransit+json) -- bson (application/vnd.masstransit+bson) -- xml (application/vnd.masstransit+xml) - -If you enable encryption: - -- aes (application/vnd.masstransit+aes) -- aes (application/vnd.masstransit.v2+aes) - -If you configure the binary serializer: - -- binary (application/vnd.masstransit+binary) - -Register custom types would during endpoint/bus configuration. - -### JSON/BSON/XML - -MassTransit uses a message envelope to encapsulate the built-in message headers, as well as the message payload. The envelope properties on the wire include: - -```csharp -string MessageId -string CorrelationId -string ConversationId -string InitiatorId -string RequestId -string SourceAddress -string DestinationAddress -string ResponseAddress -string FaultAddress -DateTime? ExpirationTime -DateTime? SentTime -IDictionary Headers -object Message -string[] MessageType -HostInfo Host -``` - -The *Id* values should be convertible to a GUID/UUID or they will fail. All are optional, but MessageId should be present at a minimum. - -The *Address* values should be convertible to a URI that is a valid MassTransit endpoint address. - -The *MessageType* entries should be URNs, which are convertible to .NET types. MassTransit defines the format of the URN in the following structure: - -``` -urn:message:Namespace:TypeName -``` - -Examples include: - -```text -urn:message:MyProject.Messages:UpdateAccount -urn:message:MyProject.Messages.Events:AccountUpdated -urn:message:MyProject:ChangeAccount -urn:message:MyProject.AccountService:MyService+AccountUpdatedEvent -``` - -The last one is a nested class, as indicated by the '+' symbol. - -The *Host* is an internal data type, but is a set of strings that define the host that produced the message. - -```csharp -string MachineName -string ProcessName -int ProcessId -string Assembly -string AssemblyVersion -string FrameworkVersion -string MassTransitVersion -string OperatingSystemVersion -``` - -#### Example message - -This is a minimal message: - -```json -{ - "message": { - "value": "Some Value", - "customerId": 27 - }, - "messageType": [ - "urn:message:MassTransit.Tests:ValueMessage" - ] -} -``` diff --git a/docs/architecture/newid.md b/docs/architecture/newid.md deleted file mode 100644 index 9464e9e5f0a..00000000000 --- a/docs/architecture/newid.md +++ /dev/null @@ -1,79 +0,0 @@ -# NewId - -NewId generates sequential unique identifiers that are 128-bit (16-bytes) and fit nicely into a `Guid`. It was inspired from [Snowflake][1] and [flake][2]. - -## The Problem - -Many applications use unique identifiers to identify data. Common approaches applications use to generate unique identifiers in a relational database delegate identifier generation to the database, using an identity column or another similar auto-incrementing value. - -While this approach can be adequate for a small application, it quickly becomes a bottleneck at scale. And it's a common problem, as evidenced by [this post][1] from Twitter Engineering in 2010. - -::: tip Quote -We needed something that could generate tens of thousands of ids per second in a highly available manner. This naturally led us to choose an uncoordinated approach. -::: - -A key use case, specifically related to MassTransit, is applications that use messages to communicate between services – which is common in a service-based architecture. In these applications, sequential identifiers generated by NewId can serve dual purposes. First and foremost, it is a sequential unique identifier. Second, it is also a timestamp, as every NewId includes a UTC timestamp. - -### Why does order matter now? - -For a .NET developer, it is easy to reach for `Guid.NewGuid()` and run with it. And while that works, the identifiers created are not sequential. They're completely randomized. And when it comes to data, being able to sort it matters. Using a _uniqueidentifier_ column as a primary key clustered index with SQL Server was frowned upon for years because it caused massive index fragmentation. This led developers to use an _int_ (or _bigint_ once they realized that four billion isn't a lot) primary key and create a separate unique index on the _uniqueidentifier_ column (to use the AK, one might say, it wasn't a good day). - -## The Solution - -NewId was created to solve the problem. NewId generates sequential 128-bit identifiers that are collation compatible with SQL Server as a clustered primary key. Using the host MAC address, along with an optional offset (in case multiple processes are on the same host), combined with a timestamp and an incrementing sequence number, generate identifiers are unique across a network of systems and can be safely inserted into a database without conflicts. - -> NewId is largely inspired by the [Erlang library flake][2], which adopted an approach of generating 128-bit, k-ordered ids (read time-ordered lexically) using the machines MAC, timestamp and a per-thread sequence number. These identifiers are sequential and do not collide in a cluster of nodes running applications that use these as UUIDs. - -## Using NewId - -NewIds can be generated using one of two methods. The first returns a `NewId`, whereas the second returns a `Guid`. - -```cs -NewId newId = NewId.Next(); - -Guid guid = NewId.NextGuid(); -``` - -NewId implements many of the same methods and constructors as _Guid_, and can be converted to and from a _Guid_. - -```cs -// Formats to 11790000-CF25-B808-2365-08D36732603A -string identifier = NewId.Next().ToString("D").ToUpperInvariant(); - -// Convert from a string -NewId newId = new NewId("11790000-cf25-b808-dc58-08d367322210"); - -// Convert from a byte array -var bytes = new byte[] { 16, 23, 54, 74, 21, 14, 75, 32, 44, 41, 31, 10, 11, 12, 86, 42 }; -NewId newId = new NewId(bytes); -``` - -### Configuration - -Some features of NewId can be configured. - -#### Process Id - -In cases where multiple processes are on the same host generating identifiers, it may be necessary to include the _processId_ when generating identifiers. To enable the use of the _processId_, call the method below on startup. - -```cs -NewId.SetProcessIdProvider(new CurrentProcessIdProvider()); -``` - -This will replace two of the six network address bytes with the current _processId_. - -::: danger -There are situations where using a predictable, sequential identifier is discouraged – cases where unpreditability is a desired feature. These include: - -- Generating passwords -- Creating security tokens -- Anything where someone should not be able to guess an identifier - -NewId generated identifiers may expose the MAC address of the machine that generated the identifier along with the time the identifier was generated. While this isn't typically an issue in the modern world of networked computers with soft MAC addresses, some security-sensitive applications may need to be aware of the algorithm and any ramifications. - -Oh, and **don't** do modulo 2 arithmetic on NewId-generated Guids with an expectation of random distribution. -::: - -[1]: https://blog.twitter.com/engineering/en_us/a/2010/announcing-snowflake.html -[2]: https://github.com/boundary/flake - diff --git a/docs/architecture/nservicebus.md b/docs/architecture/nservicebus.md deleted file mode 100644 index e440fa53b02..00000000000 --- a/docs/architecture/nservicebus.md +++ /dev/null @@ -1,141 +0,0 @@ -# NServiceBus - -[NServiceBus](https://particular.net/nservicebus) is a commercial .NET messaging and workflow solution. - -### Interoperability - -- [MassTransit.Interop.NServiceBus](https://nuget.org/packages/MassTransit.Interop.NServiceBus/) - -::: danger -This package was built using a black box, clean room approach based on observed message formats within the message broker. As such, there may be edge cases and situations which are not handled by this package. Extensive testing is recommended to ensure all message properties are being properly interpreted. -::: - -MassTransit has limited message exchange support with NServiceBus, tested with the formats and configurations listed below. - -### Supported Serialization Formats - -- JSON (Newtonsoft) -- XML - -### Header Support - -MassTransit delivers messages to consumers using `ConsumeContext`, which includes various header properties. From looking at the transport message format, headers are serialized outside of the message body. To support access to these headers in MassTransit, the transport headers are mapped as shown. - -| Property | Header Used | Out | Notes -|:--------------|:-------------------------|:---:|:-------- -| ContentType | NServiceBus.ContentType | Y -| ConversationId | NServiceBus.ConversationId | Y -| CorrelationId | NServiceBus.CorrelationId | Y -| MessageId | NServiceBus.MessageId | Y -| SourceAddress | NServiceBus.OriginatingEndpoint | Y | formatted as `queue:name` -| ResponseAddress | NServiceBus.ReplyToAddress | Y | formatted as `queue:name` -| SentTime | NServiceBus.TimeSent | Y -| Host.MachineName | NServiceBus.OriginatingMachine | Y -| Host.MassTransitVersion | NServiceBus.Version | N | translated to NServiceBus x.x.x -| Supported Message Types | NServiceBus.EnclosedMessageTypes | Y | converted from AssemblyQualifiedName, types must be resolvable via Type.GetType() -| | NServiceBus.MessageIntent | N | Ignored - -### RabbitMQ - -NServiceBus follows the same broker topology conventions established by MassTransit. This means exchange names should be the same, which means that publish/subsribe with receive endpoints should work as expected. - -RabbitMQ was tested using the following NServiceBus configuration: - -```cs -var endpointConfiguration = new EndpointConfiguration("Gateway.Producer"); -endpointConfiguration.UseSerialization(); -endpointConfiguration.EnableInstallers(); - -var transport = endpointConfiguration.UseTransport(); -transport.UseConventionalRoutingTopology(); -transport.ConnectionString("host=localhost"); -``` - -Two message contracts were created: - -```cs - public class ClockUpdated : - IEvent -{ - public DateTime CurrentTime { get; set; } -} - -public class ClockSynchronized : - IEvent -{ - public string Host {get;set;} -} -``` - -From the NServiceBus endpoint, the `ClockUpdated` message was published: - -```cs -var session = _provider.GetRequiredService(); -await session.Publish(new ClockUpdated {CurrentTime = DateTime.UtcNow}, new PublishOptions()); -``` - -And the handler in NServiceBus for the `ClockSynchronized` message: - -```cs -public class ClockSynchronizedHandler : - IHandleMessages -{ - readonly ILogger _logger; - - public ClockSynchronizedHandler(ILogger logger) - { - _logger = logger; - } - - public Task Handle(ClockSynchronized message, IMessageHandlerContext context) - { - _logger.LogInformation("Clock synchronized: {Host}", message.Host); - - return Task.CompletedTask; - } -} -``` - -On the MassTransit side, the bus was configured to use the consumer with the same message contract assembly. - -```cs -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseNServiceBusJsonSerializer(); - - cfg.ConfigureEndpoints(context); - })); -}); -``` - -With the consumer: - -```cs -class TimeConsumer : - IConsumer -{ - readonly ILogger _logger; - - public TimeConsumer(ILogger logger) - { - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - _logger.LogInformation("Clock was updated: {CurrentTime}", context.Message.CurrentTime); - - await context.Publish(new ClockSynchronized - { - Host = HostMetadataCache.Host.MachineName - }); - } -} -``` - - - diff --git a/docs/architecture/packages.md b/docs/architecture/packages.md deleted file mode 100644 index ce0b3452fde..00000000000 --- a/docs/architecture/packages.md +++ /dev/null @@ -1,48 +0,0 @@ -# Packages - -The officially supported MassTransit packages include: - -* [MassTransit](https://nuget.org/packages/MassTransit/) - - [![master](https://github.com/MassTransit/MassTransit/actions/workflows/build.yml/badge.svg?branch=master&event=push)](https://github.com/MassTransit/MassTransit/actions/workflows/build.yml) - [![alt MassTransit on NuGet](https://img.shields.io/nuget/v/MassTransit.svg "MassTransit on NuGet")](https://nuget.org/packages/MassTransit/) - [![alt MassTransit Documentation](https://img.shields.io/badge/docs-latest-brightgreen.svg "MassTransit Documentation")](/) - -### Transports - -* [MassTransit.ActiveMQ](https://nuget.org/packages/MassTransit.ActiveMQ/) -* [MassTransit.AmazonSQS](https://nuget.org/packages/MassTransit.AmazonSQS/) -* [MassTransit.Azure.ServiceBus.Core](https://nuget.org/packages/MassTransit.Azure.ServiceBus.Core/) - * [MassTransit.WebJobs.ServiceBus](https://nuget.org/packages/MassTransit.WebJobs.ServiceBus/) - * [MassTransit.WebJobs.EventHubs](https://nuget.org/packages/MassTransit.WebJobs.EventHubs/) -* [MassTransit.RabbitMQ](https://nuget.org/packages/MassTransit.RabbitMQ/) -* **Riders** - * [MassTransit.EventHub](https://nuget.org/packages/MassTransit.EventHub/) - * [MassTransit.Kafka](https://nuget.org/packages/MassTransit.Kafka/) - -### Saga Persistence - -* [MassTransit.Azure.Cosmos](https://nuget.org/packages/MassTransit.Azure.Cosmos/) -* [MassTransit.Azure.Cosmos.Table](https://nuget.org/packages/MassTransit.Azure.Cosmos.Table/) -* [MassTransit.EntityFramework](https://nuget.org/packages/MassTransit.EntityFramework/) -* [MassTransit.EntityFrameworkCore](https://nuget.org/packages/MassTransit.EntityFrameworkCore/) -* [MassTransit.Marten](https://nuget.org/packages/MassTransit.Marten/) -* [MassTransit.MongoDb](https://nuget.org/packages/MassTransit.MongoDb/) -* [MassTransit.NHibernate](https://nuget.org/packages/MassTransit.NHibernate/) -* [MassTransit.Redis](https://nuget.org/packages/MassTransit.Redis/) - -### Scheduling - -* [MassTransit.Hangfire](https://nuget.org/packages/MassTransit.Hangfire/) -* [MassTransit.Quartz](https://nuget.org/packages/MassTransit.Quartz/) - -### Other - -* [MassTransit.Analyzers](https://nuget.org/packages/MassTransit.Analyzers/) -* [MassTransit.SignalR](https://nuget.org/packages/MassTransit.SignalR/) -* [MassTransit.TestFramework](https://nuget.org/packages/MassTransit.TestFramework/) - -### Interoperability - -* [MassTransit.Interop.NServiceBus](https://nuget.org/packages/MassTransit.Interop.NServiceBus/) - diff --git a/docs/architecture/versioning.md b/docs/architecture/versioning.md deleted file mode 100644 index bcbe9f392d1..00000000000 --- a/docs/architecture/versioning.md +++ /dev/null @@ -1,145 +0,0 @@ -# Versioning messages - -Versioning of messages is going to happen, services evolve and requirements change. - -## Versioning existing message contracts - -Consider a command to fetch and cache a local copy of an image from a remote system. - -```csharp -public interface FetchRemoteImage -{ - Guid CommandId { get; } - DateTime Timestamp { get; } - Uri ImageSource { get; } - string LocalCacheKey { get; } -} -``` - -After the initial deployment, a requirement is added to resize the image to a -maximum dimension before saving it to the cache. The new message contract -includes the additional property specifying the dimension. - -```csharp -public interface FetchRemoteImage -{ - Guid CommandId { get; } - DateTime Timestamp { get; } - Uri ImageSource { get; } - string LocalCacheKey { get; } - int? MaximumDimension { get; } -} -``` - -By making the *int* value nullable, commands that are submitted using the -original contract can still be accepted as the missing value does not break the -new contract. If the value was added as a regular *int*, it would be assigned a -default value of zero, which may not convey the right information. String -values can also be added as they will be *null* if the value is not present in -the serialized message. The consumer just needs to check if the value is -present and process it accordingly. - -# Versioning existing events - -Consider an event to notify that an image has been cached is now available. - -```csharp -public interface RemoteImageCached -{ - Guid EventId { get; } - DateTime Timestamp { get; } - Guid InitiatingCommandId { get; } - Uri ImageSource { get; } - string LocalCacheKey { get; } -} -``` - -An application will publish the event using an implementation of the class, as shown below. - -```csharp -class RemoteImageCachedEvent : - RemoteImageCached -{ - Guid EventId { get; set; } - DateTime Timestamp { get; set; } - Guid InitiatingCommandId { get; set; } - Uri ImageSource { get; set; } - string LocalCacheKey { get; set; } -} -``` - -The class implements the event interface, and when published, is delivered to -consumers that are subscribed to the *RemoteImageCached* event interface. -MassTransit dynamically creates a backing class for the interface, and -populates the properties with the values from the serialized message. - -> Note that you cannot dynamically cast the *RemoteImageCached* interface in -> the consumer to the RemoteImageCachedEvent, as the actual class is not -> deserialized. This can be confusing, but is intentional to prevent classes (and -> the behavior that comes along with it) from being serialized and deserialized. - -As the event evolves, additional event contracts can be defined that include -additional information without modifying the original contract. For example. - -```csharp -public interface RemoteImageCachedV2 -{ - Guid EventId { get; } - DateTime Timestamp { get; } - Guid InitiatingCommandId { get; } - Uri ImageSource { get; } - - // the string is changed from LocalCacheKey to a full URI - Uri LocalImageAddress { get; } -} -``` - -The event class is then modified to include the additional property, while -still implementing the previous interface. - -```csharp -class RemoteImageCachedEvent : - RemoteImageCached, - RemoteImageCachedV2 -{ - Guid EventId { get; set; } - DateTime Timestamp { get; set; } - Guid InitiatingCommandId { get; set; } - Uri ImageSource { get; set; } - string LocalCacheKey { get; set; } - Uri LocalImageAddress { get; set; } -} -``` - -When the event class is published now, both interfaces are available in the -message. When a consumer subscribes to one of the interfaces, that consumer -will receive a copy of the message. It is important that both interfaces are -not consumed in the same context, as duplicates will be received. If a service -is updated, it should use the new contract. - -> Note that ownership of the contract belongs to the event publisher, not the -> event observer/subscriber. And contracts should not be shared between event -> producers as this can create some extensive leakage of multiple events making -> it difficult to consume unique events. - -As mentioned above, depending upon the interface type subscribed, a dynamic -backing class is created by MassTransit. Therefore, if a consumer subscribes to -RemoteImageCached, it is not possible to cast the message to -RemoteImageCachedV2, as the dynamic implementation does not support that -interface. - -> It should be noted, however, that on the IConsumeContext interface, there -> is a method to TryGetContext method, which can be used to attempt to -> deserialize the message as type T. So it is possible to check if the -> message also implements the new version of the interface and not process as -> the original version knowing that the new version will be processed on the -> same message consumption if both types are subscribed. - -The message is a single message on the wire, but the available/known types are -captured in the message headers so that types can be deserialized from the -message body. - -A lot of flexibility and power, it's up to the application developer to ensure -that it is used in a way that ensures application evolution over time without -requiring forklift/switchover upgrades due to breaking message changes. - diff --git a/docs/articles/durable-futures.md b/docs/articles/durable-futures.md deleted file mode 100644 index 98cdf4aa56f..00000000000 --- a/docs/articles/durable-futures.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# Durable Futures - -[[toc]] - -## Introduction - -Durable Futures are a concept I've come up with to address the complexity inherent to a distributed, event-based architecture. - -The concepts in this article are covered [in Season 3](https://youtube.com/playlist?list=PLx8uyNNs1ri2JeyDGFWfCYyAjOB1GP-t1) of the [MassTransit video series on YouTube](https://youtube.com/playlist?list=PLx8uyNNs1ri2MBx6BjPum5j9_MMdIfM9C). - -The code exploring the concepts is [available on GitHub](https://github.com/MassTransit/Sample-ForkJoint). - -## Request, Response - -One of the most understood concepts in software development is request/response. In the simplest form, call/return, this conversation pattern between a client and a service, is the most commonly used idiom in software development. - -```cs -var response = service.Method(request); -``` - -As programming languages have evolved, along with the common use of asynchronous programming models, remote procedure calls (RPC) via HTTP and other protocols, and message-based systems, the most understood pattern continues to be request/response. - -#### HTTP Client - -```cs -var responseMessage = await httpClient.GetAsync(); -``` - -#### MassTransit Request Client - -```cs -var response = await client.GetResponse(new Request()); -``` - -In each of these examples, _await_ is a key enabler. Requests are sent asynchronously over a network connection to the remote service that produces a response which is then delivered to the client. - -With HTTP, a connection is maintained by the client on which the response is sent. With MassTransit, a _requestId_ and _responseAddress_ passed to the service are used to send the response which is then read from the queue by the client bus and correlated back to the request. - -![Request Response](/requestResponse.svg "Request Response") - -## Task - -The return type, `Task`, is a C# language feature that represents a _future_. It's a reference type which means it is only accessible by reference. Since `Task` is a future, it promises to deliver at some point: - -- `T` _(completed)_ -- An exception _(faulted)_ -- A task canceled exception _(canceled)_ - -_I'm intentionally ignoring `ValueTask` for now. It behaves similarly but has a few restrictions, one of which being that it should only be evaluated once._ - -::: tip C# -Prior to the addition of `async` and `await`, writing asynchronous code was significantly more complex. _Continuation passing_ was commonly used, resulting in deeply nested code that was difficult to understand and even more difficult to debug. Without a doubt, `async` and `await` are two of the best keywords in C#. -::: diff --git a/docs/articles/mediator.md b/docs/articles/mediator.md deleted file mode 100644 index da9b2303272..00000000000 --- a/docs/articles/mediator.md +++ /dev/null @@ -1,171 +0,0 @@ ---- -title: "Mediator" ---- - -# Enter the Mediator - -This post covers a new MassTransit feature, _Mediator_, and how it can be used to consume messages from any transport using MassTransit consumers and sagas. - - - -[Mediator](/usage/mediator), a new feature added in MassTransit v6.2, is a new way to use MassTransit. Mediator is entirely in-memory, does not require a transport, and does not serialize messages. Mediator sends messages directly to the receive pipeline, which then sends them to configured consumers, handlers, and sagas. - -### Why mediator? - -Mediator is a behavioral design pattern that reduces coupling using an intermediate layer that encapsulates the communication between objects. A MassTransit bus is a type of mediator, it supports sending and publishing messages to consumers, sagas, activities, and handlers that are decoupled from the message producer. _Mediator_ is another form, one that can be used in many of the same situations but without the overhead of message serialization and the distributed system complexity. By using _mediator_, the power of MassTransit is now available for a broader set of use cases with the same flexibility and programming model. - - -#### Kafka - -Kafka support is a fairly common request. MassTransit is a bus, and it was designed to work with message brokers. A lot of people think Kafka is a message broker, but it isn't the type of broker that MassTransit expects. For instance, Kafka should not be used for RPC or a request-response conversation pattern such as a query. Kafka is designed as a streaming log writer, with topics that can have messages (each of which is a key-value pair of byte arrays). Messages are not delivered to consumers, they are read by consumers – similar to how records are read from a file. - -So while Kafka has atoms like _topics_ and _messages_, those atoms are semantically different than those used by a typical message broker used with MassTransit. - -However, it _would_ be pretty awesome to process messages read from a Kafka topic using MassTransit. And that's how _mediator_ started – a way to send any type (call it `T`) to the [receive pipeline](/advanced/middleware/receive) so that it can be consumed. And like any endeavor to add functionality, the same question, "_how hard can it be?_" Using _mediator_ to consume Kafka messages is now possible by sending the deserialized type using `await mediator.Send(T message)`. - -> Note that [Kafka support is now built-in!](/usage/riders/kafka) - -#### Speed - -Mediator is fast. Even using the in-memory transport, MassTransit will serialize and deserialize messages, which adds considerable overhead. Mediator doesn't serialize, which means it isn't slow. Using the [MassTransit-Benchmark](https://github.com/MassTransit/MassTransit-Benchmark) with the `--mediator` option, send/consume is blazingly fast (over 650,000 messages/second on my 8-core Windows desktop), and request/response is pretty fast as well. Of course, these are fairly synthetic numbers – consumers will typically do more than just add a counter to a bucket. - -### Using Mediator - -To configure mediator in an ASP.NET Core project, add the following to the _ConfigureServices_ method. - -> Packages used: [MassTransit](https://nuget.org/packages/MassTransit/), [MassTransit.Extensions.DependencyInjection](https://nuget.org/packages/MassTransit.Extensions.DependencyInjection/) - -```cs -public void ConfigureServices(IServiceCollection services) -{ - services.AddControllers(); - - services.AddMediator(x => - { - x.AddConsumersFromNamespaceContaining(); - - x.AddRequestClient(); - }); -} -``` - -Any consumers in the same namespace as the `OrderConsumer` will be added, along with any consumer definition classes found. The `AddMediator` configuration method is a `IReceiverEndpointConfigurator`, on which all of the consumers are configured. - -> The configuration can include all kinds of middleware, including popular favorites such as `UseMessageRetry` and `UseConcurrencyLimit`. - -A request client is added for use by a controller where a response is expected. If no response is needed, call the `Send` method on the `IMediator` interface. - -The consumer is a standard MassTransit consumer: - -```cs -public class OrderConsumer : - IConsumer -{ - public Task Consume(ConsumeContext context) - { - return context.RespondAsync(new OrderAccepted - { - Text = $"Received: {context.Message.OrderNumber} {DateTime.UtcNow}" - }); - } -} -``` - -And the message contracts are simple classes (yes, interfaces can be used as well – and are still recommended). - -```cs -public class SubmitOrder -{ - public int OrderNumber { get; set; } -} - -public class OrderAccepted -{ - public string Text { get; set; } -} -``` - -The controller method uses the request client to communicate (indirectly, via the mediator) with the `OrderConsumer`. - -```cs -[Route("/orders")] -public class OrderController : - Controller -{ - readonly IRequestClient _requestClient; - - public OrderController(IRequestClient requestClient) - { - _requestClient = requestClient; - } - - [HttpPost] - public async Task Submit([FromBody] OrderDto order, CancellationToken cancellationToken) - { - try - { - var response = await _requestClient.GetResponse(new { OrderNumber = order.ON }, cancellationToken); - - return Content($"Order Accepted 123: {response.Message.Text}"); - } - catch (RequestTimeoutException) - { - return StatusCode((int)HttpStatusCode.RequestTimeout); - } - catch (Exception) - { - return StatusCode((int)HttpStatusCode.RequestTimeout); - } - } -} -``` - -### What about Kafka? - -::: tip UPDATE -[Kafka support is now built-in!](/usage/riders/kafka) _This section is retained for historical purposes, but is no longer recommended_ -::: - -Using the Confluent Kafka client, AVRO, and the schema registry – it is possible to send messages to _mediator_. - -```cs -CancellationTokenSource cts = new CancellationTokenSource(); -var consumeTask = Task.Run(() => -{ - using var schemaRegistry = new CachedSchemaRegistryClient(schemaRegistryConfig); - using var consumer = new ConsumerBuilder(consumerConfig) - .SetKeyDeserializer(new AvroDeserializer(schemaRegistry).AsSyncOverAsync()) - .SetValueDeserializer(new AvroDeserializer(schemaRegistry).AsSyncOverAsync()) - .SetErrorHandler((_, e) => Console.WriteLine($"Error: {e.Reason}")) - .Build()); - - consumer.Subscribe("order-updates"); - - try - { - while (true) - { - try - { - var consumeResult = consumer.Consume(cts.Token); - - await mediator.Send(consumeResult.Message, cts.Token); - } - catch (ConsumeException e) - { - Console.WriteLine($"Consume error: {e.Error.Reason}"); - } - } - } - catch (OperationCanceledException) - { - consumer.Close(); - } -}); -``` - -This is just an example, based off a sample from the Confluent site. - - - - diff --git a/docs/articles/net5.md b/docs/articles/net5.md deleted file mode 100644 index af7de8998be..00000000000 --- a/docs/articles/net5.md +++ /dev/null @@ -1,85 +0,0 @@ -# Moving to .NET 5 - -Microsoft has [released](https://devblogs.microsoft.com/dotnet/announcing-net-5-0/) .NET 5, which can be [downloaded](https://dotnet.microsoft.com/download/dotnet/5.0) now. There are significant new features available while maintaining compatibility with previous versions (including the LTS 3.1 release, which will continue to be supported). - -This release also includes [C# 9](https://devblogs.microsoft.com/dotnet/c-9-0-on-the-record/), which has a bunch of new language features – one of the most important being _records_. - -This article summarizes some of the new features and how they relate to MassTransit, along with some general thoughts. These are just what has been found so far, there are surely more useful applications of new runtime and language features. - -## Records - -One of the coolest new features in .NET 5, a record is a read-only (immutable) data structure. A record is a reference type, which makes it a great message type. For example, consider the following message contract. - -```cs -public record OrderSubmitted -{ - public string OrderId { get; init; } - public DateTime OrderDate { get; init; } -} -``` - -To publish the _OrderSubmitted_ event, a message initializer is used to create the event. - -```cs -bus.Publish(new { OrderId = "46", OrderDate = DateTime.UtcNow }); -``` - -This calls the _Publish_ overload with the following signature: - -```cs -Task Publish(object values, CancellationToken cancellationToken) -``` - -Using the new record type, this contract could be rewritten as shown below. - -```cs -public record OrderSubmitted -{ - public string OrderId { get; init; } - public DateTime OrderDate { get; init; } -} -``` - -The record type event could be published the same way, using a message initializer, as shown above. Another way would be to use the record initializer to send the actual message type created without using a message initializer. - -```cs -bus.Publish(new() { OrderId = "46", OrderDate = DateTime.UtcNow }); -``` - -Did you spot the difference? It's subtle. C# 9 includes "target-typing", where the target type is known for the _new_ expression. This means that the type no longer needs to be specified when using _new_. The `()` is the only difference, which creates a specific instance of the record type instead of an anonymous type (which is passed to the message initializer). This in turn calls a different _Publish_ overload. - -```cs -Task Publish(T message, CancellationToken cancellationToken) -``` - -::: tip NOTE -When using record type initializers, message type initializers, along with type conversion and shortcut variables (via `InVar`) are not available. -::: - -Since a record is a reference type, and under the covers a record has private setters for properties, they serialize as expected. If record constructors are used, it may be necessary to include a default constructor to support proper deserialization. - -## Module Initializers - -I recently commented during one of the [Season 2](https://www.youtube.com/playlist?list=PLx8uyNNs1ri1UA_Nerr7Ej3g9nT2PxbbH) episodes on YouTube that I wished C# had module initializers. Well, sure as s--t, they're now part of C# 9. And one of the other clever tricks is the ability to include a method in an interface, in this case, a static internal method, that is marked with the `[ModuleInitializer]` attribute. In this method, MassTransit's global topology is being used to configure the `CorrelationId` for the message contract. - -```cs -public record OrderSubmitted -{ - public Guid OrderId { get; init; } - public DateTime OrderDate { get; init; } - - [ModuleInitializer] - internal static void Init() - { - MessageCorrelation.UseCorrelationId(x => x.OrderId); - } -} -``` - -This method will be called automatically by the runtime, yet not be visible in the interface. This assumes that message contracts are in a separate assembly from consumers and producers. - - - - - - diff --git a/docs/articles/outbox.md b/docs/articles/outbox.md deleted file mode 100644 index 9b98d0eaf5a..00000000000 --- a/docs/articles/outbox.md +++ /dev/null @@ -1,125 +0,0 @@ ---- -title: "In-Memory Outbox" ---- - -# In-Memory Outbox, Take out the Trash - -This post details the _In-Memory Outbox_, including what it does, how to configure it, and how it ensures eventual consistency in the presence of database or message transport failures. - - - - - -MassTransit implements messaging patterns, many of which are designed to ease the transition from a tightly-coupled, database-centric application to a set of services that are highly available, reliable, and eventually consistent. Some of these patterns are obvious, but some of them require a little more explanation to truly understand how they are best utilized. - -### Commands - -Commands are used to do things, like update a database record. Updating a database record usually includes publishing events to notify services that a change in state has occurred. - -In a transactional mindset, updating the database and publishing the event is expected to be performed as a single atomic operation. In distributed systems, performing a distributed transaction between the database and the message broker is unrealistic. - -### Sagas - -In MassTransit, sagas are message handlers that maintain state. An initial message creates a saga _instance_ and subsequent messages may correlate to the same instance. Between messages, the saga instance _state_ is persisted using a database. While consuming a message, a saga may send commands and/or publish events. - -The message flow for a saga includes: - -1. On message receipt, an existing saga instance is loaded from the database. If a matching instance does not exist, a new instance is created. -1. The message is delivered to the saga instance. -1. Once the message is handled, the saga instance is saved or updated in the database. - -Step 2 is where _the magic happens_. The state can be changed, messages can be sent and published, anything. - -So, what's the problem? A few things. - -#### Failures - -An obvious problem is a database failure saving the saga instance. If messages were already sent or published, and the instance was not saved, other services would receive those messages yet the database has not been updated. - -A race condition is another concern, since the events may be consumed before the database update is complete. Yes, message brokers are _fast_, and many times messages are already being consumed long before (in computer time) the database update is started. - -##### So, retry? - -Retrying operations is a key trait of a resilient system. Transient failures happen, even more so in distributed systems, so it makes sense to retry failures in the presence of failures. Of course, not all failures are transient. For instance, trying to take out the trash when it has already been taken out isn't possible (well, until tomorrow). - -> In this example, designing idempotent services such that duplicate commands do not result in duplicate operations would be the best solution. But that's another topic worth studying. - -If retrying the database failure isn't enough, it may make sense to retry the entire message processing sequence – starting at step 1. In this case, the saga instance is discarded, and the message is retried from the beginning. The saga instance is loaded (or created), the message is delivered, and the instance is saved. This is repeated until it is successful or until the retry policy expires and the message is moved to the *_error* queue. - -> Because it's bad. Study _poison message handling_. - -A new retry-related issue is duplicate messages. Messages may be sent or published multiple times – once for each attempt. This can create non-deterministic behavior in services that consume those messages. Therefore, a method to delay messages from being sent until the saga instance is saved is needed. - -### The Outbox - -The outbox holds messages and delivers them after the _transactional_ portion of the message processing has completed. With a saga, the messages are delivered after the saga instance is saved successfully. This ensures that the database is updated before any consumers can start processing any of the produced messages. - -#### The In-Memory Outbox - -The In-Memory Outbox, a feature included with MassTransit, holds published and sent messages in memory until the message is processed successfully (such as the saga being saved to the database). Once the received message has been processed, the message is delivered to the broker and the received message is acknowledged as successful. - -> MassTransit consumes messages in _acknowledgement_ mode. The broker locks the message and the message is invisible to other consumers until it is either acknowledged (ack'd) by the consumer or negatively-acknowledged (n'ack'd) explictly by the consumer or implicitly due to a service or network failure. - -The full configuration is in the [documentation](/usage/exceptions.md#redelivery), a simple example is shown below. - -```cs -cfg.ReceiveEndpoint("r-trashy-saga", e => -{ - e.UseInMemoryOutbox(); - - e.StateMachineSaga(machine, repository); -}); -``` - -> In the example above, retry and redelivery was left out on purpose. The broker will redeliver the message if the process crashes or the network splits. For production services, retry filters should be added to handle transient database errors and ignore failures caused by business constraint violations. - -#### But what if the message doesn't send? - -This question comes up, and it is a fair question. If the broker goes down, the outbox would be unable to deliver the messages. If the process crashes, the messages in the outbox would be lost. Both of these failures can happen, though it is rare. And if computer science has one rule, it is that the rare will always happen. In production. On a Friday afternoon. - -### Time to Take out the Trash - -Imagine you're twelve, sitting on the sofa, playing video games with your friends. Suddenly, from the other room, you hear your mom call out, "Take out the trash!" Of course, you're in the middle of a battle, and while you've explained many times that you can't pause a multiplayer game, mom just doesn't get it. So you do what any 12-year-old does, you ignore her. The trash remains right where it is, in the kitchen. - -After a while, the lack of a door opening and closing, the still present smell of burnt popcorn from the kitchen, and your mom calls out again, "Take out the trash." At this point, you're dead, in spectator mode, and decide to comply – you take out the trash. Then you slide back onto the sofa and get ready for round two. - -More time passes, the squad is ready and you're about to get on the bus. Your mom, however, didn't hear from you and shouts once more, "I said take out the trash." "Mom, I already took it out" you reply, realizing after that you forgot to mute your mic. The jests and jokes commence as you thank the bus driver and head out. - -#### The Story - -This real-world example includes both failures scenarios that are brought up when considering the in-memory outbox. - -First, it didn't happen. The database may have been unavailable or the service crashed deserializing the message. Either way, it failed. And the message? It's still on the broker. It will be redelivered. _Mom will keep telling you to take out the trash until you take it out._ - -Second, it happened but the messages were not delivered. _You didn't tell her you took it out._ In this case, the message will also be retried. But in this case, this rare case, this is where the previously mentioned term _idempotence_ comes back onto the field. - -When the message is attempted a third time (and face it, the third time is dangerously close to getting a chancla to the head), the database was already updated. The invoice is already approved, _in the database_. The messages weren't sent, however, so other services may not know that the invoice was approved. In this case, for the service to be idempotent, it should assume that: - -1. The message delivery failed because it is being delivered – again. -2. Since the invoice is approved, and this is the approve invoice command, something must have failed after the database was updated. -3. The only thing after the database update is the outbox delivering messages. - -> Study Occam's Razer (okay, yeah, I'm a fan of Razer gaming gear so I'm leaving it spelled that way) - -The correct thing to do at this point is to use the state in the database, along with any information that is contained in the message, to produce the same commands and events that were produced in the previous attempt. Those messages will be delivered by the outbox, and the message will be acknowledged. - -#### !! Victory !! - -That's it, an easy-to-use, reliable solution to perform atomic operations that update a database and send/publish messages, and it works for any database updates that are sent as commands (delivered by durable message queues). - -And a big thank you to Jimmy Bogard, who's tweet prompted me to write this article! - - - -##### Other Reading - -[Transactional Outbox](https://microservices.io/patterns/data/transactional-outbox.html) -[NServiceBus Outbox](https://docs.particular.net/nservicebus/outbox/) - - diff --git a/docs/code/advanced/BatchingConsumer.cs b/docs/code/advanced/BatchingConsumer.cs deleted file mode 100644 index 95e719a771c..00000000000 --- a/docs/code/advanced/BatchingConsumer.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace BatchingConsumer; - -using System.Threading.Tasks; -using MassTransit; - -public record OrderAudit -{ -} - -class OrderAuditConsumer : - IConsumer> -{ - public async Task Consume(ConsumeContext> context) - { - for(int i = 0; i < context.Message.Length; i++) - { - ConsumeContext audit = context.Message[i]; - } - } -} - -class OrderAuditConsumerDefinition : - ConsumerDefinition -{ - public OrderAuditConsumerDefinition() - { - Endpoint(x => x.PrefetchCount = 1000); - } - - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) - { - consumerConfigurator.Options(options => options - .SetMessageLimit(100) - .SetTimeLimit(1000) - .SetConcurrencyLimit(10)); - } -} diff --git a/docs/code/advanced/BatchingConsumerAzure.cs b/docs/code/advanced/BatchingConsumerAzure.cs deleted file mode 100644 index af92ce1003c..00000000000 --- a/docs/code/advanced/BatchingConsumerAzure.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace BatchingConsumerAzure; - -using System; -using System.Threading.Tasks; -using BatchingConsumer; -using MassTransit; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingAzureServiceBus(cfg => - { - cfg.ReceiveEndpoint("audit-service", e => - { - e.PrefetchCount = 100; - e.MaxConcurrentCalls = 100; - - e.Batch(b => - { - b.MessageLimit = 100; - b.TimeLimit = TimeSpan.FromSeconds(3); - - b.Consumer(() => new OrderAuditConsumer()); - }); - }); - }); - } -} diff --git a/docs/code/advanced/BatchingConsumerBus.cs b/docs/code/advanced/BatchingConsumerBus.cs deleted file mode 100644 index 7cea9bcc509..00000000000 --- a/docs/code/advanced/BatchingConsumerBus.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace BatchingConsumerBus; - -using System.Threading.Tasks; -using BatchingConsumer; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddConsumer(typeof(OrderAuditConsumerDefinition)); - - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); - }); - } -} diff --git a/docs/code/advanced/BatchingConsumerExplicit.cs b/docs/code/advanced/BatchingConsumerExplicit.cs deleted file mode 100644 index 6a17f763761..00000000000 --- a/docs/code/advanced/BatchingConsumerExplicit.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace BatchingConsumerExplicit; - -using System; -using System.Threading.Tasks; -using BatchingConsumer; -using MassTransit; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => - { - cfg.ReceiveEndpoint("audit-service", e => - { - e.PrefetchCount = 1000; - - e.Batch(b => - { - b.MessageLimit = 100; - b.ConcurrencyLimit = 10; - b.TimeLimit = TimeSpan.FromSeconds(1); - - b.Consumer(() => new OrderAuditConsumer()); - }); - }); - }); - } -} diff --git a/docs/code/advanced/BusHostTopologyMatch.cs b/docs/code/advanced/BusHostTopologyMatch.cs deleted file mode 100644 index 27881b03929..00000000000 --- a/docs/code/advanced/BusHostTopologyMatch.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace BusHostTopologyMatch; - -using System.Threading.Tasks; -using MassTransit; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingAzureServiceBus(cfg => - { - cfg.Host("connection-string"); - }); - - if (busControl.Topology is IServiceBusBusTopology serviceBusTopology) - { - - } - } -} diff --git a/docs/code/audit/AuditAzureTableWithCustomPartitionKey.cs b/docs/code/audit/AuditAzureTableWithCustomPartitionKey.cs deleted file mode 100644 index fdb93ebc3dd..00000000000 --- a/docs/code/audit/AuditAzureTableWithCustomPartitionKey.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace AuditAzureTableWithCustomPartitionKey; - -using MassTransit; -using MassTransit.AzureTable; -using Microsoft.Azure.Cosmos.Table; -using Microsoft.Extensions.DependencyInjection; - -class Program -{ - static void Main(string[] args) - { - var services = new ServiceCollection(); - - CloudStorageAccount storageAccount = CloudStorageAccount.Parse("INSERT STORAGE ACCOUNT CONNECTION STRING"); - string auditTableName = "messageaudittable"; - string PartitionKey = "CustomPartitionKey"; - - services.AddMassTransit(x => - { - x.UsingInMemory((context, cfg) => - { - cfg.UseAzureTableAuditStore(storageAccount, auditTableName, new ConstantPartitionKeyFormatter(PartitionKey)); - }); - }); - } -} - -class ConstantPartitionKeyFormatter : - IPartitionKeyFormatter -{ - readonly string _partitionKey; - - public ConstantPartitionKeyFormatter(string partitionKey) - { - _partitionKey = partitionKey; - } - - public string Format(AuditRecord record) - where T : class - { - return _partitionKey; - } -} diff --git a/docs/code/audit/AuditAzureTableWithMessageTypeFilter.cs b/docs/code/audit/AuditAzureTableWithMessageTypeFilter.cs deleted file mode 100644 index 7ab6cddff8f..00000000000 --- a/docs/code/audit/AuditAzureTableWithMessageTypeFilter.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace AuditAzureTableWithMessageTypeFilter; - -using System.Collections.Generic; -using MassTransit; -using Microsoft.Azure.Cosmos.Table; -using Microsoft.Extensions.DependencyInjection; - -class Program -{ - static void Main(string[] args) - { - var services = new ServiceCollection(); - - CloudStorageAccount storageAccount = CloudStorageAccount.Parse("INSERT STORAGE ACCOUNT CONNECTION STRING"); - string auditTableName = "messageaudittable"; - - services.AddMassTransit(x => - { - x.UsingInMemory((context, cfg) => - { - cfg.UseAzureTableAuditStore(storageAccount, auditTableName, filter => filter.Exclude(typeof(LargeMessage), typeof(SecretMessage))); - }); - }); - } -} - -class SecretMessage -{ - public string TopSecretData { get; set; } -} - -class LargeMessage -{ - public IEnumerable HugeArray { get; set; } -} diff --git a/docs/code/audit/AuditAzureTableWithStorageAccount.cs b/docs/code/audit/AuditAzureTableWithStorageAccount.cs deleted file mode 100644 index 76ef8c45b8e..00000000000 --- a/docs/code/audit/AuditAzureTableWithStorageAccount.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace AuditAzureTableWithStorageAccount; - -using MassTransit; -using Microsoft.Azure.Cosmos.Table; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - static void Main(string[] args) - { - var services = new ServiceCollection(); - - CloudStorageAccount storageAccount = CloudStorageAccount.Parse("INSERT STORAGE ACCOUNT CONNECTION STRING"); - string auditTableName = "messageaudittable"; - - services.AddMassTransit(x => - { - x.UsingInMemory((context, cfg) => - { - cfg.UseAzureTableAuditStore(storageAccount, auditTableName); - }); - }); - } -} diff --git a/docs/code/audit/AuditAzureTableWithTableSupplied.cs b/docs/code/audit/AuditAzureTableWithTableSupplied.cs deleted file mode 100644 index 1d166cea336..00000000000 --- a/docs/code/audit/AuditAzureTableWithTableSupplied.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace AuditAzureTableWithTableSupplied; - -using MassTransit; -using Microsoft.Azure.Cosmos.Table; -using Microsoft.Extensions.DependencyInjection; - -class Program -{ - static void Main(string[] args) - { - var services = new ServiceCollection(); - - CloudStorageAccount storageAccount = CloudStorageAccount.Parse("INSERT STORAGE ACCOUNT CONNECTION STRING"); - CloudTableClient client = storageAccount.CreateCloudTableClient(); - CloudTable table = client.GetTableReference("audittablename"); - table.CreateIfNotExists(); - - services.AddMassTransit(x => - { - x.UsingInMemory((context, cfg) => - { - cfg.UseAzureTableAuditStore(table); - }); - }); - } -} diff --git a/docs/code/code.csproj b/docs/code/code.csproj deleted file mode 100644 index 8fc96fc66f6..00000000000 --- a/docs/code/code.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - net6.0 - 10 - ConsoleEventPublisher.Program - - - - - - - - - - - - - - - - - - diff --git a/docs/code/configuration/AspNetCoreEndpointListener.cs b/docs/code/configuration/AspNetCoreEndpointListener.cs deleted file mode 100644 index 43fe6c9035d..00000000000 --- a/docs/code/configuration/AspNetCoreEndpointListener.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace AspNetCoreEndpointListener; - -using System.Threading.Tasks; -using EventContracts; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddMassTransit(x => - { - x.AddConsumer(); - - x.SetKebabCaseEndpointNameFormatter(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); - }); - } -} - -class ValueEnteredEventConsumer : - IConsumer -{ - ILogger _logger; - - public ValueEnteredEventConsumer(ILogger logger) - { - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - _logger.LogInformation("Value: {Value}", context.Message.Value); - } -} diff --git a/docs/code/configuration/AspNetCoreListener.cs b/docs/code/configuration/AspNetCoreListener.cs deleted file mode 100644 index 55580eb1d89..00000000000 --- a/docs/code/configuration/AspNetCoreListener.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace AspNetCoreListener; - -using System.Threading.Tasks; -using EventContracts; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddMassTransit(x => - { - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ReceiveEndpoint("event-listener", e => - { - e.ConfigureConsumer(context); - }); - }); - }); - } -} - -class EventConsumer : - IConsumer -{ - ILogger _logger; - - public EventConsumer(ILogger logger) - { - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - _logger.LogInformation("Value: {Value}", context.Message.Value); - } -} diff --git a/docs/code/configuration/AspNetCorePublisher.cs b/docs/code/configuration/AspNetCorePublisher.cs deleted file mode 100644 index 3a32857aab4..00000000000 --- a/docs/code/configuration/AspNetCorePublisher.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace AspNetCorePublisher; - -using System; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddMassTransit(x => - { - x.UsingRabbitMq(); - }); - - // OPTIONAL, but can be used to configure the bus options - services.AddOptions() - .Configure(options => - { - // if specified, waits until the bus is started before - // returning from IHostedService.StartAsync - // default is false - options.WaitUntilStarted = true; - - // if specified, limits the wait time when starting the bus - options.StartTimeout = TimeSpan.FromSeconds(10); - - // if specified, limits the wait time when stopping the bus - options.StopTimeout = TimeSpan.FromSeconds(30); - }); - } -} diff --git a/docs/code/configuration/AspNetCorePublisherController.cs b/docs/code/configuration/AspNetCorePublisherController.cs deleted file mode 100644 index 275516270f5..00000000000 --- a/docs/code/configuration/AspNetCorePublisherController.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace EventContracts -{ - public record ValueEntered - { - public string Value { get; init; } - } -} - -namespace AspNetCorePublisher -{ - using System.Threading.Tasks; - using EventContracts; - using MassTransit; - using Microsoft.AspNetCore.Mvc; - - public class ValueController : - ControllerBase - { - readonly IPublishEndpoint _publishEndpoint; - - public ValueController(IPublishEndpoint publishEndpoint) - { - _publishEndpoint = publishEndpoint; - } - - [HttpPost] - public async Task Post(string value) - { - await _publishEndpoint.Publish(new() - { - Value = value - }); - - return Ok(); - } - } -} diff --git a/docs/code/configuration/AspNetCorePublisherHealthCheck.cs b/docs/code/configuration/AspNetCorePublisherHealthCheck.cs deleted file mode 100644 index 61867b3d3f6..00000000000 --- a/docs/code/configuration/AspNetCorePublisherHealthCheck.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace AspNetCorePublisherHealthCheck; - -using System; -using MassTransit; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Diagnostics.HealthChecks; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddHealthChecks(); - - services.Configure(options => - { - options.Delay = TimeSpan.FromSeconds(2); - options.Predicate = (check) => check.Tags.Contains("ready"); - }); - - services.AddMassTransit(x => - { - x.UsingRabbitMq(); - }); - } - - public void Configure(IApplicationBuilder app) - { - app.UseEndpoints(endpoints => - { - endpoints.MapHealthChecks("/health/ready", new HealthCheckOptions() - { - Predicate = (check) => check.Tags.Contains("ready"), - }); - - endpoints.MapHealthChecks("/health/live", new HealthCheckOptions()); - }); - } -} diff --git a/docs/code/configuration/ConsoleAppListener.cs b/docs/code/configuration/ConsoleAppListener.cs deleted file mode 100644 index d144cc95e7d..00000000000 --- a/docs/code/configuration/ConsoleAppListener.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace ConsoleEventListener; - -using System; -using System.Threading; -using System.Threading.Tasks; -using EventContracts; -using MassTransit; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => - { - cfg.ReceiveEndpoint("event-listener", e => - { - e.Consumer(); - }); - }); - - var source = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - - await busControl.StartAsync(source.Token); - try - { - Console.WriteLine("Press enter to exit"); - - await Task.Run(() => Console.ReadLine()); - } - finally - { - await busControl.StopAsync(); - } - } - - class EventConsumer : - IConsumer - { - public async Task Consume(ConsumeContext context) - { - Console.WriteLine("Value: {0}", context.Message.Value); - } - } -} diff --git a/docs/code/configuration/ConsoleAppPublisher.cs b/docs/code/configuration/ConsoleAppPublisher.cs deleted file mode 100644 index dd97f9b47ec..00000000000 --- a/docs/code/configuration/ConsoleAppPublisher.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace ConsoleEventPublisher; - -using System; -using System.Threading; -using System.Threading.Tasks; -using EventContracts; -using MassTransit; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingRabbitMq(); - - var source = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - - await busControl.StartAsync(source.Token); - try - { - while (true) - { - string value = await Task.Run(() => - { - Console.WriteLine("Enter message (or quit to exit)"); - Console.Write("> "); - return Console.ReadLine(); - }); - - if("quit".Equals(value, StringComparison.OrdinalIgnoreCase)) - break; - - await busControl.Publish(new() - { - Value = value - }); - } - } - finally - { - await busControl.StopAsync(); - } - } -} diff --git a/docs/code/containers/ContainerConsumers.cs b/docs/code/containers/ContainerConsumers.cs deleted file mode 100644 index c5e23f47815..00000000000 --- a/docs/code/containers/ContainerConsumers.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace ContainerConsumers; - -using System.Threading.Tasks; -using ContainerContracts; -using MassTransit; -using Microsoft.Extensions.Logging; - -class SubmitOrderConsumer : - IConsumer -{ - readonly ILogger _logger; - - public SubmitOrderConsumer(ILogger logger) - { - _logger = logger; - } - - public async Task Consume(ConsumeContext context) - { - _logger.LogInformation("Order Submitted: {OrderId}", context.Message.OrderId); - - await context.Publish(new - { - context.Message.OrderId - }); - } -} - -class SubmitOrderConsumerDefinition : - ConsumerDefinition -{ - public SubmitOrderConsumerDefinition() - { - // override the default endpoint name - EndpointName = "order-service"; - - // limit the number of messages consumed concurrently - // this applies to the consumer only, not the endpoint - ConcurrentMessageLimit = 8; - } - - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) - { - // configure message retry with millisecond intervals - endpointConfigurator.UseMessageRetry(r => r.Intervals(100,200,500,800,1000)); - - // use the outbox to prevent duplicate events from being published - endpointConfigurator.UseInMemoryOutbox(); - } -} diff --git a/docs/code/containers/ContainerContracts.cs b/docs/code/containers/ContainerContracts.cs deleted file mode 100644 index e2b09dffc57..00000000000 --- a/docs/code/containers/ContainerContracts.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace ContainerContracts -{ - using System; - - public record SubmitOrder - { - public Guid OrderId { get; init; } - } - - public record OrderSubmitted - { - public Guid OrderId { get; init; } - } -} diff --git a/docs/code/containers/MicrosoftConnect.cs b/docs/code/containers/MicrosoftConnect.cs deleted file mode 100644 index bfd7cffb9d8..00000000000 --- a/docs/code/containers/MicrosoftConnect.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace MicrosoftConnect; - -using System; -using System.Threading; -using System.Threading.Tasks; -using ContainerConsumers; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddConsumer(typeof(SubmitOrderConsumerDefinition)); - - x.SetKebabCaseEndpointNameFormatter(); - - x.UsingRabbitMq((context, cfg) => - cfg.ConfigureEndpoints(context, x => x.Exclude())); - }); - - var provider = services.BuildServiceProvider(); - - var busControl = provider.GetRequiredService(); - - await busControl.StartAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); - try - { - var connector = provider.GetRequiredService(); - - var handle = connector.ConnectReceiveEndpoint("order-queue", (context,cfg) => - { - cfg.ConfigureConsumer(context); - }); - - await handle.Ready; - - Console.WriteLine("Press enter to exit"); - - await Task.Run(() => Console.ReadLine()); - } - finally - { - await busControl.StopAsync(); - } - } -} diff --git a/docs/code/containers/MicrosoftContainer.cs b/docs/code/containers/MicrosoftContainer.cs deleted file mode 100644 index ce911d31e23..00000000000 --- a/docs/code/containers/MicrosoftContainer.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace MicrosoftContainer; - -using System.Threading.Tasks; -using ContainerConsumers; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.AddConsumer(typeof(SubmitOrderConsumerDefinition)); - - x.SetKebabCaseEndpointNameFormatter(); - - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/containers/MicrosoftContainerAddConsumer.cs b/docs/code/containers/MicrosoftContainerAddConsumer.cs deleted file mode 100644 index 89907cf00b0..00000000000 --- a/docs/code/containers/MicrosoftContainerAddConsumer.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace MicrosoftContainerAddConsumer; - -using System.Threading.Tasks; -using ContainerConsumers; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - // Add a single consumer - x.AddConsumer(typeof(SubmitOrderConsumerDefinition)); - - // Add a single consumer by type - x.AddConsumer(typeof(SubmitOrderConsumer), typeof(SubmitOrderConsumerDefinition)); - - // Add all consumers in the specified assembly - x.AddConsumers(typeof(SubmitOrderConsumer).Assembly); - - // Add all consumers in the namespace containing the specified type - x.AddConsumersFromNamespaceContaining(); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/containers/MicrosoftContainerAddConsumerEndpoint.cs b/docs/code/containers/MicrosoftContainerAddConsumerEndpoint.cs deleted file mode 100644 index 216a8a7c1c4..00000000000 --- a/docs/code/containers/MicrosoftContainerAddConsumerEndpoint.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace MicrosoftContainerAddConsumerEndpoint; - -using System.Threading.Tasks; -using ContainerConsumers; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddConsumer(typeof(SubmitOrderConsumerDefinition)) - .Endpoint(e => - { - // override the default endpoint name - e.Name = "order-service-extreme"; - - // specify the endpoint as temporary (may be non-durable, auto-delete, etc.) - e.Temporary = false; - - // specify an optional concurrent message limit for the consumer - e.ConcurrentMessageLimit = 8; - - // only use if needed, a sensible default is provided, and a reasonable - // value is automatically calculated based upon ConcurrentMessageLimit if - // the transport supports it. - e.PrefetchCount = 16; - - // set if each service instance should have its own endpoint for the consumer - // so that messages fan out to each instance. - e.InstanceId = "something-unique"; - }); - - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); - }); - } -} diff --git a/docs/code/containers/MicrosoftContainerConfigureConsumer.cs b/docs/code/containers/MicrosoftContainerConfigureConsumer.cs deleted file mode 100644 index d80ceb88eef..00000000000 --- a/docs/code/containers/MicrosoftContainerConfigureConsumer.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace MicrosoftContainerConfigureConsumer; - -using System.Threading.Tasks; -using ContainerConsumers; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddConsumer(typeof(SubmitOrderConsumerDefinition)); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ReceiveEndpoint("order-service", e => - { - e.ConfigureConsumer(context); - }); - }); - }); - } -} diff --git a/docs/code/containers/MicrosoftContainerFormatter.cs b/docs/code/containers/MicrosoftContainerFormatter.cs deleted file mode 100644 index ff9c630386e..00000000000 --- a/docs/code/containers/MicrosoftContainerFormatter.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MicrosoftContainerFormatter; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.SetKebabCaseEndpointNameFormatter(); - - x.SetSnakeCaseEndpointNameFormatter(); - - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); - }); - } -} diff --git a/docs/code/containers/MicrosoftContainerFormatterInline.cs b/docs/code/containers/MicrosoftContainerFormatterInline.cs deleted file mode 100644 index 25a73f98d68..00000000000 --- a/docs/code/containers/MicrosoftContainerFormatterInline.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MicrosoftContainerFormatterInline; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.ConfigureEndpoints(context, KebabCaseEndpointNameFormatter.Instance); - }); - }); - } -} diff --git a/docs/code/containers/MicrosoftDeployTopology.cs b/docs/code/containers/MicrosoftDeployTopology.cs deleted file mode 100644 index 14cb93285a8..00000000000 --- a/docs/code/containers/MicrosoftDeployTopology.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace MicrosoftDeployTopology; - -using System; -using System.Threading; -using System.Threading.Tasks; -using ContainerConsumers; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddConsumer(typeof(SubmitOrderConsumerDefinition)); - - x.SetKebabCaseEndpointNameFormatter(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.DeployTopologyOnly = true; - - cfg.ConfigureEndpoints(context); - }); - }); - - var provider = services.BuildServiceProvider(); - - var busControl = provider.GetRequiredService(); - - try - { - await busControl.DeployAsync(new CancellationTokenSource(TimeSpan.FromMinutes(2)).Token); - - Console.WriteLine("Topology Deployed"); - } - catch (Exception ex) - { - Console.WriteLine("Failed to deploy topology: {0}", ex); - } - } -} diff --git a/docs/code/containers/MultiBusConsumers.cs b/docs/code/containers/MultiBusConsumers.cs deleted file mode 100644 index ff4c11ca4b8..00000000000 --- a/docs/code/containers/MultiBusConsumers.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace ContainerConsumers; - -using System.Threading.Tasks; -using MassTransit; - -public record AllocateInventory -{ - public string Sku { get; init; } -} - -class AllocateInventoryConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - } -} diff --git a/docs/code/containers/MultiBusContainer.cs b/docs/code/containers/MultiBusContainer.cs deleted file mode 100644 index 3329c985298..00000000000 --- a/docs/code/containers/MultiBusContainer.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MultiBusContainer; - -using ContainerContracts; -using ContainerConsumers; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddMassTransit(x => - { - x.AddConsumer(); - x.AddRequestClient(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/containers/MultiBusThreeContainer.cs b/docs/code/containers/MultiBusThreeContainer.cs deleted file mode 100644 index b600077def2..00000000000 --- a/docs/code/containers/MultiBusThreeContainer.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace MultiBusThreeContainer; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public interface IThirdBus : - IBus -{ -} - -class ThirdBus : - BusInstance, - IThirdBus -{ - public ThirdBus(IBusControl busControl, ISomeService someService) - : base(busControl) - { - SomeService = someService; - } - - public ISomeService SomeService { get; } -} - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.Host("third-host"); - }); - }); - } -} - -public interface ISomeService -{ -} diff --git a/docs/code/containers/MultiBusTwoContainer.cs b/docs/code/containers/MultiBusTwoContainer.cs deleted file mode 100644 index ad5ed354d42..00000000000 --- a/docs/code/containers/MultiBusTwoContainer.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace MultiBusTwoContainer; - -using ContainerContracts; -using ContainerConsumers; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public interface ISecondBus : - IBus -{ -} - -public class Startup -{ - public void ConfigureServices(IServiceCollection services) - { - services.AddMassTransit(x => - { - x.AddConsumer(); - x.AddRequestClient(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); - }); - - services.AddMassTransit(x => - { - x.AddConsumer(); - x.AddRequestClient(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.Host("remote-host"); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/quickstart/GettingStarted.cs b/docs/code/quickstart/GettingStarted.cs deleted file mode 100644 index 0e1d27a6174..00000000000 --- a/docs/code/quickstart/GettingStarted.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace GettingStarted.Contracts; - -public record GettingStarted -{ - public string Value { get; init; } -} diff --git a/docs/code/quickstart/GettingStartedConsumer.cs b/docs/code/quickstart/GettingStartedConsumer.cs deleted file mode 100644 index 5464b26cad9..00000000000 --- a/docs/code/quickstart/GettingStartedConsumer.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace GettingStarted.Consumers; - -using System.Threading.Tasks; -using Contracts; -using MassTransit; -using Microsoft.Extensions.Logging; - -public class GettingStartedConsumer : - IConsumer -{ - readonly ILogger _logger; - - public GettingStartedConsumer(ILogger logger) - { - _logger = logger; - } - - public Task Consume(ConsumeContext context) - { - _logger.LogInformation("Received Text: {Text}", context.Message.Value); - return Task.CompletedTask; - } -} diff --git a/docs/code/quickstart/Worker.cs b/docs/code/quickstart/Worker.cs deleted file mode 100644 index 982d594d552..00000000000 --- a/docs/code/quickstart/Worker.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace GettingStarted; - -using System; -using System.Threading; -using System.Threading.Tasks; -using Contracts; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Worker : BackgroundService -{ - readonly IBus _bus; - - public Worker(IBus bus) - { - _bus = bus; - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - await _bus.Publish(new GettingStarted { Value = $"The time is {DateTimeOffset.Now}" }, stoppingToken); - - await Task.Delay(1000, stoppingToken); - } - } -} diff --git a/docs/code/riders/EventHubConsumer.cs b/docs/code/riders/EventHubConsumer.cs deleted file mode 100644 index fb35535e68f..00000000000 --- a/docs/code/riders/EventHubConsumer.cs +++ /dev/null @@ -1,54 +0,0 @@ -namespace EventHubConsumer; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.UsingAzureServiceBus((context, cfg) => - { - cfg.Host("connection-string"); - - cfg.ConfigureEndpoints(context); - }); - - x.AddRider(rider => - { - rider.AddConsumer(); - - rider.UsingEventHub((context, k) => - { - k.Host("connection-string"); - - k.Storage("connection-string"); - - k.ReceiveEndpoint("input-event-hub", c => - { - c.ConfigureConsumer(context); - }); - }); - }); - }); - } - - class EventHubMessageConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return Task.CompletedTask; - } - } - - public record EventHubMessage - { - public string Text { get; init; } - } -} diff --git a/docs/code/riders/EventHubProducer.cs b/docs/code/riders/EventHubProducer.cs deleted file mode 100644 index 5e960685715..00000000000 --- a/docs/code/riders/EventHubProducer.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace EventHubProducer; - -using System.Threading; -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.UsingAzureServiceBus((context, cfg) => - { - cfg.Host("connection-string"); - - cfg.ConfigureEndpoints(context); - }); - - x.AddRider(rider => - { - rider.UsingEventHub((context, k) => - { - k.Host("connection-string"); - - k.Storage("connection-string"); - }); - }); - }); - - var provider = services.BuildServiceProvider(true); - - var busControl = provider.GetRequiredService(); - - await busControl.StartAsync(new CancellationTokenSource(10000).Token); - - var serviceScope = provider.CreateScope(); - - var producerProvider = serviceScope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer("some-event-hub"); - - await producer.Produce(new { Text = "Hello, Computer." }); - } - - public record EventHubMessage - { - public string Text { get; init; } - } -} diff --git a/docs/code/riders/KafkaConsumer.cs b/docs/code/riders/KafkaConsumer.cs deleted file mode 100644 index 2d7d1b42def..00000000000 --- a/docs/code/riders/KafkaConsumer.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace KafkaConsumer; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); - - x.AddRider(rider => - { - rider.AddConsumer(); - - rider.UsingKafka((context, k) => - { - k.Host("localhost:9092"); - - k.TopicEndpoint("topic-name", "consumer-group-name", e => - { - e.ConfigureConsumer(context); - }); - }); - }); - }); - } - - class KafkaMessageConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return Task.CompletedTask; - } - } - - public record KafkaMessage - { - public string Text { get; init; } - } -} diff --git a/docs/code/riders/KafkaProducer.cs b/docs/code/riders/KafkaProducer.cs deleted file mode 100644 index bffa7c168e4..00000000000 --- a/docs/code/riders/KafkaProducer.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace KafkaProducer; - -using System; -using System.Threading; -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); - - x.AddRider(rider => - { - rider.AddProducer("topic-name"); - - rider.UsingKafka((context, k) => { k.Host("localhost:9092"); }); - }); - }); - - var provider = services.BuildServiceProvider(); - - var busControl = provider.GetRequiredService(); - - await busControl.StartAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); - try - { - var producer = provider.GetRequiredService>(); - do - { - string value = await Task.Run(() => - { - Console.WriteLine("Enter text (or quit to exit)"); - Console.Write("> "); - return Console.ReadLine(); - }); - - if ("quit".Equals(value, StringComparison.OrdinalIgnoreCase)) - break; - - await producer.Produce(new - { - Text = value - }); - } while (true); - } - finally - { - await busControl.StopAsync(); - } - } - - public record KafkaMessage - { - public string Text { get; init; } - } -} diff --git a/docs/code/riders/KafkaTombstoneProducer.cs b/docs/code/riders/KafkaTombstoneProducer.cs deleted file mode 100644 index cf728269ad0..00000000000 --- a/docs/code/riders/KafkaTombstoneProducer.cs +++ /dev/null @@ -1,71 +0,0 @@ -namespace KafkaTombstoneProducer; - -using System; -using System.Threading; -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); - - x.AddRider(rider => - { - rider.AddProducer("topic-name"); - - rider.UsingKafka((context, k) => { k.Host("localhost:9092"); }); - }); - }); - - var provider = services.BuildServiceProvider(); - - var busControl = provider.GetRequiredService(); - - await busControl.StartAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); - try - { - var producer = provider.GetRequiredService>(); - do - { - string value = await Task.Run(() => - { - Console.WriteLine("Enter text (or quit to exit)"); - Console.Write("> "); - return Console.ReadLine(); - }); - - if ("quit".Equals(value, StringComparison.OrdinalIgnoreCase)) - break; - - await producer.Produce("key", new { }, Pipe.Execute>(context => - { - context.ValueSerializer = new TombstoneSerializer(); - })); - } while (true); - } - finally - { - await busControl.StopAsync(); - } - } - - class TombstoneSerializer : - IAsyncSerializer - { - public Task SerializeAsync(T data, SerializationContext context) - { - return Task.FromResult(Array.Empty()); - } - } - - public record KafkaMessage - { - } -} diff --git a/docs/code/riders/KafkaTopicTopology.cs b/docs/code/riders/KafkaTopicTopology.cs deleted file mode 100644 index 9a165f0e551..00000000000 --- a/docs/code/riders/KafkaTopicTopology.cs +++ /dev/null @@ -1,41 +0,0 @@ -namespace KafkaTopicTopology; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); - - x.AddRider(rider => - { - rider.UsingKafka((context, k) => - { - k.Host("localhost:9092"); - - k.TopicEndpoint("topic-name", "consumer-group-name", e => - { - e.CreateIfMissing(t => - { - t.NumPartitions = 2; //number of partitions - t.ReplicationFactor = 1; //number of replicas - }); - }); - }); - }); - }); - } - - - public record KafkaMessage - { - public string Text { get; init; } - } -} diff --git a/docs/code/riders/KafkaWildcardConsumer.cs b/docs/code/riders/KafkaWildcardConsumer.cs deleted file mode 100644 index f33a41e83f0..00000000000 --- a/docs/code/riders/KafkaWildcardConsumer.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace KafkaWildcardConsumer; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => cfg.ConfigureEndpoints(context)); - - x.AddRider(rider => - { - rider.AddConsumer(); - - rider.UsingKafka((context, k) => - { - k.Host("localhost:9092"); - - k.TopicEndpoint("^topic-[0-9]*", "consumer-group-name", e => - { - e.ConfigureConsumer(context); - }); - }); - }); - }); - } - - class KafkaMessageConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return Task.CompletedTask; - } - } - - public record KafkaMessage - { - public string Text { get; init; } - } -} diff --git a/docs/code/sagas/MongoDbRegisterClassMap.cs b/docs/code/sagas/MongoDbRegisterClassMap.cs deleted file mode 100644 index c46ff33948f..00000000000 --- a/docs/code/sagas/MongoDbRegisterClassMap.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace MongoDbSagaRegisterClassMap; - -using System; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using MongoDB.Bson.Serialization; -using MongoDB.Bson.Serialization.Serializers; -using PersistedSaga; - -class OrderStateClassMap : - BsonClassMap -{ - public OrderStateClassMap() - { - MapProperty(x => x.OrderDate) - .SetSerializer(new DateTimeSerializer(DateTimeKind.Utc)); - } -} - - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddSingleton, OrderStateClassMap>(); - - services.AddMassTransit(x => - { - x.AddSagaStateMachine() - .MongoDbRepository(r => - { - r.Connection = "mongodb://127.0.0.1"; - r.DatabaseName = "orderdb"; - }); - }); - } -} diff --git a/docs/code/sagas/MongoDbSagaClassMap.cs b/docs/code/sagas/MongoDbSagaClassMap.cs deleted file mode 100644 index c30af4d36be..00000000000 --- a/docs/code/sagas/MongoDbSagaClassMap.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MongoDbSagaClassMap; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using PersistedSaga; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddSagaStateMachine() - .MongoDbRepository(r => - { - r.Connection = "mongodb://127.0.0.1"; - r.DatabaseName = "orderdb"; - - r.ClassMap(m => {}); - }); - }); - } -} diff --git a/docs/code/sagas/MongoDbSagaContainer.cs b/docs/code/sagas/MongoDbSagaContainer.cs deleted file mode 100644 index f0054ed01fa..00000000000 --- a/docs/code/sagas/MongoDbSagaContainer.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MongoDbSagaContainer; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using PersistedSaga; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddSagaStateMachine() - .MongoDbRepository(r => - { - r.Connection = "mongodb://127.0.0.1"; - r.DatabaseName = "orderdb"; - }); - }); - } -} diff --git a/docs/code/sagas/MongoDbSagaContainerCollection.cs b/docs/code/sagas/MongoDbSagaContainerCollection.cs deleted file mode 100644 index 3e4a73f53af..00000000000 --- a/docs/code/sagas/MongoDbSagaContainerCollection.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MongoDbSaga; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using PersistedSaga; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddSagaStateMachine() - .MongoDbRepository(r => - { - r.Connection = "mongodb://127.0.0.1"; - r.DatabaseName = "orderdb"; - - r.CollectionName = "orders"; - }); - }); - } -} diff --git a/docs/code/sagas/OrderState.cs b/docs/code/sagas/OrderState.cs deleted file mode 100644 index 8e4ebb52a13..00000000000 --- a/docs/code/sagas/OrderState.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace PersistedSaga; - -using System; -using MassTransit; - -public class OrderState : - SagaStateMachineInstance, - ISagaVersion -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } - - public int Version { get; set; } -} diff --git a/docs/code/sagas/OrderStateMachine.cs b/docs/code/sagas/OrderStateMachine.cs deleted file mode 100644 index 046a9df34e1..00000000000 --- a/docs/code/sagas/OrderStateMachine.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace PersistedSaga; - -using MassTransit; - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - } -} diff --git a/docs/code/sagas/RedisSagaContainer.cs b/docs/code/sagas/RedisSagaContainer.cs deleted file mode 100644 index 250ae8ea98f..00000000000 --- a/docs/code/sagas/RedisSagaContainer.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace RedisSagaContainer; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using PersistedSaga; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - const string configurationString = "127.0.0.1"; - - x.AddSagaStateMachine() - .RedisRepository(configurationString); - }); - } -} diff --git a/docs/code/sagas/RedisSagaContainerConfiguration.cs b/docs/code/sagas/RedisSagaContainerConfiguration.cs deleted file mode 100644 index 98bb243ce88..00000000000 --- a/docs/code/sagas/RedisSagaContainerConfiguration.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace RedisSagaContainerConfiguration; - -using System; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using PersistedSaga; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - const string configurationString = "127.0.0.1"; - - x.AddSagaStateMachine() - .RedisRepository(r => - { - r.DatabaseConfiguration(configurationString); - - // Default is Optimistic - r.ConcurrencyMode = ConcurrencyMode.Pessimistic; - - // Optional, prefix each saga instance key with the string specified - // resulting dev:c6cfd285-80b2-4c12-bcd3-56a00d994736 - r.KeyPrefix = "dev"; - - // Optional, to customize the lock key - r.LockSuffix = "-lockage"; - - // Optional, the default is 30 seconds - r.LockTimeout = TimeSpan.FromSeconds(90); - });; - }); - } -} diff --git a/docs/code/scheduling/SchedulingActiveMQ.cs b/docs/code/scheduling/SchedulingActiveMQ.cs deleted file mode 100644 index 8281b49b29a..00000000000 --- a/docs/code/scheduling/SchedulingActiveMQ.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace SchedulingActiveMQ; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddDelayedMessageScheduler(); - - x.UsingActiveMq((context, cfg) => - { - cfg.UseDelayedMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/scheduling/SchedulingAmazonSQS.cs b/docs/code/scheduling/SchedulingAmazonSQS.cs deleted file mode 100644 index cb30a14034e..00000000000 --- a/docs/code/scheduling/SchedulingAmazonSQS.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace SchedulingAmazonSQS; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddDelayedMessageScheduler(); - - x.UsingAmazonSqs((context, cfg) => - { - cfg.UseDelayedMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/scheduling/SchedulingAzure.cs b/docs/code/scheduling/SchedulingAzure.cs deleted file mode 100644 index 2f90dbc8fff..00000000000 --- a/docs/code/scheduling/SchedulingAzure.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace SchedulingAzure; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddServiceBusMessageScheduler(); - - x.UsingAzureServiceBus((context, cfg) => - { - cfg.UseServiceBusMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/scheduling/SchedulingConsumeContext.cs b/docs/code/scheduling/SchedulingConsumeContext.cs deleted file mode 100644 index 7c76b4db599..00000000000 --- a/docs/code/scheduling/SchedulingConsumeContext.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace SchedulingConsumeContext; - -using System; -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - Uri schedulerEndpoint = new Uri("queue:scheduler"); - - services.AddMassTransit(x => - { - x.AddMessageScheduler(schedulerEndpoint); - - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseMessageScheduler(schedulerEndpoint); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} - -public class ScheduleNotificationConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - Uri notificationService = new Uri("queue:notification-service"); - - await context.ScheduleSend(notificationService, - context.Message.DeliveryTime, - new() - { - EmailAddress = context.Message.EmailAddress, - Body = context.Message.Body - }); - } -} - -public record ScheduleNotification -{ - public DateTime DeliveryTime { get; init; } - public string EmailAddress { get; init; } - public string Body { get; init; } -} - -public record SendNotification -{ - public string EmailAddress { get; init; } - public string Body { get; init; } -} diff --git a/docs/code/scheduling/SchedulingDelayed.cs b/docs/code/scheduling/SchedulingDelayed.cs deleted file mode 100644 index 9b0e2a5d6ca..00000000000 --- a/docs/code/scheduling/SchedulingDelayed.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace SchedulingDelayed; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddDelayedMessageScheduler(); - - x.UsingInMemory((context, cfg) => - { - cfg.UseDelayedMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/scheduling/SchedulingEndpoint.cs b/docs/code/scheduling/SchedulingEndpoint.cs deleted file mode 100644 index 680781bbef0..00000000000 --- a/docs/code/scheduling/SchedulingEndpoint.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace SchedulingEndpoint; - -using System; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - Uri schedulerEndpoint = new Uri("queue:scheduler"); - - services.AddMassTransit(x => - { - x.AddMessageScheduler(schedulerEndpoint); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseMessageScheduler(schedulerEndpoint); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/scheduling/SchedulingInMemory.cs b/docs/code/scheduling/SchedulingInMemory.cs deleted file mode 100644 index 7c16d7dae7d..00000000000 --- a/docs/code/scheduling/SchedulingInMemory.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace SchedulingInMemory; - -using System; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddMessageScheduler(new Uri("queue:scheduler")); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseInMemoryScheduler("scheduler"); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/scheduling/SchedulingInternalInstance.cs b/docs/code/scheduling/SchedulingInternalInstance.cs deleted file mode 100644 index 6da748061b3..00000000000 --- a/docs/code/scheduling/SchedulingInternalInstance.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace SchedulingInternalInstance; - -using System; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; -using Quartz; - -public class Program -{ - public static void Main() - { - const string schedulerQueueName = "scheduler"; - var schedulerUri = new Uri($"queue:{schedulerQueueName}"); - - var services = new ServiceCollection(); - - services.AddQuartz(q => - { - q.UseMicrosoftDependencyInjectionJobFactory(); - }); - - services.AddMassTransit(x => - { - x.AddMessageScheduler(schedulerUri); - - x.AddPublishMessageScheduler(); - - x.AddQuartzConsumers(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UsePublishMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/scheduling/SchedulingRabbitMQ.cs b/docs/code/scheduling/SchedulingRabbitMQ.cs deleted file mode 100644 index 23a2602f8ef..00000000000 --- a/docs/code/scheduling/SchedulingRabbitMQ.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace SchedulingRabbitMq; - -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static void Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddDelayedMessageScheduler(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseDelayedMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }); - } -} diff --git a/docs/code/scheduling/SchedulingScheduler.cs b/docs/code/scheduling/SchedulingScheduler.cs deleted file mode 100644 index b673a8ed177..00000000000 --- a/docs/code/scheduling/SchedulingScheduler.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace SchedulingScheduler; - -using System; -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - Uri schedulerEndpoint = new Uri("queue:scheduler"); - - services.AddMassTransit(x => - { - x.AddMessageScheduler(schedulerEndpoint); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseMessageScheduler(schedulerEndpoint); - - cfg.ConfigureEndpoints(context); - }); - }); - - var provider = services.BuildServiceProvider(); - - var scheduler = provider.GetRequiredService(); - - await scheduler.SchedulePublish( - DateTime.UtcNow + TimeSpan.FromSeconds(30), new() - { - EmailAddress = "frank@nul.org", - Body = "Thank you for signing up for our awesome newsletter!" - }); - } -} - -public record SendNotification -{ - public string EmailAddress { get; init; } - public string Body { get; init; } -} diff --git a/docs/code/testing/UsingInMemoryTestHarness.cs b/docs/code/testing/UsingInMemoryTestHarness.cs deleted file mode 100644 index 9f8e65886f9..00000000000 --- a/docs/code/testing/UsingInMemoryTestHarness.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace UsingInMemoryTestHarness; - -using System.Threading.Tasks; -using MassTransit.Testing; -using NUnit.Framework; - -[TestFixture] -public class Using_the_test_harness -{ - [Test] - public async Task Should_be_easy() - { - var harness = new InMemoryTestHarness(); - - await harness.Start(); - try - { - - } - finally - { - await harness.Stop(); - } - } -} diff --git a/docs/code/testing/UsingInMemoryTestHarnessConsumer.cs b/docs/code/testing/UsingInMemoryTestHarnessConsumer.cs deleted file mode 100644 index f2c77fb2a09..00000000000 --- a/docs/code/testing/UsingInMemoryTestHarnessConsumer.cs +++ /dev/null @@ -1,65 +0,0 @@ -namespace UsingInMemoryTestHarnessConsumer; - -using System; -using System.Threading.Tasks; -using MassTransit; -using MassTransit.Testing; -using NUnit.Framework; - -[TestFixture] -public class Submitting_an_order -{ - [Test] - public async Task Should_publish_the_order_submitted_event() - { - var harness = new InMemoryTestHarness(); - var consumerHarness = harness.Consumer(); - - await harness.Start(); - try - { - await harness.InputQueueSendEndpoint.Send(new - { - OrderId = InVar.Id - }); - - // did the endpoint consume the message - Assert.That(await harness.Consumed.Any()); - - // did the actual consumer consume the message - Assert.That(await consumerHarness.Consumed.Any()); - - // the consumer publish the event - Assert.That(await harness.Published.Any()); - - // ensure that no faults were published by the consumer - Assert.That(await harness.Published.Any>(), Is.False); - } - finally - { - await harness.Stop(); - } - } -} - -public record SubmitOrder -{ - public Guid OrderId { get; init; } -} - -public record OrderSubmitted -{ - public Guid OrderId { get; init; } -} - -class SubmitOrderConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - await context.Publish(new - { - context.Message.OrderId - }); - } -} diff --git a/docs/code/topology/TopologyContracts.cs b/docs/code/topology/TopologyContracts.cs deleted file mode 100644 index ecab5fad1f4..00000000000 --- a/docs/code/topology/TopologyContracts.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace TopologyContracts; - -using System; - -public record SubmitOrder -{ - public Guid OrderId { get; init; } -} - -public record OrderSubmitted -{ - public Guid OrderId { get; init; } -} - -public interface OrderEvent -{ -} diff --git a/docs/code/topology/TopologyRabbitMqPublish.cs b/docs/code/topology/TopologyRabbitMqPublish.cs deleted file mode 100644 index 09209062841..00000000000 --- a/docs/code/topology/TopologyRabbitMqPublish.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace TopologyRabbitMqPublish; - -using TopologyContracts; -using MassTransit; - -public class Program -{ - public static void Main() - { - var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => - { - cfg.Publish(x => - { - x.Durable = false; // default: true - x.AutoDelete = true; // default: false - x.ExchangeType = "fanout"; // default, allows any valid exchange type - }); - - cfg.Publish(x => - { - x.Exclude = true; // do not create an exchange for this type - }); - }); - } -} diff --git a/docs/code/transports/ActiveMqConsoleListener.cs b/docs/code/transports/ActiveMqConsoleListener.cs deleted file mode 100644 index d53cab8f97c..00000000000 --- a/docs/code/transports/ActiveMqConsoleListener.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace ActiveMqConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingActiveMq((context, cfg) => - { - cfg.Host("localhost", h => - { - h.UseSsl(); - - h.Username("admin"); - h.Password("admin"); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/AmazonRabbitMqConsoleListener.cs b/docs/code/transports/AmazonRabbitMqConsoleListener.cs deleted file mode 100644 index e60f14ab776..00000000000 --- a/docs/code/transports/AmazonRabbitMqConsoleListener.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace AmazonRabbitMqConsoleListener; - -using System; -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.Host(new Uri("amqps://b-12345678-1234-1234-1234-123456789012.mq.us-east-2.amazonaws.com:5671"), h => - { - h.Username("username"); - h.Password("password"); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/AmazonSqsConsoleListener.cs b/docs/code/transports/AmazonSqsConsoleListener.cs deleted file mode 100644 index 2c1fadb155c..00000000000 --- a/docs/code/transports/AmazonSqsConsoleListener.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace AmazonSqsConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - x.UsingAmazonSqs((context, cfg) => - { - cfg.Host("us-east-2", h => - { - h.AccessKey("your-iam-access-key"); - h.SecretKey("your-iam-secret-key"); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/AmazonSqsReceiveEndpoint.cs b/docs/code/transports/AmazonSqsReceiveEndpoint.cs deleted file mode 100644 index 3f894c41395..00000000000 --- a/docs/code/transports/AmazonSqsReceiveEndpoint.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace AmazonSqsReceiveEndpoint; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - x.UsingAmazonSqs((context, cfg) => - { - cfg.Host("us-east-2", h => - { - h.AccessKey("your-iam-access-key"); - h.SecretKey("your-iam-secret-key"); - }); - - cfg.ReceiveEndpoint("input-queue", e => - { - // disable the default topic binding - e.ConfigureConsumeTopology = false; - - e.Subscribe("event-topic", s => - { - // set topic attributes - s.TopicAttributes["DisplayName"] = "Public Event Topic"; - s.TopicSubscriptionAttributes["some-subscription-attribute"] = "some-attribute-value"; - s.TopicTags.Add("environment", "development"); - }); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/AmazonSqsScopedConsoleListener.cs b/docs/code/transports/AmazonSqsScopedConsoleListener.cs deleted file mode 100644 index 83beb0d0c0e..00000000000 --- a/docs/code/transports/AmazonSqsScopedConsoleListener.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace AmazonSqsScopedConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - x.UsingAmazonSqs((context, cfg) => - { - cfg.Host("us-east-2", h => - { - h.AccessKey("your-iam-access-key"); - h.SecretKey("your-iam-secret-key"); - - // specify a scope for all topics - h.Scope("dev", true); - }); - - // additionally include the queues - cfg.ConfigureEndpoints(context, new DefaultEndpointNameFormatter("dev-", false)); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/CloudAmqpConsoleListener.cs b/docs/code/transports/CloudAmqpConsoleListener.cs deleted file mode 100644 index 21c4e6ce94b..00000000000 --- a/docs/code/transports/CloudAmqpConsoleListener.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace CloudAmqpConsoleListener; - -using System.Security.Authentication; -using System.Threading.Tasks; -using MassTransit; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => - { - cfg.Host("wombat.rmq.cloudamqp.com", 5671, "your_vhost", h => - { - h.Username("your_vhost"); - h.Password("your_password"); - - h.UseSsl(s => - { - s.Protocol = SslProtocols.Tls12; - }); - }); - }); - } -} diff --git a/docs/code/transports/ConfigureBatchConsoleListener.cs b/docs/code/transports/ConfigureBatchConsoleListener.cs deleted file mode 100644 index 265adce15ab..00000000000 --- a/docs/code/transports/ConfigureBatchConsoleListener.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System; -using System.Threading.Tasks; -using MassTransit; - -namespace ConfigureBatchConsoleListener; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => - { - cfg.Host("localhost", h => - { - h.ConfigureBatchPublish(x => - { - x.Enabled = true; - x.Timeout = TimeSpan.FromMilliseconds(2); - }); - }); - }); - } -} diff --git a/docs/code/transports/GrpcConsoleListener.cs b/docs/code/transports/GrpcConsoleListener.cs deleted file mode 100644 index 102d0b75110..00000000000 --- a/docs/code/transports/GrpcConsoleListener.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace GrpcConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingGrpc((context, cfg) => - { - cfg.Host(h => - { - h.Host = "127.0.0.1"; - h.Port = 19796; - }); - - cfg.ConfigureEndpoints(context); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/GrpcMultiConsoleListener.cs b/docs/code/transports/GrpcMultiConsoleListener.cs deleted file mode 100644 index 2806646f1b2..00000000000 --- a/docs/code/transports/GrpcMultiConsoleListener.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace GrpcMultiConsoleListener; - -using System; -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingGrpc((context, cfg) => - { - cfg.Host(h => - { - h.Host = "127.0.0.1"; - h.Port = 19796; - - h.AddServer(new Uri("http://127.0.0.1:19797")); - h.AddServer(new Uri("http://127.0.0.1:19798")); - }); - - cfg.ConfigureEndpoints(context); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/InMemoryBus.cs b/docs/code/transports/InMemoryBus.cs deleted file mode 100644 index e95014d44cd..00000000000 --- a/docs/code/transports/InMemoryBus.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace InMemoryConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingInMemory((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/RabbitMqConsoleListener.cs b/docs/code/transports/RabbitMqConsoleListener.cs deleted file mode 100644 index 3199541959f..00000000000 --- a/docs/code/transports/RabbitMqConsoleListener.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace RabbitMqConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public static class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingRabbitMq((context, cfg) => - { - cfg.Host("localhost", "/", h => - { - h.Username("guest"); - h.Password("guest"); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/ServiceBusConsoleListener.cs b/docs/code/transports/ServiceBusConsoleListener.cs deleted file mode 100644 index 4f9da00b0fd..00000000000 --- a/docs/code/transports/ServiceBusConsoleListener.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace ServiceBusConsoleListener; - -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - x.UsingAzureServiceBus((context, cfg) => - { - cfg.Host("connection-string"); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/ServiceBusManagedIdentityConsoleListener.cs b/docs/code/transports/ServiceBusManagedIdentityConsoleListener.cs deleted file mode 100644 index d1db053d039..00000000000 --- a/docs/code/transports/ServiceBusManagedIdentityConsoleListener.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace ServiceBusManagedIdentityConsoleListener; - -using System; -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - x.UsingAzureServiceBus((context, cfg) => - { - cfg.Host(new Uri("sb://your-service-bus-namespace.servicebus.windows.net")); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/transports/ServiceBusReceiveEndpoint.cs b/docs/code/transports/ServiceBusReceiveEndpoint.cs deleted file mode 100644 index 3a7e6f6fc98..00000000000 --- a/docs/code/transports/ServiceBusReceiveEndpoint.cs +++ /dev/null @@ -1,42 +0,0 @@ -namespace ServiceBusReceiveEndpoint; - -using System; -using System.Threading.Tasks; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - x.UsingAzureServiceBus((context, cfg) => - { - cfg.Host("connection-string"); - - cfg.ReceiveEndpoint("input-queue", e => - { - // all of these are optional!! - - e.PrefetchCount = 100; - - // number of "threads" to run concurrently - e.MaxConcurrentCalls = 100; - - // default, but shown for example - e.LockDuration = TimeSpan.FromMinutes(5); - - // lock will be renewed up to 30 minutes - e.MaxAutoRenewDuration = TimeSpan.FromMinutes(30); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/turnout/JobSystemClient.cs b/docs/code/turnout/JobSystemClient.cs deleted file mode 100644 index 52ed1b8a1a9..00000000000 --- a/docs/code/turnout/JobSystemClient.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace JobSystemClient; - -using System; -using System.Threading; -using System.Threading.Tasks; -using JobSystem.Jobs; -using MassTransit; -using MassTransit.Contracts.JobService; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingRabbitMq(); - - var source = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - - await busControl.StartAsync(source.Token); - try - { - var requestClient = busControl.CreateRequestClient(); - - do - { - string value = await Task.Run(() => - { - Console.WriteLine("Enter video format (or quit to exit)"); - Console.Write("> "); - return Console.ReadLine(); - }); - - if("quit".Equals(value, StringComparison.OrdinalIgnoreCase)) - break; - - var response = await requestClient.GetResponse(new - { - VideoId = NewId.NextGuid(), - Format = value - }); - } - while (true); - } - finally - { - await busControl.StopAsync(); - } - } -} diff --git a/docs/code/turnout/JobSystemConsoleService.cs b/docs/code/turnout/JobSystemConsoleService.cs deleted file mode 100644 index c102a9f8af5..00000000000 --- a/docs/code/turnout/JobSystemConsoleService.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace JobSystem.Jobs -{ - using System; - - public interface ConvertVideo - { - Guid VideoId { get; } - string Format { get; } - } -} - -namespace JobSystemConsoleService -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using JobSystem.Jobs; - using MassTransit; - using Microsoft.Extensions.DependencyInjection; - - public class Program - { - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMassTransit(x => - { - x.AddConsumer(cfg => - { - cfg.Options>(options => options - .SetJobTimeout(TimeSpan.FromMinutes(15)) - .SetConcurrentJobLimit(10)); - }); - - x.SetKebabCaseEndpointNameFormatter(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ServiceInstance(instance => - { - instance.ConfigureJobServiceEndpoints(); - - instance.ConfigureEndpoints(context); - }); - }); - }); - - var provider = services.BuildServiceProvider(); - - var busControl = provider.GetRequiredService(); - - await busControl.StartAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); - try - { - Console.WriteLine("Press enter to exit"); - - await Task.Run(() => Console.ReadLine()); - } - finally - { - await busControl.StopAsync(); - } - } - - public class ConvertVideoJobConsumer : - IJobConsumer - { - public async Task Run(JobContext context) - { - // simulate converting the video - await Task.Delay(TimeSpan.FromMinutes(3)); - } - } - } -} diff --git a/docs/code/usage/UsageConsumer.cs b/docs/code/usage/UsageConsumer.cs deleted file mode 100644 index d3d2ef62294..00000000000 --- a/docs/code/usage/UsageConsumer.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace UsageConsumer; - -using System.Threading.Tasks; -using UsageContracts; -using MassTransit; - -class SubmitOrderConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - await context.Publish(new - { - context.Message.OrderId - }); - } -} diff --git a/docs/code/usage/UsageConsumerBus.cs b/docs/code/usage/UsageConsumerBus.cs deleted file mode 100644 index fc3f7f5aeb2..00000000000 --- a/docs/code/usage/UsageConsumerBus.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace UsageConsumerBus; - -using System.Threading.Tasks; -using UsageConsumer; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.AddConsumer(); - - x.UsingInMemory((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/usage/UsageConsumerConnect.cs b/docs/code/usage/UsageConsumerConnect.cs deleted file mode 100644 index 89eae3ad5fc..00000000000 --- a/docs/code/usage/UsageConsumerConnect.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace UsageConsumerConnect; - -using System; -using System.Threading; -using System.Threading.Tasks; -using UsageContracts; -using MassTransit; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - await using var provider = new ServiceCollection() - .AddMassTransit(x => - { - x.AddConsumer(); - - x.UsingRabbitMq(); - }) - .BuildServiceProvider(); - - var busControl = provider.GetRequiredService(); - - var source = new CancellationTokenSource(TimeSpan.FromSeconds(10)); - - await busControl.StartAsync(source.Token); - try - { - var handle = busControl.ConnectConsumer(); - - var endpoint = await busControl.GetSendEndpoint(new Uri("queue:order-service")); - - await endpoint.Send(new - { - OrderId = InVar.Id, - __ResponseAddress = busControl.Address - }); - - Console.WriteLine("Press enter to exit"); - - await Task.Run(() => Console.ReadLine()); - - // disconnect the consumer from the bus endpoint - handle.Disconnect(); - } - finally - { - await busControl.StopAsync(); - } - } -} - -class OrderAcknowledgedConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - } -} diff --git a/docs/code/usage/UsageConsumerOverloads.cs b/docs/code/usage/UsageConsumerOverloads.cs deleted file mode 100644 index 6798139b415..00000000000 --- a/docs/code/usage/UsageConsumerOverloads.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace UsageConsumerOverloads; - -using System; -using System.IO; -using System.Threading.Tasks; -using UsageConsumer; -using UsageContracts; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingRabbitMq((cxt, cfg) => - { - cfg.ReceiveEndpoint("order-service", e => - { - // delegate consumer factory - e.Consumer(() => new SubmitOrderConsumer()); - - // another delegate consumer factory, with dependency - e.Consumer(() => new LogOrderSubmittedConsumer(Console.Out)); - - // a type-based factory that returns an object (specialized uses) - var consumerType = typeof(SubmitOrderConsumer); - e.Consumer(consumerType, type => Activator.CreateInstance(consumerType)); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} - -class LogOrderSubmittedConsumer : - IConsumer -{ - readonly TextWriter _writer; - - public LogOrderSubmittedConsumer(TextWriter writer) - { - _writer = writer; - } - - public async Task Consume(ConsumeContext context) - { - await _writer.WriteLineAsync($"Order submitted: {context.Message.OrderId}"); - } -} diff --git a/docs/code/usage/UsageConsumerTemporaryEndpoint.cs b/docs/code/usage/UsageConsumerTemporaryEndpoint.cs deleted file mode 100644 index ead7c694219..00000000000 --- a/docs/code/usage/UsageConsumerTemporaryEndpoint.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace UsageConsumerTemporaryEndpoint; - -using System.Threading.Tasks; -using UsageConsumer; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.AddConsumer(); - - x.UsingInMemory((context, cfg) => - { - cfg.ReceiveEndpoint(new TemporaryEndpointDefinition(), e => - { - e.ConfigureConsumer(context); - }); - - cfg.ConfigureEndpoints(context); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/usage/UsageContracts.cs b/docs/code/usage/UsageContracts.cs deleted file mode 100644 index cd3f8d88981..00000000000 --- a/docs/code/usage/UsageContracts.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace UsageContracts; - -using System; - -public record SubmitOrder -{ - public Guid OrderId { get; init; } -} - -public record OrderSubmitted -{ - public Guid OrderId { get; init; } -} - -public record SubmitOrderAcknowledged -{ - public Guid OrderId { get; init; } -} diff --git a/docs/code/usage/UsageHandler.cs b/docs/code/usage/UsageHandler.cs deleted file mode 100644 index 96f4a4cf390..00000000000 --- a/docs/code/usage/UsageHandler.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace UsageHandler; - -using System; -using System.Threading.Tasks; -using UsageContracts; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingInMemory((cxt, cfg) => - { - cfg.ReceiveEndpoint("order-service", e => - { - e.Handler(async context => - { - await Console.Out.WriteLineAsync($"Submit Order Received: {context.Message.OrderId}"); - }); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/usage/UsageInstance.cs b/docs/code/usage/UsageInstance.cs deleted file mode 100644 index 7a728c65eb8..00000000000 --- a/docs/code/usage/UsageInstance.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace UsageInstance; - -using System.Threading.Tasks; -using UsageConsumer; -using MassTransit; -using Microsoft.Extensions.Hosting; - -public class Program -{ - public static async Task Main(string[] args) - { - var submitOrderConsumer = new SubmitOrderConsumer(); - - await Host.CreateDefaultBuilder(args) - .ConfigureServices(services => - { - services.AddMassTransit(x => - { - x.UsingInMemory((context, cfg) => - { - cfg.ReceiveEndpoint("order-service", e => - { - e.Instance(submitOrderConsumer); - }); - }); - }); - }) - .Build() - .RunAsync(); - } -} diff --git a/docs/code/usage/UsageMediator.cs b/docs/code/usage/UsageMediator.cs deleted file mode 100644 index 360e11e2dbc..00000000000 --- a/docs/code/usage/UsageMediator.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace UsageMediator; - -using System.Threading.Tasks; -using UsageConsumer; -using MassTransit; -using MassTransit.Mediator; - -public class Program -{ - public static async Task Main() - { - IMediator mediator = Bus.Factory.CreateMediator(cfg => - { - cfg.Consumer(); - }); - } -} diff --git a/docs/code/usage/UsageMediatorConfigure.cs b/docs/code/usage/UsageMediatorConfigure.cs deleted file mode 100644 index b2239951885..00000000000 --- a/docs/code/usage/UsageMediatorConfigure.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace UsageMediatorConfigure; - -using System; -using System.Threading.Tasks; -using UsageContracts; -using UsageConsumer; -using UsageMediatorConsumer; -using MassTransit; -using MassTransit.Mediator; -using Microsoft.Extensions.DependencyInjection; - -public class ValidateOrderStatusFilter : - IFilter> - where T : class -{ - public void Probe(ProbeContext context) - { - } - - public Task Send(SendContext context, IPipe> next) - { - if (context.Message is GetOrderStatus getOrderStatus && getOrderStatus.OrderId == Guid.Empty) - throw new ArgumentException("The OrderId must not be empty"); - - return next.Send(context); - } -} - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMediator(cfg => - { - cfg.AddConsumer(); - cfg.AddConsumer(); - - cfg.ConfigureMediator((context, mcfg) => - { - mcfg.UseSendFilter(typeof(ValidateOrderStatusFilter<>), context); - }); - }); - - var provider = services.BuildServiceProvider(); - - var mediator = provider.GetRequiredService(); - - Guid orderId = NewId.NextGuid(); - - await mediator.Send(new { OrderId = orderId }); - - var client = mediator.CreateRequestClient(); - - var response = await client.GetResponse(new { OrderId = orderId }); - - Console.WriteLine("Order Status: {0}", response.Message.Status); - } -} diff --git a/docs/code/usage/UsageMediatorConnect.cs b/docs/code/usage/UsageMediatorConnect.cs deleted file mode 100644 index 4419212bd35..00000000000 --- a/docs/code/usage/UsageMediatorConnect.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace UsageMediatorConnect; - -using System.Threading.Tasks; -using UsageConsumer; -using Microsoft.Extensions.DependencyInjection; -using MassTransit; -using MassTransit.Mediator; - -public class Program -{ - public static async Task Main() - { - await using var provider = new ServiceCollection() - .AddMediator(cfg => { }) - .BuildServiceProvider(); - - var mediator = provider.GetRequiredService(); - - var handle = mediator.ConnectConsumer(); - } -} diff --git a/docs/code/usage/UsageMediatorConsumer.cs b/docs/code/usage/UsageMediatorConsumer.cs deleted file mode 100644 index ac9c07bccb0..00000000000 --- a/docs/code/usage/UsageMediatorConsumer.cs +++ /dev/null @@ -1,29 +0,0 @@ -namespace UsageMediatorConsumer; - -using System; -using System.Threading.Tasks; -using MassTransit; - -public record GetOrderStatus -{ - public Guid OrderId { get; init; } -} - -public record OrderStatus -{ - public Guid OrderId { get; init; } - public string Status { get; init; } -} - -class OrderStatusConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - await context.RespondAsync(new - { - context.Message.OrderId, - Status = "Pending" - }); - } -} diff --git a/docs/code/usage/UsageMediatorContainer.cs b/docs/code/usage/UsageMediatorContainer.cs deleted file mode 100644 index df1f4536984..00000000000 --- a/docs/code/usage/UsageMediatorContainer.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace UsageMediatorContainer; - -using System; -using System.Threading.Tasks; -using UsageContracts; -using UsageConsumer; -using UsageMediatorConsumer; -using MassTransit; -using MassTransit.Mediator; -using Microsoft.Extensions.DependencyInjection; - -public class Program -{ - public static async Task Main() - { - var services = new ServiceCollection(); - - services.AddMediator(cfg => - { - cfg.AddConsumer(); - cfg.AddConsumer(); - }); - - var provider = services.BuildServiceProvider(); - - var mediator = provider.GetRequiredService(); - - Guid orderId = NewId.NextGuid(); - - await mediator.Send(new { OrderId = orderId }); - - var client = mediator.CreateRequestClient(); - - var response = await client.GetResponse(new { OrderId = orderId }); - - Console.WriteLine("Order Status: {0}", response.Message.Status); - } -} diff --git a/docs/code/usage/UsageMediatorRequest.cs b/docs/code/usage/UsageMediatorRequest.cs deleted file mode 100644 index 322010340a8..00000000000 --- a/docs/code/usage/UsageMediatorRequest.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace UsageMediatorRequest; - -using System; -using System.Threading.Tasks; -using UsageContracts; -using Microsoft.Extensions.DependencyInjection; -using UsageMediatorConsumer; -using MassTransit; -using MassTransit.Mediator; - -public class Program -{ - public static async Task Main() - { - await using var provider = new ServiceCollection() - .AddMediator(cfg => { }) - .BuildServiceProvider(); - - var mediator = provider.GetRequiredService(); - - Guid orderId = NewId.NextGuid(); - - await mediator.Send(new { OrderId = orderId }); - - var client = mediator.CreateRequestClient(); - - var response = await client.GetResponse(new { OrderId = orderId }); - - Console.WriteLine("Order Status: {0}", response.Message.Status); - } -} diff --git a/docs/code/usage/UsageMessageCorrelation.cs b/docs/code/usage/UsageMessageCorrelation.cs deleted file mode 100644 index 521e79de01c..00000000000 --- a/docs/code/usage/UsageMessageCorrelation.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace UsageMessageCorrelation; - -using System; -using System.Threading; -using System.Threading.Tasks; -using UsageContracts; -using MassTransit; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingRabbitMq(); - - await busControl.StartAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); - try - { - var endpoint = await busControl.GetSendEndpoint(new Uri("queue:order-service")); - - // Set CorrelationId using SendContext - await endpoint.Send(new { OrderId = InVar.Id }, context => - context.CorrelationId = context.Message.OrderId); - - // Set CorrelationId using initializer header - await endpoint.Send(new - { - OrderId = InVar.Id, - __CorrelationId = InVar.Id - - // InVar.Id returns the same value within the message initializer - }); - } - finally - { - await busControl.StopAsync(); - } - } -} diff --git a/docs/code/usage/UsageMessageSendCorrelation.cs b/docs/code/usage/UsageMessageSendCorrelation.cs deleted file mode 100644 index fa76f5f3c5d..00000000000 --- a/docs/code/usage/UsageMessageSendCorrelation.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace UsageMessageSendCorrelation; - -using System.Threading.Tasks; -using UsageContracts; -using MassTransit; - -public class Program -{ - public static async Task Main() - { - var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => - { - cfg.SendTopology.UseCorrelationId(x => x.OrderId); - }); - } -} diff --git a/docs/code/usage/UsageMessageSetCorrelation.cs b/docs/code/usage/UsageMessageSetCorrelation.cs deleted file mode 100644 index 0377f413fea..00000000000 --- a/docs/code/usage/UsageMessageSetCorrelation.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace UsageMessageSetCorrelation; - -using UsageContracts; -using MassTransit; - -public class Program -{ - public static void Main() - { - // Use the OrderId as the message CorrelationId - GlobalTopology.Send.UseCorrelationId(x => x.OrderId); - - // Previous approach, which now calls the new way above - MessageCorrelation.UseCorrelationId(x => x.OrderId); - } -} diff --git a/docs/discord.md b/docs/discord.md deleted file mode 100644 index dc04dd502d5..00000000000 --- a/docs/discord.md +++ /dev/null @@ -1,7 +0,0 @@ -# Discord - -MassTransit uses Discord for live support. Discord is a powerful chat platform, with support for text, audio, and video chat rooms. - -[![alt Join the conversation](https://img.shields.io/discord/682238261753675864.svg "Discord")](https://discord.gg/rNpQgYn) - - diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md deleted file mode 100644 index 521dc61f467..00000000000 --- a/docs/getting-started/README.md +++ /dev/null @@ -1,31 +0,0 @@ -# Getting Started - -Getting started with MassTransit is fast and easy. - -## Pre-Requisites - -- The [.NET 6 SDK](https://dotnet.microsoft.com/download) should be installed before continuing. -- Some examples use [Docker](https://www.docker.com/products/docker-desktop) to run backing services - -## Select your transport: - -- [In Memory](/quick-starts/in-memory): A dependency free way to get started, but not for production use -- [RabbitMQ](/quick-starts/rabbitmq): A high performance transport that allows both cloud based and local development -- [Azure Service Bus](/quick-starts/azure-service-bus): Use the power of Azure -- [SQS](/quick-starts/sqs): Use the power of AWS - -## What are we going to build? - -Each example goes through complete process of creating a messaging based system. We will configure the HostBuilder to use MassTransit, we will create a message, a consumer of that message. - -Next up, the `AddMassTransit` extension is used to configure the bus in the container. The `UsingInMemory` (and `UsingRabbitMq`) method specifies the transport to use for the bus. Each transport has its own `UsingXxx` method. - -### Quick note on terminology - -A _message_ in MassTransit is just a Plain Old CLR Object or `POCO` for short. In MassTransit this can be a _class_, an _interface_, or a _record_. Records are currently recommended when using .NET 5 or greater. - -A _consumer_ is a .NET class that implements `IConsumer` for one or more message types and is similar to an ASP.NET controller . Consumers are registered using the `AddConsumer` method within the `AddMassTransit` service collection extension method. Consumers are added by MassTransit to the underlying service collection as _scoped_. - -## Let's Get Started - -If you aren't sure which transport you are going to want to use yet, we'd recommend trying the [in-memory](/quick-starts/in-memory)! It has no dependencies and can easily be upgraded to another transport thanks to MassTransit's abstractions. \ No newline at end of file diff --git a/docs/getting-started/live-coding.md b/docs/getting-started/live-coding.md deleted file mode 100644 index de91ef8150f..00000000000 --- a/docs/getting-started/live-coding.md +++ /dev/null @@ -1,17 +0,0 @@ -# Live Coding - -Chris Patterson ([@PhatBoyG](https://twitter.com/PhatBoyG)) has begun production of a video series. Each episode is a live, all-code experience covering MassTransit's extensive capabilities. - -> These videos were started in response to the COVID-19 crisis when many communities were put under _shelter in place_ orders. To make good use of the time (and to avoid binge watching yet another B-series on Netflix), this series was started as a learning resources to share with others in the same situation. - -Episodes are broadcast in 1080p60, and are a continuous journey through the creation of a fictitious order and inventory management solution. - -### YouTube - -Episodes are [available on YouTube](https://www.youtube.com/playlist?list=PLx8uyNNs1ri2MBx6BjPum5j9_MMdIfM9C), new episodes are added fairly regularly. - -### Twitch - -> Live sessions are rarely broadcast at this point, most are recorded and directly posted on YouTube. - -Catch live broadcasts on [Twitch](https://twitch.tv/phatboyg). Subscribe to notifications to know when a new live show is about to start. If you miss a live show, previous episodes are retained for sixty days in the [MassTransit Collection](https://www.twitch.tv/collections/-mgc27WB_hVY-A). diff --git a/docs/getting-started/upgrade-v6.md b/docs/getting-started/upgrade-v6.md deleted file mode 100644 index b67cc570831..00000000000 --- a/docs/getting-started/upgrade-v6.md +++ /dev/null @@ -1,346 +0,0 @@ -# Upgrading - - -## Version 8 - -MassTransit v8 is the first major release since the availability of .NET 6. MassTransit v8 works a significant portion of the underlying components into a more manageable solution structure. Focused on the developer experience, while maintaining compatibility with previous versions, this release brings together the entire MassTransit stack. - -Automatonymous, Green Pipes, and NewId have been completely integrated into a single MassTransit solution. This means that every aspect of MassTransit is now within a single namespace, which makes it easy to find the right interface, extension, and whatever else is needed. A lot of common questions result in a missing `using` statement, and now that should no longer be the case. The entire developer surface area, for the most part, exists within the `MassTransit` namespace. - -When upgrading from previous versions of MassTransit, there are a few initial steps to get up and running. While this list doesn't cover everything, these are the main items experienced so far when upgrading from a previous version. - -- Remove any references to packages that were not updated with v8. This includes: - - `GreenPipes` - - `NewId` (still available separately, do not use in a project referencing MassTransit) - - `Automatonymous` - - `Automatonymous.Visualizer` -> `MassTransit.StateMachineVisualizer` - - `MassTransit.AspNetCore` - - `MassTransit.Extensions.DependencyInjection` - - Any of the third-party container assemblies. -- Remove any `using` statements that for namespaces that no longer exist - -Some configuration interfaces have been removed/changed names: - -| Original | New | -|--|--| -|`IServiceCollectionBusConfigurator`|`IBusRegistrationConfigurator`| -|`IServiceCollectionRiderConfigurator`|`IRiderRegistrationConfigurator`| -|`IServiceCollectionMediatorConfigurator`|`IMediatorRegistrationConfigurator`| - -### Serialization - -The default JSON serializer is now `System.Text.Json`. Refer to [Microsoft's Migration Guide](https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-migrate-from-newtonsoft-how-to?pivots=dotnet-6-0) if you encounter any serialization issues after upgrading. - -To continue using Newtonsoft for serialization, add the `MassTransit.Newtonsoft` package and specify one of the configuration methods when configuring the bus: -- `UseNewtonsoftJsonSerializer` -- `UseNewtonsoftRawJsonSerializer` -- `UseXmlSerializer` -- `UseBsonSerializer` - -### Hosted Service - -Previous versions of MassTransit required the use of the `MassTransit.AspNetCore` package to support registration of MassTransit's hosted service. This package is no longer required, and MassTransit will automatically add an `IHostedService` for MassTransit. - -The host can be configured using `IOptions` configuration support, such as shown below: - -```cs -services.Configure(options => -{ - options.WaitUntilStarted = true; - options.StartTimeout = TimeSpan.FromSeconds(30); - options.StopTimeout = TimeSpan.FromMinutes(1); -}); -``` - -::: tip Generic Host -The .NET Generic Host has its own internal timers for shutdown, etc. that may also need to be adjusted: - -```cs -services.Configure( - opts => opts.ShutdownTimeout = TimeSpan.FromMinutes(1)); -``` -::: - -> In addition to the hosted service, .NET health checks are added as well, and may be included on health check endpoints. - -### Observers - -Observers registered in the container will be connected to the bus automatically, including: - -| Observer Type | Registration | -|--|--| -| `IBusObserver` | `AddBusObserver` | -| `IReceiveObserver` | `AddReceiveObserver` | -| `IConsumeObserver` | `AddConsumeObserver` | -| `IReceiveEndpointObserver` | `AddReceiveEndpointObserver` | - -### State Machine Changes - -The state machine interfaces, `BehaviorContext` and `BehaviorContext` are now derived from `SagaConsumeContext` and `SagaConsumeContext`. This significantly improves the usability of MassTransit features in state machine. No more calling `GetPayload` or other methods to get access to the `ConsumeContext`! Seriously, this is awesome. - -As part of this change, the `.Data` and `.Instance` properties of `BehaviorContext` are superfluous, and have subsequently been marked as obsolete. They still work, and return `.Message` or `.Saga` respectively, but eventually the might be removed (not in the near future though). - -The previous `Automatonymous.Activity` and `Automatonymous.Activity` interfaces have been renamed, and are now `IStateMachineActivity` and `IStateMachineActivity` (both are now in the top-level `MassTransit` namespace). - -Specifying headers when using the `.Init()` message initializer with `SendAsync`, `PublishAsync`, and other related methods now works as expected! - -The saga state machine test harness type `IStateMachineSagaTestHarness` has been replaced with the properly named type `ISagaStateMachineTestHarness`, which also has consistent generic argument ordering. - -A new `.Retry()` activity has been added, allowing individual activities within a state machine to be retried. This retry is performed inline, with the same saga instance, and uses the same retry policies as message-based retry. - -### Nullable Types - -`MassTransit.Abstractions` has enabled nullable type information, so it may signal to the compiler that a null can be returned when appropriate for certain methods. - -### Unit Testing - -A new version of the test harness is now available, specifically designed for use with containers. The basics are the same, only the configuration has changed. An example test, shown below, using the in-memory transport by default. Consumer, saga, and activity test harnesses are added automatically and can be retrieved from the harness. - -```cs -[Test] -public async Task The_consumer_should_respond_to_the_request() -{ - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - var client = harness.GetRequestClient(); - - await client.GetResponse(new - { - OrderId = InVar.Id, - OrderNumber = "123" - }); - - Assert.IsTrue(await harness.Sent.Any()); - - Assert.IsTrue(await harness.Consumed.Any()); - - var consumerHarness = harness.GetConsumerHarness(); - - Assert.That(await consumerHarness.Consumed.Any()); -} -``` - -::: tip -When the _provider_ (which is an `IServiceProvider`) is disposed, it will dispose of the test harness, which will stop the bus. -::: - -Additionally, the test harness can now be used with any transport. For example, to use RabbitMQ: - -```cs -[Test] -public async Task Should_use_broker() -{ - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.Host("some-broker-address", h => - { - h.Username("joe"); - h.Password("cool"); - }); - - cfg.ConfigureEndpoints(context); - }); - }) - .BuildServiceProvider(true); -} -``` - -### Third-Party Container Support - -MassTransit is now using _Microsoft.Extensions.DependencyInjection.Abstractions_ as an integral configuration component. This means that all configuration (such as `AddMassTransit`, `AddMediator`) is built against `IServiceCollection`. Support for other containers is provided using each specific container's extensions to work with `IServiceCollection` and `IServiceProvider`. - -For example, using Autofac, the configuration might look something like what is shown below. - -```cs -var collection = new ServiceCollection(); - -collection.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); -}); -var factory = new AutofacServiceProviderFactory(); -var container = factory.CreateBuilder(collection); - -return factory.CreateServiceProvider(container); -``` - -MassTransit would then be able to use `IServiceProvider` with Autofac to create scopes, resolve dependencies, etc. - -### Transport Changes - -- [RabbitMQ batch publishing](/usage/transports/rabbitmq.md#configurebatch) is now disabled by default. If you are seeing a degradation in publishing performance after upgrading, you may benefit from enabling batch publish to increase throughput. Scenarios where batching can improve throughput include high-latency broker connectivity and HA/Lazy queues. - -## Version 7 - -As with previous major versions, MassTransit V7 includes a number of new features, but has also deprecated or changed from previous configuration syntax. For the most part, consumers, sagas, etc. should work exactly as they did with previous releases. However, some of the configuration aspects may have been updated to be more consistent. - -### .NET Standard 2.0 - -MassTransit is now built for .NET Standard 2.0 only. The packages should be compatible with .NET Standard 2.0 (or later), as well as .NET Framework 4.6.1 (or later). Specific .NET Framework packages are no longer built or packaged. - -### Riders - -[Riders](/usage/riders/) are an entirely new feature which adds Kafka and Azure Event Hub support to MassTransit. A huge thanks to Denys Kozhevnikov [GitHub](https://github.com/MassTransit/MassTransit/commits?author=NooNameR) [@noonamer](https://twitter.com/noonamer) for his amazing effort! - -### Configuration - -Configuring MassTransit using a container has been streamlined. Refer to the [configuration](/usage/configuration) section for details. A brief summary: - -- The `.Host` methods are now void. The _IHost_ interfaces are no longer accessible (or needed). -- `AddBus` has been superseded by `UsingRabbitMq` (and other transport-specific extension methods) - -### Sagas - -The SagaRepository standardization is now completed, and all other repositories have been removed (InMemorySagaRepository is still there though). - -The send topology can now be used to configure the _CorrelationId_ that should be used, allowing state machine sagas to automatically configure events that correlate on a property that isn't implemented by `CorrelatedBy`. - -### Message Scheduler - -A number of new container configuration options for configuring and registering the [message scheduler](/advanced/scheduling/scheduling-api) have been added. - -### Turnout - -Turnout, which has been poorly supported since the beginning, has been rewritten from the ground up. Consumers can now use the `IJobConsumer` interface to support long-running jobs managed by MassTransit. They are supported using Conductor, and a series of state machines to track job execution, retry, and concurrency. Check out the [job consumers](/advanced/job-consumers) section for details. - -### Mediator - -- Container configuration has changed, and now uses the `AddMediator` method (instead of `AddMassTransit`). -- Publish no longer throws if there are no consumers. To throw when publishing and no consumers are registered, set the _Mandatory_ flag on the _PublishContext_. -- Consumers can now be connected/detached after the mediator has been created. - -### Testing - -- Test harnesses now use an inactivity timer to complete sooner once the bus stops processing messages. -- Message lists, such as Consumed, Received, Sent, and Published, now have async _Any_ methods - -### Transactions - -The _transaction outbox_ has been renamed to _TransactionalBus_, to avoid confusion. See the [transactions section](/advanced/middleware/transactions) for details. - -### Changed, Deprecated - -The following packages have been deprecated and replaced with a new package: - -* [MassTransit.DocumentDb](https://nuget.org/packages/MassTransit.DocumentDb/)
Use [MassTransit.Azure.Cosmos](https://nuget.org/packages/MassTransit.Azure.Cosmos/) instead. -* [MassTransit.Lamar](https://nuget.org/packages/MassTransit.Lamar/)
Use [MassTransit.Extensions.DependencyInjection](https://nuget.org/packages/MassTransit.Extensions.DependencyInjection/) to configure the container. -* [MassTransit.Host](https://nuget.org/packages/MassTransit.Host/)
Use [MassTransit Platform](/platform) instead. - -The following packages have been deprecated and are no longer supported: - -* [MassTransit.Http](https://nuget.org/packages/MassTransit.Http/) -* [MassTransit.Ninject](https://nuget.org/packages/MassTransit.Ninject/) -* [MassTransit.Reactive](https://nuget.org/packages/MassTransit.Reactive/) -* [MassTransit.Unity](https://nuget.org/packages/MassTransit.Unity/) - - - - - - - - - - -## Version 6 - - - -### Automatonymous - -In previous version, using Automatonymous required an additional package, `MassTransit.Automatonymous`. The contents of that package are now -included in the `MassTransit` assembly/package, which now depends on `Automatonymous`. This was done to reduce the number of extra packages -required for container support (along with state machine registration), as well as improve the saga repository persistence assemblies. - -When upgrading to v6, any references to the old `MassTransit.Automatonymous` package should be removed. - -If you are using a container with MassTransit, and were using one of the old container packages for Automatonymous, those package references -should also be removed. With version 6, only the single container integration package is required (such as `MassTransit.Autofac` or -`MassTransit.Extensions.DependencyInjection`). - -The following packages are available for the supported containers: - -- MassTransit.Autofac -- MassTransit.Extensions.DependencyInjection -- MassTransit.SimpleInjector -- MassTransit.StructureMap -- MassTransit.Windsor - -### Saga Repository Update (v6.1+) - -The saga repositories have been completely refactored, to eliminate duplicate logic and increase consistency across the various storage engines. All repositories also now support the container registration extensions, which provides a consistent syntax for registering and configuring saga repositories for use with dependency injection containers. When using the `.AddMassTransit()` container registration method, a repository can now be registered with the saga. For details, see the updated [documentation](/usage/sagas/persistence). - -### Azure Service Bus - -The previous (now legacy) **MassTransit.AzureServiceBus** package, which was only maintained to continue support for .NET 4.5.2, has been deprecated. Going forward, the **MassTransit.Azure.ServiceBus.Core** package should be used. The package supports both .NET 4.6.1 and .NET Standard 2.0. With the new package, the .NET Messaging protocol is no longer supported. The new package includes both AMQP and WebSocket support. Certain corporate firewall configurations that previously used .NET Messaging instead of AMQP may need to specify the web socket protocol to connect to Azure Service Bus. - -### Logging - -The previous log abstraction used by MassTransit has been replaced with `Microsoft.Extensions.Logging.Abstractions`. - -The previous log integration packages for Log4Net, NLog, and Serilog have been deprecated. An `ILoggerFactory` instance can be -configured for MassTransit by calling: - -```csharp -LogContext.ConfigureCurrentLogContext(loggerFactory); -``` - -This should be done prior to configuring the bus. - -::: tip -If you are using the new `.AddMassTransit()` configuration, combined with `.AddBus()`, then _ILoggerFactory_ is automatically configured for you. In this case, the statement above is not required. -::: - -### DiagnosticSource - -As of version 6, MassTransit now uses DiagnosticSource for tracking messaging operations, such as Send, Receive, Publish, Consume, etc. An `Activity` is -created for each operation, and context-relevant tags and baggage are added. - -MassTransit follows the [guidance](https://github.com/dotnet/runtime/blob/master/src/libraries/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md) from Microsoft. To connect listeners, look at the [section](https://github.com/dotnet/runtime/blob/master/src/libraries/System.Diagnostics.DiagnosticSource/src/ActivityUserGuide.md#subscribe-to-diagnosticsource) that explains how to connect. - -### Receive Endpoint Configuration - -When MassTransit underwent a major overhaul, and multiple host support was added, that seemed like a great idea. A single bus talking to more than one broker, doing messaging. *Reality* &emdot; nobody used it. It added a lot of complexity, that wasn't used. - -With version 6, a single bus has a single host. That's it. Simple. And with this change, it is no longer necessary to specify the host when configuring a receive endpoint. Yes, the old methods are there, and a pseudo-host is returned from the `.Host()` call which can still be passed, but it is ignored. All the transport-specific configuration methods are still there, without the `host` parameter. - -So, enjoy the simplicity. Under the covers some other things were also made simple &emdot; but I doubt you'll notice. - -### Courier - -To be consistent with the rest of MassTransit, many of the interfaces in Courier has been renamed. For example, `ExecuteActivity` is now -`IExecuteActivity`. The previous interfaces are still supported, but have been marked obsolete. - -### Conductor (coming soon) - -Hard things are hard. Building distributed applications at scale is a hard thing, and it's hard. In fact, it is really hard. - -So hard that it isn't ready yet - but there is enough other stuff to warrant releasing v6 without it. - -_Conductor wants to make it easier, with less complexity._ - - -### MassTransit Platform - -Previous version of MassTransit provided a generalized service host, built using Topshelf, to get started with your first project. But the world has changed. With ASP.NET Core 3.1, and all the goodness that is the generic host, the developer community has moved to a new place. - -MassTransit.Host is being replaced with the new [Platform](/platform), which is a Docker-based solution for consistent service deployment using MassTransit. diff --git a/docs/learn/README.md b/docs/learn/README.md deleted file mode 100644 index 219e2999418..00000000000 --- a/docs/learn/README.md +++ /dev/null @@ -1,39 +0,0 @@ -# Getting Help - -Writing distributed applications is hard, and sometimes you need help. There are several ways to get in touch with the developers behind MassTransit. In most cases, you can expect a response within a few days, sometimes sooner. The speed of the response is going to depend upon how well you've done your homework before raising the white flag. - -However, before attempting to contact a developer directly, there are many active forums for support that are available. - -## Stack Overflow - -There is a MassTransit tag on [Stack Overflow][1], which has many questions that have already been asked. Several developers regularly monitor this tag for new questions, so that's a great place to start. Be sure to search and see if your question has already been asked, that is the fastest way to an answer if someone else has already experienced the same issue. - -Before you just post your question, however, spend a few moments to compose your thoughts and formulate your question. There is nothing as pointless as simply telling us "MassTransit does not work for me" with no further information to give any clue to why. Before you post, search the web to see if your question has already been asked or even answered. And if it has been, you will already have your answer. - -## GitHub Discussions - -Questions, ideas, and suggestions can be posted on [GitHub Discussions](https://github.com/MassTransit/MassTransit/discussions). Similar to Stack Overflow, but integrated with GitHub, this is a great place to post code samples, share ideas, and get help using MassTransit. The same guidelines above apply to discussions. - -> If you have an issue _using_ MassTransit, this is the place to start **before** creating an issue. - -## Discord - -MassTransit has an active Discord server, which is a great place to get quick answers to short questions as well as share in engaging conversations about MassTransit, messaging, or whatever. - - - -## Twitter - -You might be able to get some attention on Twitter, and you're highly encouraged to tweet about MassTransit. Feel free to tag `@mtproj` (strangely, MassTransit is a noisy search term -- go figure). - -## GitHub Issues - -Please **do not open an issue on GitHub**, unless you have spotted an actual bug in MassTransit. If you are unsure, pursue one of the alternate options first. If we confirm it's a bug, we'll ask you to [create the issue][3]. - -**Issues are not the place for questions, and they'll likely be closed.** - -This policy is in place to avoid bugs being drowned out in a pile of sensible suggestions for future enhancements and calls for help from people who forget to check back if they get it and so on. - -[1]: http://stackoverflow.com/questions/tagged/masstransit -[3]: https://github.com/masstransit/masstransit/issues -[4]: https://gist.github.com/ diff --git a/docs/learn/analyzers.md b/docs/learn/analyzers.md deleted file mode 100644 index 67ac31a0c6d..00000000000 --- a/docs/learn/analyzers.md +++ /dev/null @@ -1,15 +0,0 @@ -# Roslyn Analyzers - -MassTransit has a code analyzer which detects and provides code fixes which can be helpful to identify potential issues. - -> Package: [MassTransit.Analyzers](https://www.nuget.org/packages/MassTransit.Analyzers) - -## Message Initializers - -[Message Initializers](/usage/producers.md##message-initializers) are used to initialize a message without having to create a backing class. - -The analyzer supports methods that accept an `object` _values_ argument, including: - -- `ISendEndpoint.Send(object values)` -- `IPublishEndpoint.Publish(object values)` -- `ConsumeContext.RespondAsync(object values)` \ No newline at end of file diff --git a/docs/learn/contributing.md b/docs/learn/contributing.md deleted file mode 100644 index 2d096a090d3..00000000000 --- a/docs/learn/contributing.md +++ /dev/null @@ -1,28 +0,0 @@ -# Contributing - -MassTransit is an open-source project and it means that it relies not only on the core group of people who created -it, but also on **you**! - -If you have a brilliant idea how to make MassTransit even better - get in touch with others using GitHub issues, -Google group or Gitter, and if you are ready to make a code contribution, do the following: - -* Make your fork of the MassTransit repository -* Clone the fork to your machine -* HACK! -* Submit a pull request - -::: tip NOTE -I use JetBrains Rider on a Mac for almost all development, including MassTransit. Which also means that I don't use Visual Studio (or VS Code). The solution and project files are fully compatible, and can be modified on either operating system using any of the supported development tools. The Resharper code formatting tools are used to ensure a consistent source code structure. -::: - -### Documentation - -MassTransit documentation is hosted on GitHub Pages and uses [VuePress](https://vuepress.vuejs.org/). In order to be able to build the documentation locally, you need to have Node on your machine. - -If you want to contribute to this documentation, clone MassTransit, and type `npm run docs:dev` to launch the server. The server automatically rebuilds and pushes updates to the browser as changes are saved, so it's super easy. I might like it too much, at this point, compared to everything I've used previously. - -When all these steps are done, you will be able to see the site [locally](http://localhost:8080/). - -You can also edit pages in place using GitHub. - -[1]: https://toolchain.gitbook.com/syntax/markdown.html \ No newline at end of file diff --git a/docs/learn/loving-the-community.md b/docs/learn/loving-the-community.md deleted file mode 100644 index aa7ee4c28dd..00000000000 --- a/docs/learn/loving-the-community.md +++ /dev/null @@ -1,14 +0,0 @@ -# Community - -MassTransit has a great community of developers, many of which have contributed their time and energy to help make MassTransit what it is today. To that end, I felt it would be useful to compile some of the blog posts and articles that have been written by developers using MassTransit. - -* [Loosely Coupled Labs](http://looselycoupledlabs.com/) - * [MassTransit 3 Update A Simple Publish Subscribe Example](http://looselycoupledlabs.com/2015/07/masstransit-3-update-a-simple-publishsubscribe-example/) - * [Error Handling in MassTransit Consumers](http://looselycoupledlabs.com/2014/07/error-handling-in-masstransit-consumers/) - * [Monitoring RabbitMQ](http://looselycoupledlabs.com/2014/08/monitoring-rabbitmq/) - -* [Running MassTransit with Topshelf](http://forloop.co.uk/blog/running-masstransit-within-a-topshelf-windows-service) - -* [SignalR Chat with MassTransit 3](http://www.maldworth.com/2015/07/19/signalrchat-with-masstransit-v3/) - -You can also read about the history and development of MassTransit on [Chris's Blog](http://blog.phatboyg.com/). \ No newline at end of file diff --git a/docs/learn/samples.md b/docs/learn/samples.md deleted file mode 100644 index 189277a4f71..00000000000 --- a/docs/learn/samples.md +++ /dev/null @@ -1,167 +0,0 @@ -# Samples - -Working code is an excellent way to learn how to use MassTransit features. The samples below show the capabilities of MassTransit, and can be cloned, forked, and explored to get a better understanding. - -The new samples are standalone repositories, which use NuGet to pull dependencies exactly as a developerwould use MassTransit. - -### Getting Started - -This project is [part of the MassTransit documentation](https://masstransit-project.com/getting-started/). Refer to that link for details. - -**If you're new to MassTransit, start with this sample to understand how MassTransit works** - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-GettingStarted) - -### Sample Twitch - -This sample was created along with the [Twitch/YouTube video series](/getting-started/live-coding). - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-Twitch) - -### Sample Library - -This sample was created along with Season 2 of the [Twitch/YouTube video series](/getting-started/live-coding). - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-Library) - -### Sample ForkJoint - -This sample was created along with Season 3 of the [Twitch/YouTube video series](/getting-started/live-coding). - -Fork Joint is a fictional restaurant built during Season 3 of the MassTransit Live Code Video Series. You can [watch the episodes on YouTube](https://youtube.com/playlist?list=PLx8uyNNs1ri2JeyDGFWfCYyAjOB1GP-t1) and follow along by resetting to the various commits in the Git history. - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-ForkJoint) - -### Trashlantis - -This sample was created to show how the in-memory outbox is used and ensures message delivery in the presence of transaction failures. - -Clone the sample: [GitHub Repository](https://github.com/phatboyg/Trashlantis) - -### Node (MassTransit in TypeScript) - -This sample uses MassTransit (for .NET) combined with the [MassTransit (for JavaScript) NPM package](https://www.npmjs.com/package/masstransit-rabbitmq) to send requests from a node application and handle the subsequent response from a MassTransit Consumer (running in .NET). The services communicate via RabbitMQ (included in the `docker-compose.yml` file). - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-Node) - -### Scoped Filters - -> .NET 5, ASP.NET - -This sample uses an HTTP header named `Token` to pass credentials to an API Controller. That header is read into a scoped type (`Token`) using an action filter as part of the API request. The action method then uses the MassTransit request client to send a request to a consumer. Scoped message filters are configured for publish, send, and consume to transfer the header value (via the `Token` object in the container scope) to outbound messages, and then on the consumer side extract that MassTransit message header back into the _Token_ type in the consumer scope. - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-ScopedFilters) - -### Azure Functions - -Shows how to use MassTransit with Azure Functions (v3). - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-AzureFunction) - -### Job Consumers - -Shows how to use the job consumers with Entity Framework Core. - -Features used: -- Job Consumers -- Entity Framework Core - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-JobConsumers) - -### Batch Processing using Sagas - -Shows how to perform batch processing and tracking using sagas. - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-Batch) - -### SignalR - -This sample will show a variety of built in tools and techniques in MassTransit. - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-SignalR) - -### Request Response - -This sample demonstrates how to create a client that sends a request to a service which responds with a response. - -Features used: -- Request Client - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-RequestResponse) - -### Shopping Cart - -This was a fun sample, created in response to a [blog post][1] on how to send an email to a customer that abandoned a shopping cart. My response to that post is [located here][2]. - -Features used: -- Automatonymous -- Quartz - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-ShoppingWeb) - -[1]: http://joshkodroff.com/2015/08/21/an-elegant-abandoned-cart-email-using-nservicebus/ -[2]: http://blog.phatboyg.com/general/2015/09/12/sagas-state-machines-and-abandoned-carts.html - -### Courier - -Courier is MassTransit's routing-slip implementation, which makes it possible to orchestrate distributed services into a business transaction. This sample demonstrates how to create and execute a routing slip, record routing slip events, and track transaction state using [Automatonymous](https://github.com/MassTransit/Automatonymous). - -This sample includes multiple console applications, which can be started simultaneously, to observe how the services interact. - -Features used: -- Courier -- Automatonymous - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-Courier) - -### Race Registration - -This sample has multiple console applications, and a web API, allowing registrations to be submitted. The routing slip is tracked using a saga, and can compensate when an activity faults. - -Features used: -- Courier -- Automatonymous - -Clone the sample: [GitHub Repository](https://github.com/phatboyg/Demo-Registration) - -### Quartz - -Features used: -- Scheduling -- Quartz - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-Quartz) - -### Hangfire - -Features used: -- Scheduling -- Hangfire - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-Hangfire) - -### Application Insights - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-ApplicationInsights) - -### RabbitMQ Direct Exchange - -Shows how to configure a consumer and a producer to use RabbitMQ direct exchange routing. - -Features used: -- RabbitMQ - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/Sample-Direct) - -### Benchmark - -Test the performance of MassTransit in your environment. - -Clone the sample: [GitHub Repository](https://github.com/MassTransit/MassTransit-Benchmark) - - -### RabbitMQ MQTT Consumer - -Utilise RabbitMQ as a MQTT server and consume IOT data. - -Clone the sample: [GitHub Repository](https://github.com/morganphilo/MassTransit.Mqtt) diff --git a/docs/learn/support.md b/docs/learn/support.md deleted file mode 100644 index 362842a1f9e..00000000000 --- a/docs/learn/support.md +++ /dev/null @@ -1,63 +0,0 @@ -# Support - -Commercial support is available for MassTransit and provided by Loosely Coupled, LLC. - -Loosely Coupled, LLC was founded Chris Patterson, the author of MassTransit, to ensure the project's sustainability and provide solutions for organizations -requesting any of the support and consulting services listed below. - -## Support Services - -Commercial support agreements include the support services outlined below. Support agreements can be purchased for a specific service period (typically one -year). - -### General Support - -General Support means the Support Services provided for a defined period from general availability of a Major Release. General Support includes bug and security -fixes and Developer Support. - -### Developer Support - -Developer Support means the provision of virtual or email-based technical assistance by Loosely Coupled to Customer’s technical contact(s) with respect to -service requests, at the corresponding service level purchased by Customer. - -### Maintenance Services - -Maintenance Services means the provision of Maintenance Releases, Minor Releases, and Major Releases (defined below), if any, to the Software, along with any -corresponding Documentation. - -- Major Release means a generally available release of the Software that contains functional enhancements, extensions, and deprecations, denoted by incrementing - the first number in the version (e.g., Software 8.0.0 to Software 9.0.0). -- Minor Release means a general available release of the Software that introduces a limited amount of new features and functionality, denoted by incrementing - the - second number in the version (e.g., Software 8.0.0 to Software 8.1.0). -- Maintenance Release means a generally available release of the Software that typically provides maintenance fixes only, denoted by incrementing the third - number - in the version (e.g., Software 8.0.0 to Software 8.0.1). - -### Technical Guidance - -Technical Guidance means the Support Services provided for an additional period following General Support. Support Services for products in the Technical -Guidance period are available for customers with established applications to plan and complete upgrades to a current production version that is available within -General Support, however, there will be no new Minor Releases or Maintenance Releases for products under Technical Guidance. - -For more details and to request a quote, contact [MassTransit Support][1]. - -## Consulting Services - -Consulting services are also available for architecture, development, and operational support. Services are provided on an hourly basis, and there are even an -easy pay-as-you-go options that can be scheduled online (based upon availability). - -### Architecture Review - -If your team is new to message-based systems, a review of the application's requirements, service level objectives, deployment environments, and user personas -may generate insights to drive architecture decisions. Determine the feasibility of leveraging a message broker in your application and which broker is -appropriate for your target platform. - -### Code Review - -Ensuring the appropriate use of MassTransit consumers, sagas, routing slips, consistent message contract design, and proper configuration early in a project is -the best way to deliver a consistent and maintainable application. - -For more information or to request a quote, contact [MassTransit Support][1]. - -[1]: mailto://support@masstransit.io diff --git a/docs/learn/training.md b/docs/learn/training.md deleted file mode 100644 index e7d50db57e8..00000000000 --- a/docs/learn/training.md +++ /dev/null @@ -1,50 +0,0 @@ -# Training - -[![Improving Logo](/improving-small.png)][2] - -Improving offers certification courses for MassTransit both virtually as well as throughout North America. - -### MassTransit Developer Certification - -The MassTransit Developer Certification 3-Day course is the surest way to elevate your abilities in building distributed systems. - -[![Register Now](/register.png)][3] - -### Course Overview - -#### Part 1: Developing distributed systems -*Instructional time: 12 hours* - -* Configuring MassTransit and RabbitMQ -* Writing automated tests -* Publishing messages -* Writing handlers -* Designing workflows -* Designing messages -* Scheduling delivery -* Ensuring that handlers are idempotent -* Ensuring that handlers are commutative - -#### Part 2: Handling errors -*Instructional time: 8 hours* - -* Testing failure scenarios -* Distinguishing among failure types -* Configuring retry policies -* Buffering outgoing messages -* Enrolling the outbox within a transaction -* Publishing faults -* Executing compensating transactions -* Installing prophylactic middleware - -#### Part 3: Operations -*Instructional time: 4 hours* - -* Monitoring a production system -* Injecting custom observers -* Responding to dead letters -* Deploying and initializing new services -* Auditing messages - -[2]: https://improving.com -[3]: https://improving.com/training/class/masstransit-developer-certification-course \ No newline at end of file diff --git a/docs/learn/videos.md b/docs/learn/videos.md deleted file mode 100644 index 12bb4a14fd1..00000000000 --- a/docs/learn/videos.md +++ /dev/null @@ -1,24 +0,0 @@ -# Videos - -### YouTube - -There are several seasons of videos [available on YouTube](https://www.youtube.com/playlist?list=PLx8uyNNs1ri2MBx6BjPum5j9_MMdIfM9C), and new episodes are added fairly regularly. There are also many short-format [Commutes](https://youtube.com/playlist?list=PLx8uyNNs1ri2_ldsW1aPb7_8E2FI7ZtaI) that cover quick topics. - -### Others - -Beyond those videos, there are several other video presentations featuring MassTransit. - -[Event Driven Architecture][1] -Presented at the North Dallas .NET User Group in February, 2010 by Chris Patterson. - -[Loosely coupled applications with MassTransit and RabbitMq][2] -Presented at NDC Oslo 2016 by Roland Guijt (he is also the author of the MassTransit Intro course on Pluralsight) - -[Messaging with MassTransit (Lightning Talk)][3] -Presented at the dotnetsheff User Group in March, 2018 by Kevin Smith. - -Others I'm sure, I just need to find them and link them here. - -[1]: http://www.drowningintechnicaldebt.com/ShawnWeisfeld/archive/2010/02/04/event-driven-architecture-by-chris-patterson-north-dallas-.net.aspx -[2]: https://vimeo.com/131635506 -[3]: https://youtu.be/risAHFaHUtU?list=PL8xuokhAnn4oTRr-7TfHFVqJNuB0I1jAG \ No newline at end of file diff --git a/docs/platform/README.md b/docs/platform/README.md deleted file mode 100644 index 1b980ad94c3..00000000000 --- a/docs/platform/README.md +++ /dev/null @@ -1,99 +0,0 @@ -# Platform - -MassTransit supports building, deploying, and monitoring services on a container-based platform. The platform provides a consistent hosting environment for consumers, sagas, and activities, eliminating duplicated service code (no more cut-and-pasting `Program.cs`). The platform Docker images can be used to deploy services to any container-based environment. - -The platform image is hosted on [Docker](https://hub.docker.com/r/masstransit/platform) and is updated independent of the MassTransit package. - -There are transport images as well, including [RabbitMQ](https://hub.docker.com/r/masstransit/rabbitmq) and [ActiveMQ](https://hub.docker.com/r/masstransit/active). - -A preconfigured image for scheduling messages using [Quartz](https://hub.docker.com/r/masstransit/quartz) is also available. The [source](https://github.com/MassTransit/Platform-Quartz) is a good example of how to build an assembly for hosting on the platform. A preconfigured [SQL Server](https://hub.docker.com/r/masstransit/sqlserver-quartz) container is also available for development purposes. - -These images can be [configured](/platform/configuration) to specify the transport, as well as other options. - -There is also a [Live-Coding Video](https://www.youtube.com/watch?v=-xEnO9H62lk) showing the Twitch sample being converted to run on the platform. - -To build a service using the MassTransit Platform, create a startup class that implemented the `IPlatformStartup` interface. - -> Package: [MassTransit.Platform.Abstractions](https://nuget.org/packages/MassTransit.Platform.Abstractions) - -```cs -public class OrderServicePlatformStartup : - IPlatformStartup -{ - readonly ILogger _logger; - - public QuartzPlatformStartup(IConfiguration configuration, ILogger logger) - { - _logger = logger; - } - - public void ConfigureMassTransit(IServiceCollectionConfigurator configurator, IServiceCollection services) - { - _logger.LogInformation("Configuring Order Service"); - - configurator.AddConsumer(typeof(SubmitOrderConsumerDefinition)); - } - - public void ConfigureBus(IBusFactoryConfigurator configurator, IServiceProvider provider) - where TEndpointConfigurator : IReceiveEndpointConfigurator - { - } -} -``` - -The example adds a consumer, including a consumer definition. When the bus is started, an endpoint will be created by convention for the consumer. - -### Adding Configuration - -To access configuration options, such as `appsettings.json` or environment variables, add the configuration classes in the platform startup class. - -```cs -public class OrderServicePlatformStartup : - IPlatformStartup -{ - readonly IConfiguration _configuration; - - public QuartzPlatformStartup(IConfiguration configuration) - { - _configuration = configuration; - } - - public void ConfigureMassTransit(IServiceCollectionConfigurator configurator, IServiceCollection services) - { - services.Configure(_configuration.GetSection("OrderService")); - } -} -``` - -### Changing Logging - -To configure logging, simply re-define the Serilog logger in the ConfigureMassTransit method of your platform startup: - -```cs -public class OrderServicePlatformStartup : - IPlatformStartup -{ - readonly IConfiguration _configuration; - - public QuartzPlatformStartup(IConfiguration configuration) - { - _configuration = configuration; - } - - public void ConfigureMassTransit(IServiceCollectionBusConfigurator configurator, IServiceCollection services) - { - Log.Logger = new LoggerConfiguration() - .Enrich.FromLogContext() - .WriteTo.Console() - .MinimumLevel.Debug() - .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) - .CreateLogger(); - - // services.AddSingleton(); etc. - - configurator.AddConsumer(); - } -} -``` - -This example configures Serilog to log to the console with Debug messages - you could also load the logger configuration by using the .ReadFrom.Configuration() method. diff --git a/docs/platform/configuration.md b/docs/platform/configuration.md deleted file mode 100644 index 1fbb6ba3cd9..00000000000 --- a/docs/platform/configuration.md +++ /dev/null @@ -1,84 +0,0 @@ -# Configuration - -Containers are configured using environment variables. - - -### MT_TRANSPORT - -> `MT_TRANSPORT=ASB` - -Specify the transport used by the service. - -| Value | Transport -|----|----- -| RMQ | RabbitMQ (default) -| ASB | Azure Service Bus -| AMQ | ActiveMQ, including Amazon MQ -| SQS | Amazon SQS - - -#### RabbitMQ - -| Value | Description | Default -|:----|:-----|:----- -| MT_RMQ__HOST | The host address | `rabbitmq` or `localhost` -| MT_RMQ__PORT | The host port | `5672` (or `5671` for SSL) -| MT_RMQ__VHOST | Virtual Host name | `/` -| MT_RMQ__USER | Sign in username | `guest` -| MT_RMQ__PASS | Sign in password | `guest` -| MT_RMQ__USESSL | Use SSL | `false` -| MT_RMQ__SSL__SERVERNAME | Server name matching the CN | -| MT_RMQ__SSL__TRUST | Trust the certificate, ignoring errors | `false` -| MT_RMQ__SSL__CERTPATH | Path to a client certificate | -| MT_RMQ__SSL__CERTPASSPHRASE | Passphrase for the certificate | -| MT_RMQ__SSL__CERTIDENTITY | Use the certificate to authenticate | `false` - - -#### Azure Service Bus - -| Value | Description | Default -|:----|:-----|:----- -| MT_ASB__CONNECTIONSTRING | The full connection string | - -#### ActiveMQ (including Amazon MQ) - -| Value | Description | Default -|:----|:-----|:----- -| MT_AMQ__HOST | The host address | `activemq` or `localhost` -| MT_AMQ__PORT | The host port | `61616` -| MT_AMQ__USER | Sign in username | `admin` -| MT_AMQ__PASS | Sign in password | `admin` -| MT_AMQ__USESSL | Use SSL | `false`, `true` if _aws_ found in host name - -#### Amazon SQS - -| Value | Description | Default -|:----|:-----|:----- -| MT_SQS__REGION | The AWS region name | -| MT_SQS__SCOPE | The scope name | -| MT_SQS__ACCESSKEY | The AWS Access Key | -| MT_SQS__SECRETKEY | The AWS Secret Key | - - -### MT_SCHEDULER - -> `MT_SCHEDULER=quartz` - -If specified, the name of the queue for the message scheduler endpoint. The name will automatically be converted to the appropriate address for the transport. - -If not specified, the transport-specific delayed message delivery mechanism is configured. For RabbitMQ, this configures the delayed exchange message scheduler via the `.UseDelayedExchangeMessageScheduler()` method. - -> To configure the connection string for the [masstransit/quartz](https://hub.docker.com/r/masstransit/quartz) preconfigured Docker image, `MT_Quartz__ConnectionString` should be set to the connection string for the Quartz database. - - -### MT_PROMETHEUS - -> `MT_PROMETHEUS=serviceName` - -If present, [Prometheus](/advanced/monitoring/prometheus) metrics are enabled and exported using the specified service name. The metrics can be scraped from the service at `host:80/metrics`. - -### MT_APP - -> `MT_APP=/app` - -By default, the MT_APP variable is set to `/app`, which is the default docker path for .NET applications that are using the platform. There typically is no reason to change this value, but it is documented for completeness. \ No newline at end of file diff --git a/docs/quick-starts/README.md b/docs/quick-starts/README.md deleted file mode 100644 index 0c80b8fc738..00000000000 --- a/docs/quick-starts/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Quick Starts - -The following quick starts provide both a video and a text based example that are very minimal but are here to help you get started with using MassTransit. We recommend that you first start with the in-memory quick start, and then when you have that up and running switch to the transport of choice. - -::: tip Start Here -Everything builds off of the in-memory so start [here](/quick-starts/in-memory) -::: - -From there you can jump off to the transport of your choice. - -- [RabbitMQ](/quick-starts/rabbitmq) -- [Azure Service Bus](/quick-starts/azure-service-bus) -- [SQS](/quick-starts/sqs) diff --git a/docs/quick-starts/azure-service-bus.md b/docs/quick-starts/azure-service-bus.md deleted file mode 100644 index 60d778902b9..00000000000 --- a/docs/quick-starts/azure-service-bus.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -prev: false -next: /usage/configuration -sidebarDepth: 0 ---- - -# Azure Service Bus - -> This tutorial will get you from zero to up and running with [Azure Service Bus](/usage/transports/azure-sb) and MassTransit. - - - -## Prerequisites - -::: tip NOTE -The following instructions assume you are starting from a completed [In-Memory Quick Start](/quick-starts/in-memory) -::: - -This example requires the following: - -- a functioning installation of the dotnet runtime and sdk (at least 6.0) -- an Azure account, where you have administrative control - - -## Setup Azure Service Bus - -::: tip -To continue from this point, you must have a valid Azure subscription with an Azure Service Bus namespace. A shared access policy with _Manage_ permissions is required to use MassTransit with Azure Service Bus. -::: - -1. Navigate to [Service Bus](https://portal.azure.com/#create/Microsoft.ServiceBus) -2. Create a namespace - 1. **Pricing Tier:** This _must_ be Standard or Premium -3. Create a **Shared access policy** - 1. Make sure to grant `Manage` - 2. The `Primary Connection String` will be used for the rest of the steps. - -## Change the Transport to Azure Service Bus - -Add the _MassTransit.Azure.ServiceBus.Core_ package to the project. - -```bash -$ dotnet add package MassTransit.Azure.ServiceBus.Core -``` - -## Edit Program.cs - -Change `UsingInMemory` to `UsingAzureServiceBus`. - -```csharp {8-13} -public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - // elided ... - x.UsingAzureServiceBus((context,cfg) => - { - cfg.Host("your connection string"); - - cfg.ConfigureEndpoints(context); - }); - }); - - services.AddHostedService(); - }); -``` - -## Run the project - -```bash -$ dotnet run -``` - -The output should have changed to show the message consumer generating the output (again, press Control+C to exit). Notice that the bus address now starts with `sb`. - -``` {11} -Building... -info: MassTransit[0] - Configured endpoint Message, Consumer: GettingStarted.MessageConsumer -info: Microsoft.Hosting.Lifetime[0] - Application started. Press Ctrl+C to shut down. -info: Microsoft.Hosting.Lifetime[0] - Hosting environment: Development -info: Microsoft.Hosting.Lifetime[0] - Content root path: /Users/chris/Garbage/start/GettingStarted -info: MassTransit[0] - Bus started: sb://your-service-bus-namespace/ -info: GettingStarted.MessageConsumer[0] - Received Text: The time is 3/24/2021 12:11:10 PM -05:00 -``` - -At this point, the service is connecting to Azure Service Bus and publishing messages which are received by the consumer. - -:tada: \ No newline at end of file diff --git a/docs/quick-starts/in-memory.md b/docs/quick-starts/in-memory.md deleted file mode 100644 index 9967ab3c9df..00000000000 --- a/docs/quick-starts/in-memory.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -prev: false -next: /usage/configuration -sidebarDepth: 0 ---- - -# In Memory - -> This tutorial will get you from zero to up and running with [In Memory](/usage/transports/in-memory) and MassTransit. - - - -## Prerequisites - -This example requires the following: - -- a functioning installation of the dotnet runtime and sdk (at least 6.0) - -### Install MassTransit Templates - -MassTransit includes project and item [templates](/usage/templates) simplifying the creation of new projects. Install the templates by executing `dotnet new -i MassTransit.Templates` at the console. A video introducing the templates is available on [YouTube](https://youtu.be/nYKq61-DFBQ). - -``` -dotnet new --install MassTransit.Templates -``` - -## Initial Project Creation - -### Create the worker project - -To create a service using MassTransit, create a worker via the Command Prompt. - -```bash -$ dotnet new mtworker -n GettingStarted -$ cd GettingStarted -$ dotnet new mtconsumer -``` - -### Overview of the code - -When you open the project you will see that you have 1 class file. - -- `Program.cs` is the standard entry point and here we configure the host builder. - -### Create a Contract -Create a `Contracts` folder in the root of your project, and within that folder create a file named `GettingStarted.cs` with the following contents: - -``` cs -namespace GettingStarted.Contracts; - -public record GettingStarted() -{ - public string Value { get; init; } -} -``` - -### Add A BackgroundService - -In the root of the project add `Worker.cs` - -<<< @/docs/code/quickstart/Worker.cs - -### Register Worker - -In `Program.cs` at the bottom of the `ConfigureServices` method add - -```csharp -services.AddHostedService(); -``` - -### Create a Consumer - -Create a `Consumers` folder in the root of your project, and within that folder create a file named `GettingStartedConsumer.cs` with the following contents: - -<<< @/docs/code/quickstart/GettingStartedConsumer.cs - -### Run the project - -```bash -$ dotnet run -``` - -The output should have changed to show the message consumer generating the output (again, press Control+C to exit). - -``` {2-5,12-15} -Building... -info: MassTransit[0] - Configured endpoint Message, Consumer: GettingStarted.MessageConsumer -info: MassTransit[0] - Bus started: loopback://localhost/ -info: Microsoft.Hosting.Lifetime[0] - Application started. Press Ctrl+C to shut down. -info: Microsoft.Hosting.Lifetime[0] - Hosting environment: Development -info: Microsoft.Hosting.Lifetime[0] - Content root path: /Users/chris/Garbage/start/GettingStarted -info: GettingStarted.MessageConsumer[0] - Received Text: The time is 3/24/2021 12:02:01 PM -05:00 -info: GettingStarted.MessageConsumer[0] - Received Text: The time is 3/24/2021 12:02:02 PM -05:00 -``` diff --git a/docs/quick-starts/rabbitmq.md b/docs/quick-starts/rabbitmq.md deleted file mode 100644 index ed5cdd3499b..00000000000 --- a/docs/quick-starts/rabbitmq.md +++ /dev/null @@ -1,109 +0,0 @@ ---- -prev: false -next: /usage/configuration -sidebarDepth: 0 ---- - -# RabbitMQ - -> This tutorial will get you from zero to up and running with [RabbitMQ](/usage/transports/rabbitmq) and MassTransit. - - - -- The source for this sample is available [on GitHub](https://github.com/MassTransit/Sample-GettingStarted). - -## Prerequisites - -::: tip NOTE -The following instructions assume you are starting from a completed [In-Memory Quick Start](/quick-starts/in-memory) -::: - -This example requires the following: - -- a functioning installation of the dotnet runtime and sdk (at least 6.0) -- a functioning installation of `docker` with `docker compose` support - -## Get RabbitMQ up and running - -For this quick start we recommend running the preconfigured [Docker image maintained by the MassTransit team](https://github.com/MassTransit/docker-rabbitmq). It includes the delayed exchange plug-in, as well as the Management interface enabled. - -```bash -$ docker run -p 15672:15672 -p 5672:5672 masstransit/rabbitmq -``` - -If you are running on an ARM platform - -```bash -$ docker run --platform linux/arm64 -p 15672:15672 -p 5672:5672 masstransit/rabbitmq -``` - -Once its up and running you can [log into](http://localhost:15672) the broker using `guest`, `guest`. You can see message rates, routings and active consumers using this interface. - - -## Change the Transport to RabbitMQ - -Add the _MassTransit.RabbitMQ_ package to the project. - -```bash -$ dotnet add package MassTransit.RabbitMQ -``` - -## Edit Program.cs - -Change `UsingInMemory` to `UsingRabbitMq` - -```csharp {9-17} -public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - // elided... - - x.UsingRabbitMq((context,cfg) => - { - cfg.Host("localhost", "/", h => { - h.Username("guest"); - h.Password("guest"); - }); - - cfg.ConfigureEndpoints(context); - }); - }); - - services.AddHostedService(); - }); -``` - -`localhost` is where the docker image is running. We are inferring the default port of `5672` and are using `\` as the [virtual host](https://www.rabbitmq.com/vhosts.html). `guest` and `guest` are the default username and password to talk to the cluster and [management dashboard](http://localhost:15672). - -## Run the project - -```bash -$ dotnet run -``` - -The output should have changed to show the message consumer generating the output (again, press Control+C to exit). Notice that the bus address now starts with `rabbitmq`. - -``` {11} -Building... -info: MassTransit[0] - Configured endpoint Message, Consumer: GettingStarted.MessageConsumer -info: Microsoft.Hosting.Lifetime[0] - Application started. Press Ctrl+C to shut down. -info: Microsoft.Hosting.Lifetime[0] - Hosting environment: Development -info: Microsoft.Hosting.Lifetime[0] - Content root path: /Users/chris/Garbage/start/GettingStarted -info: MassTransit[0] - Bus started: rabbitmq://localhost/ -info: GettingStarted.MessageConsumer[0] - Received Text: The time is 3/24/2021 12:11:10 PM -05:00 -``` - -At this point the service is connecting to RabbbitMQ on `localhost` and publishing messages which are received by the consumer. - -:tada: \ No newline at end of file diff --git a/docs/quick-starts/sqs.md b/docs/quick-starts/sqs.md deleted file mode 100644 index 08e55021821..00000000000 --- a/docs/quick-starts/sqs.md +++ /dev/null @@ -1,142 +0,0 @@ ---- -prev: false -next: /usage/configuration -sidebarDepth: 0 ---- - -# SQS - -> This tutorial will get you from zero to up and running with [AWS SQS](/usage/transports/amazonsqs) and MassTransit. - - - -- The source for this sample is available [on GitHub](https://github.com/MassTransit/Sample-GettingStarted). - - -## Prerequisites - -::: tip NOTE -The following instructions assume you are starting from a completed [In-Memory Quick Start](/quick-starts/in-memory) -::: - -This example requires the following: - -- a functioning installation of the dotnet runtime and sdk (at least 6.0) -- an AWS account, where you have the ability to control the IAM permissions for SQS and SNS - -## Setup AWS - -1. Log into AWS -2. Create a User with `Access key - Programmatic access` - 1. Grant the user the `Sample IAM Policy` below - - -### Sample IAM Policy - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "SqsAccess", - "Effect": "Allow", - "Action": [ - "sqs:SetQueueAttributes", - "sqs:ReceiveMessage", - "sqs:CreateQueue", - "sqs:DeleteMessage", - "sqs:SendMessage", - "sqs:GetQueueUrl", - "sqs:GetQueueAttributes", - "sqs:ChangeMessageVisibility", - "sqs:PurgeQueue", - "sqs:DeleteQueue", - "sqs:TagQueue" - ], - "Resource": "arn:aws:sqs:*:YOUR_ACCOUNT_ID:*" - },{ - "Sid": "SnsAccess", - "Effect": "Allow", - "Action": [ - "sns:GetTopicAttributes", - "sns:CreateTopic", - "sns:Publish", - "sns:Subscribe" - ], - "Resource": "arn:aws:sns:*:YOUR_ACCOUNT_ID:*" - },{ - "Sid": "SnsListAccess", - "Effect": "Allow", - "Action": [ - "sns:ListTopics" - ], - "Resource": "*" - } - ] -} -``` - -## Change the Transport to AmazonSQS - -Add the _MassTransit.AmazonSQS_ package to the project. - -```bash -$ dotnet add package MassTransit.AmazonSQS -``` - -## Edit Program.cs - -Change `UsingInMemory` to `UsingAmazonSQS` - -```csharp {8-16} -public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddMassTransit(x => - { - // elided ... - x.UsingAmazonSqs((context, cfg) => - { - cfg.Host("us-east-1", h => { - h.AccessKey("your-iam-access-key"); - h.SecretKey("your-iam-secret-key"); - }); - - cfg.ConfigureEndpoints(context); - }); - }); - - services.AddHostedService(); - }); -``` - -### Run the project - -```bash -$ dotnet run -``` - -The output should have changed to show the message consumer generating the output (again, press Control+C to exit). Notice that the bus address now starts with `amazonsqs`. - -``` {11} -Building... -info: MassTransit[0] - Configured endpoint Message, Consumer: GettingStarted.MessageConsumer -info: Microsoft.Hosting.Lifetime[0] - Application started. Press Ctrl+C to shut down. -info: Microsoft.Hosting.Lifetime[0] - Hosting environment: Development -info: Microsoft.Hosting.Lifetime[0] - Content root path: /Users/chris/Garbage/start/GettingStarted -info: MassTransit[0] - Bus started: amazonsqs://us-east-1/a-topic-name -info: GettingStarted.MessageConsumer[0] - Received Text: The time is 3/24/2021 12:11:10 PM -05:00 -``` - -At this point the service is connecting to Amazon SQS/SNS in the region `us-east-1` and publishing messages which are received by the consumer. - -:tada: diff --git a/docs/releases/README.md b/docs/releases/README.md deleted file mode 100644 index 28822547a95..00000000000 --- a/docs/releases/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Release Notes - -Release notes are created when there are new features worth mentioning, etc. - -> Seriously, don't expect every release to include release notes. Just like Fetch, it isn't going to happen. \ No newline at end of file diff --git a/docs/releases/v7.0.4.md b/docs/releases/v7.0.4.md deleted file mode 100644 index fbfa2f76d2e..00000000000 --- a/docs/releases/v7.0.4.md +++ /dev/null @@ -1,192 +0,0 @@ -# 7.0.4 - -Version 7.0.4 includes several new features which are detailed below. There were also numerous bug fixes, as well as tweaks to job consumers and batch consumers. - -### General - -MassTransit now references the .NET Core 2.1 version of Microsoft.Extensions.Logging.Abstractions, which should now make v7 usable with that SDK version. - -The endpoint name formatters now support adding a prefix to all endpoint names, including the namespace in addition to the type name, and creating instance-specific queues for consumers. - -```cs -services.AddMassTransit(x => -{ - x.AddConsumer() - .Endpoint(e => e.InstanceId = "SomeUniqueValue"); -}); -``` - -This would format the endpoint name as either `CommonSomeUniqueValue`, `common_some_unique_value`, or `common-some-unique-value`. - -The prefix can be specified on the endpoint name formatter, as well as whether or not to include the namespace in the endpoint name. - -```cs -services.AddSingleton(provider => new KebabCaseEndpointNameFormatter("Dev", true)); -``` - -This would format the endpoint name as `dev-my-service-contacts-common`, assuming that `CommonConsumer` was in the `MyService.Contracts` namespace. - -### Transports - -#### Amazon SNS - -Topics are no longer created for published message types that include/implement other message types. Since SNS does not support polymorphic message routing, these topics were extraneous. - -Queue attributes are now copied to error/skipped queues. - -#### RabbitMQ, Azure Service Bus - -Message types can now be excluded from being created as exchanges or topics. Some other frameworks have stereotype interfaces such as `IAmAnEvent` or `IAmACommand`, or even base type interfaces like `IShouldBeTreatedAsAMessage`, and those interfaces would be used to create exchanges/topics for polymorphic message support. These interfaces can now be excluded using the publish topology configuration. - -Using an attribute, a type can be excluded: - -```cs -[ExcludeFromTopology] -public interface IEvent -{ - Guid TransactionId { get; } -} -``` - -The alternative is to configure the publish topology message type: - -```cs -Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Publish(p => p.Exclude = true); -}); -``` - -### Containers - -#### In-Memory Test Harness - -Registration for the test harness has been added, making it easier to test consumers using a container. To configure the in-memory test harness, see the example below. - -```cs - var provider = new ServiceCollection() - .AddMassTransitInMemoryTestHarness(cfg => - { - cfg.AddConsumer(); - }) - .BuildServiceProvider(true); - -var harness = provider.GetRequiredService(); - -await harness.Start(); -try -{ - var bus = provider.GetRequiredService(); - - IRequestClient client = bus.CreateRequestClient(); - - await client.GetResponse(new PingMessage()); - - Assert.That(await harness.Consumed.Any()); -} -finally -{ - await harness.Stop(); - - await provider.DisposeAsync(); -} -``` - -To include a consumer test harness for a consumer, additional configuration is required. - -```cs - var provider = new ServiceCollection() - .AddMassTransitInMemoryTestHarness(cfg => - { - cfg.AddConsumer(); - - cfg.AddConsumerTestHarness(); - }) - .BuildServiceProvider(true); - -var harness = provider.GetRequiredService(); - -await harness.Start(); -try -{ - var bus = provider.GetRequiredService(); - - IRequestClient client = bus.CreateRequestClient(); - - await client.GetResponse(new PingMessage()); - - Assert.That(await harness.Consumed.Any()); - - var consumerHarness = provider.GetRequiredService>(); - - Assert.That(await consumerHarness.Consumed.Any()); -} -finally -{ - await harness.Stop(); - - await provider.DisposeAsync(); -} -``` - -Saga test harnesses can also be used, as shown below. - -```cs - var provider = new ServiceCollection() - .AddMassTransitInMemoryTestHarness(cfg => - { - cfg.AddSaga() - .InMemoryRepository(); - - cfg.AddSagaTestHarness(); - }) - .BuildServiceProvider(true); - -var harness = provider.GetRequiredService(); - -await harness.Start(); -try -{ - _sagaId = Guid.NewGuid(); - _testValueA = "TestValueA"; - - await harness.Bus.Publish(new A - { - CorrelationId = _sagaId, - Value = _testValueA - }); - - Assert.That(await harness.Published.Any()); - - Assert.That(await harness.Consumed.Any()); - - var sagaHarness = provider.GetRequiredService>(); - - Assert.That(await sagaHarness.Consumed.Any()); - - Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == _sagaId)); - - var saga = sagaHarness.Created.Contains(_sagaId); - Assert.That(saga, Is.Not.Null); - Assert.That(saga.ValueA, Is.EqualTo(_testValueA)); - - Assert.That(await harness.Published.Any()); - - Assert.That(await harness.Published.Any(), Is.False); -} -finally -{ - await harness.Stop(); - - await provider.DisposeAsync(); -} -``` - -#### Scoped Filters - -Container registration for [Scoped filters](/advanced/middleware/scoped) is now optional. - -#### State Machine Activities - -Container registration for state machine activities is now optional for Microsoft Extensions Dependency Injection and Autofac. - diff --git a/docs/releases/v7.0.6.md b/docs/releases/v7.0.6.md deleted file mode 100644 index b9fae9868f3..00000000000 --- a/docs/releases/v7.0.6.md +++ /dev/null @@ -1,61 +0,0 @@ -# 7.0.6 - -Version 7.0.6 includes several new features which are detailed below. - -> Just FYI, 7.0.5 was skipped due to a package reference issue - -### Container Configuration - -A new method, `AddSagaRepository`, allows a saga repository to be added separately from the saga/state machine saga. This is particularly useful when the saga is configured in one section of code and the saga repository must be configured elsewhere. This was created to allow the job service saga repositories to be more easily configured. For example, to configure an existing `DbContext` and then use it. - -```cs -.AddDbContext(builder => ApplyBuilderOptions(builder)) -.AddMassTransit(x => -{ - x.AddSagaRepository() - .EntityFrameworkRepository(r => - { - r.ExistingDbContext(); - r.LockStatementProvider = RawSqlLockStatements; - }); -}); -``` - -> This is documented in the [sagas](/usage/sagas/efcore) section. - -### Entity Name - -A new attribute, `EntityName`, has been added which allows a message type to specify an entity name use for the topic/exchange name. - -```cs -[EntityName("some-other-topic")] -public interface TopicMessage -{ -} -``` - -### Automatonymous - -The state machine was updated to allow query building against the State property, including conversion of types between `State`, `int`, and `string`. This includes new methods for checking the existence of state machine instances within unit tests. - -### RabbitMQ - -Fixed issue where `IsHeadersPresent` is wrong, causing a null-reference exception. - -### SimpleInjector - -Package has been upgraded to v5. - -### Quartz - -Package has been updated to a more recent version, and the integration has added some additional features including the ability to not-start the scheduler on a node. - -### Transactional Bus - -Breaking Change! `.AddTransactionalBus()` is renamed to `.AddTransactionalEnlistmentBus()` - -[Read more about the two](/advanced/middleware/transactions) - - - - diff --git a/docs/releases/v7.0.7.md b/docs/releases/v7.0.7.md deleted file mode 100644 index 6bd5f07c7eb..00000000000 --- a/docs/releases/v7.0.7.md +++ /dev/null @@ -1,117 +0,0 @@ -# 7.0.7 - -### Message Scope for Scoped Filters - -[Scoped Filters](/advanced/middleware/scoped) are resolved from the container for each message. When used with the _InMemoryOutbox_, which publishes/sends messages after the consumer has completed, errors may occur or the scope may not be the same as the scope used by the consumer. To deal with this issue, new configuration methods have been added to create a message scope. - -#### Microsoft Dependency Injection - -```cs -Bus.Factory.CreateUsingInMemory(cfg => -{ - cfg.ReceiveEndpoint("input-queue", endpoint => - { - endpoint.UseMessageRetry(r => r.Intervals(1000, 2000)); - endpoint.UseMessageScope(serviceProvider); - endpoint.UseInMemoryOutbox(); - endpoint.ConfigureConsumer(Registration); - }); -}); -``` - -#### Autofac - -```cs -Bus.Factory.CreateUsingInMemory(cfg => -{ - cfg.ReceiveEndpoint("input-queue", endpoint => - { - endpoint.UseMessageRetry(r => r.Intervals(1000, 2000)); - endpoint.UseMessageLifetimeScope(container); - endpoint.UseInMemoryOutbox(); - endpoint.ConfigureConsumer(Registration); - }); -}); -``` - -### New Consumer Saga Interface Type - -[Consumer Sagas](/usage/sagas/consumer-saga) can now be correlated using the _new or existing_ policy, allowing a single message type to either create a new or use an existing saga instance. By adding the `InitiatedByOrOrchestrates` interface to the saga, the appropriate policy will be configured. - -```cs -public class ConsumerSaga : - InitiatedByOrOrchestrates -{ - public async Task Consume(ConsumeContext context) - { - } -} -``` - -### ReadOnly State Machine Events - -Automatonymous state machine sagas can now specify an _Event_ as read-only. When an event is read-only, any changes to the saga instance are **NOT** saved to the saga repository. - -```cs -class ReadOnlyStateMachine : - MassTransitStateMachine -{ - public ReadOnlyStateMachine() - { - InstanceState(x => x.CurrentState); - - Event(() => StatusCheckRequested, x => - { - x.ReadOnly = true; - }); - - Initially( - When(Started) - .Then(context => context.Instance.StatusText = "Started") - .Respond(context => new StartupComplete {CorrelationId = context.Instance.CorrelationId}) - .TransitionTo(Running) - ); - - During(Running, - When(StatusCheckRequested) - .Respond(context => new Status - { - CorrelationId = context.Instance.CorrelationId, - StatusText = context.Instance.StatusText - }) - .Then(context => context.Instance.StatusText = "Running") // this change won't be saved - ); - } - - public State Running { get; private set; } - public Event Started { get; private set; } - public Event StatusCheckRequested { get; private set; } -} -``` - -> The in-memory saga repository isn't able to undo changes, since they are applied to the saga instance in memory, just FYI. - -### Transport Headers - -When using the `RawJsonMessageDeserializer`, the `ConsumeContext.Headers` collection now includes the transport headers. - -### MongoDB Audit Store - -MongoDB can now be configured as an audit store. - -```cs -Bus.Factory.CreateUsingInMemory(cfg => -{ - cfg.UseMongoDbAuditStore(database, auditCollectionName); -}); -``` - -### C# 9 Record Support - -The Rosyln analyzer has been updated to work with C# 9 records, and should no longer report invalid contract types for records. - -### Job Consumers - -Suspect jobs (jobs that disappear, either due to a service failure/shutdown or whatever) are now faulted properly. Suspect jobs can also be retried by specifying a `SuspectJobRetryCount` and `SuspectJobRetryDelay` during the job service configuration. - - diff --git a/docs/releases/v7.1.0.md b/docs/releases/v7.1.0.md deleted file mode 100644 index 67ef4fc2ad5..00000000000 --- a/docs/releases/v7.1.0.md +++ /dev/null @@ -1,185 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.1.0 - -[[toc]] - -::: tip NOTE -MassTransit 7.1.0 is a minor version update. There are a couple of minor interface changes, such as `IRequestClient` detailed below. Any required changes after updating the package references should be minor. -::: - -[View the Pull Request for a list of all commits](https://github.com/MassTransit/MassTransit/pull/2195) - -## Re(Start) the Bus – Finally! - -Since MassTransit's inception, it has never been possible to _Start_ a bus that was previously _Started_ and then _Stopped_. With this release, a bus can now be started, stopped, started, and stopped, and started, and stopped... - -> It’s like removing a doorstop you’ve tripped over for years. - -Seriously, this is a big deal. Since containers are mainstream at this point along with having the _most excellent_ `.AddMassTransit` configuration experience, the ability to stop the bus (temporarily, or whatever) without having to configure a new bus instance from scratch is huge. There are a lot of ideas brewing that take advantage of this new capability, so stay tuned! - -## Request Client - -There have been a couple of updates to the request client to improve usability and interoperability. - -### Request Client Multiple Response Types - -The method signature for the `GetResponse(...)` method has been changed. The previous method signature returned a tuple, as shown below: - -```cs -Task<(Task>, Task>)> GetResponse(TRequest message, CancellationToken cancellationToken, RequestTimeout timeout) -``` - -The new signature uses a new type, `Response`, which is a _readonly_ struct that can be used to more easily identify which response was received. - -```cs -Task> GetResponse(TRequest message, CancellationToken cancellationToken, RequestTimeout timeout) -``` - -Using the new return type, handling multiple responses is now cleaner: - -```cs -var response = await client.GetResponse(new Request()); - -if (response.Is(out Response responseA)) -{ - // do something with responseA -} -else if (response.Is(out Response responseB)) -{ - // do something with responseB -} -``` - -As always, if the request times out, or if a `Fault` is produced by the consumer, the initial _await_ will throw a `RequestTimeoutException` or `RequestFaultException` respectively. The signature change only introduces a new return type. - -To retain backwards compatibility, a _Deconstruct_ method is available to access the two response tasks. The side effect of this choice is that there can only be a single _Deconstruct_ method with two arguments. So, a creative choice was made to provide a pattern-matching solution as well. - -By specifying the explicit type, `Response` for the return value, modern C# pattern can be used via deconstruction. - -```cs -Response response = await client.GetResponse(new Request()); - -// Using a regular switch statement -switch (response) -{ - case (_, ResponseA a) responseA: - // responseA in the house - break; - case (_, ResponseB b) responseB: - // responseB if it isn't A - break; - default: - // wow, we really should NOT get here - break; -} - -// Or using a switch expression -var accepted = response switch -{ - (_, ResponseA a) => true, - (_, ResponseB b) => false, - _ => throw new InvalidOperationException() -}; -``` - -The first tuple element is the `Response` type, which includes `MessageContext` so that the message headers can be examined. In the example above, it is discarded since the `response` variable is already in scope. - -### Request Client Accept Response Types - -Another change to the request client is the addition of a new message header, `MT-Request-AcceptType`, which is set by the request client and contains the message types that have been specified by the request client. This allows the request consumer to determine if the client can handle a response type, which can be useful as services evolve and new response types may be added to handle new conditions. For instance, if a consumer adds a new response type, such as `OrderAlreadyShipped`, if the response type isn't supported an exception may be thrown instead. - -To see this in code, check out the client code: - -```cs -var response = await client.GetResponse(new CancelOrder()); - -if (response.Is(out Response canceled)) -{ - return Ok(); -} -else if (response.Is(out Response responseB)) -{ - return NotFound(); -} -``` - -The original consumer, prior to adding the new response type: - -```cs -public async Task Consume(ConsumeContext context) -{ - var order = _repository.Load(context.Message.OrderId); - if(order == null) - { - await context.ResponseAsync(new { context.Message.OrderId }); - return; - } - - order.Cancel(); - - await context.RespondAsync(new { context.Message.OrderId }); -} -``` - -Now, the new consumer that checks if the order has already shipped: - -```cs -public async Task Consume(ConsumeContext context) -{ - var order = _repository.Load(context.Message.OrderId); - if(order == null) - { - await context.ResponseAsync(new { context.Message.OrderId }); - return; - } - - if(order.HasShipped) - { - if (context.IsResponseAccepted()) - { - await context.RespondAsync(new { context.Message.OrderId, order.ShipDate }); - return; - } - else - throw new InvalidOperationException("The order has already shipped"); // to throw a RequestFaultException in the client - } - - order.Cancel(); - - await context.RespondAsync(new { context.Message.OrderId }); -} -``` - -This way, the consumer can check the request client response types and act accordingly. - -::: tip NOTE -For backwards compatibility, if the new `MT-Request-AcceptType` header is not found, `IsResponseAccepted` will return true for all message types. -::: - -## In-Memory Outbox Configuration - -There were some unexpected changes in 7.0.7 related to how the In-Memory Outbox gets added to the consume pipeline for batch consumers. This was changed again to ensure that only one outbox is created for the batch consumer and that it is added prior to the consumer factory so that resolving consumer dependencies get the proper `ConsumeContext` (the outbox one). - -## Scoped Consume Filters - -The scoped consume filter was updated to ensure the scoped filter is resolved from the container _after_ the consumer/saga scope is created. - -## Receive Endpoint Connector - -When using container integration, it is now possible to connect receive endpoints using previously added consumer types. To connect a receive endpoint using MS DI, consider the following example. - -```cs -var connector = provider.GetRequiredService(); - -var handle = connector.ConnectReceiveEndpoint("some-queue", (context,cfg) => cfg.ConfigureConsumer(context)); - -await handle.Ready; -``` - -> The `ConfigureEndpoints` method has an optional filter that can be used to exclude `SomeConsumer` from having a receive endpoint created. - - - diff --git a/docs/releases/v7.1.1.md b/docs/releases/v7.1.1.md deleted file mode 100644 index 5574533101d..00000000000 --- a/docs/releases/v7.1.1.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.1.1 - -[[toc]] - -## Kill Switch - -A [Kill Switch](/advanced/middleware/killswitch) is used to prevent failing consumers from moving all the messages from the input queue to the error queue. By monitoring message consumption and tracking message successes and failures, a Kill Switch stops the receive endpoint when a trip threshold has been reached. - diff --git a/docs/releases/v7.1.3.md b/docs/releases/v7.1.3.md deleted file mode 100644 index 9832358a0b4..00000000000 --- a/docs/releases/v7.1.3.md +++ /dev/null @@ -1,53 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.1.3 - -[[toc]] - -This release is mostly bug fixes and minor tweaks, but any notable items are listed below - -## Request Client - -There was a bug in the request client when using multiple response types where consumer faults were not causing the request to fault. - -## Broker Disconnects - -There was a case where a broker disconnect could prevent a receive endpoint from restarting after reconnection. The receive transport has been restructured to eliminate the complexity and ensure reconnection until stopped. Riders were also updated to use the new receive transport. - -## Message Data - -A `MessageData` property is now supported, in addition to `string` and `byte[]`. - -## Automatonymous - -The `Finalize()` extension was not working properly in `Catch` blocks. - -## Kafka Topic Creation - -Kafka Topics can now be created. Within the topic endpoint configuration, you can specify: - -```cs -k.TopicEndpoint("topic-name", "consumer-group-name", e => -{ - e.CreateIfMissing(t => - { - t.NumPartitions = 2; //number of partitions - t.ReplicationFactor = 1; //number of replicas - }); -}); -``` - -## RabbitMQ Delay Exchange - -The delay exchange should no longer create/bind a queue of the same name. - -## Fault Publish Configuration - -The publishing of faults can now be disable by setting `PublishFaults` to false on a receive endpoint. - -## Managed Identity in Azure Functions - -When configuring Azure functions, if no key is found in the connection string, the Managed Identity token provider is automatically configured. - diff --git a/docs/releases/v7.1.4.md b/docs/releases/v7.1.4.md deleted file mode 100644 index 004b6bb5943..00000000000 --- a/docs/releases/v7.1.4.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.1.4 - -[[toc]] - -## Broker Disconnects (reprise) - -The last released addressed various broker disconnect issues, and introduced some new ones. There were some strange behaviors with bus start, stop, restart, and timing related to the broker availability. There were also startup issues with Kafka. - -## State Machine Request - -The state machine requests have been updated, the RequestId property is now optional – if not specified, the `CorrelationId` will be used as the RequestId. - -The RequestId property is only cleared when the request completes. If the request times out or faults, the RequestId is retained to allow for message replay, etc. - -The ServiceAddress is now optional, requests will be published if it is not specified (finally). - -> Also, a Timeout of `TimeSpan.Zero` has always eliminated the need for a scheduler by not having a timeout. - -## Request Client (silent) Exceptions - -A silent/caught exception in the request client has been eliminated by restructuring the `HandleFault` logic. - -## Amazon SQS - -The Amazon packages were updated to the latest, along with a few create queue/topic fixes. - -## Event Hub - -A blob container permissions issue was addressed. - -## UWP - -The GetProcess exception is caught and ignored, so UWP applications should be able to use MassTransit now. - diff --git a/docs/releases/v7.1.5.md b/docs/releases/v7.1.5.md deleted file mode 100644 index e6ced9c6d60..00000000000 --- a/docs/releases/v7.1.5.md +++ /dev/null @@ -1,52 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.1.5 - -[[toc]] - -## Health Checks - -Disconnected receive endpoints, which were connected using `ConnectReceiveEndpoint` were staying in the health check list and showing up as degraded after being disconnected. - -## Azure Service Bus Session Saga Repository - -The saga repository was upgraded to the level of the others, using the same underlying components – which means it should now support property dependency injection of state machine activities. - -## Published Message Scheduler Messages - -The messages published to Quartz were using the wrong message type, which wasn't really an issue with RabbitMQ, but non-polymorphic brokers would not get the message to the scheduler. - -## State Machine Event Topology - -A state machine saga can now specify events that do not configure the consume topology. This will eliminate excessive exchanges/topics for directed events that are sent to the saga such as responses. The option is specified during event configuration: - -```cs -Event(() => WaitTimeout, x => x.ConfigureConsumeTopology = false); -``` - -## Kafka Producer Registration - -The Kafka registration methods were changed from method chaining to nested closures, to be consistent. This also allows the _context_ to be used to resolve dependencies (such as the Confluent schema registry client). - -## AddGenericRequestClient - -Users of Microsoft DI can specify `services.AddGenericRequestClient()` to automatically resolve any request client type (requests will be published). - -## Use Message(Lifetime) Scope Faults - -Multiple faults were being published in combination with retry, the outbox, and message lifetime scope filters. Edge case, but the issue was resolved. - -## Kafka / Event Hub Checkpoint Concurrency Resolved - -The issue with non-deterministic checkpoints when using concurrent event consumption was resolved. - -## RawJsonSerializer Message Headers - -The RawJsonSerializer was improperly transferred transport headers from the inbound message to outgoing messages. This was causing exceptions, such as invalid table value, using RabbitMQ and may have been a problem with other brokers. - -## Amazon SNS - -GroupId/DeduplicationId is now set on published (topic) messages. Previously it was only set on Sent (queue) messages. - diff --git a/docs/releases/v7.1.6.md b/docs/releases/v7.1.6.md deleted file mode 100644 index 5d529292b47..00000000000 --- a/docs/releases/v7.1.6.md +++ /dev/null @@ -1,98 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.1.6 - -[[toc]] - - -## IConfigureReceiveEndpoint - -It is now easy to apply configuration to all receive endpoints when using `ConfigureEndpoints` or connecting a receive endpoint using the new `IReceiveEndpointConnector`. Depending upon the container, register a class that implements `IConfigureReceiveEndpoint`, and it will be used automatically. If multiple instances are registered, they will be executed in registration order (or should, depending on the container). - -An example class is shown below: - -```cs -class ConfigureMyEndpoint : - IConfigureReceiveEndpoint -{ - public void Configure(string name, IReceiveEndpointConfigurator configurator) - { - configurator.QueueAttributes["some-key"] = "some-value"; - } -} -``` - -The _name_ argument is the queue name being configured. - -To register the object, use: - -#### Microsoft Dependency Injection - -```cs -services.AddTransient(); -``` - -#### Autofac - -```cs -builder.RegisterType().As(); -``` - -#### Castle Windsor - -```cs -container.Register(Component.For().ImplementedBy()); -``` -#### Simple Injector - -```cs -Container.Collection.Register(typeof(ConfigureMyEndpoint)); -``` - -#### StructureMap - -```cs -expression.For().Add(); -``` - -## PrefetchCount and ConcurrentMessageLimit - -The PrefetchCount and ConcurrentMessageLimit can now be specified directly on `IReceiveEndpointConfigurator`, eliminating the need to cast to a transport-specific interface. The same values can also be set on `IBusFactoryConfigurator`, which will apply to all receive endpoints. - -The PrefetchCount is passed to the transport where supported, otherwise it is used when reading messages from the broker. - -The ConcurrentMessageLimit is only passed to transports that support it (currently, Azure Service Bus), and is otherwise used to control how many messages are dispatched concurrently on the receive endpoint. - -::: tip -The `ConcurrentMessageLimit` is not initialized by default, and does not need to be specified. If no limit is specified, which is the default, it will equal the PrefetchCount. -::: - -## Azure Service Bus Subscription Endpoint Configurator - -A new interface, `ISubscriptionEndpointConnector`, was added. It is similar to the recently added `IReceiveEndpointConnector` but is specific to Azure Service Bus for connecting subscription endpoints (which are on a specific topic). - -To use the new interface, look at the example below: - -```cs -var connector = provider.GetRequiredService(); - -var handle = connector.ConnectSubscriptionEndpoint("my-sub-name", e => -{ -}); -``` - -> The message type is used to generate the topic name based on the bus message topology, the same used for connecting message types for consumers on receive endpoints. - -## Subscription Endpoint Changes - -Prior to this version, separate queues were created for subscription endpoints to store *_error* and *_skipped* messages. By default, these messages will be dead-lettered instead, and moved to the Azure Service Bus dead-letter queue related to the subscription. The previous approach was a hack, so this should be *better* – or maybe it won't be better. - -## Transport Reconnection - -There was a regression in 7.1.4, in which publishing/sending messages when the broker was not connected would immediately throw an exception. This has been resolved, and hopefully in a much better way. The send pipeline now uses the same retry policy as the receive transport, but on its own pipeline. Previously (in version 7.1.3 and earlier anyway), it was using the receive transport pipeline to reconnect. This change should hopefully smooth out some threading issues. - -::: warning -It is important to pass a `CancellationToken` to `Publish` or `Send`, to specify a timeout or whatever, or the calls will wait until the broker is available. -::: diff --git a/docs/releases/v7.1.7.md b/docs/releases/v7.1.7.md deleted file mode 100644 index b839065d29d..00000000000 --- a/docs/releases/v7.1.7.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.1.7 - -[[toc]] - -## Fixes - -- SignalR Hubs now properly configure the broker topology (broken in 7.1.6) -- Job State Machine message order fix -- Removed the ListJsonConverter from the message deserializer for JSON messages (used by XML conversion only now) -- TransactionBus pending operation fix -- Invalid Content-Type causing endless loop on receiver -- Removed unintentional topology configuration delay for RabbitMQ -- Amazon SQS prefetch count algorithm always read 10 messages at a time, now will read actual number if prefetch count is < 10 - - -## Container Scope - -In previous versions, the container scope was not properly registered when resolving the consumer, resulting in the `IPublishEndpoint` and `ISendEndpointProvider` interfaces on dependent objects being resolved from a different scope. A single scope is now properly used to resolve all consumer dependencies. - -## ActiveMQ Virtual Topic Prefix - -The virtual topic prefix can now be specified for ActiveMQ publish/subscribe, which is useful if the default broker configuration is changed to use a different prefix (or no prefix at all). - diff --git a/docs/releases/v7.1.8.md b/docs/releases/v7.1.8.md deleted file mode 100644 index 57259b7bfb4..00000000000 --- a/docs/releases/v7.1.8.md +++ /dev/null @@ -1,134 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.1.8 - -[[toc]] - -## Resolved Issues - -- Responding from a state machine through the InMemoryOutbox [wasn't working as expected](https://github.com/MassTransit/MassTransit/issues/2396) -- Kafka producer options can now be configured -- `IsResponseAccepted` will now return _false_ if a _ResponseAddress_ is not present -- Azure Service Bus receive endpoints should now allow `PrefetchCount = 0` -- RabbitMQ Quorum queues can now be configured using `.SetQuorumQueue()`, which sets the appropriate queue attributes -- RabbitMQ queue attributes are now copied to the `_error` and `_skipped` queues - -## MassTransit Templates, Getting Started - -A set of `dotnet new` templates were released, which can be installed using `dotnet new -i MassTransit.Templates`. The [Getting Started](/getting-started) documentation was updated to use the new templates and a new [sample was added](https://github.com/MassTransit/Sample-GettingStarted) based on the documentation. Most of the new documentation will be written from a container-first perspective, starting with _AddMassTransit_ and subsequently documenting the container-free versions where available. - -## Delayed Message Delivery / Redelivery - -It is now possible to configure _delayed_ redelivery using the message transport. This approach works directly with the message transport, and does not use any configured message scheduler. A message scheduler can still be configured and used for regular message scheduling, but the transport message delay feature will be used for redelivery. This can significantly reduce the load on Quartz/Hangfire scheduling and separates the concerns of message redelivery and message scheduling. - -The configuration syntax is the same as scheduled redelivery, but with a new transport-independent extension method: - -```cs -cfg.ReceiveEndpoint("submit-order", e => -{ - e.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); - e.UseMessageRetry(r => r.Immediate(5)); - e.UseInMemoryOutbox(); - - e.ConfigureConsumer(); -}); -``` - -> The in-memory transport was updated to support delayed message delivery, making it easier to test delayed message scenarios. - -The new delayed support can also be used a message scheduler, regardless of transport. To configure the delayed exchange scheduler, see the example below. - -<<< @/docs/code/scheduling/SchedulingDelayed.cs - -## ExceptionInfo Data - -The `ExceptionInfo` type has a new property, `Data`, which is a `IDictionary`. This extra property supports the inclusion of application/exception specific data with the exception details. Since MassTransit does not support the directly serialization of `Exception` types (nor should it), this makes it possible to include additional values when an exception is thrown. - -This is an opt-in approach, and by default no exception data will be added. **This was decided to avoid accidental leakage of sensitive data.** - -A new exception type, `MassTransitApplicationException`, can be used to opt-in to the automatic propagation of Data properties into `ExceptionInfo.Data`. For example, data values can be explicitly added when the exception is caught and thrown. - -```cs -class SubmitOrderConsumer : - IConsumer -{ - public Task Consume(ConsumeContext context) - { - try - { - throw new IntentionalTestException("This was intentional"); - } - catch (Exception exception) - { - throw new MassTransitApplicationException(exception, new - { - context.Message.OrderNumber, - context.Message.CustomerNumber, - }); - } - } -} -``` - -The built-in `Data` property of `Exception` will also be used as a source when using `MassTransitApplicationException`. In the example below, the `Data`properties are added to the exception (framework and other libraries use this to store information, this example just does it to show the usage). - -```cs -class SubmitOrderConsumer : - IConsumer -{ - public Task Consume(ConsumeContext context) - { - try - { - var exception = new IntentionalTestException("This was intentional"); - exception.Data.Add("Username", "Frank"); - exception.Data.Add("CustomerId", 27); - throw exception; - } - catch (Exception exception) - { - throw new MassTransitApplicationException(exception); - } - } -} -``` - -## Raw JSON Message Headers - -The Raw JSON message serializer/deserializer was updated to support transport message headers, offering better support for messages without a regular MassTransit message envelope. The supported message headers include: - -| Name |Type| Property | Notes | -|:---|:---|:---|:---| -| MessageId |`Guid`| `MessageId` -| CorrelationId |`Guid`| `CorrelationId` -| ConverationId |`Guid`| `ConversationId` -| RequestId |`Guid`| `RequestId` -| MT-InitiatorId |`Guid`| `InitiatorId` -| MT-Source-Address |`Uri`| `SourceAddress` -| MT-Response-Address |`Uri`| `ResponseAddress` -| MT-Fault-Address |`Uri`| `FaultAddress` -| MT-MessageType |`string`| `SupportedMessageTypes` |`;`delimited -| MT-Host-Info |`JSON`| `Host` |serialized `Host` - -The default configuration doesn't change the existing behavior. When configuring the use of the Raw JSON serializer, new options are available: - -| Option |Description| -|:---|:---| -| AnyMessageType | Default behavior, any `T` is allowed. Do not specify to check the transport message type header -| AddTransportHeaders | Default behavior, headers are written to the transport and used -| CopyHeaders | Optional, will copy non-MassTransit headers to outbound messages - -These options are specified as a parameter to `cfg.UseRawJsonSerializer(options)`. - -## State Machine Delay Provider - -When configuring a message schedule in an Automatonymous state machine, instead of specifying a fixed delay, a `DelayProvider` can be configured. This enables dynamic delay periods based upon the contents of the state machine instance. The delay can still be dynamically specified when using `.Schedule()` based upon the instance and/or message contents. - - -## Azure Blob Storage Package Change - -The Azure Blob Storage Message Data repository was changed to use the new standard Azure packages. This may require slight reconfiguration, but should work as it did previously. - - diff --git a/docs/releases/v7.2.0.md b/docs/releases/v7.2.0.md deleted file mode 100644 index 78dbd02e597..00000000000 --- a/docs/releases/v7.2.0.md +++ /dev/null @@ -1,145 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.2.0 - -[[toc]] - -::: danger -This release changes the JobType table format for job consumers. If you are upgrading, be sure to update your database storage migrations. It may be necessary to truncate the JobType _table_ before starting services after the upgrade to avoid any weird storage errors. See [below](#job-consumers) for more details. -::: - -## Durable Futures - -This release includes an _early access_, _experimental_, _brand new_, _ready to be proven_ release of Durable Futures. Durable Futures are extensively covered in [Season 3](https://youtube.com/playlist?list=PLx8uyNNs1ri2JeyDGFWfCYyAjOB1GP-t1) and are ready to be tested in real applications. They are supported, and I expect there will be edge cases to resolve with subsequent releases, but they have been run through many times and tested with multiple saga repositories. - -_And they're pretty cool if I say so myself!_ - -The [ForkJoint](https://github.com/MassTransit/Sample-ForkJoint) sample is a great place to start looking at the code, how it works, and how it should be used. The videos provide all the background and reasoning behind the design. So kick the tires, try them out, and see how they work for you. - -## gRPC Transport - -> [MassTransit.Grpc](https://www.nuget.org/packages/MassTransit.Grpc) - -A new gRPC transport, designed to be a peer-to-peer distributed non-durable message transport, is now included. It's entirely in-memory, has zero dependencies, and allows multiple service instances to exchange messages across a shared message fabric. - -[Introduction Video (YouTube)](https://youtu.be/ChtpCM3N5a8) - -The gRPC is modeled after RabbitMQ, and supports many of the same features. It uses exchanges and queues, and follows the same topology structure as the RabbitMQ transport. - -Fanout, Direct, and Topic exchanges are supported, along with routing key support. - -And, it, is fast. Using the server GC, message throughput is pretty impressive. - -On a single node (essentially in-memory, but serialized via protocol buffers): - -``` -Send: 253,774 msg/s -Consume: 172,996 msg/s -``` - -Across two nodes, load balanced via competing consumer: -``` -Send: 232,597 msg/s -Consume: 36,331 msg/s -``` - -> Consume rate is slower because the messages are evenly split across the local and remote node. - -Full documentation is coming soon, but for now the host configuration is shown below. - -To configure the host using a complete address, such as `http://localhost:19796`, a `Uri` can be specified. The following configures a standalone instance, no servers are specified. Incoming connections are of course accepted. - -```cs -cfg.Host(new Uri("http://localhost:19796")); -``` - -To configure a host that connects to other bus instances, use the _AddServer_ method in the host. In this example, the _host_ and _port_ are configured separately. The bus will not start until the server connections are established. - -```cs -cfg.Host(h => -{ - h.Host = "127.0.0.1"; - h.Port = 19796; - - h.AddServer(new Uri("127.0.0.1:19797")); - h.AddServer(new Uri("127.0.0.1:19798")); -}); -``` - -A complete bus configuration is shown below, using gRPC: - -```cs -services.AddMassTransit(x => -{ - x.SetKebabCaseEndpointNameFormatter(); - - x.AddConsumer(); - - x.UsingGrpc((context, cfg) => - { - cfg.Host(h => - { - h.Host = "127.0.0.1"; - h.Port = 19796; - }); - - cfg.ConfigureEndpoints(context); - }); -}); -``` - -Check out [the discussion thread](https://github.com/MassTransit/MassTransit/discussions/2455) for more information. - -## Isolated Worker .NET 5 Azure Functions - -With .NET 5, the new Azure Function isolated worker support (which actually uses gRPC to communicate between the host and workers) means that the Azure Service Bus and Event Hub SDKs are no longer exposed to functions. This also means that none of the SDK types are available when executing functions. In fact, all you get is `byte[]` and `FunctionContext`. Well, that isn't going to stop us from supporting them and initial (early) support for this model is now available. - -The [Sample](https://github.com/MassTransit/Sample-AzureFunction/tree/v5/src/Sample.AzureFunction) has a v5 branch which uses the new updated support to dispatch messages to consumers, sagas, etc. Since the new isolated worker model is a real service, it's possible to use only the `MassTransit.AspNetCore` package, which includes the MS DI support along with the hosted service, so you can start MassTransit normally in the function using the worker's `IHostedService` support. Check out the sample, it's a lot simpler. - -> But yes, you still need to create your topics, queues, subscriptions, etc. yourself – or use DeployTopologyOnly with a separate console application. - -## Resolved Issues - -- [Mediator Response Header Serialization](https://github.com/MassTransit/MassTransit/discussions/2443) -- [Message Deserialization Helper Class](https://github.com/MassTransit/MassTransit/issues/2451) -- [Health Check Connect Status](https://github.com/MassTransit/MassTransit/discussions/2446) -- TimeToLive not property overridden when using request client handle to set header value - -## Conductor - -Conductor was first conceptualized back in early 2018 (or sooner) and after three years has failed to turn into anything close to the original design. It turns out, building a distributed, reliable, and scalable service mesh style service discovery service is really, really hard. So it's done, removed, finished, gone, get out of the code. Along with it, the service client, and all the Up/Down/Link/Unlink exchanges created when using it. If you were using it, sorry, I'd suggest **just using publish** for service discovery – after all, it works and is really all the service client did anyway. - -Seriously, it's dead. Buried. The only remaining bits support the job consumers for per-instance job execution (and no, this isn't an advertisement for job consumers). - -## BusHealth - -The interface `IBusHealth` and the `BusHealth` class are now obsolete. The `CheckHealth` method is now on `IBusControl` and health state is maintained internally by the bus. It always was, it just wasn't used – this eliminates the redundancy. The supported ASP.NET health checks have already been updated to use the new method, but any previous usage should be changed to `IBusControl`. - -## ConfigureMessageTopology - -On a receive endpoint, `ConfigureMessageTopology` can now be configured per message type, using `ConfigureMessageTopology(true | false)`. - -## UseMessageData Storage Configuration - -Added new methods to configure the message data repository using a selector, following a similar approach to what is used by saga repositories. For example, to configure Azure Storage as a message data repository: - -```cs -cfg.UseMessageData(x => x.AzureStorage("storage account connection string")); -``` - -## Job Consumers - -[Several updates](https://github.com/MassTransit/MassTransit/issues/2312) were made to job consumer, changing the previous behavior and improving the scalability when running multiple job consumer service instances. - -- Job Consumers are now scaled out per instance, with the concurrency limit applying to each instance. If the concurrency limit specified is 5, and there are three instances, that's 15 jobs running at the same time. Jobs are assigned round robin using the least allocated instance and tracking using the same JobType state machine. -- Faulted jobs (after retries) will now publish a standard `Fault` event. -- Completed jobs will now publish a `JobCompleted` event, in addition to the previously published JobCompleted (not job specific) event. -- Many noisy exchange bindings have been removed, cleaning up the exchanges/topics - -::: tip -With the removal of Conductor and the service client, the minimal code needed to support the job server instance endpoint remains. It is configured by default to create the instance endpoint, which is used to start jobs on specific instances. There may be some minor tweaks to the configuration required when upgrading. -::: - - diff --git a/docs/releases/v7.2.3.md b/docs/releases/v7.2.3.md deleted file mode 100644 index 646c91f4670..00000000000 --- a/docs/releases/v7.2.3.md +++ /dev/null @@ -1,81 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 7.2.3 - -[[toc]] - -> Release notes from 7.2.1 and 7.2.2 were not created, those releases were mostly bug fixes and stability improvements. - -### Message Data Objects - -Originally, message data (claim check) only supported `byte[]` and `string`, with `Stream` added. With this release, it is now possible to specify an object type `T`. For instance: - -```cs -public interface ValidateOrder -{ - Guid CorrelationId { get; } - MessageData Order { get; } -} -``` - -MassTransit will serialize the `Order` object, and transfer that object as message data (which may be inline, if the size is below the threshold). The object `T` must be a valid message type, to meet the serialization requirements. The message data can be initialized by passed the appropriate type via the message initializer. - -```cs -await endpoint.Send(new -{ - InVar.CorrelationId, - Order = new { - OrderId = orderId, - BigProperty = bigProperty, - // etc... - } -}); -``` - -### SQS Visibility Timeout - -MassTransit will now adjust the visibility timeout of messages until consumers complete, extending the timeout at regular intervals automatically to prevent message re-delivery after the default timeout (typically 30 seconds). - -### Request Client Pipe Configuration - -The request client has new overloads to set headers, etc. when calling `GetResponse`. For example: - -```cs -await client.GetResponse(new A(), context => context.TimeToLive = TimeSpan.FromMinutes(30), x.CancellationToken); -``` - -### System.Text.Json - -While Newtonsoft.JSON is still the default serializer, experimental support has been added for `System.Text.Json`. By default, it's a separate serialization media type to avoid compatibility issues. However, it can be configured to replace the default media type by configuring the bus to use `System.Text.Json` only. - -```cs -x.UsingRabbitMQ((context, cfg) => -{ - cfg.UseSystemTextJsonOnly(); -}); -``` - -Due to limitations in `System.Text.Json`, it is not 100% compatible with Newtonsoft.JSON. But in most cases, it works. It's clearly some edge message types that are unable to be serialized and/or deserialized. - -### Raw XML Serialization - -To complement the built-in raw JSON support, a new raw XML serializer has been added. - -### Non-Generic AddConsumer, AddSaga methods - -The non-generic methods such as `AddConsumer` and `AddSaga` can now configure `Endpoint` details. Previously this was only available on the generic `AddConsumer` style methods. The bulk methods remain unchanged. - -### Miscellaneous - -- FutureState storage for Entity Framework Core now works with multiple `DbContext` types in the container. -- Better support for SQS FIFO queues/topics -- Optimistic concurrency support for job consumer saga repository (EFCore) -- Fixed weird shutdown issue with ActiveMQ consumers draining the queue -- Fixed _tokenId_ header issue with Azure Service Bus scheduler that prevented state machine scheduled messages from being properly correlated to the job service sagas -- Changed checkpoint logic for Event Hub and Kafka to update after a timeout, not just a message count - - - - diff --git a/docs/releases/v8.0.0.md b/docs/releases/v8.0.0.md deleted file mode 100644 index 9272dec7610..00000000000 --- a/docs/releases/v8.0.0.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -sidebarDepth: 0 ---- - -# 8.0.0 - -MassTransit v8 is the first major release since the availability of .NET 6. MassTransit v8 works a significant portion of the underlying components into a more manageable solution structure. Focused on the developer experience, while maintaining compatibility with previous versions, this release brings together the entire MassTransit stack. - -Refer to the [upgrading](/getting-started/upgrade-v6) section for details. diff --git a/docs/support.md b/docs/support.md deleted file mode 100644 index 00bbf9efe53..00000000000 --- a/docs/support.md +++ /dev/null @@ -1,52 +0,0 @@ -# Support - -Commercial support is available for MassTransit and provided by Loosely Coupled, LLC. - -Loosely Coupled, LLC was founded Chris Patterson, the author of MassTransit, to ensure the project's sustainability and provide solutions for organizations requesting any of the support and consulting services listed below. - -## Support Services - -Commercial support agreements include the support services outlined below. Support agreements can be purchased for a specific service period (typically one year). - -### General Support - -General Support means the Support Services provided for a defined period from general availability of a Major Release. General Support includes bug and security fixes and Developer Support. - -### Developer Support - -Developer Support means the provision of virtual or email-based technical assistance by Loosely Coupled to Customer’s technical contact(s) with respect to -service requests, at the corresponding service level purchased by Customer. - -### Maintenance Services - -Maintenance Services means the provision of Maintenance Releases, Minor Releases, and Major Releases (defined below), if any, to the Software, along with any -corresponding Documentation. - -- Major Release means a generally available release of the Software that contains functional enhancements, extensions, and deprecations, denoted by incrementing -the first number in the version (e.g., Software 8.0.0 to Software 9.0.0). -- Minor Release means a general available release of the Software that introduces a limited amount of new features and functionality, denoted by incrementing the -second number in the version (e.g., Software 8.0.0 to Software 8.1.0). -- Maintenance Release means a generally available release of the Software that typically provides maintenance fixes only, denoted by incrementing the third number -in the version (e.g., Software 8.0.0 to Software 8.0.1). - -### Technical Guidance - -Technical Guidance means the Support Services provided for an additional period following General Support. Support Services for products in the Technical Guidance period are available for customers with established applications to plan and complete upgrades to a current production version that is available within General Support, however, there will be no new Minor Releases or Maintenance Releases for products under Technical Guidance. - -For more details and to request a quote, contact [MassTransit Support][1]. - -## Consulting Services - -Consulting services are also available for architecture, development, and operational support. Services are provided on an hourly basis, and there are even an easy pay-as-you-go options that can be scheduled online (based upon availability). - -### Architecture Review - -If your team is new to message-based systems, a review of the application's requirements, service level objectives, deployment environments, and user personas may generate insights to drive architecture decisions. Determine the feasibility of leveraging a message broker in your application and which broker is appropriate for your target platform. - -### Code Review - -Ensuring the appropriate use of MassTransit consumers, sagas, routing slips, consistent message contract design, and proper configuration early in a project is the best way to deliver a consistent and maintainable application. - -For more information or to request a quote, contact [MassTransit Support][1]. - -[1]: mailto://support@masstransit.io diff --git a/docs/troubleshooting/common-gotchas.md b/docs/troubleshooting/common-gotchas.md deleted file mode 100644 index 988c282ef70..00000000000 --- a/docs/troubleshooting/common-gotchas.md +++ /dev/null @@ -1,71 +0,0 @@ -# Common Mistakes - -Over the years, there are certain concepts that can be confusing and lead to questions for developers new to MassTransit (or message-based asynchronous programming). A few of the common mistakes, issues, and gotchas are described below. - -::: danger Have you started the bus? -Seriously, this is so common it's worth repeating at the top of every page. If you are seeing messages not being consumed, or responses timing out on request, or anything that feels weird, make sure you are calling `Start` or `StartAsync` on the `IBusControl`. -::: - -### Sharing a queue - -> While a common mistake in MassTransit 2.x, the new receive endpoint syntax of MassTransit 3 should make it easier to recognize that queue names should not be shared. - -Each receive endpoint needs to have a unique queue name! If multiple receive endpoints are created, -each should have a different queue name so that messages are not skipped. - -If two receive endpoints share the same queue name, yet have different consumers subscribed, messages -which are received by one endpoint but meant for the other will be moved to the _skipped_ queue. It -would be like sharing a mailbox with your neighbor, sometimes you get all the mail, sometimes they -get all the mail. - -### Send/Publish Only - -When creating a bus instance only to send or publish messages, it must be started. Failure to start the bus can lead to some strange side effects. Every bus, even ones without receive endpoints, must be started (and eventually stopped). - -### How do I load balance consumers across machines? - -To load balance consumers, the process with the receive endpoint can be hosted on multiple servers. -As long as each receive endpoint has the same consumers registered, the messages will be received -by the first available consumer across all of the machines. - -#### What links two bus instances together? - -This is a common question. The binding element, really is the -message contract. If you want message A, then you subscribe to -message A. The internals of MT wires it all together. - -### Why aren't queue / message priorities supported? - -Message Priorities are used to allow a message to jump to the front -of the line. When people ask for this feature they usually have multiple -types of messages all being delivered to the same queue. The problem -is that each message has a different SLA (usually the one with the -shorter time window is the one getting the priority flag). The problem -is that w/o priorities the important message gets stuck behind the -less important/urgent ones. - -The solution is to stop sharing a single queue, and instead establish -a second queue. In MassTransit you would establish a second instance -of IServiceBus and have it subscribe to the important/urgent -message. Now you have two queues, one for the important things and one -for the less urgent things. This helps with monitoring queue depths, -error rates, etc. By placing each IServiceBus in its own Topshelf host -/ process you further enhance each bus's ability to process messages, and -isolate issues / downtime. - -#### Request client throws a timeout exception - -MassTransit uses a temporary non-durable queue and has a consumer to handle responses. This temporary queue only get configured and created when you _start the bus_. If you forget to start the bus in your application code, the request client will fail with a timeout, waiting for a response. - -#### Reading - -https://lostechies.com/jimmybogard/2010/11/18/queues-are-still-queues/ - -### I want to know if another bus is subscribed to my message. - -> So, if you try to program this way, you're going to have a bad time. ;) - -Knowing that you have a subscriber is not the concern of your application. -It is something the system architect should know, but not the application. -Most likely, we just need to introduce all of the states in our protocol -more explicitly, by using a Saga. diff --git a/docs/troubleshooting/show-config.md b/docs/troubleshooting/show-config.md deleted file mode 100644 index 986970b6b8a..00000000000 --- a/docs/troubleshooting/show-config.md +++ /dev/null @@ -1,172 +0,0 @@ -# Show Configuration - -A bus instance is composed of many classes, all of which are wired together to form a connection pipeline of -message processing goodness. This brings a bit of complexity, as there are many moving parts behind the curtain. -To help troubleshoot and understand how a bus is configured, it is possible to probe the bus and return an object -graph of the bus. - -To probe bus configuration, use the `GetProbeResult` method as shown below. - -```cs -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Host("rabbitmq://localhost/test"); - - sbc.ReceiveEndpoint("input_queue", ec => - { - ec.Consumer(); - }) -}); - -ProbeResult result = busControl.GetProbeResult(); - -Console.WriteLine(result.ToJsonString()); -``` - -The resulting output for the configuration above would be similar to the following. - -```json -{ - "resultId": "7f280000-2961-000c-1dcc-08d2c68a08ac", - "probeId": "7f280000-2961-000c-fd27-08d2c68a08ab", - "startTimestamp": "2015-09-26T15:49:16.594521Z", - "duration": "00:00:00.4850036", - "host": { - "machineName": "LOCALHOST", - "processName": "TestService", - "processId": 5808, - "assembly": "MassTransit", - "assemblyVersion": "6.0.0.0", - "frameworkVersion": "4.0.30319.42000", - "massTransitVersion": "6.0.0.0", - "operatingSystemVersion": "Microsoft Windows NT 6.3.9600.0" - }, - "results": { - "bus": { - "address": "rabbitmq://[::1]:5672/test/bus-testservice-xhwyyybjcryy3ofjbdjcpnoenx?durable=false&autodelete=true&prefetch=8", - "host": { - "type": "RabbitMQ", - "host": "[::1]", - "port": 5672, - "virtualHost": "test", - "username": "guest", - "password": "*****", - "connected": true - }, - "receiveEndpoint": [ - { - "transport": { - "type": "RabbitMQ", - "queueName": "input_queue", - "exchangeName": "input_queue", - "prefetchCount": 16, - "durable": true, - "queueArguments": {}, - "exchangeArguments": {}, - "purgeOnStartup": true, - "exchangeType": "fanout", - "bindings": [ - { - "exchange": { - "exchangeName": "TestService.Contracts:UpdateCustomerAddress", - "exchangeType": "fanout", - "durable": true, - "arguments": {} - }, - "routingKey": "", - "arguments": {} - } - ] - }, - "filters": [ - { - "filterType": "deadLetter", - "filters": { - "filterType": "move", - "destinationAddress": "rabbitmq://[::1]:5672/test/input_queue_skipped?bind=true&queue=input_queue_skipped" - } - }, - { - "filterType": "rescue", - "filters": { - "filterType": "moveFault", - "destinationAddress": "rabbitmq://[::1]:5672/test/input_queue_error?bind=true&queue=input_queue_error" - } - }, - { - "filterType": "deserialize", - "deserializers": { - "json": { - "contentType": "application/vnd.masstransit+json" - }, - "bson": { - "contentType": "application/vnd.masstransit+bson" - }, - "xml": { - "contentType": "application/vnd.masstransit+xml" - } - }, - "pipe": { - "TestService.Contracts.UpdateCustomerAddress": { - "filters": { - "filterType": "instance", - "type": "MassTransit.Testing.MultiTestConsumer+Of" - } - } - } - } - ] - }, - { - "transport": { - "type": "RabbitMQ", - "queueName": "bus-testservice-xhwyyybjcryy3ofjbdjcpnoenx", - "exchangeName": "bus-testservice-xhwyyybjcryy3ofjbdjcpnoenx", - "prefetchCount": 8, - "autoDelete": true, - "queueArguments": { - "x-expires": 60000 - }, - "exchangeArguments": { - "x-expires": 60000 - }, - "exchangeType": "fanout", - "bindings": [] - }, - "filters": [ - { - "filterType": "deadLetter", - "filters": { - "filterType": "move", - "destinationAddress": "rabbitmq://[::1]:5672/test/bus-testservice-xhwyyybjcryy3ofjbdjcpnoenx_skipped?bind=true&queue=bus-testservice-xhwyyybjcryy3ofjbdjcpnoenx_skipped" - } - }, - { - "filterType": "rescue", - "filters": { - "filterType": "moveFault", - "destinationAddress": "rabbitmq://[::1]:5672/test/bus-testservice-xhwyyybjcryy3ofjbdjcpnoenx_error?bind=true&queue=bus-testservice-xhwyyybjcryy3ofjbdjcpnoenx_error" - } - }, - { - "filterType": "deserialize", - "deserializers": { - "json": { - "contentType": "application/vnd.masstransit+json" - }, - "bson": { - "contentType": "application/vnd.masstransit+bson" - }, - "xml": { - "contentType": "application/vnd.masstransit+xml" - } - }, - "pipe": {} - } - ] - } - ] - } - } -} -``` \ No newline at end of file diff --git a/docs/understand/README.md b/docs/understand/README.md deleted file mode 100644 index 1290ecaa68a..00000000000 --- a/docs/understand/README.md +++ /dev/null @@ -1,16 +0,0 @@ -# Understanding MassTransit - -MassTransit is a large framework, and there is a lot going on under the hood. To understand what is happening, -there are a number of subjects covered below that explain in detail how everything works. - -
-Note: -Okay, not everything -- at least not yet. But we're working on getting it all documented. -
- -* [Key terminology](key-ideas.md) -* [Under the hood](under-the-hood.md) -* [What we add to the transport](additions-to-transport.md) -* [Publishing messages](publishing.md) -* [Sending messages](sending.md) -* [Performance counters](perfcounters.md) \ No newline at end of file diff --git a/docs/understand/additions-to-transport.md b/docs/understand/additions-to-transport.md deleted file mode 100644 index 886b78504a3..00000000000 --- a/docs/understand/additions-to-transport.md +++ /dev/null @@ -1,92 +0,0 @@ -# What does MassTransit add to the transport? - -MassTransit is a lightweight service bus for building distributed .NET applications. The main goal is to provide -a consistent, .NET friendly abstraction over the message transport (whether it is RabbitMQ, Azure Service Bus, etc.). -To meet this goal, MassTransit brings a lot of the application-specific logic closer to the developer in an easy to -configure and understand manner. - -The benefits of using MassTransit over the message transport, as opposed to using the raw transport APIs and building -everything from scratch, are shown below. These are just a few, and some are more significant than others. The fact -that the hosting of your consumers, handlers, sagas, etc. are all managed consistently with a well documented -production ready framework is the biggest advantage. You can also find numerous blog posts, podcasts, and articles -written about MassTransit online. - -### Concurrency - -Concurrent, asynchronous message consumers for maximum receive throughput and high server utilization. - -### Connection management - -The network is unreliable. If the application is disconnected from the message broker, MassTransit takes care of -reconnecting and making sure all of the exchanges, queues, and bindings are restored. - -### Exception, retries, and poison messages - -Your message consumers don't need to know about broker acknowledgement protocols. If your message consumer runs to -completion, the message is acknowledged and removed from the queue. If you throw an exception, MassTransit uses a -retry policy to redeliver the message to the consumer. If the retries are exhausted due to continued failures or -other reasons, MassTransit moves the message to an error queue. If the message did not reach a consumer due to being -misrouted to the queue, the message is moved to a skipped queue. - -### Serialization - -C# is a statically typed language, and developers work with types. RabbitMQ works with bytes. So how do you format -a message over the wire? How do you handle different date/time formats (local, UTC, unspecified)? How do you deal -with numbers, are they integers, longs, or decimals? MassTransit has already thought about this and implemented -sensible defaults for you. And there are many serializers provided out of the box, including JSON, BSON, and XML as -well as the .NET binary formatter as a last resort. - -You can even protect your messages using AES-256 encryption, to keep prying eyes away and to ensure the safety of -private information (to meet PCI or HIPAA requirements). - -### Message header and correlation - -Designing a common message envelope can be a nitty-gritty affair until things stabilize. And MassTransit is already -stable having been used in production since 2008. The format is [well documented](../advanced/interoperability.html) -and has been tested with billions of messages. Furthermore, the envelope includes headers for tracking messages, -including conversations, correlations, and requests. The address and host information in the envelope make it easy to -build any messaging pattern. - -### Consumer lifecycle management - -MassTransit handles consumer creation and disposal, and integrates with most major dependency injection containers -using their built-in lifetime scope management. This ensures that dependencies are created and destroyed as part of -the message consumption pipeline. - -### Routing - -MassTransit provides a heavily production tested convention for using RabbitMQ exchanges to route published messages -to the subscribed consumers. The structure is CPU and memory friendly, which keeps RabbitMQ happy. - -### Rx integration - -Interested in or already using Reactive Extensions? MassTransit makes it easy to connect Rx to RabbitMQ. - -### Unit testing made easy - -One of the first rules of unit testing is to avoid hitting infrastructure. And RabbitMQ is just that. MassTransit -includes a high-performance in-memory transport for testing every consumer using the same code that would be used -in production. And the MassTransit.TestFramework NuGet package includes test harnesses -that handle the setup and teardown of the bus so you can easily test your message consumers and sagas. - -### Sagas - -Sagas are a powerful abstraction that supports message orchestration with durable state. Whether you use the original -somewhat explicit syntax, or the powerful state machine syntax of **Automatonymous**, you can build highly available -distributed workflow and coordination services easily. MassTransit supports both Entity Framework and NHibernate, using -code-based mapping and migrations to simply code deployments and upgrades. - -### Scheduling - -MassTransit has strong integration with Quartz.NET, to make it easy to schedule messages for future delivery. This brings -distributed applications into the fourth dimension, making time a first-class citizen. Some incredibly powerful routing -systems have been built by the authors using Quartz in combination with other MassTransit features. - -There are also other scheduling providers that are supported by MassTransit, such as RabbitMQ deferred messages and -Azure Service Bus scheduled enqueueing. - -### Monitoring performance counters - -Keeping an eye on your services performance is critical, and having the right tools is a huge plus. MassTransit updates -a range of performance counters as messages are processed so operations can keep an eye on message flow and compare -the throughput to that of RabbitMQ. diff --git a/docs/understand/key-ideas.md b/docs/understand/key-ideas.md deleted file mode 100644 index 64c490b3a1c..00000000000 --- a/docs/understand/key-ideas.md +++ /dev/null @@ -1,116 +0,0 @@ -# Key concepts - -When getting started using MassTransit, it is a good idea to have a handle on messaging concepts and terminology. To ensure that you are on the right path when looking at a class or interface, here are some of the terms used when working with MassTransit. - -## Message contracts - -A message contract defines a _contract_ between a message producer and a message consumer. In MassTransit, message contracts are declared using types, which should be _interfaces_ in .NET, but may also be classes. For example, a simple command contract for submitting an order is shown below. - -```csharp -public interface SubmitOrder -{ - Guid OrderId { get; } - DateTimeOffset SubmitDate { get; } - string OrderNumber { get; } - OrderItem[] Items { get; } -} - -public interface OrderItem -{ - string ItemNumber { get; } - int Quantity { get; } -} -``` - -## Serialization - -MassTransit is a service bus, and a service bus is designed to move *messages*. At the lowest level, a message is a byte array (`byte[]`) which may represent JSON, XML, or encrypted data. - -When sending a message, MassTransit uses a message serializer to convert message contract instances (objects) into JSON, XML, or whatever output format is generated by the serializer. When receiving that message, a message deserializer is used to convert the formatted data back into the message contract. - -By default, MassTransit uses JSON to serialize messages, while simultaneously suppporting the deserialization of JSON, BSON, and XML messages. Additional deserializers can be added, and the serializer can be changed for each receive endpoint or the entire bus. - -To specify that XML should be used when sending messages: - -```csharp -Bus.Factory.CreateUsingInMemory(cfg => -{ - cfg.UseXmlSerializer(); -}); -``` - -Other serializers can be configured using other configuration methods. - -## Consumers - -Consumers are classes used to _consume_ messages. Conceptually they are similar to _Controllers_ in a Web API, in that consumers implement a generic interface to specify the message types to be consumed. - -An example consumer that consumes the sample message contract above is shown below. - -```csharp -public class SubmitOrderConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - // do the thing - } -} -``` - -Consumers are stateless by design, and MassTransit makes no effort to correlate messages to a specific consumer instance. For each message, a consumer factory is used to create a consumer instance to which that message is delivered. - -## Sagas - -In MassTransit, a saga is a stateful consumer that allows multiple messages to be correlated to a single consumer instance. A saga, sometimes called a workflow, is typically used to orchestrate other consumers or services in a distributed system while maintaining state between messages. MassTransit persists a saga's state using a saga repository, and automatically creates a new or uses an existing state instance as messages are received. - -> Consider a saga a long-running transaction that is managed at the application layer instead of being handled inside a database or by a distributed transaction coordinator. - -MassTransit supports simple class-based sagas and powerful, advanced state machine sagas using _Automatonymous_ - a declarative state machine library. In a class-based saga, messages are correlated using a `Guid CorrelationId` property and must implement the `CorrelatedBy` interface. Class-based sagas can also _observe_ messages by specifying a correlation expression, but caution should be used to avoid matching a message to hundreds of saga instances which may cause performance issues. - -A simple saga is shown below as an example, which monitors the completion time of orders. - -```csharp -public class OrderToCompletionSaga : - ISaga, - InitiatedBy, - Orchestrates -{ - public Guid CorrelationId { get; set; } - public DateTime Submitted { get; set; } - public DateTime Completed { get; set; } - - public async Task Consume(ConsumeContext context) - { - Submitted = context.Message.OrderDate; - } - - public async Task Consume(ConsumeContext context) - { - Completed = context.Message.CompletionDate; - } -} -``` - -## Transports and endpoints - -MassTransit is a framework, and being a framework has certain rules. The first of which is known as the Hollywood principle -- "Don't call us, we'll call you." Once the bus is configured and running, message consumer are called by the framework as messages are received. There is no need for the application to poll a message queue or make repeated calls to a framework method in a loop. - -
-Note: -A way to understand this is to think of a message consumer as being similar to a controller in a web application. With a web application, the socket and HTTP protocol are under the hood, and the controller is created and an action method is called by the web framework. MassTransit is similar, it handles the message reception, creates the consumers, and calls the Consume method. -
- -To initiate the calls into your application code, MassTransit creates an abstraction on top of the messaging platform (such as RabbitMQ). - -### Transports -The transport is at the lowest level and is closest to the actual message broker. The transport communicates with the broker, responsible for sending and receiving messages. The send and receive sections of the transport are completely independent, keeping reads and writes separate in line with the Command Query Responsibility Segregation (CQRS) pattern. - -### Receive endpoints -A receive endpoint receives messages from a transport, deserializes the message body, and routes the message to the consumers. Applications do not interact with receive endpoints, other than to configure and connect consumers. The rest of the work is done entirely by MassTransit. - -### Send endpoints -A send endpoint is used by an application to send a message to a specific address. They can be obtained from the `ConsumeContext` or the `IBus` and support a variety of message types. - -### Endpoint addressing -MassTransit uses Universal Resource Identifiers (URIs) to identify endpoints. URIs are flexible and easy to include additional information, such as queue or exchange types. An example RabbitMQ endpoint address for *my_queue* on the local machine would be: `rabbitmq://localhost/my_queue` diff --git a/docs/understand/publishing.md b/docs/understand/publishing.md deleted file mode 100644 index 6087c737856..00000000000 --- a/docs/understand/publishing.md +++ /dev/null @@ -1,110 +0,0 @@ -# Publishing messages - -When a message is published (by way of a call to `bus.Publish`), it's important to understand what MassTransit actually -does under the hood. While the explicit implementation details depend upon the message transport being used, the general -pattern is the same. - -MassTransit follows the [publish subscribe][1] message pattern, where a copy of the message is delivered to each subscriber. -The message transport determines how the actual routing is performed, but the conventions of each transport are described below. - -## Routing on RabbitMQ - -RabbitMQ provides powerful routing capabilities out of the box, in the form of exchanges and queues. Exchanges can be bound -to queues, as well as other exchanges, making it easy to create a message routing fabric. MassTransit leverages exchanges -and queues combined with the .NET type system to connect subscribers to publishers. - -MassTransit uses the message type to declare exchanges and exchange bindings that match the hierarchy of types implemented -by the message type. Interfaces are declared as separate exchanges (using a fully-qualified type name that is compatible with -the naming structure of exchanges) and bound to the published message type exchange. When a message is first published, the -exchanges are declared once, and then used for the life of the channel. - -> Private types, such as classes, are declared as auto-delete so they do not clutter up the exchange namespace. - -Once declared, published messages are to the message type exchange, and copies are routed to all the subscribers by RabbitMQ. -This approach was [based on an article][2] on how to maximize routing performance in RabbitMQ. - -This dynamic, type-based routing model has proved very powerful in many large applications. The ability to add -new consumers to an existing message publisher is a great way to manage dependencies and keep projects from becoming tightly -coupled. - -To see how this plays out, consider the following message types: - -```csharp -namespace Company.Messages -{ - public interface CustomerAddressUpdated - { - } - - public interface UpdateCustomerAddress - { - } - - public class UpdateCustomerAddressCommand : - UpdateCustomerAddress - { - } -} -``` - -Once the messages have been published, exchanges are created in RabbitMQ for each of the message types: - -``` -Exchanges - -Company.Messages.CustomerAddressUpdated -Company.Messages.UpdateCustomerAddress -Company.Messages.UpdateCustomerAddressCommand - - Includes a binding to Company.Messages.UpdateCustomerAddress -``` - -When a receive endpoint is started, the second half of the exchange/queue binding is performed, where the consumer subscriptions -are bound to the consumer message type exchanges, closing the loop. - -```csharp -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Host("localhost"); - - cfg.ReceiveEndpoint("customer_update_queue", e => - { - e.Consumer(); - }); -}); -``` - -This results in the creation of a queue, as well as a binding to the queue from the `UpdateCustomerAddress` exchange. - -``` -Exchanges - -customer_update_queue - - Includes a binding from Company.Messages.UpdateCustomerAddress - -Queues - -customer_update_queue - - Includes a binding from the customer_update_queue exchange -``` - -> Because RabbitMQ only allows messages to be sent to exchanges, an exchange matching the name of the queue is created and bound to the queue. -This makes it easy to send messages directly to the queue using the same name. It's actually a pretty cool abstraction, and RabbitMQ makes -it very flexible by allowing exchange-to-exchange bindings. By keeping the bindings at the exchange level, it eliminates any impact to message -flow. Dru [shared his experience][3] with this as well. - -### Balancing the load - -Because RabbitMQ is a message broker, it supports multiple readers from the same queue. This makes it super easy to setup a -load balancing scenario where the same service is running on multiple servers, each of which is connected to the same queue. As -messages arrive on the queue, they are delivered to the first available consumer that can receive the message. To get good -load balancing, it's important to set the `PrefetchCount` to a sensible value on the consumer so that messages are well distributed. - -### Routing on Azure Service Bus - -MassTransit uses a similar approach for Azure Service Bus, but uses Topics, Subscriptions, and Queues. - -> More details to come... - -[1]: http://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html -[2]: http://spring.io/blog/2011/04/01/routing-topologies-for-performance-and-scalability-with-rabbitmq/ -[3]: http://codebetter.com/drusellers/2011/05/08/brain-dump-conventional-routing-in-rabbitmq/ diff --git a/docs/understand/under-the-hood.md b/docs/understand/under-the-hood.md deleted file mode 100644 index 4b4364e3012..00000000000 --- a/docs/understand/under-the-hood.md +++ /dev/null @@ -1,89 +0,0 @@ -# Under the hood - -MassTransit hides all the details of messages and delivery from the developer. -However, when there are issues it is important to know how it works so you can troubleshoot the issues. - -## Setup - -To see how this plays out, consider the following message types: - -```csharp -namespace MySystem.Messages { - interface SomeMessage {} -} -``` - -Configure the bus that will listen to `SomeMessage` with an endpoint of `my_endpoint`. - -## Starting a bus - -When creating the endpoint above you indicated the name of the queue where the messages will end up. -See queue naming rules below. Starting the bus with the consumers registered, causes the following configuration to happen: - -- A queue named `my_endpoint`is created for all messages on this endpoint -- An exchange named `my_endpoint` is created for all messages on this endpoint -- An exchange named `MySystem.Messages.SomeMessage`is created for the message -- An exchange to exchange binding from `MySystem.Messages.SomeMessage` to `my_endpoint` is created -- A binding from the `my_endpoint` exchange to `my_endpoint` queue is created. - -
-Note: - All exchanges created are of type FanOut -
- -## Publishing a message - -When you publish a message on the bus here is what happens: - -- Publish `MySystem.Messages.SomeMessage` -- This message gets published by the publishing logic to the exchange `MySystem.Messages.SomeMessage` -- The message is routed by messaging infrastructure to the `my_endpoint` exchange -- The message is then routed to the `my_endpoint` queue - -
-Note: -If you publish a message before the consumer has been started (and created its configuration), the exchange -MySystem.Messages.SomeMessage will be created. It will not be bound to anything until the consumer starts, -so if you publish to it, the message will just disappear. -
- -## Queues - -- Each application you write should use a unique queue name. -- If you run multiple copies of your consumer service, they would listen to the same queue (as they are copies). - This would mean you have multiple applications listening to `my_endpoint` queue - This would result in a 'competing consumer' scenario. (Which is what you want if you run same service multiple times) -- If there is an exception from your consumer, the message will be sent to `my_endpoint_error` queue. -- If a message is received in a queue that the consumer does not know how to handle, the message will be sent to `my_endpoint_skipped` queue. - -## Design Benefits - -- Any application can listen to any message and that will not affect any other application that may or may not be listening for that message -- Any application(s) that bind a group of messages to the same queue will result in the competing consumer pattern. -- You do not have to concern yourself with anything but what message type to produce and what message type to consume. - -## Faq - -- How many messages at a time will be simultaneously processed? - - Each endpoint you create represents 1 queue. That queue can receive any number of different message types (based on what you subscribe to it) - - The configuration of each endpoint you can set the number of consumers with a call to `PrefetchCount(x)`. - - This is the total number of consumers for all message types sent to this queue. - - In MT2, you had to add ?prefetch=X to the Rabbit URL. This is handled automatically now. - -- Can I have a set number of consumers per message type? - - Yes. This uses middleware. - - `x.Consumer(new AutofacConsumerFactory<…>(), p => p.UseConcurrencyLimit(1)); x.PrefetchCount=16;` - - PrefetchCount should be relatively high, a multiple of your concurrency limit for all message types so that RabbitMQ doesn't choke delivery messages due to network delays. Always have a queue ready to receive the message. - -- When my consumer is not running, I do not want the messages to wait in the queue. How can I do this? - - There are two ways. Note that each of these imply you would never use a 'competing consumer' pattern, so make sure that is the case. - 1. Set `PurgeOnStartup=true` in the endpoint configuration. When the bus starts, it will empty the queue of all messages. - 1. Set `AutoDelete=true` in the endpoint configuration. This causes the queue to be removed when your application stops. - -- How are Retries handled? - - This is handled by [middleware](../advanced/middleware/README.md). Each endpoint has a [retry policy](../usage/exceptions.md). - -- Can I have a different retry policy per each message type? - - No. This is set at an endpoint level. You would have to have a specific queue per consumer to achieve this. diff --git a/docs/usage/README.md b/docs/usage/README.md deleted file mode 100644 index 470e4bd5fb8..00000000000 --- a/docs/usage/README.md +++ /dev/null @@ -1,25 +0,0 @@ -# Using MassTransit - -To use and understand MassTransit, the documentation has been organized to start with the basics, and progressively move into more advanced topics. - -* [Configuration](configuration) -* [Transports](transports) -* [Messages](messages) -* [Consumers](consumers) -* [Producers](producers) -* [Exceptions](exceptions) -* [Requests](requests) -* [Sagas](sagas/) -* [Containers](containers/) -* [Testing](testing) - -### Advanced - -* [Scheduling](/advanced/scheduling/) -* [Courier](/advanced/courier/) -* [Middleware](/advanced/middleware/) -* [Message Data](/usage/message-data) -* [Monitoring](/advanced/monitoring/diagnostic-source) -* [Connect Endpoint](/advanced/connect-endpoint/) -* [Observers](/advanced/observers/) -* [Topology](/advanced/topology/) diff --git a/docs/usage/audit/azuretable.md b/docs/usage/audit/azuretable.md deleted file mode 100644 index 72c1aa361ee..00000000000 --- a/docs/usage/audit/azuretable.md +++ /dev/null @@ -1,47 +0,0 @@ -# Azure Table Audit - -Table audit supports both Cosmos DB Table API & Azure Storage account tables. - -There are support for several different configuraiton options depending on your needs. - -## Storage account configuration - -### Supply storage account - - -> Uses [MassTransit.Azure.Cosmos.Table](https://nuget.org/packages/MassTransit.Azure.Cosmos.Table/)
-> Uses [MassTransit.Extensions.DependencyInjection](https://nuget.org/packages/MassTransit.Extensions.DependencyInjection/) - -<<< @/docs/code/audit/AuditAzureTableWithStorageAccount.cs - -### Supply your own table - -> Uses [MassTransit.Azure.Cosmos.Table](https://nuget.org/packages/MassTransit.Azure.Cosmos.Table/)
-> Uses [MassTransit.Extensions.DependencyInjection](https://nuget.org/packages/MassTransit.Extensions.DependencyInjection/) - - -<<< @/docs/code/audit/AuditAzureTableWithTableSupplied.cs - -## Partition Key Strategy - -When using Azure Tables it is important to use the relevant partition key strategy to the likely queries you'll perform on the message data. The default partition key supplied is using the message context type (e.g "SEND" & "CONSUME"). However if you would like to override this, you can supply your own strategy by specifying this on configuration. - -::: tip NOTE -Note: Please consider the official documentation for guidance on partition key strategy [here](https://docs.microsoft.com/en-us/rest/api/storageservices/designing-a-scalable-partitioning-strategy-for-azure-table-storage) -::: - -> Uses [MassTransit.Azure.Cosmos.Table](https://nuget.org/packages/MassTransit.Azure.Cosmos.Table/)
-> Uses [MassTransit.Extensions.DependencyInjection](https://nuget.org/packages/MassTransit.Extensions.DependencyInjection/) - - -<<< @/docs/code/audit/AuditAzureTableWithCustomPartitionKey.cs - -## Audit Filter - -You can configure the message filter on auditing as below. - -> Uses [MassTransit.Azure.Cosmos.Table](https://nuget.org/packages/MassTransit.Azure.Cosmos.Table/)
-> Uses [MassTransit.Extensions.DependencyInjection](https://nuget.org/packages/MassTransit.Extensions.DependencyInjection/) - - -<<< @/docs/code/audit/AuditAzureTableWithMessageTypeFilter.cs diff --git a/docs/usage/configuration.md b/docs/usage/configuration.md deleted file mode 100644 index 235fab0605d..00000000000 --- a/docs/usage/configuration.md +++ /dev/null @@ -1,215 +0,0 @@ -# Configuration - -MassTransit can be used in most .NET application types. Commonly used application types are documented below, including the package references used, and show the minimal configuration required. More thorough configuration details can be found throughout the documentation. - -## Configuration - -> Uses [MassTransit](https://nuget.org/packages/MassTransit/), [MassTransit.RabbitMQ](https://nuget.org/packages/MassTransit.RabbitMQ/) - -MassTransit is easily configured in ASP.NET Core or .NET Generic Host applications (using .NET 3.1 or later). The built-in configuration will: - - * Add several interfaces (and their implementations) - * _IBusControl_ (singleton) - * _IBus_ (singleton) - * _IReceiveEndpointConnector_ (singleton) - * _ISendEndpointProvider_ (scoped) - * _IPublishEndpoint_ (scoped) - * Add a hosted service to start and stop the bus (or buses) - * Add health checks for the bus and receive endpoints - * Use `ILoggerFactory` to create log writers - -To configure MassTransit so that it can be used to send/publish messages, the configuration below is recommended as a starting point. - -<<< @/docs/code/configuration/AspNetCorePublisher.cs - -In this example, MassTransit is configured to connect to RabbitMQ (which should be accessible on localhost) and publish messages. The messages can be published from a controller as shown below. - -<<< @/docs/code/configuration/AspNetCorePublisherController.cs - -> The configuration examples all use the `EventContracts.ValueEntered` message type. The message type is only included in the first example's source code. - -## Consumers - -To configure a bus using RabbitMQ and register the consumers, sagas, and activities to be used by the bus, call the `AddMassTransit` extension method. The _UsingRabbitMq_ method can be changed to the appropriate method for the proper transport if RabbitMQ is not being used. - -<<< @/docs/code/containers/MicrosoftContainer.cs - -The `AddConsumer` method is one of several methods used to register consumers, some of which are shown below. - -<<< @/docs/code/containers/MicrosoftContainerAddConsumer.cs - -### Consumer Definition - -A consumer definition is used to configure the receive endpoint and pipeline behavior for the consumer. When scanning assemblies or namespaces for consumers, consumer definitions are also found and added to the container. The _SubmitOrderConsumer_ and matching definition are shown below. - -<<< @/docs/code/containers/ContainerConsumers.cs - -### Endpoint Definition - -To configure the endpoint for a consumer registration, or override the endpoint configuration in the definition, the `Endpoint` method can be added to the consumer registration. This will create an endpoint definition for the consumer, and register it in the container. This method is available on consumer and saga registrations, with separate execute and compensate endpoint methods for activities. - -<<< @/docs/code/containers/MicrosoftContainerAddConsumerEndpoint.cs - -When the endpoint is configured after the _AddConsumer_ method, the configuration then overrides the endpoint configuration in the consumer definition. However, it cannot override the `EndpointName` if it is specified in the constructor. The order of precedence for endpoint naming is explained below. - -1. Specifying `EndpointName = "submit-order-extreme"` in the constructor which cannot be overridden - - ```cs - x.AddConsumer() - - public SubmitOrderConsumerDefinition() - { - EndpointName = "submit-order-extreme"; - } - ``` - -2. Specifying `.Endpoint(x => x.Name = "submit-order-extreme")` in the consumer registration, chained to `AddConsumer` - - ```cs - x.AddConsumer() - .Endpoint(x => x.Name = "submit-order-extreme"); - - public SubmitOrderConsumerDefinition() - { - Endpoint(x => x.Name = "not used"); - } - ``` - -3. Specifying `Endpoint(x => x.Name = "submit-order-extreme")` in the constructor, which creates an endpoint definition - - ```cs - x.AddConsumer() - - public SubmitOrderConsumerDefinition() - { - Endpoint(x => x.Name = "submit-order-extreme"); - } - ``` - -4. Unspecified, the endpoint name formatter is used (in this case, the endpoint name is `SubmitOrder` using the default formatter) - - ```cs - x.AddConsumer() - - public SubmitOrderConsumerDefinition() - { - } - ``` - - -### Saga Registration - -To add a state machine saga, use the _AddSagaStateMachine_ methods. For a consumer saga, use the _AddSaga_ methods. - -::: tip Important -State machine sagas should be added before class-based sagas, and the class-based saga methods should not be used to add state machine sagas. This may be simplified in the future, but for now, be aware of this registration requirement. -::: - -```cs -services.AddMassTransit(r => -{ - // add a state machine saga, with the in-memory repository - r.AddSagaStateMachine() - .InMemoryRepository(); - - // add a consumer saga with the in-memory repository - r.AddSaga() - .InMemoryRepository(); - - // add a saga by type, without a repository. The repository should be registered - // in the container elsewhere - r.AddSaga(typeof(OrderSaga)); - - // add a state machine saga by type, including a saga definition for that saga - r.AddSagaStateMachine(typeof(OrderState), typeof(OrderStateDefinition)) - - // add all saga state machines by type - r.AddSagaStateMachines(Assembly.GetExecutingAssembly()); - - // add all sagas in the specified assembly - r.AddSagas(Assembly.GetExecutingAssembly()); - - // add sagas from the namespace containing the type - r.AddSagasFromNamespaceContaining(); - r.AddSagasFromNamespaceContaining(typeof(OrderSaga)); -}); -``` - -To add a saga registration and configure the consumer endpoint in the same expression, a definition can automatically be created. - -```cs -services.AddMassTransit(r => -{ - r.AddSagaStateMachine() - .NHibernateRepository() - .Endpoint(e => - { - e.Name = "order-state"; - e.ConcurrentMessageLimit = 8; - }); -}); -``` - -Supported saga persistence storage engines are documented in the [saga documentation](/usage/sagas/persistence) section. - -## Receive Endpoints - -In the above examples, the bus is configured by the _UsingRabbitMq_ method, which is passed two arguments. `context` is the registration context, used to configure endpoints. `cfg` is the bus factory configurator, used to configure the bus. The above examples use the default conventions to configure the endpoints. Alternatively, endpoints can be explicitly configured. However, when configuring endpoints manually, the _ConfigureEndpoints_ methods may be excluded otherwise it should appear **after** any manually configured receive endpoints. - -_ConfigureEndpoints_ uses an `IEndpointNameFormatter` to generate endpoint names, which by default uses a _PascalCase_ formatter. There are two additional endpoint name formatters included, snake and kebab case. - -For the _SubmitOrderConsumer_, the endpoint names would be: - -| Formatter | Name -|:---|:--- -| Default | `SubmitOrder` -| Snake Case | `submit_order` -| Kebab Case | `submit-order` - -All of the included formatters trim the _Consumer_, _Saga_, or _Activity_ suffix from the end of the class name. If the consumer name is generic, the generic parameter type is used instead of the generic type. - -::: tip Video -Learn about the default conventions as well as how to tailor the naming style to meet your requirements in [this short video](https://youtu.be/bsUlQ93j2MY). -::: - -The endpoint name formatter can be set as shown below. - -<<< @/docs/code/containers/MicrosoftContainerFormatter.cs - -The endpoint formatter can also be passed to the _ConfigureEndpoints_ method as shown. - -<<< @/docs/code/containers/MicrosoftContainerFormatterInline.cs - -To explicitly configure endpoints, use the _ConfigureConsumer_ and/or _ConfigureConsumers_ methods. - -<<< @/docs/code/containers/MicrosoftContainerConfigureConsumer.cs - -When using _ConfigureConsumer_, the _EndpointName_, _PrefetchCount_, and _Temporary_ properties of the consumer definition are not used. - -MassTransit includes an endpoint name formatter (_IEndpointNameFormatter_) which can be used to automatically format endpoint names based upon the consumer, saga, or activity name. Using the _ConfigureEndpoints_ method will automatically create a receive endpoint for every added consumer, saga, and activity. To automatically configure the receive endpoints, use the updated configuration shown below. - -The example sets the kebab-case endpoint name formatter, which will create a receive endpoint named `value-entered-event` for the `ValueEnteredEventConsumer`. The _Consumer_ suffix is removed by default. The endpoint is named based upon the consumer name, not the message type, since a consumer may consume multiple message types yet still be configured on a single receive endpoint. - -<<< @/docs/code/configuration/AspNetCoreEndpointListener.cs - -An ASP.NET Core application can also configure receive endpoints. The consumer, along with the receive endpoint, is configured within the _AddMassTransit_ configuration. Separate registration of the consumer is not required (and discouraged), however, any consumer dependencies should be added to the container separately. Consumers are registered as scoped, and dependencies should be registered as scoped when possible, unless they are singletons. - -<<< @/docs/code/configuration/AspNetCoreListener.cs - -## Health Checks - -The _AddMassTransit_ method adds bus health checks to the service collection. To configure health checks, map the ready and live endpoints in your ASP.NET application. - -<<< @/docs/code/configuration/AspNetCorePublisherHealthCheck.cs - -## Console Applications - -> Uses [MassTransit.RabbitMQ](https://nuget.org/packages/MassTransit.RabbitMQ/) - -For environments where the generic host is unavailable, MassTransit can be configured without a container. In this example, MassTransit is configured in a simple console application to connect to RabbitMQ (which should be accessible on _localhost_) and publish messages. As each value is entered, the value is published as a `ValueEntered` message. No consumers are configured in this example. - -<<< @/docs/code/configuration/ConsoleAppPublisher.cs - -Another console application can be created to consume the published events. In this application, the receive endpoint is configured with a consumer that consumes the `ValueEntered` event. The message contract from the example above, in the same namespace, should be copied to this program as well (it isn't shown below). - -<<< @/docs/code/configuration/ConsoleAppListener.cs diff --git a/docs/usage/consumers.md b/docs/usage/consumers.md deleted file mode 100644 index c662cab1839..00000000000 --- a/docs/usage/consumers.md +++ /dev/null @@ -1,114 +0,0 @@ -# Consumers - -Consumer is a widely used noun for something that _consumes_ something. In MassTransit, a consumer consumes one or more message types when configured on or connected to a receive endpoint. MassTransit includes many consumer types, including consumers, [sagas](/usage/sagas/), saga state machines, [routing slip activities](/advanced/courier/), handlers, and [job consumers](/advanced/job-consumers). - -A consumer, which is the most common consumer type, is a class that consumes one or more messages types. For each message type, the `IConsumer` interface is implemented where `T` is the consumed message type. The interface has one method, _Consume_, as shown below. - -```cs -public interface IConsumer : - IConsumer - where TMessage : class -{ - Task Consume(ConsumeContext context); -} -``` - -> Messages must be reference types, and interfaces are supported (and recommended). The [messages](/usage/messages) section has more details on message types. - -An example class that consumes the _SubmitOrder_ message type is shown below. - -<<< @/docs/code/usage/UsageConsumer.cs - -MassTransit embraces _The Hollywood Principle_, which states, "Don't call us, we'll call you." It is influenced by the Dependency Inversion Principle in that control flows from the framework into the developer's code in response to an event, which in this case involves the delivery of a message by the transport. When a message is delivered from the transport on a receive endpoint and the message type is consumed by the consumer, MassTransit creates a consumer instance (using a consumer factory), and executes the _Consume_ method passing a `ConsumeContext` containing the message. - -The _Consume_ method returns a _Task_ that is awaited by MassTransit. While the consumer method is executing, the message is unavailable to other receive endpoints. If the _Task_ completes successfully, the message is acknowledged and removed from the queue. - -If the _Task_ faults in the event of an exception, or is canceled (explicitly, or via an _OperationCanceledException_), the consumer instance is released and the exception is propagated back up the pipeline. If the exception does not trigger a retry, the default pipeline will move the message to an error queue. - -## Consumer - -To receive messages, a consumer must be configured on a receive endpoint and receive endpoints are configured with the bus. A configuration example with a single receive endpoint containing the _SubmitOrderConsumer_ above is shown below. - -<<< @/docs/code/usage/UsageConsumerBus.cs - -The consumer is configured on a receive endpoint, which will receive messages from the `order-service` queue, using the `.Consumer()` method. Since the consumer has a default constructor (no constructor implies a default constructor), it can be configured without specifying a consumer factory. - -::: tip Under the Hood -When a consumer is configured on a receive endpoint, the consumer message types (one for each `IConsumer`) are used to configure the receive endpoint's _consume topology_. The consume topology is then used to configure the broker so that published messages are delivered to the queue. The broker topology varies by transport. For example, the RabbitMQ example above would result in the creation of an exchange for the _SubmitOrder_ message type and a binding from the exchange to an exchange with the same name as the queue (the latter exchange then being bound directly to the queue). - -If the queue is persistent (_AutoDelete_ is false, which is the default), the topology remains in place even after the bus has stopped. When the bus is recreated and started, the broker entities are reconfigured to ensure they are properly configured. Any messages waiting in the queue will continue to be delivered to the receive endpoint once the bus is started. -::: - -### Skipped Messages - -When a consumer is removed (or disconnected) from a receive endpoint, a message type is removed from a consumer, or if a message is mistakenly sent to a receive endpoint, messages may be delivered to the receive endpoint that do not have a consumer. - -If this occurs, the unconsumed message is moved to a *_skipped* queue (prefixed by the original queue name). The original message content is retained and additional headers are added to identify the host that skipped the message. - -::: warning Manual Intervention -It may be necessary to use the broker management tools to remove an exchange binding or topic subscription that is no longer used by the receive endpoint to prevent further skipped messages. -::: - -### Consumer Factories - -::: tip -When using a container, these methods should _NOT_ be used. -::: - -In the example shown above, the consumer had a default constructor so the default constructor consumer factory was used. There are several other consumer factories included, some of which are shown below. If you are using MassTransit with a container, MassTransit includes support for several containers and integrates with them to provide container scope for consumers. Refer to the [containers](/usage/containers) section for details. - -<<< @/docs/code/usage/UsageConsumerOverloads.cs - -To reiterate, the consumer factory is called for each message to create a consumer instance. Once the _Consume_ method completes, the consumer is dereferenced. - -::: tip IDispose / IAsyncDisposable -If using the default or delegate consumer factory and the consumer supports either `IAsyncDisposable` or `IDisposable`, the appropriate dispose method will be called. - -When using a container, it is responsible for consumer disposal when the scope is disposed. -::: - -### Temporary Consumers - -Some consumers only need to receive messages while connected, and any messages published while disconnected should be discarded. This is achieved by using a TemporaryEndpointDefinition as the endpoint definition. - -<<< @/docs/code/usage/UsageConsumerTemporaryEndpoint.cs - -### Connect Consumers - -Once a bus has been configured, the receive endpoints have been created and cannot be modified. However, the bus creates a temporary, auto-delete endpoint for itself. Consumers can be connected to the bus endpoint using any of the `Connect` methods. The bus endpoint is designed to receive responses (via the request client, see the [requests](/usage/requests) section) and **messages sent directly to the bus endpoint**. - -::: warning Consume Topology -The bus endpoint does not use its consume topology to configure the broker, and message type exchanges are not created, bound, or otherwise subscribed to the bus endpoint queue. _Published_ messages will not be delivered to the bus endpoint queue and subsequently will not be delivered to consumers connected to the bus endpoint. - -This makes the bus endpoint very fast short-lived consumers, such as the request client. -::: - -The following example connects a consumer to the bus endpoint, and then sets the _ResponseAddress_ header on the _SubmitOrder_ message to the bus endpoint address. - -<<< @/docs/code/usage/UsageConsumerConnect.cs - -There are connect methods for the other consumer types as well, including handlers, instances, sagas, and saga state machines. - -## Instance - -Creating a new consumer instance for each message is highly suggested. However, it is possible to configure an existing consumer instance to which every received message will be delivered (if the message type is consumed by the consumer). - -::: tip Thread Safety -An instance consumer **must** be thread-safe since the _Consume_ method may be called from multiple threads simultaneously. -::: - -An example instance configuration is shown below. - -<<< @/docs/code/usage/UsageInstance.cs - -## Handler - -While creating a consumer is the preferred way to consume messages, it is also possible to create a simple message handler. By specifying a method, anonymous method, or lambda method, a message can be consumed on a receive endpoint. - -> This is great for unit testing, and other simple scenarios. Beyond that, use a consumer. - -A simple handler example is shown below. - -<<< @/docs/code/usage/UsageHandler.cs - -The asynchronous handler method is called for each message delivered to the receive endpoint. Since there is no consumer to create, no scope is created if using a container, and nothing is disposed. diff --git a/docs/usage/containers/README.md b/docs/usage/containers/README.md deleted file mode 100644 index 4e3c09d1e50..00000000000 --- a/docs/usage/containers/README.md +++ /dev/null @@ -1,169 +0,0 @@ -# Containers - -MassTransit supports several dependency injection containers. And since Microsoft introduced its own container, it has become the most commonly used container. - -::: tip Optional -MassTransit does not require a container, as demonstrated in the [configuration example](/usage/configuration). So if you aren't already using a container, you can get started without having adopt one. However, when you're ready to use a container, perhaps to deploy your service using the .NET Generic Host, you will likely want to use Microsoft's built-in solution. -::: - -Regardless of which container is used, supported containers have a consistent registration syntax used to add consumers, sagas, and activities, as well as configure the bus. Behind the scenes, MassTransit is configuring the container, including container-specific features such as scoped lifecycles, consistently and correctly. Use of the registration syntax has drastically reduced container configuration support questions. - -## Consumer Registration - -> Uses [MassTransit.Extensions.DependencyInjection](https://www.nuget.org/packages/MassTransit.Extensions.DependencyInjection/) - -To configure a bus using RabbitMQ and register the consumers, sagas, and activities to be used by the bus, call the `AddMassTransit` extension method. The _UsingRabbitMq_ method can be changed to the appropriate method for the proper transport if RabbitMQ is not being used. - -<<< @/docs/code/containers/MicrosoftContainer.cs - -The `AddConsumer` method is one of several methods used to register consumers, some of which are shown below. - -<<< @/docs/code/containers/MicrosoftContainerAddConsumer.cs - -## Consumer Definition - -A consumer definition is used to configure the receive endpoint and pipeline behavior for the consumer. When scanning assemblies or namespaces for consumers, consumer definitions are also found and added to the container. The _SubmitOrderConsumer_ and matching definition are shown below. - -<<< @/docs/code/containers/ContainerConsumers.cs - -## Endpoint Definition - -To configure the endpoint for a consumer registration, or override the endpoint configuration in the definition, the `Endpoint` method can be added to the consumer registration. This will create an endpoint definition for the consumer, and register it in the container. This method is available on consumer and saga registrations, with separate execute and compensate endpoint methods for activities. - -<<< @/docs/code/containers/MicrosoftContainerAddConsumerEndpoint.cs - -When the endpoint is configured after the _AddConsumer_ method, the configuration then overrides the endpoint configuration in the consumer definition. However, it cannot override the `EndpointName` if it is specified in the constructor. The order of precedence for endpoint naming is explained below. - -1. Specifying `EndpointName = "submit-order-extreme"` in the constructor which cannot be overridden - - ```cs - x.AddConsumer() - - public SubmitOrderConsumerDefinition() - { - EndpointName = "submit-order-extreme"; - } - ``` - -2. Specifying `.Endpoint(x => x.Name = "submit-order-extreme")` in the consumer registration, chained to `AddConsumer` - - ```cs - x.AddConsumer() - .Endpoint(x => x.Name = "submit-order-extreme"); - - public SubmitOrderConsumerDefinition() - { - Endpoint(x => x.Name = "not used"); - } - ``` - -3. Specifying `Endpoint(x => x.Name = "submit-order-extreme")` in the constructor, which creates an endpoint definition - - ```cs - x.AddConsumer() - - public SubmitOrderConsumerDefinition() - { - Endpoint(x => x.Name = "submit-order-extreme"); - } - ``` - -4. Unspecified, the endpoint name formatter is used (in this case, the endpoint name is `SubmitOrder` using the default formatter) - - ```cs - x.AddConsumer() - - public SubmitOrderConsumerDefinition() - { - } - ``` - - -## Bus Configuration - -In the above examples, the bus is configured by the _UsingRabbitMq_ method, which is passed two arguments. `context` is the registration context, used to configure endpoints. `cfg` is the bus factory configurator, used to configure the bus. The above examples use the default conventions to configure the endpoints. Alternatively, endpoints can be explicitly configured. However, when configuring endpoints manually, the _ConfigureEndpoints_ methods should not be used (duplicate endpoints may result). - -_ConfigureEndpoints_ uses an `IEndpointNameFormatter` to generate endpoint names, which by default uses a _PascalCase_ formatter. There are two additional endpoint name formatters included, snake and kebab case. - -For the _SubmitOrderConsumer_, the endpoint names would be: - -| Formatter | Name -|:---|:--- -| Default | `SubmitOrder` -| Snake Case | `submit_order` -| Kebab Case | `submit-order` - -All of the included formatters trim the _Consumer_, _Saga_, or _Activity_ suffix from the end of the class name. If the consumer name is generic, the generic parameter type is used instead of the generic type. - -::: tip Video -Learn about the default conventions as well as how to tailor the naming style to meet your requirements in [this short video](https://youtu.be/bsUlQ93j2MY). -::: - -The endpoint name formatter can be set as shown below. - -<<< @/docs/code/containers/MicrosoftContainerFormatter.cs - -The endpoint formatter can also be passed to the _ConfigureEndpoints_ method as shown. - -<<< @/docs/code/containers/MicrosoftContainerFormatterInline.cs - -To explicitly configure endpoints, use the _ConfigureConsumer_ and/or _ConfigureConsumers_ methods. - -<<< @/docs/code/containers/MicrosoftContainerConfigureConsumer.cs - -When using _ConfigureConsumer_, the _EndpointName_, _PrefetchCount_, and _Temporary_ properties of the consumer definition are not used. - -## Saga Registration - -To add a state machine saga, use the _AddSagaStateMachine_ methods. For a consumer saga, use the _AddSaga_ methods. - -::: tip Important -State machine sagas should be added before class-based sagas, and the class-based saga methods should not be used to add state machine sagas. This may be simplified in the future, but for now, be aware of this registration requirement. -::: - -```cs -containerBuilder.AddMassTransit(r => -{ - // add a state machine saga, with the in-memory repository - r.AddSagaStateMachine() - .InMemoryRepository(); - - // add a consumer saga with the in-memory repository - r.AddSaga() - .InMemoryRepository(); - - // add a saga by type, without a repository. The repository should be registered - // in the container elsewhere - r.AddSaga(typeof(OrderSaga)); - - // add a state machine saga by type, including a saga definition for that saga - r.AddSagaStateMachine(typeof(OrderState), typeof(OrderStateDefinition)) - - // add all saga state machines by type - r.AddSagaStateMachines(Assembly.GetExecutingAssembly()); - - // add all sagas in the specified assembly - r.AddSagas(Assembly.GetExecutingAssembly()); - - // add sagas from the namespace containing the type - r.AddSagasFromNamespaceContaining(); - r.AddSagasFromNamespaceContaining(typeof(OrderSaga)); -}); -``` - -To add a saga registration and configure the consumer endpoint in the same expression, a definition can automatically be created. - -```cs -containerBuilder.AddMassTransit(r => -{ - r.AddSagaStateMachine() - .NHibernateRepository() - .Endpoint(e => - { - e.Name = "order-state"; - e.ConcurrentMessageLimit = 8; - }); -}); -``` - -Supported saga persistence storage engines are documented in the [saga documentation](/usage/sagas/persistence) section. diff --git a/docs/usage/containers/autofac.md b/docs/usage/containers/autofac.md deleted file mode 100644 index 4ef6bdf2bb2..00000000000 --- a/docs/usage/containers/autofac.md +++ /dev/null @@ -1,5 +0,0 @@ -# Autofac - -::: danger V8 -As of MassTransit V8, third-party containers are only supported through their specific integration packages to the standard `IServiceCollection` and `IServiceProvider` interfaces. -::: diff --git a/docs/usage/containers/castlewindsor.md b/docs/usage/containers/castlewindsor.md deleted file mode 100644 index 89fb52d9cea..00000000000 --- a/docs/usage/containers/castlewindsor.md +++ /dev/null @@ -1,5 +0,0 @@ -# Castle Windsor - -::: danger V8 -As of MassTransit V8, third-party containers are only supported through their specific integration packages to the standard `IServiceCollection` and `IServiceProvider` interfaces. -::: diff --git a/docs/usage/containers/definitions.md b/docs/usage/containers/definitions.md deleted file mode 100644 index 27de05fbd17..00000000000 --- a/docs/usage/containers/definitions.md +++ /dev/null @@ -1,53 +0,0 @@ -# Definitions - -Definitions are used to specify the behavior of consumers, sagas, and activities so that they can be automatically configured. Definitions are used by MassTransit's common container registration, and definitions can be explicitely registered or discovered automatically using the `AddConsumers...`, `AddSagaStateMachines...`, etc. methods. - -### Consumer - -```cs -public class SubmitOrderConsumerDefinition : - ConsumerDefinition -{ - public SubmitOrderConsumerDefinition() - { - // override the default endpoint name, for whatever reason - EndpointName = "ha-submit-order"; - - // limit the number of messages consumed concurrently - // this applies to the consumer only, not the endpoint - ConcurrentMessageLimit = 4; - } - - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) - { - endpointConfigurator.UseMessageRetry(r => r.Interval(5, 1000)); - endpointConfigurator.UseInMemoryOutbox(); - } -} -``` - -### Saga - -```cs -public class OrderStateDefinition : - SagaDefinition -{ - public OrderStateDefinition() - { - // specify the message limit at the endpoint level, which influences - // the endpoint prefetch count, if supported - Endpoint(e => e.ConcurrentMessageLimit = 16); - } - - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) - { - var partition = endpointConfigurator.CreatePartitioner(16); - - sagaConfigurator.Message(x => x.UsePartitioner(partition, m => m.Message.CorrelationId)); - sagaConfigurator.Message(x => x.UsePartitioner(partition, m => m.Message.CorrelationId)); - sagaConfigurator.Message(x => x.UsePartitioner(partition, m => m.Message.CorrelationId)); - } -} -``` - diff --git a/docs/usage/containers/msdi.md b/docs/usage/containers/msdi.md deleted file mode 100644 index 999af17ed52..00000000000 --- a/docs/usage/containers/msdi.md +++ /dev/null @@ -1,14 +0,0 @@ -# Microsoft Dependency Injection - -MassTransit fully supports the Microsoft Dependency Injection framework used by ASP.NET Core. In fact, most of the container-based examples in the documentation use the Microsoft container as it is the most popular. - -The [container section](/usage/containers/) includes examples and usage details, refer to that section for more details. - -> Uses [MassTransit.RabbitMQ](https://www.nuget.org/packages/MassTransit.RabbitMQ/) - -<<< @/docs/code/containers/MicrosoftContainer.cs - -### ASP.NET Core - -Using MassTransit with ASP.NET Core is details in the [configuration](/usage/configuration.md#asp-net-core) section. - diff --git a/docs/usage/containers/multibus.md b/docs/usage/containers/multibus.md deleted file mode 100644 index 5c91456209c..00000000000 --- a/docs/usage/containers/multibus.md +++ /dev/null @@ -1,78 +0,0 @@ -# MultiBus - -_pronounced mool-tee-buss_ - -MassTransit is designed so that most applications only need a single bus, and that is the recommended approach. Using a single bus, with however many receive endpoints are needed, minimizes complexity and ensures efficient broker resource utilization. Consistent with this guidance, container configuration using the `AddMassTransit` method registers the appropriate types so that they are available to other components, as well as consumers, sagas, and activities. - -However, with broader use of cloud-based platforms comes a greater variety of messaging transports, not to mention HTTP as a transfer protocol. As application sophistication increases, connecting to multiple message transports and/or brokers is becoming more common. Therefore, rather than force developers to create their own solutions, MassTransit has the ability to configure additional bus instances within specific dependency injection containers. - -> And by specific, right now it is very specific: Microsoft.Extensions.DependencyInjection. Though technically, any container that supports `IServiceCollection` for configuration _might_ work. - -### Standard Configuration - -To review, the configuration for a single bus is shown below. - -<<< @/docs/code/containers/MultiBusContainer.cs - -This configures the container so that there is a bus, using RabbitMQ, with a single consumer _SubmitOrderConsumer_, using automatic endpoint configuration. The MassTransit hosted service, which configures the bus health checks and starts/stop the bus via `IHostedService`, is also added to the container. - -There are several interfaces added to the container using this configuration: - -| Interface | Lifestyle | Notes -|:-----|:-----|:----- -| `IBusControl` | Singleton | Used to start/stop the bus -| `IBus` | Singleton | Publish/Send on this bus, starting a new conversation -| `ISendEndpointProvider` | Scoped | Send messages from consumer dependencies, ASP.NET Controllers -| `IPublishEndpoint` | Scoped | Publish messages from consumer dependencies, ASP.NET Controllers -| `IClientFactory` | Singleton | Used to create request clients (singleton, or within scoped consumers) -| `IRequestClient` | Scoped | Used to send requests -| `ConsumeContext` | Scoped | Available in any message scope, such as a consumer, saga, or activity - -When a consumer, a saga, or an activity is consuming a message the _ConsumeContext_ is available in the container scope. When the consumer is created using the container, the consumer and any dependencies are created within that scope. If a dependency includes _ISendEndpontProvider_, _IPublishEndpoint_, or even _ConsumeContext_ (should not be the first choice, but totally okay) on the constructor, all three of those interfaces result in the same reference – which is great because it ensures that messages sent and/or published by the consumer or its dependencies includes the proper correlation identifiers and monitoring activity headers. - -### MultiBus Configuration - -To support multiple bus instances in a single container, the interface behaviors described above had to be considered carefully. There are expectations as to how these interfaces behave, and it was important to ensure consistent behavior whether an application has one, two, or a dozen bus instances (please, not a dozen – think of the children). A way to differentiate between different bus instances ensuring that sent or published messages end up on the right queues or topics is needed. The ability to configure each bus instance separately, yet leverage the power of a single shared container is also a must. - -To configure additional bus instances, create a new interface that includes _IBus_. Then, using that interface, configure the additional bus using the `AddMassTransit` method, which is included in the **_MassTransit.MultiBus_** namespace. - -<<< @/docs/code/containers/MultiBusTwoContainer.cs{31-42} - -This configures the container so that there is an additional bus, using RabbitMQ, with a single consumer _AllocateInventoryConsumer_, using automatic endpoint configuration. Only a single hosted service is required that will start all bus instances so there is no need to add it twice. - -Notable differences in the new method: - -- The generic argument, _ISecondBus_, is the type that will be added to the container instead of _IBus_. This ensures that access to the additional bus is directly available without confusion. - -The registered interfaces are slightly different for additional bus instances. - -| Interface | Lifestyle | Notes -|:-----|:-----|:----- -| `IBusControl` | N/A | Not registered, but automatically started/stopped by the hosted service -| `IBus` | N/A | Not registered, the new bus interface is registered instead -| `ISecondBus` | Singleton | Publish/Send on this bus, starting a new conversation -| `ISendEndpointProvider` | Scoped | Send messages from consumer dependencies only -| `IPublishEndpoint` | Scoped | Publish messages from consumer dependencies only -| `IClientFactory` | N/A | Registered as an instance-specific client factory -| `IRequestClient` | Scoped | Created using the specific bus instance -| `ConsumeContext` | Scoped | Available in any message scope, such as a consumer, saga, or activity - -For consumers or dependencies that need to send or publish messages to a different bus instance, a dependency on that specific bus interface (such as _IBus_, or _ISecondBus_) would be added. - -::: warning -Some things do not work across bus instances. As stated above, calling Send or Publish on an IBus (or other bus instance interface) starts a new conversation. Middleware components such as the _InMemoryOutbox_ currently do not buffer messages across bus instances. -::: - -#### Bus Interface Types - -In the example above, which should be the most common of this hopefully uncommon use, the _ISecondBus_ interface is all that is required. MassTransit creates a dynamic class to delegate the `IBus` methods to the bus instance. However, it is possible to specify a class that implements _ISecondBus_ instead. - -To specify a class, as well as take advantage of the container to bring additional properties along with it, take a look at the following types and configuration. - -<<< @/docs/code/containers/MultiBusThreeContainer.cs - -This would add a third bus instance, the same as the second, but using the instance class specified. The class is resolved from the container and given `IBusControl`, which must be passed to the base class ensuring that it is properly configured. - - - - diff --git a/docs/usage/containers/simpleinjector.md b/docs/usage/containers/simpleinjector.md deleted file mode 100644 index a90e1417edf..00000000000 --- a/docs/usage/containers/simpleinjector.md +++ /dev/null @@ -1,5 +0,0 @@ -# Simple Injector - -::: danger V8 -As of MassTransit V8, third-party containers are only supported through their specific integration packages to the standard `IServiceCollection` and `IServiceProvider` interfaces. -::: diff --git a/docs/usage/containers/structuremap.md b/docs/usage/containers/structuremap.md deleted file mode 100644 index b392c8bf912..00000000000 --- a/docs/usage/containers/structuremap.md +++ /dev/null @@ -1,5 +0,0 @@ -# StructureMap - -::: danger V8 -As of MassTransit V8, third-party containers are only supported through their specific integration packages to the standard `IServiceCollection` and `IServiceProvider` interfaces. -::: diff --git a/docs/usage/correlation.md b/docs/usage/correlation.md deleted file mode 100644 index 2fac1ab93b8..00000000000 --- a/docs/usage/correlation.md +++ /dev/null @@ -1,47 +0,0 @@ -# Correlating messages - -In a distributed message-based system, message correlation is very important. Since operations are potentially executing across hundreds of nodes, the ability to correlate different messages to build a path through the system is absolutely necessary for engineers to troubleshoot problems. - -The headers on the message envelope provided by MassTransit already make it easy to specify correlation values. In fact, most are setup by default if not specified by the developer. - -MassTransit provides the interface `CorrelatedBy`, which can be used to setup a default correlationId. This is used by sagas as well, since all sagas have a unique `CorrelationId` for each instance of the saga. If a message implements `CorrelatedBy`, it will automatically be directed to the saga instance with the matching identifier. If a new saga instance is created by the event, it will be assigned the `CorrelationId` from the initiating message. - -For message types that have a correlation identifier, but are not using the `CorrelatedBy` interface, it is possible to declare the identifier for the message type and MassTransit will use that identifier by default for correlation. - -```csharp -MessageCorrelation.UseCorrelationId(x => x.SomeGuidValue); -``` - -::: warning -This should be called before you start the bus. We currently recommend that you put all of these in a static method for easy grouping and then call it at the beginning of the MassTransit configuration block. -::: - -Most transactions in a system will end up being logged and wide scale correlation is likely. Therefore, the use of consistent correlation identifiers is recommended. In fact, using a `Guid` type is highly recommended. MassTransit uses the [NewId](https://www.nuget.org/packages/NewId) library to generate identifiers that are unique and sequential that are represented as a `Guid`. The identifiers are clustered-index friendly, being ordered in a way that SQL Server can efficiently insert them into a database with the *uniqueidentifier* as the primary key. Just use `NewId.NextGuid()` to generate an identifier -- it's fast, fun, and all your friends are doing it. - -::: tip -So, what does correlated actually mean? In short it means that this message is a part of a larger conversation. For instance, you may have a message that says New Order (Item:Hammers; Qty:22; OrderNumber:45) and there may be another message that is a response to that message that says Order Allocated(OrderNumber:45). In this case, the order number is acting as your correlation identifier, it ties the messages together. -::: - -### Correlation by convention - -In addition to the explicit `CorrelatedBy` interface, a convention-based correlation is supported. If the message contract has a property named ``CorrelationId``, ``CommandId``, or ``EventId``, the correlationId header is automatically populated on Send or Publish. It can also be manually specified using the ``SendContext``. - -Bear in mind that sagas default `CorrelateById()` only support messages where the explicit `CorrelatedBy` interface is implemented. However, the header is still useful if you do not use sagas, for example for message flow analysis and debugging. - -## Tracing conversations - -There are several other built-in message headers that can be used to correlate messages. However, it is also completely acceptable to add your own custom properties to the message contract for correlation. - -In addition to the correlationId, the default headers include: - -#### RequestId - When using the `RequestClient`, or the request/response message handling of MassTransit, each request is assigned a unique `RequestId`. When the message is received by a consumer, the response message sent by the `Respond` method (on the `ConsumeContext`) is assigned the same `RequestId` so that it can be correlated by the request client. This header should not typically be set by the consumer, as it is handled automatically. - -#### ConversationId - The conversation is created by the first message that is sent or published, in which no existing context is available (such as when a message is sent or published by using `IBus.Send` or `IBus.Publish`). If an existing context is used to send or publish a message, the `ConversationId` is copied to the new message, ensuring that a set of messages within the same *conversation* have the same identifier. - -#### InitiatorId - When a message is created within the context of an existing message, such as in a consumer, a saga, etc., the `CorrelationId` of the message (if available, otherwise the `MessageId` may be used) is copied to the `InitiatorId` header. This makes it possible to combine a chain of messages into a graph of producers and consumers. - -#### MessageId - When a message is sent or published, this header is automatically generated for the message. diff --git a/docs/usage/exceptions.md b/docs/usage/exceptions.md deleted file mode 100644 index 56b09cecf1c..00000000000 --- a/docs/usage/exceptions.md +++ /dev/null @@ -1,351 +0,0 @@ -# Exceptions - -Let's face it, bad things happen. Networks partition, servers crash, remote endpoints become non-responsive. And when bad things happen, exceptions get thrown. And when exceptions get thrown, people die. Okay, maybe that's a bit dramatic, but the point is, exceptions are a fact of software development. - -Fortunately, MassTransit provides a number of features to help your application recover from and deal with exceptions. But before getting into that, an understanding of what happens when a message is consumed is needed. - -Take, for example, a consumer that simply throws an exception. - -```cs -public class SubmitOrderConsumer : - IConsumer -{ - public Task Consume(ConsumeContext context) - { - throw new Exception("Very bad things happened"); - } -} -``` - -When a message is delivered to the consumer, the consumer throws an exception. With a default bus configuration, the exception is caught by middleware in the transport (the `ErrorTransportFilter` to be exact), and the message is moved to an *_error* queue (prefixed by the receive endpoint queue name). The exception details are stored as headers with the message for analysis and to assist in troubleshooting the exception. - -::: tip Video -Learn about the error queue in [this short video](https://youtu.be/3TMKUu7c4lc). -::: - -> In addition to moving the message to an error queue, MassTransit also produces a `Fault` event. See below for more details on _faults_. - -## Retry - -Some exceptions may be caused by a transient condition, such as a database deadlock, a busy web service, or some similar type of situation which usually clears up on a second attempt. With these exception types, it is often desirable to retry the message delivery to the consumer, allowing the consumer to try the operation again. - -```cs -public class SubmitOrderConsumer : - IConsumer -{ - ISessionFactory _sessionFactory; - - public async Task Consume(ConsumeContext context) - { - using(var session = _sessionFactory.OpenSession()) - using(var transaction = session.BeginTransaction()) - { - var customer = session.Get(context.Message.CustomerId); - - // continue with order processing - - transaction.Commit(); - } - } -} -``` - -With this consumer, an `ADOException` can be thrown, say there is a deadlock or the SQL server is unavailable. In this case, the operation should be retried before moving the message to the error queue. This can be configured on the receive endpoint or the consumer. Shown below is a retry policy which attempts to deliver the message to a consumer five times before throwing the exception back up the pipeline. - -```cs -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context,cfg) => - { - cfg.UseMessageRetry(r => r.Immediate(5)); - - cfg.ConfigureEndpoints(context); - }); -}); -``` - -The `UseMessageRetry` method is an extension method that configures a middleware filter, in this case the `RetryFilter`. There are a variety of retry policies available, which are detailed in the [section below](#retry-configuration). - -::: tip -In this example, the UseMessageRetry is at the bus level, and will be configured on every receive endpoint. Additional retry filters can be added at the bus and consumer level, providing flexibility in how different consumers, messages, etc. are retried. -::: - -To configure retry on a manually configured receive endpoint: - -```cs -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ReceiveEndpoint("submit-order", e => - { - e.UseMessageRetry(r => r.Immediate(5)); - - e.ConfigureConsumer(context); - }); - }); -}); -``` - -MassTransit retry filters execute in memory and maintain a _lock_ on the message. As such, they should only be used to handle short, transient error conditions. Setting a retry interval of an hour would fall into the category of _bad things_. To retry messages after longer waits, look at the next section on redelivering messages. - -## Retry Configuration - -::: tip Video -Learn how to configure message retry in [this short video](https://youtu.be/pKxf6Ii-3ow). -::: - -When configuring message retry, there are several retry policies available, including: - -| Policy | Description | -| :-- | :-- | -| None | No retry -| Immediate | Retry immediately, up to the retry limit -| Interval | Retry after a fixed delay, up to the retry limit -| Intervals | Retry after a delay, for each interval specified -| Exponential | Retry after an exponentially increasing delay, up to the retry limit -| Incremental | Retry after a steadily increasing delay, up to the retry limit - -Each policy has configuration settings which specifies the expected behavior. - -### Exception Filters - -Sometimes you do not want to always retry, but instead only retry when some specific exception is thrown and fault for all other exceptions. To implement this, you can use an exception filter. Specify exception types using either the `Handle` or `Ignore` method. A filter can have either _Handle_ or _Ignore_ statements, combining them has unpredictable effects. - -Both methods have two signatures: - -1. Generic version `Handle` and `Ignore` where `T` must be derivate of `System.Exception`. With no argument, all exceptions of specified type will be either handled or ignored. You can also specify a function argument that will filter exceptions further based on other parameters. - -1. Non-generic version that needs one or more exception types as parameters. No further filtering is possible if this version is used. - -You can use multiple calls to these methods to specify filters for multiple exception types: - -```cs -e.UseMessageRetry(r => -{ - r.Handle(); - r.Ignore(typeof(InvalidOperationException), typeof(InvalidCastException)); - r.Ignore(t => t.ParamName == "orderTotal"); -}); -``` - -You can also specify multiple retry policies for a single endpoint: - -```cs -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ReceiveEndpoint("submit-order", e => - { - e.UseMessageRetry(r => - { - r.Immediate(5); - r.Handle(x => x.Message.Contains("SQL")); - }); - - e.ConfigureConsumer(context, c => c.UseMessageRetry(r => - { - r.Interval(10, TimeSpan.FromMilliseconds(200)); - r.Ignore(); - r.Ignore(x => x.Message.Contains("SQL")); - })); - }); - }); -}); -``` - -In the above example, if the consumer throws an `ArgumentNullException` it won't be retried (because it would obvious fail again, most likely). If a `DataException` is thrown matching the filter expression, it wouldn't be handled by the second retry filter, but would be handled by the first retry filter. - -## Redelivery - -Some errors take a while to resolve, say a remote service is down or a SQL server has crashed. In these situations, it's best to dust off and nuke the site from orbit - at a much later time obviously. Redelivery is a form of retry (some refer to it as _second-level retry_) where the message is removed from the queue and then redelivered to the queue at a future time. - -::: tip NOTE -In some frameworks, message redelivery is also referred to as second-level retry. -::: - -To use delayed redelivery, ensure the transport is properly configured. RabbitMQ required a delayed-exchange plug-in, and ActiveMQ (non-Artemis) requires the scheduler to be enabled via the XML configuration. - -```cs -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); - cfg.UseMessageRetry(r => r.Immediate(5)); - - cfg.ConfigureEndpoints(context); - }); -}); -``` - -Now, if the initial 5 immediate retries fail (the database is really, really down), the message will retry an additional three times after 5, 15, and 30 minutes. This could mean a total of 15 retry attempts (on top of the initial 4 attempts prior to the retry/redelivery filters taking control). - -::: tip Scheduled Redelivery -MassTransit also supports scheduled redelivery using the `UseScheduledRedelivery` configuration method. Scheduled redelivery requires the use of a message scheduler, which can be configured to use the message transport or Quartz.NET/Hangfire. The configuration is similar, just ensure the scheduler is properly configured. -::: - -## Outbox - -If the consumer publishes events or sends messages (using `ConsumeContext`, which is provided via the `Consume` method on the consumer) and subsequently throws an exception, it isn't likely that those messages should still be published or sent. MassTransit provides an outbox to buffer those messages until the consumer completes successfully. If an exception is thrown, the buffered messages are discarded. - -To configure the outbox with redelivery and retry: - -```cs -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); - cfg.UseMessageRetry(r => r.Immediate(5)); - cfg.UseInMemoryOutbox(); - - cfg.ConfigureEndpoints(context); - }); -}); -``` - -### Configuring for a consumer or saga - -If there are multiple consumers (or saga) on the same endpoint (which could potentially get you on the _naughty list_), and the retry/redelivery behavior should only apply to a specific consumer or saga, the same configuration can be applied specifically to the consumer or saga. - -To configure a specific consumer. - -```cs -services.AddMassTransit(x => -{ - x.AddConsumer(); - - x.UsingRabbitMq((context, cfg) => - { - cfg.ReceiveEndpoint("submit-order", e => - { - e.ConfigureConsumer(context, c => - { - c.UseDelayedRedelivery(r => r.Intervals(TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(30))); - c.UseMessageRetry(r => r.Immediate(5)); - c.UseInMemoryOutbox(); - }); - }); - }); -}); -``` - -Sagas are configured in the same way, using the saga configurator. - -## Faults - -As shown above, MassTransit delivers messages to consumers by calling the _Consume_ method. When a message consumer throws an exception instead of returning normally, a `Fault` is produced, which may be published or sent depending upon the context. - -A `Fault` is a generic message contract including the original message that caused the consumer to fail, as well as the `ExceptionInfo`, `HostInfo`, and the time of the exception. - -```cs -public interface Fault - where T : class -{ - Guid FaultId { get; } - Guid? FaultedMessageId { get; } - DateTime Timestamp { get; } - ExceptionInfo[] Exceptions { get; } - HostInfo Host { get; } - T Message { get; } -} -``` - -If the message headers specify a `FaultAddress`, the fault is sent directly to that address. If the _FaultAddress_ is not present, but a `ResponseAddress` is specified, the fault is sent to the response address. Otherwise, the fault is published, allowing any subscribed consumers to receive it. - -### Consuming Faults - -Developers may want to do something with faults, such as updating an operational dashboard. To observe faults separate of the consumer that caused the fault to be produced, a consumer can consume fault messages the same as any other message. - -```csharp -public class DashboardFaultConsumer : - IConsumer> -{ - public async Task Consume(ConsumeContext> context) - { - // update the dashboard - } -} -``` - -Faults can also be observed by state machines when specified as an event: - -```csharp -Event(() => SubmitOrderFaulted, x => x - .CorrelateById(m => m.Message.Message.OrderId) // Fault includes the original message - .SelectId(m => m.Message.Message.OrderId)); - -public Event> SubmitOrderFaulted { get; private set; } -``` - -## Error Pipe - -By default, MassTransit will move faulted messages to the *_error* queue. This behavior can be customized for each receive endpoint. - -To discard faulted messages so that they are _not_ moved to the *_error* queue: - -```cs -cfg.ReceiveEndpoint("input-queue", ec => -{ - ec.DiscardFaultedMessages(); -}); -``` - -Beyond that built-in customization, the individual filters can be added/configured as well. Shown below are the default filters, as an example. - -> This is by default, do _NOT_ configure this unless you have a reason to change the behavior. - -```cs -cfg.ReceiveEndpoint("input-queue", ec => -{ - ec.ConfigureError(x => - { - x.UseFilter(new GenerateFaultFilter()); - x.UseFilter(new ErrorTransportFilter()); - }); -}); -``` - -## Dead-Letter Pipe - -By default, MassTransit will move skipped messages to the *_skipped* queue. This behavior can be customized for each receive endpoint. - -> Skipped messages are messages that are read from the receive endpoint queue that do not have a matching handler, consumer, saga, etc. configured. For instance, receiving a _SubmitOrder_ message on a receive endpoint that only has a consumer for the _UpdateOrder_ message would cause that _SubmitOrder_ message to end up in the *_skipped* queue. - -To discard skipped messages so they are _not_ moved to the *_skipped* queue: - -```cs -cfg.ReceiveEndpoint("input-queue", ec => -{ - ec.DiscardSkippedMessages(); -}); -``` - -Beyond that built-in customization, the individual filters can be added/configured as well. Shown below are the default filters, as an example. - -> This is by default, do _NOT_ configure this unless you have a reason to change the behavior. - -```cs -cfg.ReceiveEndpoint("input-queue", ec => -{ - ec.ConfigureDeadLetter(x => - { - x.UseFilter(new DeadLetterTransportFilter()); - }); -}); -``` - - - diff --git a/docs/usage/faas/README.md b/docs/usage/faas/README.md deleted file mode 100644 index 192e9277ca8..00000000000 --- a/docs/usage/faas/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# Functions as a Service - -MassTransit can also be run with FaaS models like [Azure Functions](/usage/faas/azure-functions) and [AWS Lambda](/usage/faas/aws-lambda) \ No newline at end of file diff --git a/docs/usage/faas/aws-lambda.md b/docs/usage/faas/aws-lambda.md deleted file mode 100644 index 2d8c4abb00a..00000000000 --- a/docs/usage/faas/aws-lambda.md +++ /dev/null @@ -1,4 +0,0 @@ -# AWS Lambda - -> The [Sample Code](https://github.com/MassTransit/Sample-LambdaFunction) is available for reference as well, which is based on the 8.0.0 version of MassTransit. - diff --git a/docs/usage/faas/azure-functions.md b/docs/usage/faas/azure-functions.md deleted file mode 100644 index f9bdfc9e937..00000000000 --- a/docs/usage/faas/azure-functions.md +++ /dev/null @@ -1,163 +0,0 @@ -# Azure Functions - -Azure Functions is a consumption-based compute solution that only runs code when there is work to be done. MassTransit supports Azure Service Bus and Azure Event Hubs when running as an Azure Function. - -> The [Sample Code](https://github.com/MassTransit/Sample-AzureFunction) is available for reference as well, which is based on the 8.0.0 version of MassTransit. - -The functions [host.json](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-service-bus-trigger?tabs=csharp) file needs to have messageHandlerOptions > autoComplete set to true. If this isn't set to true, MassTransit will _try_ to set it to true for you. This is so that the message is acknowledged by the Azure Functions runtime, which removes it from the queue once processing has completed successfully. - -```json -{ - "version": "2.0", - "logging": { - "applicationInsights": { - "samplingSettings": { - "isEnabled": true - } - }, - "logLevel": { - "MassTransit": "Debug", - "Sample.AzureFunctions.ServiceBus": "Information" - } - }, - "extensions": { - "serviceBus": { - "prefetchCount": 32, - "messageHandlerOptions": { - "autoComplete": true, - "maxConcurrentCalls": 32, - "maxAutoRenewDuration": "00:30:00" - } - }, - "eventHub": { - "maxBatchSize": 64, - "prefetchCount": 256, - "batchCheckpointFrequency": 1 - } - } -} -``` - -This settings for _prefetchCount_, _maxConcurrentCalls_, and _maxAutoRenewDuration_ are the most important – these will directly affect the performance of an Azure Function. - -The function should include a Startup class, which is called on startup by the Azure Functions framework. The example below configures MassTransit, registers the consumers for use, and adds a scoped type for an Azure function class. - -```cs -using MassTransit; -using Microsoft.Azure.Functions.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection; - -[assembly: FunctionsStartup(typeof(Sample.AzureFunctions.ServiceBus.Startup))] - -namespace Sample.AzureFunctions.ServiceBus -{ - public class Startup : - FunctionsStartup - { - public override void Configure(IFunctionsHostBuilder builder) - { - builder.Services - .AddScoped() // add your functions as scoped - .AddMassTransitForAzureFunctions(cfg => - { - cfg.AddConsumersFromNamespaceContaining(); - }); - } - } -} - -``` - -::: tip -Azure Functions using Azure Service Bus or Azure Event Hubs require the queue, subscription, topic, or event hub to exist prior to starting the function service. If the messaging entity does not exist, the function will not be bound, and messages or events will not be delivered. Service Bus messages sent or published by MassTransit running inside an Azure Function will, however, create the appropriate topics and/or queues as needed. -::: - -### Azure Service Bus - -The bindings for using MassTransit with Azure Service Bus are shown below. - -```csharp -using MassTransit.WebJobs.ServiceBusIntegration; -using Microsoft.Azure.ServiceBus; -using Microsoft.Azure.WebJobs; - - -public class SubmitOrderFunctions -{ - const string SubmitOrderQueueName = "input-queue"; - readonly IMessageReceiver _receiver; - - public SubmitOrderFunctions(IMessageReceiver receiver) - { - _receiver = receiver; - } - - [FunctionName("SubmitOrder")] - public Task SubmitOrderAsync([ServiceBusTrigger(SubmitOrderQueueName)] - Message message, CancellationToken cancellationToken) - { - return _receiver.HandleConsumer(SubmitOrderQueueName, message, cancellationToken); - } -} -``` - -In the example above, the _HandleConsumer_ method is used to configure a specific consumer on the message receiver. - -> Message receivers are cached by _entityName_. Once a message receiver has been used, the configuration cannot be changed. - -To configure the consumer pipeline, such as to add `UseMessageRetry` middleware, use a `ConsumerDefinition` for the consumer type. - -### Event Hub - -The bindings for using MassTransit with Azure Event Hub are shown below. In addition to the event hub name, the _Connection_ must also be specified. - -> At least I think so, the documentation isn't great and I only found this approach when digging through the extension source code. - -```csharp -using MassTransit.WebJobs.EventHubsIntegration; -using Microsoft.Azure.EventHubs; -using Microsoft.Azure.WebJobs; - -public class AuditOrderFunctions -{ - const string AuditOrderEventHubName = "input-hub"; - readonly IEventReceiver _receiver; - - public AuditOrderFunctions(IEventReceiver receiver) - { - _receiver = receiver; - } - - [FunctionName("AuditOrder")] - public Task AuditOrderAsync([EventHubTrigger(AuditOrderEventHubName, Connection = "AzureWebJobsEventHub")] - EventData message, CancellationToken cancellationToken) - { - return _receiver.HandleConsumer(AuditOrderEventHubName, message, cancellationToken); - } -} -``` - -::: warning -With this refresh of the Azure Function code, it is no longer possible to send messages to other event hubs. Messages published or sent are done so using Azure Service Bus. -::: - -### Testing Locally - -To test locally, a settings files must be created. Connections strings for the various services, along with the Application Insights connection string, can be configured. - -```js -{ - "IsEncrypted": false, - "Values": { - "FUNCTIONS_WORKER_RUNTIME": "dotnet", - "AzureWebJobsStorage": "", - "AzureWebJobsServiceBus": "", - "AzureWebJobsEventHub": "", - "FUNCTIONS_EXTENSION_VERSION": "~4", - "APPINSIGHTS_INSTRUMENTATIONKEY": "", - "APPLICATIONINSIGHTS_CONNECTION_STRING": "InstrumentationKey=" - } -} - -```` - diff --git a/docs/usage/guidance.md b/docs/usage/guidance.md deleted file mode 100644 index 4c57f6e2507..00000000000 --- a/docs/usage/guidance.md +++ /dev/null @@ -1,30 +0,0 @@ -# Guidance - -The following recommendations should be considered _best practices_ for building applications using MassTransit, specifically with RabbitMQ. - -- Published messages are routed to a receive endpoint queue by message type, using exchanges and exchange bindings. A service's receive endpoints do not affect other services or their receive endpoints, as long as they do not share the same queue. -- Consumers and sagas should have their own receive endpoint, with a unique queue name - - Each receive endpoint maps to one queue - - A queue may contain more than one message type, the message type is used to deliver the message to the appropriate consumer configured on the receive endpoint. - - If a received message is not handled by a consumer, the skipped message will be moved to a skipped queue, which is named with a \_skipped suffix. -- When running multiple instances of the same service - - Use the same queue name for each instance - - Messages from the queue will be load balanced across all instances (the _competing consumer_ pattern) -- If a consumer exception is thrown, the faulted message will be moved to an error queue, which is named with the \_error suffix. -- The number of concurrently processed messages can be up to the _PrefetchCount_, depending upon the number of cores available. -- For temporary receive endpoints that should be deleted when the bus is stopped, use _TemporaryEndpointDefinition_ as the receive endpoint definition. -- To configure _PrefetchCount_ higher than the desired concurrent message count, add _UseConcurrencyLimit(n)_ to the configuration. _This must be added before any consumers are configured._ Depending upon your consumer duration, higher values may greatly improve overall message throughput. - - - - - -### Receive Endpoints - -Consumers and sagas should be configured on their own receive endpoints. Running multiple unrelated consumers or sagas on a single receive endpoint is highly discouraged. - -Running multiple instances of a service should use the same endpoint names for the same consumers and sagas to allow service instances to load balance messages from the same queue. - -Calling `ConfigureEndpoints` will generate a queue name for each receive endpoint based on the consumer, saga, or activity name (removing the _Consumer_, _Saga_, or _Activity_ suffix) using the specified _endpoint name formatter_ and configure the consumers and sagas using their respective endpoints. - -In specialized scenarios where multiple consumers are closely related and have similar partitioning or ordering concerns, running those consumers on the same receive endpoint might be acceptable. \ No newline at end of file diff --git a/docs/usage/lifecycle-observers.md b/docs/usage/lifecycle-observers.md deleted file mode 100644 index 25ddc67bf6d..00000000000 --- a/docs/usage/lifecycle-observers.md +++ /dev/null @@ -1,83 +0,0 @@ -# Observing life cycle events - -When integrating a framework into your application, it can be useful to understand when the framework is "doing stuff." -Whether it is starting up, shutting down, or anything in between, being notified and thereby able to take action is a -huge benefit. - -MassTransit supports a number of life cycle events that can be observed, making it easy to build components that are -started and stopped along with the bus. - -
-Note: - A good example of this is the UseInMemoryMessageScheduler, which is part of the Quartz.NET integration - package. Using the life cycle events, Quartz is able to be started and stopped on a receive endpoint without - any additional development. And that saves you time. -
- -To observe bus events, create a class which implements `IBusObserver`, as shown below. - -```csharp -public class BusObserver : - IBusObserver -{ - public void PostCreate(IBus bus) - { - // called after the bus has been created, but before it has been started. - } - - public void CreateFaulted(Exception exception) - { - // called if the bus creation fails for some reason - } - - public Task PreStart(IBus bus) - { - // called just before the bus is started - } - - public Task PostStart(IBus bus, Task busReady) - { - // called once the bus has been started successfully. The task can be used to wait for - // all of the receive endpoints to be ready. - } - - public Task StartFaulted(IBus bus, Exception exception) - { - // called if the bus fails to start for some reason (dead battery, no fuel, etc.) - } - - public Task PreStop(IBus bus) - { - // called just before the bus is stopped - } - - public Task PostStop(IBus bus) - { - // called after the bus has been stopped - } - - public Task StopFaulted(IBus bus, Exception exception) - { - // called if the bus fails to stop (no brakes) - } -} -``` - -Bus observers can only be configured during bus configuration. To connect a bus observer during -bus configuration, refer to the example shown below. - -```csharp -var busObserver = new BusObserver(); - -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.Host("localhost"); - - cfg.ReceiveEndpoint("customer_update_queue", e => - { - e.Consumer(); - }); - - cfg.BusObserver(busObserver); -}); -``` diff --git a/docs/usage/logging.md b/docs/usage/logging.md deleted file mode 100644 index d05fb16659e..00000000000 --- a/docs/usage/logging.md +++ /dev/null @@ -1,46 +0,0 @@ -# Logging - -The MassTransit framework has fully adopted the [`Microsoft.Extensions.Logging`](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/?view=aspnetcore-5.0) framework. -So, it will use whatever logging configuration is already in your container. - - -# Examples - -## Basic Configuration - -By integrating with `Microsoft.Extensions.Logging` the basic configuration is no configuration. :tada: -When you run a project using the HostBuilder features of .Net you will get a basic logging experience right -out of the box. - -## Serilog - -At MassTransit, we are big fans of [Serilog](https://serilog.net/) and use this default configuration as a starting point in -most projects. - -```sh -dotnet add package Serilog.Extensions.Hosting -dotnet add package Serilog -dotnet add package Serilog.Sinks.Console -``` - -```csharp -public static IHostBuilder CreateHostBuilder(string[] args) -{ - return Host.CreateDefaultBuilder(args) - .UseSerilog((host, log) => - { - if (host.HostingEnvironment.IsProduction()) - log.MinimumLevel.Information(); - else - log.MinimumLevel.Debug(); - - log.MinimumLevel.Override("Microsoft", LogEventLevel.Warning); - log.MinimumLevel.Override("Quartz", LogEventLevel.Information); - log.WriteTo.Console(); - }) - .ConfigureWebHostDefaults(webBuilder => - { - webBuilder.UseStartup(); - }); -} -``` diff --git a/docs/usage/mediator.md b/docs/usage/mediator.md deleted file mode 100644 index 18aa082bff9..00000000000 --- a/docs/usage/mediator.md +++ /dev/null @@ -1,157 +0,0 @@ -# Mediator - -MassTransit includes a mediator implementation, with full support for consumers, handlers, and sagas (including saga state machines). MassTransit Mediator runs in-process and in-memory, no transport is required. For maximum performance, messages are passed by reference, instead than being serialized, and control flows directly from the _Publish_/_Send_ caller to the consumer. If a consumer throws an exception, the _Publish_/_Send_ method throws and the exception should be handled by the caller. - -::: tip Mediator -Mediator is a [behavioral design pattern](https://en.wikipedia.org/wiki/Mediator_pattern) in which a _mediator_ encapsulates communication between objects to reduce coupling. -::: - -### Configuration - -To configure Mediator, use the _AddMediator_ method. - -<<< @/docs/code/usage/UsageMediatorContainer.cs - -Consumers and sagas (including saga repositories) can be added, routing slip activities are not supported using mediator. Consumer and saga definitions are supported as well, but certain properties like _EndpointName_ are ignored. Middleware components, including _UseMessageRetry_ and _UseInMemoryOutbox_, are fully supported. - -Once created, Mediator doesn't need to be started or stopped and can be used immediately. _IMediator_ combines several other interfaces into a single interface, including _IPublishEndpoint_, _ISendEndpoint_, and _IClientFactory_. - -<<< @/src/MassTransit.Abstractions/Mediator/IMediator.cs - -MassTransit dispatches the command to the consumer asynchronously. Once the _Consume_ method completes, the _Send_ method will complete. If the consumer throws an exception, it will be propagated back to the caller. - -::: tip Send vs Publish -_Send_ expects the message to be consumed. If there is no consumer configured for the message type, an exception will be thrown. - -_Publish_, on the other hand, does not require the message to be consumed and does not throw an exception if the message isn't consumed. To throw an exception when a published message is not consumed, set the _Mandatory_ property to _true_ on _PublishContext_. -::: - -### Connect - -Consumers can be connected and disconnected from mediator at run-time, allowing components and services to temporarily consume messages. Use the _ConnectConsumer_ method to connect a consumer. The handle can be used to disconnect the consumer. - -<<< @/docs/code/usage/UsageMediatorConnect.cs - -### Requests - -To send a request using the mediator, a request client can be created from _IMediator_. The example below configures two consumers and then sends the _SubmitOrder_ command, followed by the _GetOrderStatus_ request. - -<<< @/docs/code/usage/UsageMediatorRequest.cs - -The _OrderStatusConsumer_, along with the message contracts, is shown below. - -<<< @/docs/code/usage/UsageMediatorConsumer.cs - -Just like _Send_, the request is executed asynchronously. If an exception occurs, the exception will be propagated back to the caller. If the request times out, or if the request is canceled, the _GetResponse_ method will throw an exception (either a _RequestTimeoutException_ or an _OperationCanceledException_). - -### Middleware - -MassTransit Mediator is built using the same components used to create a bus, which means all the same middleware components can be configured. For instance, to configure the Mediator pipeline, such as adding a scoped filter, see the example below. - -<<< @/docs/code/usage/UsageMediatorConfigure.cs - -### HTTP Context Scope - -A common question lately has been around the use of MassTransit's Mediator with ASP.NET Core, specifically the scope created for controllers. In cases where it is desirable to use the same scope for Mediator consumers that was created by the controller, the `HttpContextScopeAccessor` can be used as shown below. - -First, to configure the scope accessor, add the following to the services configuration: - -```cs -services.AddHttpContextAccessor(); - -services.AddMediator(configurator => -{ - configurator.AddConsumer(); - - configurator.ConfigureMediator((context, cfg) => cfg.UseHttpContextScopeFilter(context)); -}); -``` - -The `UseHttpContextScopeFilter` is an extension method that needs to be added to the project: - -```cs -public static class MediatorHttpContextScopeFilterExtensions -{ - public static void UseHttpContextScopeFilter(this IMediatorConfigurator configurator, IServiceProvider serviceProvider) - { - var filter = new HttpContextScopeFilter(serviceProvider.GetRequiredService()); - - configurator.ConfigurePublish(x => x.UseFilter(filter)); - configurator.ConfigureSend(x => x.UseFilter(filter)); - configurator.UseFilter(filter); - } -} -``` - -The extension method uses the `HttpContextScopeFilter`, shown below, which also needs to be added to the project: - -```cs -public class HttpContextScopeFilter : - IFilter, - IFilter, - IFilter -{ - private readonly IHttpContextAccessor _httpContextAccessor; - - public HttpContextScopeFilter(IHttpContextAccessor httpContextAccessor) - { - _httpContextAccessor = httpContextAccessor; - } - - private void AddPayload(PipeContext context) - { - if (_httpContextAccessor.HttpContext == null) - return; - - var serviceProvider = _httpContextAccessor.HttpContext.RequestServices; - context.GetOrAddPayload(() => serviceProvider); - context.GetOrAddPayload(() => new NoopScope(serviceProvider)); - } - - public Task Send(PublishContext context, IPipe next) - { - AddPayload(context); - return next.Send(context); - } - - public Task Send(SendContext context, IPipe next) - { - AddPayload(context); - return next.Send(context); - } - - public Task Send(ConsumeContext context, IPipe next) - { - AddPayload(context); - return next.Send(context); - } - - public void Probe(ProbeContext context) - { - } - - private class NoopScope : - IServiceScope - { - public NoopScope(IServiceProvider serviceProvider) - { - ServiceProvider = serviceProvider; - } - - public void Dispose() - { - } - - public IServiceProvider ServiceProvider { get; } - } -} -``` - -Once the above have been added, the controller scope will be passed through the mediator send and consume filters so that the controller scope is used for the consumers. - -### Legacy Configuration - -When not using a container, Mediator can be created as shown below. Consumers and sagas are configured the same way they would on a receive endpoint. The example below configures the mediator with a single consumer. - -<<< @/docs/code/usage/UsageMediator.cs - diff --git a/docs/usage/message-data.md b/docs/usage/message-data.md deleted file mode 100644 index 6cb4bd45152..00000000000 --- a/docs/usage/message-data.md +++ /dev/null @@ -1,211 +0,0 @@ -# Message Data - -Message brokers are built to be fast, and when it comes to messages, _size matters_. In this case, however, bigger is not better — large messages negatively impact broker performance. - -MassTransit offers a built-in solution which stores large data (either a string or a byte array) in a separate repository and replaces it with a reference to the stored data (yes, it's a URI, shocking I know) in the message body. When the message is consumed, the reference is replaced with the original data which is loaded from the repository. - -Message data is an implementation of the [Claim Check](https://www.enterpriseintegrationpatterns.com/patterns/messaging/StoreInLibrary.html) pattern. - -::: tip NOTE -The message producer and consumer must both have access to the message data repository. -::: - -## Usage - -To use message data, add a `MessageData` property to a message. Properties can be anywhere in the message, nested within message properties, or in collections such as arrays, lists, or dictionaries. The generic argument `T` must be `string`, `byte[]`, or `Stream`. - -```cs -public interface IndexDocumentContent -{ - Guid DocumentId { get; } - MessageData Document { get; } -} -``` - -The bus must be configured to use message data, specifying the message data repository. - -```cs -IMessageDataRepository messageDataRepository = new InMemoryMessageDataRepository(); - -var busControl = Bus.Factory.CreateUsingRabbitMq(cfg => -{ - cfg.UseMessageData(messageDataRepository); - - cfg.ReceiveEndpoint("document-service", e => - { - e.Consumer(); - }); -}); - -``` - -::: tip -Previous versions of MassTransit required a generic type to be specified on the `UseMessageData` method. Individual receive endpoints could also be configured separately. The previous methods are deprecated and now a single bus configuration applies to all receive endpoints. -::: - -Configuring the message data middleware (via `UseMessageData`) adds a transform to replace any deserialized message data reference with an object that loads the message data asynchronously. By using middleware, the consumer doesn't need to use the message data repository. The consumer can simply use the property value to access the message data (asynchronously, of course). If the message data was not loaded, an exception will be thrown. The `HasValue` property is `true` if message data is present. - -```cs -public class IndexDocumentConsumer : - IConsumer - -public async Task Consume(ConsumeContext context) -{ - byte[] document = await context.Message.Document.Value; -} -``` - -To initialize a message contract with one or more `MessageData` properties, the `byte[]`, `string`, or `Stream` value can be specified and the data will be stored to the repository by the initializer. If the message has the _TimeToLive_ header property specified, that same value will be used for the message data in the repository. - -```cs -Guid documentId = NewId.NextGuid(); -byte[] document = new byte[100000]; // get byte array, or a big string - -await endpoint.Send(new -{ - DocumentId = documentId, - Document = document -}); -``` - -If using a message class, or not using a message initializer, the data must be stored to the repository explicitly. - -```cs -class IndexDocumentContentMessage : - IndexDocumentContent -{ - public Guid DocumentId { get; set; } - public MessageData Document { get; set; } -} - -Guid documentId = NewId.NextGuid(); -byte[] document = new byte[100000]; // get byte array, or a big string - -await endpoint.Send(new IndexDocumentContentMessage -{ - DocumentId = documentId, - Document = await repository.PutBytes(document, TimeSpan.FromDays(1)) -}); -``` - -The message data is stored, and the reference added to the outbound message. - -::: tip NOTE -In the event of message retries in consumer memory a reference to the stream is held. -On the first attempt the message stream is read then you may need to rewind the stream to make it available to read from again on retries - -```cs - if (msg.Payload.HasValue) -{ - Stream s = await msg.Payload.Value; - - using (StreamReader sr = new StreamReader(s, leaveOpen: true)) - { - messageBody = await sr.ReadToEndAsync(); - - // In the case of retries, the Stream has likely already been read on previous attempts. - // The consumer doesn't put a message back on the queue if it is being retried and is held in memory by the consumer for retries - // so you will need to 'rewind' the stream to the beginning so it is ready to be read again on subsequent attempts - s.Seek(0, SeekOrigin.Begin); - } -} -``` - -Note that in this example the StreamReader argument `leaveOpen` is set to true to avoid disposing of the stream. -This means that you may need to manually disponse of the stream to avoid memory leaks when the message has been successful or faulted - -::: - -## Configuration - -There are several configuration settings available to adjust message data behavior. - -### Time To Live - -By default, there is no default message data time-to-live. To specify a default time-to-live, set the default as shown. - -```cs -MessageDataDefaults.TimeToLive = TimeSpan.FromDays(2); -``` - -This settings simply specifies the default value when calling the repository, it is up to the repository to apply any time-to-live values to the actual message data. - -If the `SendContext` has specified a time-to-live value, that value is applied to the message data automatically (when using message initializers). To add extra time, perhaps to account for system latency or differences in time, extra time can be added. - -```cs -MessageDataDefaults.ExtraTimeToLive = TimeSpan.FromMinutes(5); -``` - -### Inline Threshold - -Newly added is the ability to specify a threshold for message data so that smaller values are included in the actual message body. This eliminates the need to read the data from storage, which increases performance. The message data can also be configured to _not_ write that data to the repository if it is under the threshold. By default (for now), data is written to the repository to support services that have not yet upgraded to the latest MassTransit. - -> If you know your systems are upgraded, you can change the default so that data sizes under the threshold are not written to the repository. - -To configure the threshold, and to optionally turn off storage of data sizes under the threshold: - -```cs -// the default value is 4096 -MessageDataDefaults.Threshold = 8192; - -// to disable writing to the repository for sizes under the threshold -// defaults to false, which may change to true in a future release -MessageDataDefaults.AlwaysWriteToRepository = false; -``` - -## Repositories - -MassTransit includes several message data repositories. - -| Name | Description | -|:-----------|:------------| -| InMemoryMessageDataRepository | Entirely in memory, meant for unit testing -| FileSystemMessageDataRepository | Writes message data to the file system, which may be a network drive or other shared storage -| MongoDbMessageDataRepository | Stores message data using MongoDB's GridFS -| AzureStorageMessageDataRepository | Stores message data using Azure Blob Storage -| EncryptedMessageDataRepository | Adds encryption to any other message data repository - - -### File System - -To configure the file system message data repository: - -```cs -IMessageDataRepository CreateRepository(string path) -{ - var dataDirectory = new DirectoryInfo(path); - - return new FileSystemMessageDataRepository(dataDirectory); -} -``` - -### MongoDB - -> [MassTransit.MongoDb](https://www.nuget.org/packages/MassTransit.MongoDb/) - -To configure the MongoDB GridFS message data repository, follow the example shown below. - -```cs -IMessageDataRepository CreateRepository(string connectionString, string databaseName) -{ - return new MongoDbMessageDataRepository(connectionString, databaseName); -} -``` - -### Azure Storage - -> [MassTransit.Azure.Storage](https://www.nuget.org/packages/MassTransit.Azure.Storage/) - -An Azure Cloud Storage account can be used to store message data. To configure Azure storage, first create the BlobServiceClient object using your connection string, and then use the extension method to create the repository as shown below. You can replace `message-data` with the desired container name. - -```cs -var client = new BlobServiceClient(""); -_repository = client.CreateMessageDataRepository("message-data"); -``` - -Previous to version 7.1.8 of MassTransit this was done creating a CloudStorageAccount object from your connection string the following way. - -```cs -var account = CloudStorageAccount.Parse(""); -_repository = account.CreateMessageDataRepository("message-data"); -``` diff --git a/docs/usage/messages.md b/docs/usage/messages.md deleted file mode 100644 index ff62c039673..00000000000 --- a/docs/usage/messages.md +++ /dev/null @@ -1,186 +0,0 @@ -# Messages - -In MassTransit, a message contract is defined _code first_ by creating a .NET type. A message can be defined using a record, class, or interface. Messages should only consist of properties, methods and other behavior should not be included. - -::: warning Important -MassTransit uses the full type name, including the _namespace_, for message contracts. When creating the same message *type* in two separate projects, the namespaces **must** match or the message will not be consumed. -::: - -The message examples below show the same command to update a customer address using each of the supported contract types. - -### Using a record (recommended for .NET 5+) - -```cs -namespace Company.Application.Contracts -{ - using System; - - public record UpdateCustomerAddress - { - public Guid CommandId { get; init; } - public DateTime Timestamp { get; init; } - public string CustomerId { get; init; } - public string HouseNumber { get; init; } - public string Street { get; init; } - public string City { get; init; } - public string State { get; init; } - public string PostalCode { get; init; } - } -} -``` - -### Using an interface - -```cs -namespace Company.Application.Contracts -{ - using System; - - public interface UpdateCustomerAddress - { - Guid CommandId { get; } - DateTime Timestamp { get; } - string CustomerId { get; } - string HouseNumber { get; } - string Street { get; } - string City { get; } - string State { get; } - string PostalCode { get; } - } -} -``` - -When defining a message type using an interface, MassTransit will create a dynamic class implementing the interface for serialization, allowing the interface with get-only properties to be presented to the consumer. To create an interface message, use a [message initializer](/usage/producers.md#message-initializers). - -### Using a class - -```cs -namespace Company.Application.Contracts -{ - using System; - - public class UpdateCustomerAddress - { - public Guid CommandId { get; set; } - public DateTime Timestamp { get; set; } - public string CustomerId { get; set; } - public string HouseNumber { get; set; } - public string Street { get; set; } - public string City { get; set; } - public string State { get; set; } - public string PostalCode { get; set; } - } -} -``` - -> Properties with `private set;` are not recommended as they are not serialized by default when using `System.Text.Json`. - -::: tip -A common mistake when engineers are new to messaging is to create a base class for messages, and try to dispatch that base class in the consumer – including the behavior of the subclass. Ouch. This always leads to pain and suffering, so just say no to base classes. -::: - -## Message Names - -There are two main message types, _events_ and _commands_. When choosing a name for a message, the type of message should dictate the tense of the message. - -### Commands - -A command tells _a_ service to do something, and typically a command should only be consumed by a single consumer. If you have a command, such as `SubmitOrder`, then you should have only one consumer that implements `IConsumer` or one saga state machine with the `Event` configured. By maintaining the one-to-one relationship of a command to a consumer, commands may by _published_ and they will be automatically routed to the consumer. - -When using RabbitMQ, there is _no additional overhead_ using this approach. However, both Azure Service Bus and Amazon SQS have a more complicated routing structure and because of that structure, additional charges may be incurred since messages need to be forwarded from topics to queues. For low- to medium-volume message loads this isn't a major concern, but for larger high-volume loads it may be preferable to _[send](producers.md#send)_ (using `Send`) commands directly to the queue to reduce latency and cost. - -Commands should be expressed in a verb-noun sequence, following the _tell_ style. - -Example Commands: - -* UpdateCustomerAddress -* UpgradeCustomerAccount -* SubmitOrder - -### Events - -An event signifies that something has happened. Events are [published](producers.md#publish) (using `Publish`) via either `ConsumeContext` (within a message consumer), `IPublishEndpoint` (within a container scope), or `IBus` (standalone). - -Events should be expressed in a noun-verb (past tense) sequence, indicating that something happened. - -Example Events: - -* CustomerAddressUpdated -* CustomerAccountUpgraded -* OrderSubmitted, OrderAccepted, OrderRejected, OrderShipped - -## Message Headers - -MassTransit encapsulates every sent or published message in a message envelope (described by the [Envelope Wrapper](https://www.enterpriseintegrationpatterns.com/patterns/messaging/EnvelopeWrapper.html) pattern). The envelope adds a series of message headers, including: - -| Property | Type | Description | -| :---------------- |:------:| :--------------------------------------- | -| MessageId |Auto | Generated for each message using `NewId.NextGuid`.| -| CorrelationId |User | Assigned by the application, or automatically by convention, and should uniquely identify the operation, event, etc.| -| RequestId |Request| Assigned by the request client, and automatically copied by the _Respond_ methods to correlate responses to the original request.| -| InitiatorId |Auto | Assigned when publishing or sending from a consumer, saga, or activity to the value of the _CorrelationId_ on the consumed message.| -| ConversationId |Auto | Assigned when the first message is sent or published and no consumed message is available, ensuring that a set of messages within the same conversation have the same identifier.| -| SourceAddress |Auto | Where the message originated (may be a temporary address for messages published or sent from `IBus`).| -| DestinationAddress|Auto | Where the message was sent | -| ResponseAddress |Request| Where responses to the request should be sent. If not present, responses are _published_.| -| FaultAddress |User | Where consumer faults should be sent. If not present, faults are _published_.| -| ExpirationTime |User | When the message should expire, which may be used by the transport to remove the message if it isn't consumed by the expiration time.| -| SentTime |Auto | When the message was sent, in UTC.| -| MessageType |Auto | An array of message types, in a `MessageUrn` format, which can be deserialized.| -| Host |Auto | The host information of the machine that sent or published the message.| -| Headers |User | Additional headers, which can be added by the user, middleware, or diagnostic trace filters.| - -Message headers can be read using the `ConsumeContext` interface and specified using the `SendContext` interface. - -## Correlation - -Messages are usually part of a conversation and identifiers are used to connect messages to that conversation. In the previous section, the headers supported by MassTransit, including _ConversationId_, _CorrelationId_, and _InitiatorId_, are used to combine separate messages into a conversation. Outbound messages that are published or sent by a consumer will have the same _ConversationId_ as the consumed message. If the consumed message has a _CorrelationId_, that value will be copied to the _InitiatorId_. These headers capture the flow of messages involved in the conversation. - -_CorrelationId_ may be set, when appropriate, by the developer publishing or sending a message. _CorrelationId_ can be set explicitly on the _PublishContext_ or _SendContext_ or when using a message initializer via the *\_\_CorrelationId* property. The example below shows how either of these methods can be used. - -<<< @/docs/code/usage/UsageMessageCorrelation.cs{20-31} - -### Correlation Conventions - -_CorrelationId_ can also be set by convention. MassTransit includes several conventions by default, which may be used as the source to initialize the _CorrelationId_ header. - -1. If the message implements the `CorrelatedBy` interface, which has a `Guid CorrelationId` property, its value will be used. -1. If the message has a property named _CorrelationId_, _CommandId_, or _EventId_ that is a _Guid_ or _Guid?_, its value will be used. -1. If the developer registered a _CorrelationId_ provider for the message type, it will be used get the value. - -The final convention requires the developer to register a _CorrelationId_ provider prior to bus creation. The convention can be registered two ways, one of which is the new way, and the other which is the original approach that simply calls the new way. An example of the new approach, as well as the previous method, is shown below. - -<<< @/docs/code/usage/UsageMessageSetCorrelation.cs - -The convention can also be specified during bus configuration, as shown. In this case, the convention applies to the configured bus instance. The previous approach was a global configuration shared by all bus instances. - -<<< @/docs/code/usage/UsageMessageSendCorrelation.cs - -Registering _CorrelationId_ providers should be done early in the application, prior to bus configuration. An easy approach is putting the registration methods into a class method and calling it during application startup. - -### Saga Correlation - -Sagas _must_ have a _CorrelationId_, it is the primary key used by the saga repository and the way messages are correlated to a specific saga instance. MassTransit follows the conventions above to obtain the _CorrelationId_ used to create a new or load an existing saga instance. Newly created saga instances will be assigned the _CorrelationId_ from the initiating message. - -::: tip New in Version 7 -Previous versions of MassTransit only supported automatic correlation when the message implemented the `CorrelatedBy` interface. Starting with Version 7, all of the above conventions are used. -::: - -### Identifiers - -MassTransit uses and highly encourages the use of _Guid_ identifiers. Distributed systems would crumble using monotonically incrementing identifiers (such as _int_ or _long_) due to the bottleneck of locking and incrementing a shared counter. Historically, certain types (okay, we'll call them out - SQL DBAs) have argued against using _Guid_ (or, their term, _uniqueidentifier_) as a key – a clustered primary key in particular. However, with MassTransit, we solved that problem. - -MassTransit uses [NewId](https://www.nuget.org/packages/NewId) to generate identifiers that are unique, sequential, and represented as a _Guid_. The generated identifiers are clustered-index friendly, and are ordered so that SQL Server can efficiently insert them into a database with the *uniqueidentifier* as the primary key. - -To create a _Guid_, call `NewId.NextGuid()` where you would otherwise call `Guid.NewGuid()` – and start enjoying the benefits of fast, distributed identifiers. - -## Guidance - -When defining message contracts, what follows is general guidance based upon years of using MassTransit combined with continued questions raised by developers new to MassTransit. - -- Use interfaces and message initializers. Once you adjust it starts to make more sense. Use the Roslyn Analyzer to identify missing or incompatible property initializers. - - Inheritance is okay, but keep it sensible as the type hierarchy will be applied to the broker. A message type containing a dozen interfaces is a bit annoying to untangle if you need to delve deep into message routing to troubleshoot an issue. -- Class inheritance has the same guidance as interfaces, but with more caution. - - Consuming a base class type, and expecting polymorphic method behavior almost always leads to problems. - - Message design is not object-oriented design. Messages should contain state, not behavior. Behavior should be in a separate class or service. - - A big base class may cause pain down the road as changes are made, particularly when supporting multiple message versions. diff --git a/docs/usage/monitoring.md b/docs/usage/monitoring.md deleted file mode 100644 index f91302d8b11..00000000000 --- a/docs/usage/monitoring.md +++ /dev/null @@ -1,6 +0,0 @@ -# Monitoring - -## Diagnostic Source - -## Application Insights - diff --git a/docs/usage/producers.md b/docs/usage/producers.md deleted file mode 100644 index e39e91a9445..00000000000 --- a/docs/usage/producers.md +++ /dev/null @@ -1,438 +0,0 @@ -# Producers - -An application or service can produce messages using two different methods. A message can be sent or a message can be published. The behavior of each method is very different, but it's easy to understand by looking at the type of messages involved with each particular method. - -When a message is sent, it is delivered to a specific endpoint using a _DestinationAddress_. When a message is published, it is not sent to a specific endpoint, but is instead broadcasted to any consumers which have *subscribed* to the message type. For these two separate behavior, we describe messages sent as commands, and messages published as events. - -> These are discussed in depth in the [Messages](messages) section of the documentation. - -## Send - -::: tip Video -Learn about `Send` in [this short video](https://youtu.be/t6FsmqZsdJk). -::: - -To send a message, the _DestinationAddress_ is used to deliver the message to an endpoint — such as a queue. One of the `Send` method overloads on the `ISendEndpoint` interface is called, which will then send the message to the transport. An `ISendEndpoint` is obtained from one of the following objects: - -1. The `ConsumeContext` of the message being consumed - - This ensures that the correlation headers, message headers, and trace information is propagated to the sent message. -2. An `ISendEndpointProvider` instance - - This may be passed as an argument, but is typically specified on the constructor of an object that is resolved using a dependency injection container. -3. The `IBus` - - The last resort, and should only be used for messages that are being sent by an _initiator_ — a process that is initiating a business process. - -Once the `Send` method has been called (only once or repeatedly to send a series of messages), the `ISendEndpoint` reference should fall out of scope. - -::: tip -Applications should not store the `ISendEndpoint` reference, it is automatically cached by MassTransit and discarded when it is no longer needed. -::: - -For instance, an `IBus` instance is a send endpoint provider, but it should *never* be used by a consumer to obtain an `ISendEndpoint`. `ConsumeContext` can also provide send endpoints, and should be used since it is *closer* to the consumer. - -::: warning -This cannot be stressed enough -- always obtain an `ISendEndpoint` from the closest scope. There is extensive logic to tie message flows together using conversation, correlation, and initiator identifiers. By skipping a level and going outside the closest scope, that critical information will be lost which prevents the useful trace identifiers from being propagated. -::: - -### Send Endpoint - -To obtain a send endpoint from a send endpoint provider, call the `GetSendEndpoint` method as shown below. The method is _async_, so be sure to _await_ the result. - -```cs -public record SubmitOrder -{ - public string OrderId { get; init; } -} - -public async Task SendOrder(ISendEndpointProvider sendEndpointProvider) -{ - var endpoint = await sendEndpointProvider.GetSendEndpoint(_serviceAddress); - - await endpoint.Send(new SubmitOrder { OrderId = "123" }); -} -``` - -There are many overloads for the `Send` method. Because MassTransit is built around filters and pipes, pipes are used to customize the message delivery behavior of Send. There are also some useful overloads (via extension methods) to make sending easier and less noisy due to the pipe construction, etc. - -#### Send with Timeout - -If there is a connectivity issue between the application and the broker, the _Send_ method will internally retry until the connection is restored blocking the returned _Task_ until the send operation completes. The _Send_ methods support passing a `CancellationToken` that can be used to cancel the operation. - -To specify a timeout, use a `CancellationTokenSource` as shown below. - -```cs -var timeout = TimeSpan.FromSeconds(30); -using var source = new CancellationTokenSource(timeout); - -await endpoint.Send(new SubmitOrder { OrderId = "123" }, source.Token); -``` - -Typically, the _Send_ call completes quickly, only taking a few milliseconds. If the token is canceled the send operation will throw an `OperationCanceledException`. - -### Endpoint Address - -An endpoint address is a fully-qualified URI which may include transport-specific details. For example, an endpoint on a local RabbitMQ server would be: - -``` -rabbitmq://localhost/input-queue -``` - -Transport-specific details may include query parameters, such as: - -``` -rabbitmq://localhost/input-queue?durable=false -``` - -This would configure the queue as non-durable, where messages would only be stored in memory and therefore would not survive a broker restart. - -#### Short Addresses - -Starting with MassTransit v6, short addresses are supported. For instance, to obtain a send endpoint for a queue on RabbitMQ, the caller would only have to specify: - -``` -GetSendEndpoint(new Uri("queue:input-queue")) -``` - -This would return a send endpoint for the _input-queue_ exchange, which would be bound to the _input-queue_ queue. Both the exchange and the queue would be created if either did not exist. This short syntax eliminates the need to know the scheme, host, port, and virtual host of the broker, only the queue and/or exchange details are required. - -Each transport has a specific set of supported short addresses. - - -##### Supported Address Schemes - -| Short Address | RabbitMQ | Azure Service Bus | ActiveMQ | Amazon SQS | -| ------------- |:------------------:|:------------------:|:------------------:|:------------------:| -| queue:name | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| topic:name | | :heavy_check_mark: | :heavy_check_mark: | :heavy_check_mark: | -| exchange:name | :heavy_check_mark: | | | | - - -### Address Conventions - -> While these conventions are available, the author tends to dislike them [based upon this Stack Overflow answer](https://stackoverflow.com/questions/62713786/masstransit-endpointconvention-azure-service-bus/62714778#62714778). - -Using send endpoints might seem too verbose, because before sending any message, you need to get the send endpoint and to do that you need to have an endpoint address. Usually, addresses are kept in the configuration and accessing the configuration from all over the application is not a good practice. - -Endpoint conventions solve this issue by allowing you to configure the mapping between message types and endpoint addresses. A potential downside here that you will not be able to send messages of the same type to different endpoints by using conventions. If you need to do this, keep using the `GetSendEndpoint` method. - -Conventions are configured like this: - -```cs -EndpointConvention.Map(new Uri("rabbitmq://mq.acme.com/order/order_processing")); -``` - -Now, you don't need to get the send endpoint anymore for this type of message and can send it like this: - -```cs -public async Task Post(SubmitOrderRequest request) -{ - if (AllGoodWith(request)) - await _bus.Send(ConvertToCommand(request)); -} -``` - -Also, from inside the consumer, you can do the same using the `ConsumeContext.Send` overload: - -```cs -EndpointConvention.Map(new Uri(ConfigurationManager.AppSettings["deliveryServiceQueue"])); -``` - -```cs -public class SubmitOrderConsumer : - IConsumer -{ - private readonly IOrderSubmitter _orderSubmitter; - - public SubmitOrderConsumer(IOrderSubmitter submitter) - => _orderSubmitter = submitter; - - public async Task Consume(IConsumeContext context) - { - await _orderSubmitter.Process(context.Message); - - await context.Send(new StartDelivery(context.Message.OrderId, DateTime.UtcNow)); - } -} -``` - -The `EndpointConvention.Map` method is static, so it can be called from everywhere. It is important to remember that you cannot configure conventions for the same message twice. If you try to do this - the `Map` method will throw an exception. This is also important when writing tests, so you need to configure the conventions at the same time as you configure your test bus (harness). - -It is better to configure send conventions before you start the bus. - - - -## Publish - -::: tip Video -Learn about `Publish` in [this short video](https://youtu.be/-MAEZq5G7lk). -::: - -Messages are published similarly to how messages are sent, but in this case, a single `IPublishEndpoint` is used. The same rules for endpoints apply, the closest instance of the publish endpoint should be used. So the `ConsumeContext` for consumers, and `IBus` for applications that are published outside of a consumer context. - -::: tip Key Concept -In MassTransit, _Publish_ follows the [publish subscribe][1] messaging pattern. For each message published, a copy of the message is delivered to each subscriber. The mechanism by which this happens is implemented by the message transport, but semantically the operation is the same regardless of which transport is used. -::: - -The same guidelines apply for publishing messages, the closest object should be used. - -1. The `ConsumeContext` of the message being consumed - - This ensures that the correlation headers, message headers, and trace information is propagated to the published message. -2. An `IPublishEndpoint` instance - - This may be passed as an argument, but is typically specified on the constructor of an object that is resolved using a dependency injection container. -3. The `IBus` - - The last resort, and should only be used for messages that are being published by an _initiator_ — a process that is initiating a business process. - -To publish a message, see the code below: - -```cs -public record OrderSubmitted -{ - public string OrderId { get; init; } - public DateTime OrderDate { get; init; } -} - -public async Task NotifyOrderSubmitted(IPublishEndpoint publishEndpoint) -{ - await publishEndpoint.Publish(new() - { - OrderId = "27", - OrderDate = DateTime.UtcNow, - }); -} -``` - -> Publish also supports cancellation, including timeouts. See [the note above](#send-with-timeout) for details. - -If you are planning to publish messages from within your consumers, this example would suit better: - -```csharp -public class SubmitOrderConsumer : IConsumer -{ - private readonly IOrderSubmitter _orderSubmitter; - - public SubmitOrderConsumer(IOrderSubmitter submitter) - => _orderSubmitter = submitter; - - public async Task Consume(IConsumeContext context) - { - await _orderSubmitter.Process(context.Message); - - await context.Publish(new() - { - OrderId = context.Message.OrderId, - OrderDate = DateTime.UtcNow - }) - } -} - -``` - -### Sending via interfaces - -Since the general recommendation is to use interfaces, there are convenience methods to initialize the interface without requiring the creation of a message class underneath. While versioning of messages still requires a class which supports multiple interfaces, a simple approach to send an interface message is shown below. - -```csharp -public record SubmitOrder -{ - public string OrderId { get; init; } - public DateTime OrderDate { get; init; } - public decimal OrderAmount { get; init; } -} - -public async Task SendOrder(ISendEndpoint endpoint) -{ - await endpoint.Send(new() - { - OrderId = "27", - OrderDate = DateTime.UtcNow, - OrderAmount = 123.45m - }); -} -``` - -## Message Initialization - -Messages can be initialized by MassTransit using an anonymous object passed as an _object_ to the _publish_ or _send_ methods. While originally designed to support the initialization of interface-based message types, anonymous objects can also be used to initialize message types defined using classes or records. - -### Anonymous object values - -`Send`, `Publish`, and most of the methods that behave in similar ways (scheduling, responding to requests, etc.) all support passing an object of _values_ which is used to set the properties on the specified interface. A simple example is shown below. - -Consider this example message contract to submit an order. - -```cs -public record SubmitOrder -{ - public Guid OrderId { get; init; } - public DateTime OrderDate { get; init; } - public string OrderNumber { get; init; } - public decimal OrderAmount { get; init; } -} -``` - -To send this message to an endpoint: - -```cs -await endpoint.Send(new // <-- notice no () -{ - OrderId = NewId.NextGuid(), - OrderDate = DateTime.UtcNow, - OrderNumber = "18001", - OrderAmount = 123.45m -}); -``` - -The anonymous object properties are matched by name and there is an extensive set of type conversions that may be used to match the types defined by the interface. Most numeric, string, and date/time conversions are supported, as well as several advanced conversions (including variables, and asynchronous `Task` results). - -Collections, including arrays, lists, and dictionaries, are broadly supported, including the conversion of list elements, as well as dictionary keys and values. For instance, a dictionary of (int,decimal) could be converted on the fly to (long, string) using the default format conversions. - -Nested objects are also supported, for instance, if a property was of type `Address` and another anonymous object was created (or any type whose property names match the names of the properties on the message contract), those properties would be set on the message contract. - -### Headers - -Header values can be specified in the anonymous object using a double-underscore (pronounced 'dunder' apparently) property name. For instance, to set the message time-to-live, specify a property with the duration. Remember, any value that can be converted to a `TimeSpan` works! - -```cs -public record GetOrderStatus -{ - public Guid OrderId { get; init; } -} - -var response = await requestClient.GetResponse(new -{ - __TimeToLive = 15000, // 15 seconds, or in this case, 15000 milliseconds - OrderId = orderId, -}); -``` - -> actually, that's a bad example since the request client already sets the message expiration, but you, get, the, point. - -To add a custom header value, a special property name format is used. In the name, underscores are converted to dashes, and double underscores are converted to underscores. In the following example: - -```cs -var response = await requestClient.GetResponse(new -{ - __Header_X_B3_TraceId = zipkinTraceId, - __Header_X_B3_SpanId = zipkinSpanId, - OrderId = orderId, -}); -``` - -This would include set the headers used by open tracing (or Zipkin, as shown above) as part of the request message so the service could share in the span/trace. In this case, `X-B3-TraceId` and `X-B3-SpanId` would be added to the message envelope, and depending upon the transport, copied to the transport headers as well. - -### Variables - -MassTransit also supports variables, which are special types added to the anonymous object. Following the example above, the initialization could be changed to use variables for the `OrderId` and `OrderDate`. Variables are consistent throughout the message creation, using the same variable multiple times returns the value. For instance, the Id created to set the _OrderId_ would be the same used to set the _OrderId_ in each item. - -```csharp -public record OrderItem -{ - public Guid OrderId { get; init; } - public string ItemNumber { get; init; } -} - -public record SubmitOrder -{ - public Guid OrderId { get; init; } - public DateTime OrderDate { get; init; } - public string OrderNumber { get; init; } - public decimal OrderAmount { get; init; } - public OrderItem[] OrderItems { get; init; } -} - -await endpoint.Send(new -{ - OrderId = InVar.Id, - OrderDate = InVar.Timestamp, - OrderNumber = "18001", - OrderAmount = 123.45m, - OrderItems = new[] - { - new { OrderId = InVar.Id, ItemNumber = "237" }, - new { OrderId = InVar.Id, ItemNumber = "762" } - } -}); -``` - -### Awaiting Task results - -Message initializers are now asynchronous, which makes it possible to do some pretty cool things, including waiting for Task input properties to complete and use the result to initialize the property. An example is shown below. - -```cs -public record OrderUpdated -{ - public Guid CorrelationId { get; init; } - public DateTime Timestamp { get; init; } - public Guid OrderId { get; init; } - public Customer Customer { get; init; } -} - -public async Task LoadCustomer(Guid orderId) -{ - // work happens up in here -} - -await context.Publish(new -{ - InVar.CorrelationId, - InVar.Timestamp, - OrderId = context.Message.OrderId, - Customer = LoadCustomer(context.Message.OrderId) -}); -``` - -The property initializer will wait for the task result and then use it to initialize the property (converting all the types, etc. as it would any other object). - -> While it is of course possible to await the call to `LoadCustomer`, properties are initialized in parallel, and thus, allowing the initializer to await the Task can result in better overall performance. Your mileage may vary, however. - - -## Send Headers - -There are a variety of message headers available which are used for correlation and tracking of messages. It is also possible to override some of the default behaviors of MassTransit when a fault occurs. For instance, a fault is normally *published* when a consumer throws an exception. If instead the application wants faults delivered to a specific address, the ``FaultAddress`` can be specified via a header. How this is done is shown below. - -```csharp -public record SubmitOrder -{ - public string OrderId { get; init; } - public DateTime OrderDate { get; init; } - public decimal OrderAmount { get; init; } -} - -public async Task SendOrder(ISendEndpoint endpoint) -{ - await endpoint.Send(new - { - OrderId = "27", - OrderDate = DateTime.UtcNow, - OrderAmount = 123.45m - }, context => context.FaultAddress = new Uri("rabbitmq://localhost/order_faults")); -} -``` - -Since a message initializer is being used, this can actually be simplified. - -```csharp -public async Task SendOrder(ISendEndpoint endpoint) -{ - await endpoint.Send(new - { - OrderId = "27", - OrderDate = DateTime.UtcNow, - OrderAmount = 123.45m, - - // header names are prefixed with __, and types are converted as needed - __FaultAddress = "rabbitmq://localhost/order_faults" - }); -} -``` - - -[1]: http://www.enterpriseintegrationpatterns.com/patterns/messaging/PublishSubscribeChannel.html -[2]: http://spring.io/blog/2011/04/01/routing-topologies-for-performance-and-scalability-with-rabbitmq/ -[3]: http://codebetter.com/drusellers/2011/05/08/brain-dump-conventional-routing-in-rabbitmq/ - diff --git a/docs/usage/requests.md b/docs/usage/requests.md deleted file mode 100644 index a7e1b00f638..00000000000 --- a/docs/usage/requests.md +++ /dev/null @@ -1,360 +0,0 @@ -# Requests - -Request/response is a common pattern in application development, where a component sends a request to a service and continues once the response is received. In a distributed system, this can increase the latency of an application since the service may be hosted in another process, on another machine, or may even be a remote service in another network. While in many cases it is best to avoid request/response use in distributed applications, particularly when the request is a command, it is often necessary and preferred over more complex solutions. - -Fortunately for .NET developers, C# with TPL makes it easier to program applications that call services asynchronously. By using *Tasks* and the *async* and *await* keywords, developers can write procedural code and avoid the complex use of callbacks and handlers. Additionally, multiple asynchronous requests can be executed at once, reducing the overall execution time to that of the longest request. - -### Message Contracts - -To get started, the message contracts need to be created. In this example, an order status check is being created. - -```csharp -public interface CheckOrderStatus -{ - string OrderId { get; } -} - -public interface OrderStatusResult -{ - string OrderId { get; } - DateTime Timestamp { get; } - short StatusCode { get; } - string StatusText { get; } -} -``` - -### Request Consumer - -In order for the request to return anything, it needs to be handled. Handling requests is done by using normal consumers. The only difference is that such consumer needs to send a response back. - -For the aforementioned message contracts, the request handler can look like this: - -```csharp -public class CheckOrderStatusConsumer : - IConsumer -{ - readonly IOrderRepository _orderRepository; - - public CheckOrderStatusConsumer(IOrderRepository orderRepository) - { - _orderRepository = orderRepository; - } - - public async Task Consume(ConsumeContext context) - { - var order = await _orderRepository.Get(context.Message.OrderId); - if (order == null) - throw new InvalidOperationException("Order not found"); - - await context.RespondAsync(new - { - OrderId = order.Id, - order.Timestamp, - order.StatusCode, - order.StatusText - }); - } -} -``` - -The response will be sent back to the requester. In case the exception is thrown, MassTransit will create a `Fault` message and send it back to the requester. The requester address is available in the consume context of the request message as `context.ResponseAddress`. - -### Request Client Configuration - -Most interactions of the request/response nature consist of four elements: the request arguments, the response values, exception handling, and the time to wait for a response. The .NET framework gives us one additional element, a `CancellationToken`, which can cancel waiting for the response (the request is still sent, and may be processed, the CancellationToken only cancels waiting for the response). - -MassTransit includes a request client which encapsulates the request/response messaging pattern. - -::: tip V8 -By default, MassTransit registers a generic request client in the container that publishes requests using the default request parameters. The only time a request client needs to be manually configured is when a specific _DestinationAddress_ or _Timeout_ is specified. -::: - -To configure the request client with a specific destination address, use the `AddRequestClient` method as shown below. The default request timeout may also be specified. - -```cs -public void ConfigureServices(IServiceCollection services) -{ - services.AddMassTransit(x => - { - x.AddConsumer(); - - x.UsingInMemory((context, cfg) => - { - cfg.ConfigureEndpoints(context); - })); - - // ONLY required if the destination address must be specified - x.AddRequestClient(new Uri("exchange:order-status")); - }); -} -``` - -::: warning IMPORTANT -The bus _must_ be started, always. If requests are timing out, the bus is likely not started. -MassTransit registers a hosted service in the container, which is automatically started by the .NET Generic Host and ASP.NET Core. -::: - -To use the request client, a controller (or a consumer) uses the client via a constructor dependency. - -```csharp -public class RequestController : - Controller -{ - IRequestClient _client; - - public RequestController(IRequestClient client) - { - _client = client; - } - - public async Task Get(string id) - { - var response = await _client.GetResponse(new {OrderId = id}); - - return View(response.Message); - } -} -``` - -The controller method will send the request and return the view once the response has been received. - -### Request Headers - -To create a request and add a header to the `SendContext`, use the callback overload as shown below. - -```cs -await client.GetResponse(new { OrderId = id }, - x => x.UseExecute(context => context.Headers.Set("tenant-id", "some-value"))); -``` - -Calling the `GetResponse` method triggers the request to be sent, after which the caller awaits the response. To add additional response types, see below for the tuple syntax, or just add multiple `GetResponse` methods, passing _false_ for the _readyToSend_ parameter. - -### Multiple Requests - -If there were multiple requests to be performed, it is easy to wait on all results at the same time, benefiting from the concurrent operation. - -```csharp -public class RequestController : - Controller -{ - IRequestClient _clientA; - IRequestClient _clientB; - - public RequestController(IRequestClient clientA, IRequestClient clientB) - { - _clientA = clientA; - _clientB = clientB; - } - - public async Task Get() - { - var resultA = _clientA.GetResponse(new RequestA()); - var resultB = _clientB.GetResponse(new RequestB()); - - await Task.WhenAll(resultA, resultB); - - var a = await resultA; - var b = await resultB; - - var model = new Model(a.Message, b.Message); - - return View(model); - } -} -``` - -The power of concurrency, for the win! - -### Multiple Response Types - -Another powerful feature with the request client is the ability support multiple (such as positive and negative) result types. For example, adding an `OrderNotFound` response type to the consumer as shown eliminates throwing an exception since a missing order isn't really a fault. - -```csharp -public class CheckOrderStatusConsumer : - IConsumer -{ - public async Task Consume(ConsumeContext context) - { - var order = await _orderRepository.Get(context.Message.OrderId); - if (order == null) - await context.RespondAsync(context.Message); - else - await context.RespondAsync(new - { - OrderId = order.Id, - order.Timestamp, - order.StatusCode, - order.StatusText - }); - } -} -``` - -The client can now wait for multiple response types (in this case, two) by using a little tuple magic. - -```csharp -var response = await client.GetResponse(new { OrderId = id}); - -if (response.Is(out Response responseA)) -{ - // do something with the order -} -else if (response.Is(out Response responseB)) -{ - // the order was not found -} -``` - -This cleans up the processing, an eliminates the need to catch a `RequestFaultException`. - -It's also possible to use some of the switch expressions via deconstruction, but this requires the response variable to be explicitly specified as `Response`. - -```cs -Response response = await client.GetResponse(new { OrderId = id}); - -// Using a regular switch statement -switch (response) -{ - case (_, OrderStatusResult a) responseA: - // order found - break; - case (_, OrderNotFound b) responseB: - // order not found - break; -} - -// Or using a switch expression -var accepted = response switch -{ - (_, OrderStatusResult a) => true, - (_, OrderNotFound b) => false, - _ => throw new InvalidOperationException() -}; -``` - -### Request Client Accept Response Types - -The request client sets a message header, `MT-Request-AcceptType`, that contains the response types supported by the request client. This allows the request consumer to determine if the client can handle a response type, which can be useful as services evolve and new response types may be added to handle new conditions. For instance, if a consumer adds a new response type, such as `OrderAlreadyShipped`, if the response type isn't supported an exception may be thrown instead. - -To see this in code, check out the client code: - -```cs -var response = await client.GetResponse(new CancelOrder()); - -if (response.Is(out Response canceled)) -{ - return Ok(); -} -else if (response.Is(out Response responseB)) -{ - return NotFound(); -} -``` - -The original consumer, prior to adding the new response type: - -```cs -public async Task Consume(ConsumeContext context) -{ - var order = _repository.Load(context.Message.OrderId); - if(order == null) - { - await context.ResponseAsync(new { context.Message.OrderId }); - return; - } - - order.Cancel(); - - await context.RespondAsync(new { context.Message.OrderId }); -} -``` - -Now, the new consumer that checks if the order has already shipped: - -```cs -public async Task Consume(ConsumeContext context) -{ - var order = _repository.Load(context.Message.OrderId); - if(order == null) - { - await context.ResponseAsync(new { context.Message.OrderId }); - return; - } - - if(order.HasShipped) - { - if (context.IsResponseAccepted()) - { - await context.RespondAsync(new { context.Message.OrderId, order.ShipDate }); - return; - } - else - throw new InvalidOperationException("The order has already shipped"); // to throw a RequestFaultException in the client - } - - order.Cancel(); - - await context.RespondAsync(new { context.Message.OrderId }); -} -``` - -This way, the consumer can check the request client response types and act accordingly. - -::: tip NOTE -For backwards compatibility, if the new `MT-Request-AcceptType` header is not found, `IsResponseAccepted` will return true for all message types. -::: - -### Request Client Details - -> The internals are documented for understanding, but what follows is optional reading. The above container-based configuration handles all the details to ensure the property context is used. - -The request client is composed of two parts, a client factory, and a request client. The client factory is created from the bus, or a connected endpoint, and has the interface below (some overloads are omitted, but you get the idea). - -```csharp -public interface IClientFactory -{ - IRequestClient CreateRequestClient(ConsumeContext context, Uri destinationAddress, RequestTimeout timeout); - - IRequestClient CreateRequestClient(Uri destinationAddress, RequestTimeout timeout); - - RequestHandle CreateRequest(T request, Uri destinationAddress, CancellationToken cancellationToken, RequestTimeout timeout); - - RequestHandle CreateRequest(ConsumeContext context, T request, Uri destinationAddress, CancellationToken cancellationToken, RequestTimeout timeout); -} -``` - -As shown, the client factory can create a request client, or it can create a request directly. There are advantages to each approach, although it's typically best to create a request client and use it if possible. If a consumer is sending the request, a new client should be created for each message (and is handled automatically if you're using a dependency injection container and the container registration methods). - -To create a client factory, call `bus.CreateClientFactory` or `host.CreateClientFactory` -- after the bus has been started. - -The request client can be used to create requests (returning a `RequestHandle`, which must be disposed after the request completes) or it can be used directly to send a request and get a response (asynchronously, of course). - -> Using `Create` returns a request handle, which can be used to set headers and other attributes of the request before it is sent. - -```csharp -public interface IRequestClient - where TRequest : class -{ - RequestHandle Create(TRequest request, CancellationToken cancellationToken, RequestTimeout timeout); - - Task> GetResponse(TRequest request, CancellationToken cancellationToken, RequestTimeout timeout); -} -``` - -> For `RequestTimeout` three options are available, `None`, `Default`, and a factory with `RequestTimeout.After`. `None` would never be recommended since it would essentially wait forever for a response. There is always a relevant timeout, or you're using the wrong pattern. - -### Sending a Request - -To create a request client, and use it to make a standalone request (not from a consumer, API controller, etc.): - -```csharp -var serviceAddress = new Uri("rabbitmq://localhost/check-order-status"); -var client = bus.CreateRequestClient(serviceAddress); - -var response = await client.GetResponse(new { OrderId = id}); -``` - -The response type, `Response` includes the _MessageContext_ from when the response was received, providing access to the message properties (such as `response.ConversationId`) and headers (`response.Headers`). - - - diff --git a/docs/usage/riders/README.md b/docs/usage/riders/README.md deleted file mode 100644 index 0d4049751ac..00000000000 --- a/docs/usage/riders/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# Riders - -MassTransit includes full support for several transports, most of which are traditional message brokers. RabbitMQ, ActiveMQ, and Azure Service Bus all support topics and queues, as does Amazon SQS when combined with SNS. They all support dispatch and lock messages while they are consumed, and if the client is disconnected, the message is requeued so it is handled by another consumer. They all remove messages from the queue as they are consumed. - -Meanwhile, event streaming has become mainstream, and both Kafka and Event Hub are now commonly used. Coming up with a solution to support these message delivery platforms in MassTransit has been challenging, as many of the concepts and idioms do not apply. New patterns are needed when processing event streams, and it is important to differentiate dispatch style brokers from these new platforms. - -**Riders**, introduced with MassTransit v7, provide a new way to deliver messages from any source to a bus. Riders are configured along with a bus, and board the bus when it is started. Riders have access to receive endpoints, can send and publish messages, and if supported they can _produce_ messages as well. - -To interact with the bus, riders should use either _ConsumeContext_, _ISendEndpointProvider_, or _IPublishEndpoint_ – the implementations of these interfaces will transfer contextual headers to the outgoing messages and will be injected into the consumer and its dependencies by the container. - -To produce messages, the rider-specific producer interfaces should be used (if available). For example, the Kafka rider includes the _ITopicProducer_ interface. - -## Kafka - -Kafka topics can be consumed using MassTransit consumers and sagas, including saga state machines, and messages can be produced to Kafka topics. For details, refer to the [Kafka Rider documentation](/usage/riders/kafka). - -## Azure Event Hub - -Event hubs can be consumed using MassTransit consumers and sagas, including saga state machines, and messages can be produced to event hubs. For details, refer to the [Event Hub Rider documentation](/usage/riders/eventhub). \ No newline at end of file diff --git a/docs/usage/riders/eventhub.md b/docs/usage/riders/eventhub.md deleted file mode 100644 index 3eecffae6bb..00000000000 --- a/docs/usage/riders/eventhub.md +++ /dev/null @@ -1,44 +0,0 @@ -# Event Hub - -Azure Event Hub is included as a [Rider](/usage/riders/), and supports consuming and producing messages from/to Azure event hubs. - -> Uses [MassTransit.Azure.ServiceBus.Core](https://nuget.org/packages/MassTransit.Azure.ServiceBus.Core/), [MassTransit.EventHub](https://nuget.org/packages/MassTransit.EventHub/), [MassTransit.Extensions.DependencyInjection](https://www.nuget.org/packages/MassTransit.Extensions.DependencyInjection/) - -To consume messages from an event hub, configure a Rider within the bus configuration as shown. - -<<< @/docs/code/riders/EventHubConsumer.cs - -The familiar _ReceiveEndpoint_ syntax is used to configure an event hub. The consumer group specified should be unique to the application, and shared by a cluster of service instances for load balancing. Consumers and sagas can be configured on the receive endpoint, which should be registered in the rider configuration. While the configuration for event hubs is the same as a receive endpoint, there is no implicit binding of consumer message types to event hubs (there is no pub-sub using event hub). - -### Configuration - -#### Checkpoint - -Rider implementation is taking full responsibility of Checkpointing, there is no ability to change it. -Checkpointer can be configured on topic bases through next properties: - -| Name | Description | Default | -|:-----------------------|:------------------------------------------------------|:-----| -| CheckpointInterval | Checkpoint frequency based on time | 1 min -| CheckpointMessageCount | Checkpoint every X messages | 5000 -| MessageLimit | Checkpointer buffer size without blocking consumption | 10000 - -> Please note, each topic partition has it's own checkpointer and configuration is applied to partition and not to entire topic. - -During graceful shutdown Checkpointer will try to "checkpoint" all already consumed messages. Force shutdown should be avoided to prevent multiple consumption for the same message. - -#### Scalability -Riders are designed with performance in mind, handling each topic partition withing separate threadpool. As well, allowing to scale-up consumption within same partition by using PartitionKey, as long as keys are different they will be processed concurrently and all this **without** sacrificing ordering. - -| Name | Description | Default | -|:------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----| -| ConcurrentDeliveryLimit | Number of Messages delivered concurrently within same partition + PartitionKey. Increasing this value will **break ordering**, helpful for topics where ordering is not required | 1 -| ConcurrentMessageLimit | Number of Messages processed concurrently witin different keys (preserving ordering). When keys are the same for entire partition `ConcurrentDeliveryLimit` will be used instead | 1 -| PrefetchCount | Number of Messages to prefetch from kafka topic into memory | 1000 - -### Producers - -Producing messages to event hubs uses a producer. In the example below, a messages is produced to the event hub. - -<<< @/docs/code/riders/EventHubProducer.cs - diff --git a/docs/usage/riders/kafka.md b/docs/usage/riders/kafka.md deleted file mode 100644 index c969ff84b71..00000000000 --- a/docs/usage/riders/kafka.md +++ /dev/null @@ -1,100 +0,0 @@ -# Kafka - -Kafka is supported as a [Rider](/usage/riders/), and supports consuming and producing messages from/to Kafka topics. The Confluent .NET client is used, and has been tested with the community edition (running in Docker). - -### Topic Endpoints - -> Uses [MassTransit.RabbitMQ](https://nuget.org/packages/MassTransit.RabbitMQ/), [MassTransit.Kafka](https://nuget.org/packages/MassTransit.Kafka/), [MassTransit.Extensions.DependencyInjection](https://www.nuget.org/packages/MassTransit.Extensions.DependencyInjection/) - -> Note: the following examples are using the RabbitMQ Transport. You can also use InMemory Transport to achieve the same effect when developing. With that, there is no need to install MassTransit.RabbitMQ. -> `x.UsingInMemory((context,config) => config.ConfigureEndpoints(context));` - - -To consume a Kafka topic, configure a Rider within the bus configuration as shown. - -<<< @/docs/code/riders/KafkaConsumer.cs - -A _TopicEndpoint_ connects a Kafka Consumer to a topic, using the specified topic name. The consumer group specified should be unique to the application, and shared by a cluster of service instances for load balancing (but it is possible to consume messages from multiple groups using separate endpoints). Consumers and sagas can be configured on the topic endpoint, which should be registered in the rider configuration. While the configuration for topic endpoints is the same as a receive endpoint, there is no implicit binding of consumer message types to Kafka topics. The message type is specified on the TopicEndpoint as a generic argument. - -#### Wildcard support - -Kafka allows to subscribe to multiple topics by using Regex (also called wildcard) which matches multiple topics: - -<<< @/docs/code/riders/KafkaWildcardConsumer.cs - -### Configuration - -The configuration includes through [Confluent](https://docs.confluent.io/kafka-clients/dotnet/current/overview.html) client configs or using configurators to overrides it with style. - -#### Checkpoint - -Rider implementation is taking full responsibility of Checkpointing, there is no ability to change it. -Checkpointer can be configured on topic bases through next properties: - -| Name | Description | Default | -|:-----------------------|:------------------------------------------------------|:-----| -| CheckpointInterval | Checkpoint frequency based on time | 1 min -| CheckpointMessageCount | Checkpoint every X messages | 5000 -| MessageLimit | Checkpointer buffer size without blocking consumption | 10000 - -> Please note, each topic partition has it's own checkpointer and configuration is applied to partition and not to entire topic. - -During graceful shutdown Checkpointer will try to "checkpoint" all already consumed messages. Force shutdown should be avoided to prevent multiple consumption for the same message. - -#### Scalability -Riders are designed with performance in mind, handling each topic partition withing separate threadpool. As well, allowing to scale-up consumption within same partition by using Key, as long as keys are different they will be processed concurrently and all this **without** sacrificing ordering. - -| Name | Description | Default | -|:------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:-----| -| ConcurrentConsumerLimit | Number of Confluent Consumer instances withing same endpoint | 1 -| ConcurrentDeliveryLimit | Number of Messages delivered concurrently within same partition + key. Increasing this value will **break ordering**, helpful for topics where ordering is not required | 1 -| ConcurrentMessageLimit | Number of Messages processed concurrently witin different keys (preserving ordering). When keys are the same for entire partition `ConcurrentDeliveryLimit` will be used instead | 1 -| PrefetchCount | Number of Messages to prefetch from kafka topic into memory | 1000 - -::: warning -`ConcurrentConsumerLimit` is very powerful setting as Confluent consumer is reading one partition at a time, this will allow creating multiple consumers to read from separate partitions. But having higher number of Consumers than Number of Total Partitions would result of having **idle** consumers -::: - -#### Configure Topology -::: warning -Kafka is not intended to create topology during startup. Topics should be created with correct number of partitions and replicas beforehand -::: - -When client has *required* permissions and `CreateIfMissing` is configured, topic can be created on startup - -<<< @/docs/code/riders/KafkaTopicTopology.cs - -### Producers - -Producing messages to Kafka topics requires the producer to be registered. The producer can then be used to produce messages to the specified Kafka topic. In the example below, messages are produced to the Kafka topic as they are entered by the user. - -<<< @/docs/code/riders/KafkaProducer.cs - -#### Tombstone message - -A record with the same key from the record we want to delete is produced to the same topic and partition with a null payload. These records are called tombstones. -This could be done by setting custom value serializer during produce: - -<<< @/docs/code/riders/KafkaTombstoneProducer.cs - -> Note, `null` message is not possible to consume and will be always skipped - -### Producing and Consuming Multiple Message Types on a Single Topic - -There are situations where you might want to produce / consume events of different types on the same Kafka topic. A common use case is to use a single topic to log ordered meaningful state change events like `SomethingRequested`, `SomethingStarted`, `SomethingFinished`. - -Confluent have some documentation about how this can be implemented on the Schema Registry side: - -- [Confluent Docs - Multiple Event Types in the Same Topic](https://docs.confluent.io/platform/current/schema-registry/serdes-develop/index.html#multiple-event-types-in-the-same-topic) -- [Confluent Docs - Multiple Event Types in the Same Topic with Avro](https://docs.confluent.io/platform/current/schema-registry/serdes-develop/serdes-avro.html#multiple-event-types-in-the-same-topic) -- [Confluent Blog - Multiple Event Types in the Same Topic](https://www.confluent.io/blog/multiple-event-types-in-the-same-kafka-topic/) - -Unfortunately, it is [not yet widely supported in client tools and products](https://docs.confluent.io/platform/current/schema-registry/serdes-develop/index.html#limitations) and there is limited documentation about how to support this in your own applications. - -However, it is possible... The following demo uses the MassTransit Kafka Rider with custom [Avro](https://avro.apache.org/docs/current/) serializer / deserializer implementations and the Schema Registry to support multiple event types on a single topic: - -[MassTransit-Kafka-Demo](https://github.com/danmalcolm/masstransit-kafka-demo) - -The custom serializers / deserializer implementations leverage the wire format used by the standard Confluent schema-based serializers, which includes the schema id in the data stored for each message. This is also good news for interoperability with non-MassTransit applications. - -**Warning: It's a little hacky and only supports the Avro format, but there's enough there to get you started.** diff --git a/docs/usage/sagas/README.md b/docs/usage/sagas/README.md deleted file mode 100644 index 74aa3912c7d..00000000000 --- a/docs/usage/sagas/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Sagas - -The ability to orchestrate a series of events is a powerful feature, and MassTransit makes this possible. - -A saga is a long-lived transaction managed by a coordinator. Sagas are initiated by an event, sagas orchestrate events, and sagas maintain the state of the overall transaction. Sagas are designed to manage the complexity of a distributed transaction without locking and immediate consistency. They manage state and track any compensations required if a partial failure occurs. - -We didn't create it, we learned it from the [original Princeton paper][1] and from Arnon Rotem-Gal-Oz's [description][2]. - -## State Machine Sagas - -MassTransit includes [Automatonymous](automatonymous), which provides a powerful state machine syntax to create sagas. This approach is highly recommended when using MassTransit. - -## Consumer Sagas - -MassTransit supports [consumer sagas](consumer-saga), which implement one or more interfaces to consume correlated saga events. This support is included so that it is easy to move applications from other saga implementations to MassTransit. - -## Guidance - -To address some common questions related to sagas, retries, Outbox, and concurrency, this [page](guidance) has been compiled. - - -[1]: http://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf -[2]: https://arnon.me/wp-content/uploads/Files/SOAPatterns/Saga.pdf - diff --git a/docs/usage/sagas/automatonymous.md b/docs/usage/sagas/automatonymous.md deleted file mode 100644 index a41dc9da9a7..00000000000 --- a/docs/usage/sagas/automatonymous.md +++ /dev/null @@ -1,1243 +0,0 @@ ---- -sidebarDepth: 2 ---- - -# Automatonymous - -## Introduction - -Automatonymous is a state machine library for .NET and provides a C# syntax to define a state machine, including states, events, and behaviors. MassTransit includes Automatonymous, and adds instance storage, event correlation, message binding, request and response support, and scheduling. - -::: tip V8 -Automatonymous is no longer a separate NuGet package and has been assimilated by _MassTransit_. In previous versions, an additional package reference was required. If _Automatonymous_ is referenced, that reference must be removed as it is no longer compatible. -::: - -### State Machine - -A state machine defines the states, events, and behavior of a finite state machine. Implemented as a class, which is derived from `MassTransitStateMachine`, a state machine is created once, and then used to apply event triggered behavior to state machine _instances_. - -```cs -public class OrderStateMachine : - MassTransitStateMachine -{ -} -``` - -### Instance - -An instance contains the data for a state machine _instance_. A new instance is created for every consumed _initial_ event where an existing instance with the same _CorrelationId_ was not found. A saga repository is used to persist instances. Instances are classes, and must implement the `SagaStateMachineInstance` interface. - -```cs -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - InstanceState(x => x.CurrentState); - } -} -``` - -An instance must store the current state, which can be one of three types: - -| Type | Description | -|:-----|:------------| -|State | The interface `State` type. Can be difficult to serialize, typically only used for in-memory instances, but could be used if the repository storage engine supports mapping user types to a storage type. | -|string| Easy, stores the state name. However, it takes a lot of space as the state name is repeated for every instance.| -|int | Small, fast, but requires that each possible state be specified, in order, to assign _int_ values to each state.| - -The _CurrentState_ instance state property is automatically configured if it is a `State`. For `string` or `int` types, the `InstanceState` method must be used. - -To specify the _int_ state values, configure the instance state as shown below. - -```cs -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public int CurrentState { get; set; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - InstanceState(x => x.CurrentState, Submitted, Accepted); - } -} -``` - -This results in the following values: 0 - None, 1 - Initial, 2 - Final, 3 - Submitted, 4 - Accepted - -### State - -States represent previously consumed events resulting in an instance being in a current _state_. An instance can only be in one state at a given time. A new instance defaults to the _Initial_ state, which is automatically defined. The _Final_ state is also defined for all state machines and is used to signify the instance has reached the final state. - -In the example, two states are declared. States are automatically initialized by the _MassTransitStateMachine_ base class constructor. - -```cs -public class OrderStateMachine : - MassTransitStateMachine -{ - public State Submitted { get; private set; } - public State Accepted { get; private set; } -} -``` - -### Event - -An event is something that happened which may result in a state change. An event can add or update instance data, as well as changing an instance's current state. The `Event` is generic, where `T` must be a valid message type. - -In the example below, the _SubmitOrder_ message is declared as an event including how to correlate the event to an instance. - -> Unless events implement `CorrelatedBy`, they must be declared with a correlation expression. - -```cs -public interface SubmitOrder -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId)); - } - - public Event SubmitOrder { get; private set; } -} -``` - -### Behavior - -Behavior is what happens when an _event_ occurs during a _state_. - -Below, the _Initially_ block is used to define the behavior of the _SubmitOrder_ event during the _Initial_ state. When a _SubmitOrder_ message is consumed and an instance with a _CorrelationId_ matching the _OrderId_ is not found, a new instance will be created in the _Initial_ state. The _TransitionTo_ activity transitions the instance to the _Submitted_ state, after which the instance is persisted using the saga repository. - -```cs -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .TransitionTo(Submitted)); - } -} -``` - -Subsequently, the _OrderAccepted_ event could be handled by the behavior shown below. - -```cs -public interface OrderAccepted -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => OrderAccepted, x => x.CorrelateById(context => context.Message.OrderId)); - - During(Submitted, - When(OrderAccepted) - .TransitionTo(Accepted)); - } - - public Event OrderAccepted { get; private set; } -} -``` - -#### Message Order - -Message brokers typically do not guarantee message order. Therefore, it is important to consider out-of-order messages in state machine design. - -In the example above, receiving a _SubmitOrder_ message after an _OrderAccepted_ event could cause the _SubmitOrder_ message to end up in the *_error* queue. If the _OrderAccepted_ event is received first, it would be discarded since it isn't accepted in the _Initial_ state. Below is an updated state machine that handles both of these scenarios. - - -```cs -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .TransitionTo(Submitted), - When(OrderAccepted) - .TransitionTo(Accepted)); - - During(Submitted, - When(OrderAccepted) - .TransitionTo(Accepted)); - - During(Accepted, - Ignore(SubmitOrder)); - } -} -``` - -In the updated example, receiving a _SubmitOrder_ message while in an _Accepted_ state ignores the event. However, data in the event may be useful. In that case, adding behavior to copy the data to the instance could be added. Below, data from the event is captured in both scenarios. - -```cs -public interface SubmitOrder -{ - Guid OrderId { get; } - - DateTime OrderDate { get; } -} - -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .Then(x => x.Saga.OrderDate = x.Message.OrderDate) - .TransitionTo(Submitted), - When(OrderAccepted) - .TransitionTo(Accepted)); - - During(Submitted, - When(OrderAccepted) - .TransitionTo(Accepted)); - - During(Accepted, - When(SubmitOrder) - .Then(x => x.Saga.OrderDate = x.Message.OrderDate)); - } -} -``` - -### Configuration - -To configure a saga state machine: - -```cs -services.AddMassTransit(x => -{ - x.AddSagaStateMachine() - .InMemoryRepository(); -}); -``` - -The example above uses the in-memory saga repository, but any saga repository could be used. The [persistence](persistence.md) section includes details on the supported saga repositories. - -To test the state machine, see the [testing](/usage/testing.md#state-machine-saga) section. - -## Event - -As shown above, an event is a message that can be consumed by the state machine. Events can specify any valid message type, and each event may be configured. There are several event configuration methods available. - -The built-in `CorrelatedBy` interface can be used in a message contract to specify the event `CorrelationId`. - -```cs -public interface OrderCanceled : - CorrelatedBy -{ -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => OrderCanceled); // not required, as it is the default convention - } -} -``` - -While the event is declared explicitly above, it is not required. The default convention will automatically configure events that have a `CorrelatedBy` interface. - -While convenient, some consider the interface an intrusion of infrastructure to the message contract. MassTransit also supports a declarative approach to specifying the `CorrelationId` for events. By configuring the global message topology, it is possible to specify a message property to use for correlation. - -```cs -public interface SubmitOrder -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - // this is shown here, but can be anywhere in the application as long as it executes - // before the state machine instance is created. Startup, etc. is a good place for it. - // It only needs to be called once per process. - static OrderStateMachine() - { - GlobalTopology.Send.UseCorrelationId(x => x.OrderId); - } - - public OrderStateMachine() - { - Event(() => SubmitOrder); - } - - public Event SubmitOrder { get; private set; } -} -``` - -An alternative is to declare the event correlation, as shown below. This should be used when neither of the approaches above are used. - -```cs -public interface SubmitOrder -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => SubmitOrder, x => x.CorrelateById(context => context.Message.OrderId)); - } - - public Event SubmitOrder { get; private set; } -} -``` - -Since `OrderId` is a `Guid`, it can be used for event correlation. When `SubmitOrder` is accepted in the _Initial_ state, and because the _OrderId_ is a _Guid_, the `CorrelationId` on the new instance is automatically assigned the _OrderId_ value. - -Events can also be correlated using a query expression, which is required when events are not correlated to the instance's _CorrelationId_ property. Queries are more expensive, and may match multiple instances, which should be considered when designing state machines and events. - -> Whenever possible, try to correlation using the CorrelationId. If a query is required, it may be necessary to create an index on the property so that database queries are optimized. - -To correlate events using another type, additional configuration is required. - -```cs -public interface ExternalOrderSubmitted -{ - string OrderNumber { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => ExternalOrderSubmitted, e => e - .CorrelateBy(i => i.OrderNumber, x => x.Message.OrderNumber) - .SelectId(x => NewId.NextGuid())); - } - - public Event ExternalOrderSubmitted { get; private set; } -} -``` - -Queries can also be written with two arguments, which are passed directly to the repository (and must be supported by the backing database). - -```cs -public interface ExternalOrderSubmitted -{ - string OrderNumber { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => ExternalOrderSubmitted, e => e - .CorrelateBy((instance,context) => instance.OrderNumber == context.Message.OrderNumber) - .SelectId(x => NewId.NextGuid())); - } - - public Event ExternalOrderSubmitted { get; private set; } -} -``` - -When the event doesn't have a _Guid_ that uniquely correlates to an instance, the `.SelectId` expression must be configured. In the above example, [NewId](https://www.nuget.org/packages/NewId) is used to generate a sequential identifier which will be assigned to the instance _CorrelationId_. Any property on the event can be used to initialize the _CorrelationId_. - -::: warning -Initial events that do not correlate on CorrelationId, and use `SelectId` to generate a _CorrelationId_ should use a unique constraint on the instance property (_OrderNumber_ in this example) to avoid duplicate instances. If two events correlate to the same property value at the same time, only one of the two will be able to store the instance, the other will fail (and, if retry is configured, which it should be when using a saga) and retry at which time the event will be dispatched based upon the current instance state (which is likely no longer Initial). Failure to apply a unique constraint (on _OrderNumber_ in this example) will result in duplicates. -::: - -The message headers are also available, for example, instead of always generating a new identifier, the _CorrelationId_ header could be used if present. - -```cs - .SelectId(x => x.CorrelationId ?? NewId.NextGuid()); -``` - -::: tip -Event correlation is critical, and should be consistently applied to all events on a state machine. Consider how events received in different orders may affect subsequent event correlations. -::: - -### Ignore Event - -It may be necessary to ignore an event in a given state, either to avoid fault generation, or to prevent messages from being moved to the *_skipped* queue. To ignore an event in a state, use the `Ignore` method. - -```cs -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .TransitionTo(Submitted), - When(OrderAccepted) - .TransitionTo(Accepted)); - - During(Submitted, - When(OrderAccepted) - .TransitionTo(Accepted)); - - During(Accepted, - Ignore(SubmitOrder)); - } -} -``` - -### Composite Event - -A composite event is configured by specifying one or more events that must be consumed, after which the composite event will be raised. A composite event uses an instance property to keep track of the required events, which is specified during configuration. - -To define a composite event, the required events must first be configured along with any event behaviors, after which the composite event can be configured. - -```cs -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public int ReadyEventStatus { get; set; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .TransitionTo(Submitted), - When(OrderAccepted) - .TransitionTo(Accepted)); - - During(Submitted, - When(OrderAccepted) - .TransitionTo(Accepted)); - - CompositeEvent(() => OrderReady, x => x.ReadyEventStatus, SubmitOrder, OrderAccepted); - - DuringAny( - When(OrderReady) - .Then(context => Console.WriteLine("Order Ready: {0}", context.Saga.CorrelationId))); - } - - public Event OrderReady { get; private set; } -} -``` - -Once the _SubmitOrder_ and _OrderAccepted_ events have been consumed, the _OrderReady_ event will be triggered. - -::: warning -The order of events being declared can impact the order in which they execute. Therefore, it is best to declare composite events at the end of the state machine declaration, after all other events and behaviors are declared. That way, the composite events will be raised _after_ the dependent event behaviors. -::: - -### Missing Instance - -If an event is not matching to an instance, the missing instance behavior can be configured. - -```cs -public interface RequestOrderCancellation -{ - Guid OrderId { get; } -} - -public interface OrderNotFound -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => OrderCancellationRequested, e => - { - e.CorrelateById(context => context.Message.OrderId); - - e.OnMissingInstance(m => - { - return m.ExecuteAsync(x => x.RespondAsync(new { x.OrderId })); - }); - }); - } - - public Event OrderCancellationRequested { get; private set; } -} - -``` - -In this example, when a cancel order request is consumed without a matching instance, a response will be sent that the order was not found. Instead of generating a `Fault`, the response is more explicit. Other missing instance options include `Discard`, `Fault`, and `Execute` (a synchronous version of _ExecuteAsync_). - -### Initial Insert - -To increase new instance performance, configuring an event to directly insert into a saga repository may reduce lock contention. To configure an event to insert, it should be in the _Initially_ block, as well as have a saga factory specified. - -```cs -public interface SubmitOrder -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => SubmitOrder, e => - { - e.CorrelateById(context => context.Message.OrderId)); - - e.InsertOnInitial = true; - e.SetSagaFactory(context => new OrderState - { - CorrelationId = context.Message.OrderId - }) - }); - - Initially( - When(SubmitOrder) - .TransitionTo(Submitted)); - } - - public Event SubmitOrder { get; private set; } -} -``` - -When using _InsertOnInitial_, it is critical that the saga repository is able to detect duplicate keys (in this case, _CorrelationId_ - which is initialized using _OrderId_). In this case, having a clustered primary key on _CorrelationId_ would prevent duplicate instances from being inserted. If an event is correlated using a different property, make sure that the database enforces a unique constraint on the instance property and the saga factory initializes the instance property with the event property value. - -```cs -public interface ExternalOrderSubmitted -{ - string OrderNumber { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => ExternalOrderSubmitted, e => - { - e.CorrelateBy(i => i.OrderNumber, x => x.Message.OrderNumber) - e.SelectId(x => NewId.NextGuid()); - - e.InsertOnInitial = true; - e.SetSagaFactory(context => new OrderState - { - CorrelationId = context.CorrelationId ?? NewId.NextGuid(), - OrderNumber = context.Message.OrderNumber, - }) - }); - - Initially( - When(SubmitOrder) - .TransitionTo(Submitted)); - } - - public Event ExternalOrderSubmitted { get; private set; } -} -``` - -The database would use a unique constraint on the _OrderNumber_ to prevent duplicates, which the saga repository would detect as an existing instance, which would then be loaded to consume the event. - -### Completed Instance - -By default, instances are not removed from the saga repository. To configure completed instance removal, specify the method used to determine if an instance has completed. - -```cs -public interface OrderCompleted -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => OrderCompleted, x => x.CorrelateById(context => context.Message.OrderId)); - - DuringAny( - When(OrderCompleted) - .Finalize()); - - SetCompletedWhenFinalized(); - } - - public Event OrderCompleted { get; private set; } -} -``` - -When the instance consumes the _OrderCompleted_ event, the instance is finalized (which transitions the instance to the _Final_ state). The `SetCompletedWhenFinalized` method defines an instance in the _Final_ state as completed – which is then used by the saga repository to remove the instance. - -To use a different completed expression, such as one that checks if the instance is in a _Completed_ state, use the `SetCompleted` method as shown below. - -```cs -public interface OrderCompleted -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => OrderCompleted, x => x.CorrelateById(context => context.Message.OrderId)); - - DuringAny( - When(OrderCompleted) - .TransitionTo(Completed)); - - SetCompleted(async instance => - { - State currentState = await this.GetState(instance); - - return Completed.Equals(currentState); - }); - } - - public State Completed { get; private set; } - public Event OrderCompleted { get; private set; } -} -``` - -## Activities - -State machine behaviors are defined as a sequence of activities which are executed in response to an event. In addition to the activities included with Automatonymous, MassTransit includes activities to send, publish, and schedule messages, as well as initiate and respond to requests. - -### Publish - -To publish an event, add a `Publish` activity. - -```cs -public interface OrderSubmitted -{ - Guid OrderId { get; } -} - -public class OrderSubmittedEvent : - OrderSubmitted -{ - public OrderSubmittedEvent(Guid orderId) - { - OrderId = orderId; - } - - public Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .Publish(context => (OrderSubmitted)new OrderSubmittedEvent(context.Saga.CorrelationId)) - .TransitionTo(Submitted)); - } -} -``` - -Alternatively, a message initializer can be used to eliminate the _Event_ class. - -```cs -public interface OrderSubmitted -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .PublishAsync(context => context.Init(new { OrderId = context.Saga.CorrelationId })) - .TransitionTo(Submitted)); - } -} -``` - -### Send - -To send a message, add a `Send` activity. - -```cs -public interface UpdateAccountHistory -{ - Guid OrderId { get; } -} - -public class UpdateAccountHistoryCommand : - UpdateAccountHistory -{ - public UpdateAccountHistoryCommand(Guid orderId) - { - OrderId = orderId; - } - - public Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine(OrderStateMachineSettings settings) - { - Initially( - When(SubmitOrder) - .Send(settings.AccountServiceAddress, context => new UpdateAccountHistoryCommand(context.Saga.CorrelationId)) - .TransitionTo(Submitted)); - } -} -``` - -Alternatively, a message initializer can be used to eliminate the _Command_ class. - -```cs -public interface UpdateAccountHistory -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine(OrderStateMachineSettings settings) - { - Initially( - When(SubmitOrder) - .SendAsync(settings.AccountServiceAddress, context => context.Init(new { OrderId = context.Saga.CorrelationId })) - .TransitionTo(Submitted)); - } -} -``` - -### Respond - -A state machine can respond to requests by configuring the request message type as an event, and using the `Respond` method. When configuring a request event, configuring a missing instance method is recommended, to provide a better response experience (either through a different response type, or a response that indicates an instance was not found). - -```cs -public interface RequestOrderCancellation -{ - Guid OrderId { get; } -} - -public interface OrderCanceled -{ - Guid OrderId { get; } -} - -public interface OrderNotFound -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Event(() => OrderCancellationRequested, e => - { - e.CorrelateById(context => context.Message.OrderId); - - e.OnMissingInstance(m => - { - return m.ExecuteAsync(x => x.RespondAsync(new { x.OrderId })); - }); - }); - - DuringAny( - When(OrderCancellationRequested) - .RespondAsync(context => context.Init(new { OrderId = context.Saga.CorrelationId })) - .TransitionTo(Canceled)); - } - - public State Canceled { get; private set; } - public Event OrderCancellationRequested { get; private set; } -} -``` - -There are scenarios where it is required to _wait_ for the response from the state machine. In these scenarios the information that is required to respond to the original request should be stored. - -```cs -public record CreateOrder(Guid CorrelationId) : CorrelatedBy; - -public record ProcessOrder(Guid OrderId, Guid ProcessingId); - -public record OrderProcessed(Guid OrderId, Guid ProcessingId); - -public record OrderCancelled(Guid OrderId, string Reason); - -public class ProcessOrderConsumer : IConsumer -{ - public async Task Consume(ConsumeContext context) - { - await context.RespondAsync(new OrderProcessed(context.Message.OrderId, context.Message.ProcessingId)); - } -} - -public class OrderState : SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - public Guid? ProcessingId { get; set; } - public Guid? RequestId { get; set; } - public Uri ResponseAddress { get; set; } - public Guid OrderId { get; set; } -} - -public class OrderStateMachine : MassTransitStateMachine -{ - public State Created { get; set; } - - public State Cancelled { get; set; } - - public Event OrderSubmitted { get; set; } - - public Request ProcessOrder { get; set; } - - public OrderStateMachine() - { - InstanceState(m => m.CurrentState); - Event(() => OrderSubmitted); - Request(() => ProcessOrder, order => order.ProcessingId, config => { config.Timeout = TimeSpan.Zero; }); - - Initially( - When(OrderSubmitted) - .Then(context => - { - context.Saga.CorrelationId = context.Message.CorrelationId; - context.Saga.ProcessingId = Guid.NewGuid(); - - context.Saga.OrderId = Guid.NewGuid(); - - context.Saga.RequestId = context.RequestId; - context.Saga.ResponseAddress = context.ResponseAddress; - }) - .Request(ProcessOrder, context => new ProcessOrder(context.Saga.OrderId, context.Saga.ProcessingId!.Value)) - .TransitionTo(ProcessOrder.Pending)); - - During(ProcessOrder.Pending, - When(ProcessOrder.Completed) - .TransitionTo(Created) - .ThenAsync(async context => - { - var endpoint = await context.GetSendEndpoint(context.Saga.ResponseAddress); - await endpoint.Send(context.Saga, r => r.RequestId = context.Saga.RequestId); - }), - When(ProcessOrder.Faulted) - .TransitionTo(Cancelled) - .ThenAsync(async context => - { - var endpoint = await context.GetSendEndpoint(context.Saga.ResponseAddress); - await endpoint.Send(new OrderCancelled(context.Saga.OrderId, "Faulted"), r => r.RequestId = context.Saga.RequestId); - }), - When(ProcessOrder.TimeoutExpired) - .TransitionTo(Cancelled) - .ThenAsync(async context => - { - var endpoint = await context.GetSendEndpoint(context.Saga.ResponseAddress); - await endpoint.Send(new OrderCancelled(context.Saga.OrderId, "Time-out"), r => r.RequestId = context.Saga.RequestId); - })); - } -} -``` - -### Schedule - -::: tip NOTE -The bus must be configured to include a message scheduler to use the scheduling activities. See the [scheduling](/advanced/scheduling/) section to learn how to setup a message scheduler. -::: - -A state machine can schedule events, which uses the message scheduler to schedule a message for delivery to the instance. First, the schedule must be declared. - -```cs {1-4,12,20-25,28} -public interface OrderCompletionTimeoutExpired -{ - Guid OrderId { get; } -} - -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public Guid? OrderCompletionTimeoutTokenId { get; set; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Schedule(() => OrderCompletionTimeout, instance => instance.OrderCompletionTimeoutTokenId, s => - { - s.Delay = TimeSpan.FromDays(30); - - s.Received = r => r.CorrelateById(context => context.Message.OrderId); - }); - } - - public Schedule OrderCompletionTimeout { get; private set; } -} -``` - -The configuration specifies the _Delay_, which can be overridden by the schedule activity, and the correlation expression for the _Received_ event. The state machine can consume the _Received_ event as shown. The _OrderCompletionTimeoutTokenId_ is a `Guid?` instance property used to keep track of the scheduled message _tokenId_ which can later be used to unschedule the event. - -```cs {12} -public interface OrderCompleted -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - During(Accepted, - When(OrderCompletionTimeout.Received) - .PublishAsync(context => context.Init(new { OrderId = context.Saga.CorrelationId })) - .Finalize()); - } - - public Schedule OrderCompletionTimeout { get; private set; } -} -``` - -The event can be scheduled using the `Schedule` activity. - -```cs {8} -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - During(Submitted, - When(OrderAccepted) - .Schedule(OrderCompletionTimeout, context => context.Init(new { OrderId = context.Saga.CorrelationId })) - .TransitionTo(Accepted)); - } -} -``` - -As stated above, the _delay_ can be overridden by the _Schedule_ activity. Both instance and message (_context.Data_) content can be used to calculate the delay. - -```cs {14-15} -public interface OrderAccepted -{ - Guid OrderId { get; } - TimeSpan CompletionTime { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - During(Submitted, - When(OrderAccepted) - .Schedule(OrderCompletionTimeout, context => context.Init(new { OrderId = context.Saga.CorrelationId }), - context => context.Message.CompletionTime) - .TransitionTo(Accepted)); - } -} -``` - -Once the scheduled event is received, the `OrderCompletionTimeoutTokenId` property is cleared. - -If the scheduled event is no longer needed, the _Unschedule_ activity can be used. - -```cs {15} -public interface OrderAccepted -{ - Guid OrderId { get; } - TimeSpan CompletionTime { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - DuringAny( - When(OrderCancellationRequested) - .RespondAsync(context => context.Init(new { OrderId = context.Saga.CorrelationId })) - .Unschedule(OrderCompletionTimeout) - .TransitionTo(Canceled)); - } -} -``` - -### Request - -A request can be sent by a state machine using the _Request_ method, which specifies the request type and the response type. Additional request settings may be specified, including the _ServiceAddress_ and the _Timeout_. - -If the _ServiceAddress_ is specified, it should be the endpoint address of the service that will respond to the request. If not specified, the request will be published. - -The default _Timeout_ is thirty seconds but any value greater than or equal to `TimeSpan.Zero` can be specified. When a request is sent with a timeout greater than zero, a _TimeoutExpired_ message is scheduled. Specifying `TimeSpan.Zero` will not schedule a timeout message and the request will never time out. - -::: tip NOTE -When a _Timeout_ greater than `Timespan.Zero` is configured, a message scheduler must be configured. See the [scheduling](/advanced/scheduling/) section for details on configuring a message scheduler. -::: - -When defining a `Request`, an instance property _should_ be specified to store the _RequestId_ which is used to correlate responses to the state machine instance. While the request is pending, the _RequestId_ is stored in the property. When the request has completed the property is cleared. If the request times out or faults, the _RequestId_ is retained to allow for later correlation if requests are ultimately completed (such as moving requests from the *_error* queue back into the service queue). - -A recent enhancement making this property optional, instead using the instance's `CorrelationId` for the request message `RequestId`. This can simplify response correlation, and also avoids the need of a supplemental index on the saga repository. However, reusing the `CorrelationId` for the request might cause issues in highly complex systems. So consider this when choosing which method to use. - -#### Configuration - -To declare a request, add a `Request` property and configure it using the `Request` method. - -```cs -public interface ProcessOrder -{ - Guid OrderId { get; } -} - -public interface OrderProcessed -{ - Guid OrderId { get; } - Guid ProcessingId { get; } -} - -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public Guid? ProcessOrderRequestId { get; set; } - public Guid? ProcessingId { get; set; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine(OrderStateMachineSettings settings) - { - Request( - () => ProcessOrder, - x => x.ProcessOrderRequestId, // Optional - r => { - r.ServiceAddress = settings.ProcessOrderServiceAddress; - r.Timeout = settings.RequestTimeout; - }); - } - - public Request ProcessOrder { get; private set; } -} -``` - -Once defined, the request activity can be added to a behavior. - -```cs -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - During(Submitted, - When(OrderAccepted) - .Request(ProcessOrder, x => x.Init(new { OrderId = x.Saga.CorrelationId})) - .TransitionTo(ProcessOrder.Pending)); - - During(ProcessOrder.Pending, - When(ProcessOrder.Completed) - .Then(context => context.Saga.ProcessingId = context.Message.ProcessingId) - .TransitionTo(Processed), - When(ProcessOrder.Faulted) - .TransitionTo(ProcessFaulted), - When(ProcessOrder.TimeoutExpired) - .TransitionTo(ProcessTimeoutExpired)); - } - - public State Processed { get; private set; } - public State ProcessFaulted { get; private set; } - public State ProcessTimeoutExpired { get; private set; } -} -``` - -The _Request_ includes three events: _Completed_, _Faulted_, and _TimeoutExpired_. These events can be consumed during any state, however, the _Request_ includes a _Pending_ state which can be used to avoid declaring a separate pending state. - -::: tip NOTE -The request timeout is scheduled using the message scheduler, and the scheduled message is canceled when a response or fault is received. Not all message schedulers support cancellation, so it may be necessary to _Ignore_ the `TimeoutExpired` event in subsequent states. -::: - -### Custom - -There are scenarios when an event behavior may have dependencies that need to be managed at a scope level, such as a database connection, or the complexity is best encapsulated in a separate class rather than being part of the state machine itself. Developers can create their own activities for state machine use, and optionally create their own extension methods to add them to a behavior. - -To create an activity, create a class that implements `IStateMachineActivity` as shown. - -```cs -public class PublishOrderSubmittedActivity : - IStateMachineActivity -{ - readonly ConsumeContext _context; - - public PublishOrderSubmittedActivity(ConsumeContext context) - { - _context = context; - } - - public void Probe(ProbeContext context) - { - context.CreateScope("publish-order-submitted"); - } - - public void Accept(StateMachineVisitor visitor) - { - visitor.Visit(this); - } - - public async Task Execute(BehaviorContext context, IBehavior next) - { - // do the activity thing - await _context.Publish(new { OrderId = context.Saga.CorrelationId }).ConfigureAwait(false); - - // call the next activity in the behavior - await next.Execute(context).ConfigureAwait(false); - } - - public Task Faulted(BehaviorExceptionContext context, - IBehavior next) - where TException : Exception - { - return next.Faulted(context); - } -} -``` - -Once created, configure the activity in a state machine as shown. - -```cs -public interface OrderSubmitted -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .Activity(x => x.OfType()) - .TransitionTo(Submitted)); - } -} -``` - -When the `SubmitOrder` event is consumed, the state machine will resolve the activity from the container, and call the `Execute` method. The activity will be scoped, so any dependencies will be resolved within the message `ConsumeContext`. - -In the above example, the event type was known in advance. If an activity for any event type is needed, it can be created without specifying the event type. - -```cs -public class PublishOrderSubmittedActivity : - IStateMachineActivity -{ - readonly ConsumeContext _context; - - public PublishOrderSubmittedActivity(ConsumeContext context) - { - _context = context; - } - - public void Probe(ProbeContext context) - { - context.CreateScope("publish-order-submitted"); - } - - public void Accept(StateMachineVisitor visitor) - { - visitor.Visit(this); - } - - public async Task Execute(BehaviorContext context, IBehavior next) - { - await _context.Publish(new { OrderId = context.Saga.CorrelationId }).ConfigureAwait(false); - - await next.Execute(context).ConfigureAwait(false); - } - - public async Task Execute(BehaviorContext context, IBehavior next) - { - await _context.Publish(new { OrderId = context.Saga.CorrelationId }).ConfigureAwait(false); - - await next.Execute(context).ConfigureAwait(false); - } - - public Task Faulted(BehaviorExceptionContext context, IBehavior next) - where TException : Exception - { - return next.Faulted(context); - } - - public Task Faulted(BehaviorExceptionContext context, IBehavior next) - where TException : Exception - { - return next.Faulted(context); - } -} -``` - -To register an instance activity, use the following syntax. - -```cs -public interface OrderSubmitted -{ - Guid OrderId { get; } -} - -public class OrderStateMachine : - MassTransitStateMachine -{ - public OrderStateMachine() - { - Initially( - When(SubmitOrder) - .Activity(x => x.OfInstanceType()) - .TransitionTo(Submitted)); - } -} -``` - -[2]: https://github.com/MassTransit/Sample-ShoppingWeb diff --git a/docs/usage/sagas/azure-table.md b/docs/usage/sagas/azure-table.md deleted file mode 100644 index 203457a5b91..00000000000 --- a/docs/usage/sagas/azure-table.md +++ /dev/null @@ -1,57 +0,0 @@ -# Azure Table - -Azure Tables are exposed in two ways in Azure - via Storage accounts & via the premium offering within Cosmos DB APIs. This persistence supports both implementations and behind the curtains uses the Microsoft.Azure.Cosmos.Table library for communication. - -::: tip NOTE -Azure Tables currently only supports Optimistic Concurrency. Mass Transit manages the ETag property in Payload Context and uses this property for state machine updates. Concurrency errors can be spotted in logs via standard "Precondition Failed" errors from Table Storage. -::: - -::: warning -Be sure to set DateTime properties as nullable when updated later in the saga. Failure to do this can result in 400 bad requests from Table Storage. -::: - -```cs {10} -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } -} -``` - -### Container Integration - -To configure a Table as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. - -```cs -CloudTable cloudTable; -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .AzureTableRepository(endpointUri, key, r => - { - cfg.ConnectionFactory(() => cloudTable); - }); -}); -``` - -The container extension will register the saga repository in the container. For more details on container configuration, review the [container configuration](/usage/containers/) section of the documentation. - -To configure the saga repository with a specific key formatter, use the code shown below with _KeyFormatter_ configuration extension. - -```cs -CloudTable cloudTable; -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .AzureTableRepository(endpointUri, key, r => - { - cfg.ConnectionFactory(() => cloudTable); - cfg.KeyFormatter(() => new ConstRowSagaKeyFormatter(typeof(OrderState).Name))) - }); -}); -``` - -Unlike the default `ConstPartitionSagaKeyFormatter`, `ConstRowSagaKeyFormatter` in this example uses `PartitionKey` to store the correlationId which may benefit from [scale-out capability of Tables](https://docs.microsoft.com/en-us/rest/api/storageservices/designing-a-scalable-partitioning-strategy-for-azure-table-storage#scalability). diff --git a/docs/usage/sagas/consumer-saga.md b/docs/usage/sagas/consumer-saga.md deleted file mode 100644 index c56b5a828c1..00000000000 --- a/docs/usage/sagas/consumer-saga.md +++ /dev/null @@ -1,151 +0,0 @@ -# Consumer Sagas - -Consumer sagas use a class, similar to a consumer, and declare interfaces for the correlated event types. - -Consumer sagas combined data and behavior in a single class. In the above example, a new saga instance is created by the _SubmitOrder_ message. - -```cs {16} -public interface SubmitOrder : - CorrelatedBy -{ - DateTime OrderDate { get; } -} - -public class OrderSaga : - ISaga, - InitiatedBy -{ - public Guid CorrelationId { get; set; } - - public DateTime? SubmitDate { get; set; } - public DateTime? AcceptDate { get; set; } - - public async Task Consume(ConsumeContext context) - { - SubmitDate = context.Message.OrderDate; - } -} -``` - -To add the _OrderAccepted_ message to the saga, an additional interface and method is specified. - -```cs {19} -public interface OrderAccepted : - CorrelatedBy -{ - DateTime Timestamp { get; } -} - -public class OrderSaga : - ISaga, - InitiatedBy, - Orchestrates, -{ - public Guid CorrelationId { get; set; } - - public DateTime? SubmitDate { get; set; } - public DateTime? AcceptDate { get; set; } - - public async Task Consume(ConsumeContext context) {...} - - public async Task Consume(ConsumeContext context) - { - AcceptDate = context.Message.Timestamp; - } -} -``` - -To add the _OrderShipped_ message to the saga, which is correlated by a separate property, an additional interface and method is specified. - -```cs {22,27-28} -public interface OrderShipped -{ - Guid OrderId { get; } - DateTime ShipDate { get; } -} - -public class OrderSaga : - ISaga, - InitiatedBy, - Orchestrates, - Observes -{ - public Guid CorrelationId { get; set; } - - public DateTime? SubmitDate { get; set; } - public DateTime? AcceptDate { get; set; } - public DateTime? ShipDate { get; set; } - - public async Task Consume(ConsumeContext context) {...} - public async Task Consume(ConsumeContext context) {...} - - public async Task Consume(ConsumeContext context) - { - ShipDate = context.Message.ShipDate; - } - - public Expression> CorrelationExpression => - (saga,message) => saga.CorrelationId == message.OrderId; -} -``` - -In some cases, a single message may either initiate a new saga instance or orchestrate an existing instance. Introduced in version 7.0.7, the `InitiatedByOrOrchestrates` interface supports this in a consumer saga. - -```cs -public interface OrderInvoiced : - CorrelatedBy -{ - DateTime Timestamp { get; } - decimal Amount { get; } -} - -public class OrderPaymentSaga : - ISaga, - InitiatedByOrOrchestrates -{ - public Guid CorrelationId { get; set; } - - public DateTime? InvoiceDate { get; set; } - public decimal? Amount { get; set; } - - public async Task Consume(ConsumeContext context) - { - InvoiceDate = context.Message.Timestamp; - Amount = context.Message.Amount; - } -} -``` - -If you're using a container, saga registration is fully supported. The example below configures the saga using an in-memory repository with an in-memory transport. - -```cs -services.AddMassTransit(x => -{ - x.AddSaga() - .InMemoryRepository(); - - x.UsingInMemory((context, cfg) => - { - cfg.ConfigureEndpoints(context); - }); -}); -``` - -If using the legacy configuration syntax, the saga can be configured on a receive endpoint using the `.Saga` method. - -```cs -var repository = new InMemorySagaRepository(); - -var busControl = Bus.Factory.CreateUsingInMemory(cfg => -{ - cfg.ReceiveEndpoint("order-saga", e => - { - e.Saga(repository); - }); -}); -``` - -### Container Registration - - -The configuration for the various supported saga persistence storage engines is detailed in the [persistence](persistence.md) documentation. diff --git a/docs/usage/sagas/cosmos.md b/docs/usage/sagas/cosmos.md deleted file mode 100644 index f945e547a1b..00000000000 --- a/docs/usage/sagas/cosmos.md +++ /dev/null @@ -1,76 +0,0 @@ -# Cosmos DB - -DocumentDb is the predecessor of Azure Cosmos DB. Microsoft now refers to the DocumentDb API as the Core (SQL) API. MassTransit supports saga persistence in Cosmos by using both MongoDb API (using the `MassTransit.MongoDb` package) or using the Core (SQL) API (using the `MassTransit.Azure.Cosmos` package). - -Out of the box, the only additional saga property required to use Cosmos DB is _ETag_, which is managed by Cosmos for optimistic concurrency. Once added to your saga class, when using the container configuration method below, the class is properly configured to map the _CorrelationId_ and _ETag_ properties to the associated `id` and `_etag` properties. - -```cs {10} -public class OrderState : - SagaStateMachineInstance, - IVersionedSaga -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } - - public string ETag { get; set; } -} -``` - -### Container Integration - -To configure Cosmos DB as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. - -```cs {4-10} -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .CosmosRepository(endpointUri, key, r => - { - r.DatabaseId = "production-db"; // required - - // kebab case formatter is used by default if not specified (OrderState -> order-state) - r.CollectionId = "sagas"; - }); -}); -``` - -To use the CosmosDb emulator, specify it in the configuration. - -```cs {4-9} -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .CosmosRepository(r => - { - r.ConfigureEmulator(); - - r.DatabaseId = "test"; - }); -}); -``` - -The container extension will register the saga repository in the container. For more details on container configuration, review the [container configuration](/usage/containers/) section of the documentation. - -### Other Considerations - -Cosmos DB requires that any document stored there has a property called `id`, to be used as the document identity. Saga instances have `CorrelationId` for the same purpose, so there are two ways to create your Cosmos DB saga class, which can have different implications depending on your usage. ETag must also be present, which is used for optimistic concurrency. Please never set this property yourself, it managed 100% by Cosmos DB. - -If event correlation expressions are used which include the _CorrelationId_ property, it's important to add the JSON property names that match what's used in Cosmos DB. - -```cs {5,11} -public class OrderState : - SagaStateMachineInstance, - IVersionedSaga -{ - [JsonProperty("id")] - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } - - [JsonProperty("_etag")] - public string ETag { get; set; } -} -``` diff --git a/docs/usage/sagas/dapper.md b/docs/usage/sagas/dapper.md deleted file mode 100644 index f6240a6d80f..00000000000 --- a/docs/usage/sagas/dapper.md +++ /dev/null @@ -1,54 +0,0 @@ -# Dapper - -[MassTransit.Dapper](https://www.nuget.org/packages/MassTransit.Dapper) - -[Dapper][1] is a [super lightweight Micro-ORM][2] usable for saga persistence with Microsoft SQL Server. Dapper.Contrib is used for inserts and updates. The methods are virtual, so if you'd rather write the SQL yourself it is supported. - -If you do not write your own sql, the model requires you use the `ExplicitKey` attribute for the `CorrelationId`. And if you have properties that are not available as columns, you can use the `Computed` attribute to not include them in the generated SQL. If you are using event correlation using other properties, it's highly recommended that you create indices for performance. - -```csharp -public class OrderState : - SagaStateMachineInstance -{ - [ExplicitKey] - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } -} -``` - -### Container Integration - -To configure Dapper as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. - -```cs {4} -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .DapperRepository(connectionString); -}); -``` - -The container extension will register the saga repository in the container. For more details on container configuration, review the [container configuration](/usage/containers/) section of the documentation. - -### Limitations - -#### Table Names - -The tablename can only be the pluralized form of the class name. So `OrderState` would translate to table OrderState**s**. This applies even if you write your own SQL for updates and inserts. - -#### Correlation Expressions - -The expressions you can use for correlation is somewhat limited. These types of expressions are handled: - -```cs - x => x.CorrelationId == someGuid; - x => x.IsDone; - x => x.CorrelationId == someGuid && x.IsDone; -``` - -You can use multiple `&&` in the expression. What you can not use is `||` and negations. So a bool used like this `x.IsDone` can only be handled as true and nothing else. - -[1]: https://dapper-tutorial.net/ -[2]: https://github.com/StackExchange/Dapper diff --git a/docs/usage/sagas/ef.md b/docs/usage/sagas/ef.md deleted file mode 100644 index cb9d346564b..00000000000 --- a/docs/usage/sagas/ef.md +++ /dev/null @@ -1,122 +0,0 @@ -# Entity Framework - -> [MassTransit.EntityFramework](https://www.nuget.org/packages/MassTransit.EntityFramework) - -Entity Framework is a commonly used ORM used with SQL. - -An example saga instance is shown below, which is orchestrated using an Automatonymous state machine. The _CorrelationId_ will be the primary key, and _CurrentState_ will be used to store the current state of the saga instance. - -```cs -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } - - // If using Optimistic concurrency, this property is required - public byte[] RowVersion { get; set; } -} -``` - -The instance properties are configured using a _SagaClassMap_. - -::: warning Important -The `SagaClassMap` has a default mapping for the `CorrelationId` as the primary key. If you create your own mapping, you must follow the same convention, or at least make it a Clustered Index + Unique, otherwise you will likely experience deadlock exceptions and/or performance issues in high throughput scenarios. -::: - -```cs -public class OrderStateMap : - SagaClassMap -{ - protected override void Configure(EntityTypeBuilder entity, ModelBuilder model) - { - entity.Property(x => x.CurrentState).Length(64); - entity.Property(x => x.OrderDate); - - // If using Optimistic concurrency, otherwise remove this property - entity.Property(x => x.RowVersion).IsRowVersion(); - } -} -``` - -Include the instance map in a _DbContext_ class that will be used by the saga repository. - -```cs -public class OrderStateDbContext : - SagaDbContext -{ - public OrderStateDbContext(string nameOrConnectionString) - : base(nameOrConnectionString) - { - } - - protected override IEnumerable Configurations - { - get { yield return new OrderStateMap(); } - } -} -``` - -::: warning Important -In case your application has its own `DbContext` with a custom `DbConfiguration`, it is mandatory to use the config file / static method call approach in order to set the `DbConfiguration`. More details can be found on [Code-based configuration - Setting DbConfiguration explicitly](https://docs.microsoft.com/en-us/ef/ef6/fundamentals/configuring/code-based#setting-dbconfiguration-explicitly). -::: - -### Container Integration - -Once the class map and associated _DbContext_ class have been created, the saga repository can be configured with the saga registration, which is done using the configuration method passed to _AddMassTransit_. The following example shows how the repository is configured for using Microsoft Dependency Injection Extensions, which are used by default with Entity Framework. - -> When using container configuration, the `DbContext` used by the saga repository is scoped. - -```cs -services.AddMassTransit(x => -{ - x.AddSagaStateMachine() - .EntityFrameworkRepository(r => - { - r.ConcurrencyMode = ConcurrencyMode.Pessimistic; // or use Optimistic, which requires RowVersion - - r.DatabaseFactory(() => new OrderStateDbContext(connectionString)); - }); -}); -``` - -### Optimistic Concurrency - -If you are using optimistic concurrency, it's best to configure the endpoint to retry on concurrency exceptions. - -```cs -services.AddMassTransit(x => -{ - x.AddSagaStateMachine() - .EntityFrameworkRepository(r => - { - r.ConcurrencyMode = ConcurrencyMode.Optimistic; - - r.DatabaseFactory(() => new OrderStateDbContext(connectionString)); - }); - - x.UsingInMemory((context, cfg) => - { - cfg.ReceiveEndpoint("order-state", e => - { - e.UseRetry(r => - { - r.Handle(); - - // This is the SQLServer error code for duplicate key, if you are using another database, - // the code might be different - r.Handle(y => y.InnerException is SqlException e && e.Number == 2627); - - r.Interval(5, TimeSpan.FromMilliseconds(100)); - }); - - e.ConfigureSaga(context); - }); - }); -}); -``` - -> If you have retry policy without an exception filter, it will also handle the concurrency exception, so explicit configuration is not required in this case. - diff --git a/docs/usage/sagas/efcore.md b/docs/usage/sagas/efcore.md deleted file mode 100644 index 19657646ae3..00000000000 --- a/docs/usage/sagas/efcore.md +++ /dev/null @@ -1,191 +0,0 @@ -# Entity Framework Core - -[MassTransit.EntityFrameworkCore](https://www.nuget.org/packages/MassTransit.EntityFrameworkCore) - -An example saga instance is shown below, which is orchestrated using an Automatonymous state machine. The _CorrelationId_ will be the primary key, and _CurrentState_ will be used to store the current state of the saga instance. - -```cs -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } - - // If using Optimistic concurrency, this property is required - public byte[] RowVersion { get; set; } -} -``` - -The instance properties are configured using a _SagaClassMap_. - -::: warning Important -The `SagaClassMap` has a default mapping for the `CorrelationId` as the primary key. If you create your own mapping, you must follow the same convention, or at least make it a Clustered Index + Unique, otherwise you will likely experience deadlock exceptions and/or performance issues in high throughput scenarios. -::: - -```cs -public class OrderStateMap : - SagaClassMap -{ - protected override void Configure(EntityTypeBuilder entity, ModelBuilder model) - { - entity.Property(x => x.CurrentState).HasMaxLength(64); - entity.Property(x => x.OrderDate); - - // If using Optimistic concurrency, otherwise remove this property - entity.Property(x => x.RowVersion).IsRowVersion(); - } -} -``` - -Include the instance map in a _DbContext_ class that will be used by the saga repository. - -```cs -public class OrderStateDbContext : - SagaDbContext -{ - public OrderStateDbContext(DbContextOptions options) - : base(options) - { - } - - protected override IEnumerable Configurations - { - get { yield return new OrderStateMap(); } - } -} -``` - -### Container Integration - -Once the class map and associated _DbContext_ class have been created, the saga repository can be configured with the saga registration, which is done using the configuration method passed to _AddMassTransit_. The following example shows how the repository is configured for using Microsoft Dependency Injection Extensions, which are used by default with Entity Framework Core. - -```cs -services.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .EntityFrameworkRepository(r => - { - r.ConcurrencyMode = ConcurrencyMode.Pessimistic; // or use Optimistic, which requires RowVersion - - r.AddDbContext((provider,builder) => - { - builder.UseSqlServer(connectionString, m => - { - m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); - m.MigrationsHistoryTable($"__{nameof(OrderStateDbContext)}"); - }); - }); - }); -}); -``` - -#### Single DbContext - -> New in 7.0.5 - -A single `DbContext` can be registered in the container which can then be used to configure sagas that are mapped by the `DbContext`. For example, [Job Consumers](/advanced/job-consumers) need three saga repositories, and the Entity Framework Core package includes the `JobServiceSagaDbContext` which can be configured using the `AddSagaRepository` method as shown below. - -```cs -services.AddDbContext(builder => - builder.UseNpgsql(Configuration.GetConnectionString("JobService"), m => - { - m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); - m.MigrationsHistoryTable($"__{nameof(JobServiceSagaDbContext)}"); - })); - -services.AddMassTransit(x => -{ - x.AddSagaRepository() - .EntityFrameworkRepository(r => - { - r.ExistingDbContext(); - r.LockStatementProvider = new PostgresLockStatementProvider(); - }); - x.AddSagaRepository() - .EntityFrameworkRepository(r => - { - r.ExistingDbContext(); - r.LockStatementProvider = new PostgresLockStatementProvider(); - }); - x.AddSagaRepository() - .EntityFrameworkRepository(r => - { - r.ExistingDbContext(); - r.LockStatementProvider = new PostgresLockStatementProvider(); - }); - - // other configuration, such as consumers, etc. -}); -``` - -The above code using the standard Entity Framework configuration extensions to add the _DbContext_ to the container, using PostgreSQL. Because the job service state machine receive endpoints are configured by _ConfigureJobServiceEndpoints_, the saga repositories must be configured separately. The _AddSagaRepository_ method is used to register a repository for a saga that has already been added, and uses the same extension methods as the _AddSaga_ and _AddSagaStateMachine_ methods. - -Once configured, the job service sagas can be configured as shown below. - -```cs -cfg.ServiceInstance(options, instance => -{ - instance.ConfigureJobServiceEndpoints(js => - { - js.ConfigureSagaRepositories(context); - }); -}); -``` - -The [Job Consumers](https://github.com/MassTransit/Sample-JobConsumers) sample is a working version of this configuration style. - - -#### Multiple DbContext - -Multiple `DbContext` can be registered in the container which can then be used to configure sagas that are mapped by the `DbContext` and injected into other components. Calling the ```AddDbContext``` extension method will register a scoped ```DbContext``` by default. For simple scenarios where there is a single ```DbContext``` this will work. However, in scenarios where there is at least one other ```DbContext``` the dotnet command that generates Entity Framework migrations will not work. To resolve this issue, you'll need to perform the following steps: -1. Make sure that all ```DbContext``` has a constructor that takes ```DbContextOptions``` instead of ```DbContextOptions```. - -2. Run the Entity Framework Core command to create your migrations as shown below. - -```cs -dotnet ef migrations add InitialCreate -c JobServiceSagaDbContext -``` - -3. Run the Entity Framework Core command to sync with the database as shown below. - - ```cs - dotnet ef database update -c JobServiceSagaDbContext - ``` - -### Custom schemas - -#### Single schema - -In case there is a custom schema set up in your database and you are relying on the user credentials from the `ConnectionString` to access correct schema you must include the schema name when specifying the lock statement provider. - -```cs -services.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .EntityFrameworkRepository(r => - { - r.ConcurrencyMode = ConcurrencyMode.Pessimistic; // or use Optimistic, which requires RowVersion - - r.UseSqlServer("custom_schema"); - - r.AddDbContext((provider,builder) => - { - builder.UseSqlServer(connectionString, m => - { - m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); - m.MigrationsHistoryTable($"__{nameof(OrderStateDbContext)}"); - }); - }); - }); -}); -``` - -#### Multiple schemas - -In case there are multiple schemas defined for your models you need to define them in code, and there are a few different ways to do that. -https://learn.microsoft.com/en-us/ef/core/modeling/entity-types?tabs=data-annotations#table-schema - - - diff --git a/docs/usage/sagas/guidance.md b/docs/usage/sagas/guidance.md deleted file mode 100644 index fe953d51676..00000000000 --- a/docs/usage/sagas/guidance.md +++ /dev/null @@ -1,85 +0,0 @@ -# Saga Guidance - -What follows is a set of guidelines related to sagas that was collected from Discord, Stack Overflow, and other sources to provide an easy way to link answers to commonly asked questions. - - -### Concurrency - -Saga concurrency issues happen, particularly when using optimistic concurrency. The most common reasons include: - -- Simultaneous events correlating to the same instance, typically from multiple sources running in parallel -- Commands from the saga to consumers, where the consumer is quick and responds before the saga has finished processing the initiating event - -There are certainly others, but anytime multiple events correlate to the same instance, concurrency issues are a concern. For that reason, the following baseline receive endpoint configuration is recommended as a starting point (tuning will depend upon the saga, repository, environment, etc.). - -To configure the receive endpoint directly: - -```cs -services.AddMassTransit(x => -{ - x.AddStateMachineSaga() - .MongoDbRepository(r => - { - r.Connection = "mongodb://127.0.0.1"; - r.DatabaseName = "orderdb"; - }); - - x.UsingRabbitMq((context,cfg) => - { - cfg.ReceiveEndpoint("saga-queue", e => - { - const int ConcurrencyLimit = 20; // this can go up, depending upon the database capacity - - e.PrefetchCount = ConcurrencyLimit; - - e.UseMessageRetry(r => r.Interval(5, 1000)); - e.UseInMemoryOutbox(); - - e.ConfigureSaga(context, s => - { - var partition = endpointConfigurator.CreatePartitioner(ConcurrencyLimit); - - s.Message(x => x.UsePartitioner(partition, m => m.Message.OrderId)); - s.Message(x => x.UsePartitioner(partition, m => m.Message.OrderId)); - s.Message(x => x.UsePartitioner(partition, m => m.Message.OrderId)); - }); - }); - } -}); -``` - -Alternatively if using a [saga definition](/usage/containers/definitions): - -```cs -public sealed class OrderStateSagaDefinition : SagaDefinition -{ - private const int ConcurrencyLimit = 20; // this can go up, depending upon the database capacity - - public OrderStateSagaDefinition() - { - // specify the message limit at the endpoint level, which influences - // the endpoint prefetch count, if supported. - Endpoint(e => - { - e.Name = "saga-queue"; - e.PrefetchCount = ConcurrencyLimit; - }); - } - - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) - { - endpointConfigurator.UseMessageRetry(r => r.Interval(5, 1000)); - endpointConfigurator.UseInMemoryOutbox(); - - var partition = endpointConfigurator.CreatePartitioner(ConcurrencyLimit); - - sagaConfigurator.Message(x => x.UsePartitioner(partition, m => m.Message.OrderId)); - sagaConfigurator.Message(x => x.UsePartitioner(partition, m => m.Message.OrderId)); - sagaConfigurator.Message(x => x.UsePartitioner(partition, m => m.Message.OrderId)); - } -} -``` - -This example uses message retry (because concurrency issues throw exceptions), the _InMemoryOutbox_ (to avoid duplicate messages in the event of a concurrency failure), and uses a partitioner to limit the receive endpoint to only one concurrent message for each OrderId (the partitioner uses hashing to meet the partition count). - -> The partitioner in this case is only for this specific receive endpoint, multiple service instances (competing consumer) may still consume events for the same saga instance. diff --git a/docs/usage/sagas/marten.md b/docs/usage/sagas/marten.md deleted file mode 100644 index b5670edcf98..00000000000 --- a/docs/usage/sagas/marten.md +++ /dev/null @@ -1,96 +0,0 @@ -# Marten - -> Package: [MassTransit.Marten](https://nuget.org/packages/MassTransit.Marten) - -[Marten][2] is an open source library that provides provides .NET developers with the ability to easily use the proven PostgreSQL database engine and its fantastic [JSON support][1] as a fully fledged document database. To use Marten and PostgreSQL as saga persistence, you need to install `MassTransit.Marten` NuGet package and add some code. - -> MassTransit will automatically configure the _CorrelationId_ property so that Marten will use that property as the primary key. No attribute is necessary. - -```cs -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } -} -``` - -### Container Integration - -To configure Marten as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. This will configure Marten to connect to the local Marten instance on the default port using Optimistic concurrency. - -```cs {6} -container.AddMassTransit(cfg => -{ - var connectionString = "host=localhost;port=5432;database=orders;username=web;password=webpw;"; - - cfg.AddSagaStateMachine() - .MartenRepository(connectionString); -}); -``` - -### Optimistic Concurrency - -To use Marten's built-in Optimistic concurrency, use the configuration options to configure the schema. Marten supports optimistic concurrency by using an eTag-like version field in the metadata, which does not require any additional fields in the saga class. - -```cs {8} -container.AddMassTransit(cfg => -{ - var connectionString = "host=localhost;port=5432;database=orders;username=web;password=webpw;"; - - cfg.AddSagaStateMachine() - .MartenRepository(connectionString, r => - { - r.Schema.For().UseOptimisticConcurrency(true); - }); -}); -``` - -Alternatively, you can add the `UseOptimisticConcurrency` attribute to the class. - -```cs -[UseOptimisticConcurrency] -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - // ... -} -``` - -### Index Creation - -Marten can create indices for properties, which greatly increases query performance. If your saga is correlating events using other fields, index creation is recommended. For example, if an _OrderNumber_ property was added to the _OrderState_ class, it could be indexed by configuring it in the repository. - -```cs {7} -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public string OrderNumber { get; set; } - - public DateTime? OrderDate { get; set; } -} -``` - -```cs {8} -container.AddMassTransit(cfg => -{ - var connectionString = "host=localhost;port=5432;database=orders;username=web;password=webpw;"; - - cfg.AddSagaStateMachine() - .MartenRepository(connectionString, r => - { - r.Schema.For().Index(x => x.OrderNumber); - }); -}); -``` - -Details on how Marten creates indices is available in the [Computed Index](https://martendb.io/documentation/documents/customizing/computed_index/) documentation. - -[1]: https://www.postgresql.org/docs/9.5/static/functions-json.html -[2]: http://jasperfx.github.io/marten/ diff --git a/docs/usage/sagas/mongodb.md b/docs/usage/sagas/mongodb.md deleted file mode 100644 index f5a5e29404c..00000000000 --- a/docs/usage/sagas/mongodb.md +++ /dev/null @@ -1,25 +0,0 @@ -# MongoDB - -MongoDB is easy to setup as a saga repository. MassTransit includes sensible defaults, and there is no need to explicitly map sagas. - -Storing a saga in MongoDB requires an additional interface, _ISagaVersion_, which has a _Version_ property used for optimistic concurrency. An example saga is shown below. - -<<< @/docs/code/sagas/OrderState.cs - -### Configuration - -To configure MongoDB as a saga repository, use the code shown below using the _AddMassTransit_ container extension. This will configure MongoDB to connect to the local MongoDB instance on the default port using Optimistic concurrency. The _CorrelationId_ property will be automatically mapped to be the document identifier. - -<<< @/docs/code/sagas/MongoDbSagaContainer.cs - -In the example above, saga instances are stored in a collection named `order.states`. The collection name can be specified using the _CollectionName_ property. Alternatively, a collection name formatter can be specified using the _CollectionNameFormatter_ method. - -<<< @/docs/code/sagas/MongoDbSagaContainerCollection.cs - -Container integration gives you ability to configure class map based on saga type. You can use `Action` explicitly: - -<<< @/docs/code/sagas/MongoDbSagaClassMap.cs - -`BsonClassMap` registered inside container will be used by default for `TSaga` configuration: - -<<< @/docs/code/sagas/MongoDbRegisterClassMap.cs diff --git a/docs/usage/sagas/nhibernate.md b/docs/usage/sagas/nhibernate.md deleted file mode 100644 index 757ab21e2ed..00000000000 --- a/docs/usage/sagas/nhibernate.md +++ /dev/null @@ -1,60 +0,0 @@ -# NHibernate - -> Package: [MassTransit.NHibernate](https://www.nuget.org/packages/MassTransit.NHibernate) - -NHibernate is a widely used ORM and it is supported by MassTransit for saga storage. The example below shows the code-first approach to using NHibernate for saga persistence. - -```cs -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } - - // If using Optimistic concurrency, this property is required - public int Version { get; set; } -} -``` - -The instance properties are configured using a _SagaClassMapping_. - -::: warning Important -The `SagaClassMapping` has a default mapping for the `CorrelationId` as the primary key. If you create your own mapping, you must follow the same convention, or at least make it a Clustered Index + Unique, otherwise you will likely experience deadlock exceptions and/or performance issues in high throughput scenarios. -::: - -```cs -public class OrderStateMap : - SagaClassMapping -{ - public OrderStateMap() - { - Property(x => x.CurrentState, x => x.Length(64)); - Property(x => x.OrderDate); - - Property(x => x.Version); // If using Optimistic concurrency - } -} -``` - -### Container Integration - -To configure NHibernate as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. This will configure NHibernate to connect to the local NHibernate instance on the default port using Optimistic concurrency. - -```cs {2,7} -// the session factory should be registered as a single instance -container.RegisterSingleInstance(...); - -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .NHibernateRepository(); -}); -``` - -### Concurrency - -NHibernate natively supports multiple concurrency handling mechanisms. The easiest is probably adding a `Version` property of type `int` to the saga instance class and map it to the column with the same name - - diff --git a/docs/usage/sagas/persistence.md b/docs/usage/sagas/persistence.md deleted file mode 100644 index 59f84485ebf..00000000000 --- a/docs/usage/sagas/persistence.md +++ /dev/null @@ -1,114 +0,0 @@ -# Saga Persistence - -Sagas are stateful event-based message consumers -- they retain state. Therefore, saving state between events is important. Without persistent state, a saga would consider each event a new event, and orchestration of subsequent events would be meaningless. - -In order to store the saga state, you need to use one form of saga persistence. There are several types of storage that MassTransit supports, all of those, which are included to the main distribution, are listed below. There is also a in-memory unreliable storage, which allows to temporarily store your saga state. It is useful to try things out since it does not require any infrastructure. - -### Order State - -An example state machine instance is shown below. This example will be used across every storage engine to show how each is configured. - -```cs -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } -} -``` - -### Container Integration - -When using the _AddMassTransit_ container extension, the repository should be specified at saga registration. The example below specifies the InMemory saga repository. - -```cs {4} -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .InMemoryRepository(); -}); -``` - -The saga repository is always registered with a singleton container lifecycle. - -If the container registration is not being used, the InMemory saga repository can be created manually and specified on receive endpoint. - -```cs -var orderStateMachine = new OrderStateMachine(); -var repository = new InMemorySagaRepository(); - -var busControl = Bus.Factory.CreateUsingInMemory(x => -{ - x.ReceiveEndpoint("order-state", e => - { - e.StateMachineSaga(orderStateMachine, repository); - }); -}); -``` - -There are two types of saga repository: -* Query repository -* Identity-only repository - -Depending on the persistence mechanism, repository implementation can be either identity-only or identity plus query. - -When using identity-only repository, such as Azure Service Bus message session or Redis, you can only use correlation by identity. This means that all events that the saga receives, must hold the saga correlation id, and the correlation for each event can only use `CorrelateById` method to define the correlation. - -Query repository by definition support identity correlation too, but in addition support other properties of events being received and saga state properties. Such correlations are defined using `CorrelateBy` method and you can use any logical expression that involve the event data and saga state data to establish such correlation. Repository implementation such as Entity Framework, NHibernate and Marten support correlation by query. Of course, in-memory repository supports it as well. - -### Identity - -Saga instances are identified by a unique identifier (`Guid`), represented by the `CorrelationId` on the saga instance. Events are correlated to the saga instance using either the unique identifier, or alternatively using an expression that correlates properties on the saga instance to each event. If the `CorrelationId` is used, it's always a one-to-one match, either the saga already exists, or it's a new saga instance. With a correlation expression, the expression might match to more than one saga instance, so care should be used -- because the event would be delivered to all matching instances. - -> Seriously, don't sent an event to all instances -- unless you want to watch your messages consumers lock your entire saga storage engine. - -It is strongly advised to have `CorrelationId` as your table/document key. This will enable better concurrency handling and will make the saga state consistent. - -### Publishing and Sending From Sagas - -Sagas are completely message-driven and therefore not only consume but also publish events and send commands. However, if your saga received a lot of messages coming roughly at the same time and the endpoint is set to process multiple messages in parallel - this can lead to a conflict between message processing and saga persistence. - -This means that there could be more than one saga state updates that are being persisted at the same time. Depending on the saga repository type, this might fail for different reasons - versioning issue, row or table lock or eTag mismatch. All those problems are basically saying that you are having a concurrency issue. - -It is normal for the saga repository to throw an exception in such case but if your saga is publishing messages, they were already published but the saga state has not been updated. MassTransit will eventually use retry policy on the endpoint and more messages will be send, potentially leading to mess. Or, if there are no retry policies configured, messages might be sent indicating that the process needs to continue but saga instance will be in the old state and will not accept any further messages because they will come in a wrong state. - -This issue is common and can be solved by postponing the message publish and send operations until all persistence work is done. All messages that should be published, are collected in a buffer, which is called *Outbox*. MassTransit implements this feature and it can be configured by adding these lines to your endpoint configuration: - -```csharp -c.ReceiveEndpoint("queue", e => -{ - e.UseInMemoryOutbox(); - // other endpoint configuration here -} -``` - -### Relational DB Recommendations - -While it's nice if you are developing a green-field system and you can define your Saga Db Entity with CorrelationId as the Primary Key (Clustered), sometimes we have to work within existing db entities. If this is the case, please remember in order to keep your saga's performing quickly (optimistic OR pessimistic, it doesn't matter), follow the note below. - -::: tip -The CorrelationId should preferably be the Primary Key + Clustered for your saga table. If unable, then it must be a Clustered Index + Unique. And it's also highly recommended to use the NewId package for creating nice Db Friendly guids. -::: - -### Optimistic vs pessimistic concurrency - -Most persistence mechanisms for sagas supported by MassTransit need some way to guarantee ACID when processing sagas. Because there can be multiple threads _consuming_ multiple bus events meant for the same saga instance, they could end up overwriting each other (race condition). - -Relational databases can easily handle this by setting the transaction type to *serializable* or (page/row) locking. This would be considered as _pessimistic concurrency_. - -Another way to handle concurrency is to have some attribute like version or timestamp, which updates every time a saga is persisted. By doing that we can instruct the database only to update the record if this attribute matches between what we are trying to persist and what is stored in the database record we are trying to update. - -This is type of concurrency is called an _optimistic concurrency_. It doesn't guarantee your unit of work with the database will succeed (must retry after these exceptions), but it also doesn't block anybody else from working within the same database page (not locking the table/page). - -#### So, which one should I use? - -For almost every scenario, it is recommended using the optimistic concurrency, because most state machine logic should be fairly quick. - -If the chosen persistence method supports optimistic concurrency, race conditions can be handled rather easily by specifying a retry policy for concurrency exceptions or using generic retry policy. - - -[1]: https://www.postgresql.org/docs/9.5/static/functions-json.html -[3]: https://github.com/StackExchange/Dapper -[4]: https://github.com/StackExchange/Dapper/issues/889 diff --git a/docs/usage/sagas/quickstart.md b/docs/usage/sagas/quickstart.md deleted file mode 100644 index bdedf6d184f..00000000000 --- a/docs/usage/sagas/quickstart.md +++ /dev/null @@ -1,123 +0,0 @@ -## Automatonymous Quick Start - -So you've got the chops and want to get started quickly using Automatonymous. Maybe you are a bad ass and can't be bothered with reading documentation, or perhaps you are already familiar with the Magnum StateMachine and want to see what things have changed. Either way, here it is, your first state machine configured using Automatonymous. - -```csharp -class Relationship -{ - public State CurrentState { get; set; } - public string Name { get; set; } -} - -class RelationshipStateMachine : - MassTransitStateMachine -{ - public RelationshipStateMachine() - { - Event(() => Hello); - Event(() => PissOff); - Event(() => Introduce); - - State(() => Friend); - State(() => Enemy); - - Initially( - When(Hello) - .TransitionTo(Friend), - When(PissOff) - .TransitionTo(Enemy), - When(Introduce) - .Then(ctx => ctx.Instance.Name = ctx.Data.Name) - .TransitionTo(Friend) - ); - } - - public State Friend { get; private set; } - public State Enemy { get; private set; } - - public Event Hello { get; private set; } - public Event PissOff { get; private set; } - public Event Introduce { get; private set; } -} - -class Person -{ - public string Name { get; set; } -} -``` - -### Seriously? - -Okay, so two classes are defined above, one that represents the state (`Relationship`) and the other that defines the behavior of the state machine (`RelationshipStateMachine`). For each state machine that is defined, it is expected that there will be at least one instance. In Automatonymous, state is separate from behavior, allowing many instances to be managed using a single state machine. - -
-Note: - For some object-oriented purists, this may be causing the hair to raise on the back of your neck. Chill out, it's not the end of the world here. If you have a penchant for encapsulating behavior with data (practices such as domain model, DDD, etc.), recognize that programming language constructs are the only thing in your way here. -
- -### Tracking State - -State is managed in Automatonymous using a class, shown above as the `Relationship`. - -### Defining Behavior - -Behavior is defined using a class that inherits from `MassTransitStateMachine`. The class is generic, and the state type associated with the behavior must be specified. This allows the state machine configuration to use the state for a better configuration experience. - -> It also makes Intellisense work better. - -States are defined in the state machine as properties. They are initialized by default, so there is no need to declare them explicitly unless they are somehow special, such as a Substate or Superstate. - -> Configuration of a state machine is done using an internal DSL, using an approach known as Object Scoping, and is explained in Martin Fowler's Domain Specific Languages book. - -### Creating Instances - - -### Creating the State Machine - - -### Raising Events - -Once a state machine and an instance have been created, it is necessary to raise an event on the state machine instance to invoke some behavior. There are three or four participants involved in raising an event: a state machine, a state machine instance, and an event. If the event includes data, the data for the event is also included. - -The most explicit way to raise an event is shown below. - -```csharp -var relationship = new Relationship(); -var machine = new RelationshipStateMachine(); - -await machine.RaiseEvent(relationship, machine.Hello); -``` - -If the event has data, it is passed along with the event as shown. - -```csharp -var person = new Person { Name = "Joe" }; - -await machine.RaiseEvent(relationship, machine.Introduce, person); -``` - -**Lifters** - -Lifters allow events to be raised without knowing explicit details about the state machine or the instance type, making it easier to raise events from objects that do not have prior type knowledge about the state machine or the instance. Using an approach known as *currying* (from functional programming), individual arguments of raising an event can be removed. - -For example, using an event lift, the state machine is removed. - -```csharp -var eventLift = machine.CreateEventLift(machine.Hello); - -// elsewhere in the code, the lift can be used -await eventLift.Raise(relationship); -``` - -The instance can also be lifted, making it possible to raise an event without any instance type knowledge. - -```csharp -var instanceLift = machine.CreateInstanceLift(relationship); -var helloEvent = machine.Hello; - -// elsewhere in the code, the lift can be used -await instanceLift.Raise(helloEvent); -``` - -Lifts are commonly used by plumbing code to avoid dynamic methods or delegates, making code clean and fast. - diff --git a/docs/usage/sagas/redis.md b/docs/usage/sagas/redis.md deleted file mode 100644 index c2ac6e1c0b3..00000000000 --- a/docs/usage/sagas/redis.md +++ /dev/null @@ -1,33 +0,0 @@ -# Redis - -> Package: [MassTransit.Redis](https://www.nuget.org/packages/MassTransit.Redis) - -Redis is a very popular key-value store, which is known for being very fast. To support Redis, MassTransit uses the `StackExchange.Redis` library. - -::: warning -Redis only supports event correlation by _CorrelationId_, it does not support queries. If a saga uses expression-based correlation, a _NotImplementedByDesignException_ will be thrown. -::: - -Storing a saga in Redis requires an additional interface, _ISagaVersion_, which has a _Version_ property used for optimistic concurrency. An example saga is shown below. - -<<< @/docs/code/sagas/OrderState.cs - -### Configuration - -To configure Redis as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. This will configure Redis to connect to the local Redis instance on the default port using Optimistic concurrency. This will also store the _ConnectionMultiplexer_ in the container as a single instance, which will be disposed by the container. - -<<< @/docs/code/sagas/RedisSagaContainer.cs - -The example below includes all the configuration options, in cases where additional settings are required. - -<<< @/docs/code/sagas/RedisSagaContainerConfiguration.cs - -The container extension will register the saga repository in the container. For more details on container configuration, review the [container configuration](/usage/containers/) section of the documentation. - -### Concurrency - -Redis supports both Optimistic (default) and Pessimistic concurrency. - -In optimistic mode, the saga instance is not locked before reading, which can ultimately lead to a write conflict if the instance was updated by another message. The _Version_ property is used to compare that the update would not overwrite a previous update. It is recommended that a retry policy is configured (using `UseMessageRetry`, see the [exceptions](/usage/exceptions.md#retry) documentation). - -Pessimistic concurrency uses the Redis lock mechanism. During the message processing, the repository will lock the saga instance before reading it, so that any concurrent attempts to lock the same instance will wait until the current message has completed or the lock timeout expires. diff --git a/docs/usage/sagas/session.md b/docs/usage/sagas/session.md deleted file mode 100644 index abe58bbbe6f..00000000000 --- a/docs/usage/sagas/session.md +++ /dev/null @@ -1,61 +0,0 @@ -# Azure Service Bus - -Azure Service Bus provides a feature called *message sessions*, to process multiple messages at once and to store some state on a temporary basis, which can be retrieved by some key. - -The latter give us an ability to use this feature as saga state storage. Using message sessions as saga persistence, you can only use Azure Service Bus for both messaging and saga persistence purposes, without needing any additional infrastructure. You have to explicitly enable message sessions when configuring the endpoint, and use parameterless constructor to instantiate the saga repository. - -When using message sessions, concurrency is managed by Azure Service Bus. - -::: tip -Message sessions can only be correlated using the CorrelationId, which is copied to the message SessionId. Correlation expressions are not supported when using message sessions. -::: - -Here is the basic sample of how to use the Azure Service Bus message session as saga repository: - -```cs -public class OrderState : - SagaStateMachineInstance -{ - public Guid CorrelationId { get; set; } - public string CurrentState { get; set; } - - public DateTime? OrderDate { get; set; } -} -``` - -### Container Integration - -To configure a message session as the saga repository for a saga, use the code shown below using the _AddMassTransit_ container extension. - -```cs {4} -container.AddMassTransit(cfg => -{ - cfg.AddSagaStateMachine() - .MessageSessionRepository(); -}); -``` - -Then, configure the endpoint to require a message session. - -```cs -sbc.ReceiveEndpoint("order-state", ep => -{ - ep.RequiresSession = true; - ep.ConfigureSaga(provider); -}); -``` - -To configure the receive endpoint without a container, the state machine and instance type can be specified explicitly. - -```cs -var sagaStateMachine = new OrderStateMachine(); -// This gives an Obsolete-warning -// var repository = new MessageSessionSagaRepository(); -// This is suggested instead -var repository = MessageSessionSagaRepository.Create(); -cfg.ReceiveEndpoint("order-state", ep => -{ - ep.RequiresSession = true; - ep.StateMachineSaga(sagaStateMachine, repository); -}); -``` diff --git a/docs/usage/templates.md b/docs/usage/templates.md deleted file mode 100644 index f2d9b5fbd35..00000000000 --- a/docs/usage/templates.md +++ /dev/null @@ -1,90 +0,0 @@ -# Templates - -MassTransit includes several `dotnet new` templates to create MassTransit project and components. - -A video introducing the templates is available on [YouTube](https://youtu.be/nYKq61-DFBQ). - -## Installation - -```sh -dotnet new -i MassTransit.Templates -``` - -One installed, typing `dotnet new` will display the available templates: - -``` -Template Name Short Name Language Tags ------------------------------------------ -------------- -------- --------------------------- -MassTransit Consumer Saga mtsaga [C#] MassTransit/Saga -MassTransit Docker mtdocker [C#] MassTransit/Docker -MassTransit Message Consumer mtconsumer [C#] MassTransit/Consumer -MassTransit Routing Slip Activity mtactivity [C#] MassTransit/Activity -MassTransit Routing Slip Execute Activity mtexecactivity [C#] MassTransit/ExecuteActivity -MassTransit State Machine Saga mtstatemachine [C#] MassTransit/StateMachine -MassTransit Worker mtworker [C#] MassTransit/Worker -``` - -## Projects - -### MassTransit Worker - -``` -dotnet new mtworker -n -``` - -Creates a dotnet project that is configured as a MassTransit Worker. Includes project references and an example -`Program.cs` - -### MassTransit Docker - -``` -dotnet new mtdocker -``` - -Creates a `Dockerfile` and a `docker-compose.yml` in the project, configured for RabbitMQ. - - -## Items - -### MassTransit Consumer - -``` -dotnet new mtconsumer -``` - -Creates a Consumer and ConsumerDefinition in `~/Consumers` and an example message in `~/Contracts`. - -### MassTransit Saga State Machine - -``` -dotnet new mtstatemachine -``` - -Creates a StateMachine Saga in `~/StateMachines` and an example event in `~/Contracts` - - -### MassTransit Consumer Saga - -``` -dotnet new mtsaga -``` - -Creates a Saga and SagaDefinition in `~/Sagas`, along with a few messages in the `~/Contracts` folder that will -work the saga. - -### MassTransit Routing Slip Activity - -``` -dotnet new mtactivity -``` - -Creates an Activity, ActivityArguments, and ActivityLog in `~/Activities` - -### MassTransit Routing Slip Execute Activity - -``` -dotnet new mtexecactivity -``` - -Creates an Activity, ActivityArguments in `~/Activities` - diff --git a/docs/usage/testing.md b/docs/usage/testing.md deleted file mode 100644 index 651c7f1d0ee..00000000000 --- a/docs/usage/testing.md +++ /dev/null @@ -1,78 +0,0 @@ -# Testing - -MassTransit is a framework, and follows the Hollywood principle – don't call us, we'll call you. This inversion of control, combined with asynchronous execution, can complicate unit tests. To make it easy, MassTransit includes test harnesses to create unit tests that run entirely in-memory but behave close to an actual message broker. In fact, the included memory-based messaging fabric was inspired by RabbitMQ exchanges and queues. - -Since MassTransit is typically configured using `AddMassTransit`, the preferred testing approach is to use a `ServiceCollection` to configure the test combined with the test harness. - -### Consumer - -To test a consumer using container-based configuration: - -```cs -await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(cfg => - { - cfg.AddConsumer(); - }) - .BuildServiceProvider(true); - -var harness = provider.GetRequiredService(); - -await harness.Start(); - -var client = harness.GetRequestClient(); - -await client.GetResponse(new -{ - OrderId = InVar.Id, - OrderNumber = "123" -}); - -Assert.IsTrue(await harness.Sent.Any()); - -Assert.IsTrue(await harness.Consumed.Any()); - -var consumerHarness = harness.GetConsumerHarness(); - -Assert.That(await consumerHarness.Consumed.Any()); -``` - -### Saga State Machine - -To test a saga state machine using container-based configuration: - -```cs -await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(cfg => - { - cfg.AddSagaStateMachine(); - }) - .BuildServiceProvider(true); - -var harness = provider.GetRequiredService(); - -await harness.Start(); - -var sagaId = Guid.NewGuid(); -var orderNumber = "ORDER123"; - -await harness.Bus.Publish(new OrderSubmitted -{ - CorrelationId = sagaId, - OrderNumber = orderNumber -}); - -Assert.That(await harness.Consumed.Any()); - -var sagaHarness = harness.GetSagaStateMachineHarness(); - -Assert.That(await sagaHarness.Consumed.Any()); - -Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == sagaId)); - -var instance = sagaHarness.Created.ContainsInState(sagaId, sagaHarness.StateMachine, sagaHarness.StateMachine.Submitted); -Assert.IsNotNull(instance, "Saga instance not found"); -Assert.That(instance.OrderNumber, Is.EqualTo(orderNumber)); - -Assert.IsTrue(await harness.Published.Any()); -``` diff --git a/docs/usage/transports/README.md b/docs/usage/transports/README.md deleted file mode 100644 index 56ad47a8f04..00000000000 --- a/docs/usage/transports/README.md +++ /dev/null @@ -1,64 +0,0 @@ -# Transports - -MassTransit support multiple transports, including: - -* [RabbitMQ](rabbitmq) -* [Azure Service Bus](azure-sb) -* [ActiveMQ](activemq) -* [Amazon SQS](amazonsqs) -* [gRPC](grpc) -* [In Memory](in-memory) - -### What does MassTransit add to the transport? - -MassTransit is a lightweight service bus for building distributed .NET applications. The main goal is to provide a consistent, .NET friendly abstraction over the message transport. To meet this goal, MassTransit brings a lot of the application-specific logic closer to the developer in an easy to configure and understand manner. - -The benefits of using MassTransit instead of the raw transport APIs and building everything from scratch, are shown below. These are just a few, and some are more significant than others. The fact that the hosting of your consumers, handlers, sagas, etc. are all managed consistently with a well documented production ready framework is the biggest advantage. You can also find numerous blog posts, podcasts, and articles written about MassTransit online. - -#### Concurrency - -MassTransit is completely asynchronous and leverages the .NET Task Parallel Library (TPL) to consume messages concurrently to achieve maximum throughput and high server utilization. - -#### Connection management - -The network is unreliable. If the application is disconnected from the message broker, MassTransit takes care of reconnecting and making sure all of the exchanges, queues, and bindings are restored. - -#### Exception, retries, and poison messages - -Your message consumers don't need to know about broker acknowledgement protocols. If your message consumer runs to completion, the message is acknowledged and removed from the queue. If you throw an exception, MassTransit uses a retry policy to redeliver the message to the consumer. If the retries are exhausted due to continued failures or other reasons, MassTransit moves the message to an error queue. If the message did not reach a consumer due to being misrouted to the queue, the message is moved to a skipped queue. - -#### Serialization - -C# is a statically typed language, and developers work with types. RabbitMQ works with bytes. So how do you format a message over the wire? How do you handle different date/time formats (local, UTC, unspecified)? How do you deal with numbers, are they integers, longs, or decimals? MassTransit has already thought about this and implemented sensible defaults for you. And there are many serializers provided out of the box, including JSON, BSON, and XML as well as the .NET binary formatter as a last resort. - -You can even protect your messages using AES-256 encryption, to keep prying eyes away and to ensure the safety of private information (to meet PCI or HIPAA requirements). - -#### Message header and correlation - -Designing a common message envelope can be a nitty-gritty affair until things stabilize. And MassTransit is already stable having been used in production since 2008. The format is [well documented](/architecture/interoperability) and has been tested with billions of messages. Furthermore, the envelope includes headers for tracking messages, including conversations, correlations, and requests. The address and host information in the envelope make it easy to build any messaging pattern. - -#### Consumer lifecycle management - -MassTransit handles consumer creation and disposal, and integrates with most major dependency injection containers using their built-in lifetime scope management. This ensures that dependencies are created and destroyed as part of the message consumption pipeline. - -#### Routing - -MassTransit provides a heavily production tested convention for using RabbitMQ exchanges to route published messages to the subscribed consumers. The structure is CPU and memory friendly, which keeps RabbitMQ happy. - -#### Easy Unit Testing - -One of the first rules of unit testing is to avoid hitting infrastructure. And RabbitMQ is just that. MassTransit includes a high-performance in-memory transport for testing every consumer using the same code that would be used in production. And the MassTransit.TestFramework NuGet package includes test harnesses that handle the setup and teardown of the bus so you can easily test your message consumers and sagas. - -#### Sagas - -Sagas are a powerful abstraction that supports message orchestration with durable state. Whether you use the original somewhat explicit syntax, or the powerful state machine syntax of **Automatonymous**, you can build highly available distributed workflow and coordination services easily. MassTransit supports both Entity Framework and NHibernate, using code-based mapping and migrations to simply code deployments and upgrades. - -#### Scheduling - -MassTransit has strong integration with Quartz.NET, to make it easy to schedule messages for future delivery. This brings distributed applications into the fourth dimension, making time a first-class citizen. Some incredibly powerful routing systems have been built by the authors using Quartz in combination with other MassTransit features. - -There are also other scheduling providers that are supported by MassTransit, such as RabbitMQ deferred messages and Azure Service Bus scheduled enqueueing. - -#### Monitoring - -Keeping an eye on your services performance is critical, and having the right tools is a huge plus. MassTransit updates a range of performance counters as messages are processed so operations can keep an eye on message flow and compare the throughput to that of RabbitMQ. diff --git a/docs/usage/transports/activemq.md b/docs/usage/transports/activemq.md deleted file mode 100644 index f2753d01ba1..00000000000 --- a/docs/usage/transports/activemq.md +++ /dev/null @@ -1,100 +0,0 @@ -# ActiveMQ - -> [MassTransit.ActiveMQ](https://nuget.org/packages/MassTransit.ActiveMQ/) - -## Topology - -tbd - -## Examples - -### Minimal - -In the example below, the ActiveMQ settings are configured. - -<<< @/docs/code/transports/ActiveMqConsoleListener.cs - -The configuration includes: - -* The ActiveMQ host - - Host name: `localhost` - - User name and password used to connect to the host - -The port can also be specified as an additional parameter on the _Host_ method. If port 61617 is specified, SSL is automatically enabled. - -MassTransit includes several receive endpoint level configuration options that control receive endpoint behavior. - -| Property | Type | Description -|-------------------------|--------|------------------ -| PrefetchCount | ushort | The number of unacknowledged messages that can be processed concurrently (default based on CPU count) -| AutoDelete | bool | If true, the queue will be automatically deleted when the bus is stopped (default: false) -| Durable | bool | If true, messages are persisted to disk before being acknowledged (default: true) - -::: warning -When using ActiveMQ, receive endpoint queue names must _not_ include any `.` characters. Using a _dotted_ queue name will break pub/sub message routing. If using a dotted queue name is required, such as when interacting with an existing queue, disable topic binding. - -```cs -endpoint.ConfigureConsumeTopology = false; -``` - -When the consume topology is not configured, the virtual consumer queues are not created. -::: - -## Amazon MQ - -Amazon MQ uses ActiveMQ, so the same transport is used. Amazon MQ requires SSL, so if MassTransit detects the host name ends with `amazonaws.com`, SSL is automatically configured. - -## Artemis - -Artemis also supports the openwire protocol. However some differences exists that cause the Masstransit ActiveMQ transport provider not to function. -One of those causes is that Artemis works internally differentl with queues compared to ActiveMq. See [Artemis:Virtual Topics](https://activemq.apache.org/components/artemis/migration) - -The easiest way to get the ActiveMQ transport provider working with a Artemis broker: - -```cs - var busControl = Bus.Factory.CreateUsingActiveMq(cfg => - { - cfg.Host("localhost", 61618, cfgHost => - { - cfgHost.Username("admin"); - cfgHost.Password("admin"); - }); - cfg.EnableArtemisCompatibility(); - }); -``` -Calling `cfg.EnableArtemisCompatibility()` will initialize the minimum necessary features so that the Masstransit ActiveMQ transport provider will work with the Artemis broker - -Currently the only thing `cfg.EnableArtemisCompatibility()` does is setting a predefined formatter `ArtemisConsumerEndpointQueueNameFormatter` (which implements interface IActiveMqConsumerEndpointQueueNameFormatter) on the ConsumeTopology (accessible via cfg.ConsumeTopology ) - -Example of setting your own ConsumerEndpointQueueNameFormatter: -``` -cfg.SetConsumerEndpointQueueNameFormatter(new MyCustomConsumerEndpointQueueNameFormatter()); -``` -So it is still possible to create your own IActiveMqConsumerEndpointQueueNameFormatter if you want to tweak the queue name. - -The responsibility of the formatter is to create the queuename for a given - - - a given receive/consumer endpoint name - - a given topic. - - -## TemporaryQueueNameFormatter - -On the consume topology a TemporaryQueueNameFormatter can be configured. The responsibility of the formatter is to transform the 'system' generated name for a temporary queue. - -This could be used to e.g. add a prefix to the generated temporary queuenames. -This helps to support namespaces in queuenames. -Artemis can use this to enforce security policies - -For adding a prefix, a handy helper is already provided. -During the configure lambda: - -```cs -cfg.SetTemporaryQueueNamePrefix("mycustomnamespace."); -``` - -Behind the scenes this does something like this: - -```cs -cfg.SetTemporaryQueueNameFormatter( new PrefixTemporaryQueueNameFormatter("mycustomnamespace.")); -``` diff --git a/docs/usage/transports/amazonsqs.md b/docs/usage/transports/amazonsqs.md deleted file mode 100644 index f2bc7e34674..00000000000 --- a/docs/usage/transports/amazonsqs.md +++ /dev/null @@ -1,86 +0,0 @@ -# Amazon SQS - -> [MassTransit.AmazonSQS](https://nuget.org/packages/MassTransit.AmazonSQS/) - -MassTransit combines Amazon SQS (Simple Queue Service) with SNS (Simple Notification Service) to provide both send and publish support. - -Configuring a receive endpoint will use the message topology to create and subscribe SNS topics to SQS queues so that published messages will be delivered to the receive endpoint queue. - -## Minimal Example - -In the example below, the Amazon SQS settings are configured. - -<<< @/docs/code/transports/AmazonSqsConsoleListener.cs - -## Broker Topology - -With SQS/SNS, which supports topics and queues, messages are _sent_ or _published_ to SNS Topics and then routes those messages through subscriptions to the appropriate SQS Queues. - -When the bus is started, MassTransit will create SNS Topics and SQS Queues for the receive endpoint. - -## Configuration - -The configuration includes: - -* The Amazon SQS host - - Region name: `us-east-2` - - Access key and secret key used to access the resources - -## Additional Examples - -Any topic can be subscribed to a receive endpoint, as shown below. The topic attributes can also be configured, in case the topic needs to be created. - -<<< @/docs/code/transports/AmazonSqsReceiveEndpoint.cs - -## Errata - -### Scoping - -Because there is only ever one "SQS/SNS" per AWS account it can be helpful to "Scope" your queues and topics. This will prefix all SQS queues and SNS topics with scope value. - -<<< @/docs/code/transports/AmazonSqsScopedConsoleListener.cs - -### Example IAM Policy - -```json -{ - "Version": "2012-10-17", - "Statement": [ - { - "Sid": "SqsAccess", - "Effect": "Allow", - "Action": [ - "sqs:SetQueueAttributes", - "sqs:ReceiveMessage", - "sqs:CreateQueue", - "sqs:DeleteMessage", - "sqs:SendMessage", - "sqs:GetQueueUrl", - "sqs:GetQueueAttributes", - "sqs:ChangeMessageVisibility", - "sqs:PurgeQueue", - "sqs:DeleteQueue", - "sqs:TagQueue" - ], - "Resource": "arn:aws:sqs:*:YOUR_ACCOUNT_ID:*" - },{ - "Sid": "SnsAccess", - "Effect": "Allow", - "Action": [ - "sns:GetTopicAttributes", - "sns:CreateTopic", - "sns:Publish", - "sns:Subscribe" - ], - "Resource": "arn:aws:sns:*:YOUR_ACCOUNT_ID:*" - },{ - "Sid": "SnsListAccess", - "Effect": "Allow", - "Action": [ - "sns:ListTopics" - ], - "Resource": "*" - } - ] -} -``` diff --git a/docs/usage/transports/azure-sb.md b/docs/usage/transports/azure-sb.md deleted file mode 100644 index a6cbc4999d3..00000000000 --- a/docs/usage/transports/azure-sb.md +++ /dev/null @@ -1,67 +0,0 @@ -# Azure Service Bus - -> [MassTransit.Azure.ServiceBus.Core](https://nuget.org/packages/MassTransit.Azure.ServiceBus.Core/) - -MassTransit fully supports Azure Service Bus, including many of the advanced features and capabilities. - -::: warning -The Azure Service Bus transport only supports **Standard** and **Premium** tiers of the Microsoft Azure Service Bus service. Premium tier is recommended for production environments. See [Performance](#performance) section below. -::: - -## Minimal Example - -To configure Azure Service Bus, use the connection string (from the Azure portal) to configure the host as shown below. - -<<< @/docs/code/transports/ServiceBusConsoleListener.cs - -## Broker Topology - -With Azure Service Bus (ASB), which supports topics and queues, messages are _sent_ or _published_ to topics and ASB routes those messages through topics to the appropriate queues. - -## Configuration - - -Azure Service Bus queues includes an extensive set a properties that can be configured. All of these are optional, MassTransit uses sensible defaults, but the control is there when needed. - -<<< @/docs/code/transports/ServiceBusReceiveEndpoint.cs - -| Property | Type | Description | -|----------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| TokenCredential | | Use a specific token-based credential, such as a managed identity token, to access the namespace. You can use the [DefaultAzureCredential](https://docs.microsoft.com/en-us/dotnet/api/azure.identity.defaultazurecredential?view=azure-dotnet) to automatically apply any one of several credential types. | -| TransportType | | Change the transport type from the default (AMQP) to use WebSockets | -| PrefetchCount | int | The number of unacknowledged messages that can be processed concurrently (default based on CPU count) | -| MaxConcurrentCalls | int | How many concurrent messages to dispatch (transport-throttled) | -| LockDuration | TimeSpan | How long to hold message locks (max is 5 minutes) | -| MaxAutoRenewDuration | TimeSpan | How long to renew message locks (maximum consumer duration) | -| RequiresSession | bool | If true, a message SessionId must be specified when sending messages to the queue | -| MaxDeliveryCount | int | How many times the transport will redeliver the message on negative acknowledgment. This is different from retry, this is the transport redelivering the message to a receive endpoint before moving it to the dead letter queue. | - -For example, to configure the transport type to use AMQP over Web Sockets: - -```csharp -cfg.Host(connectionString, h => -{ - h.TransportType = ServiceBusTransportType.AmqpWebSockets; -}); - -``` - -## Additional Examples - -### Example with Azure Managed Identity - -The following example shows how to configure Azure Service Bus using an Azure Managed Identity: - -<<< @/docs/code/transports/ServiceBusManagedIdentityConsoleListener.cs - -During local development, in the case of Visual Studio, you can configure the account to use under Options -> Azure Service Authentication. Note that your Azure Active Directory user needs explicit access to the resource and have the 'Azure Service Bus Data Owner' role assigned. - -::: warning WARNING -To ensure that Mass Transit has sufficient permissions to perform queue management as well as messaging operations. Your identity & managed identity will need to have the correct role assignments within Azure. - -Assigning the role **Azure Service Bus Data Owner** will provide sufficient permissions for Mass Transit to function on the namespace. -::: - -## Performance - -We **really** recommend that you use the Premium subscription levels for production workloads. We have performed our own testing using [MassTransit Benchmark](https://github.com/MassTransit/MassTransit-Benchmark) on a P4 instance. It is also critical that your application is in the same DC as the ASB instance. From a home test using a 1Gb fiber connection we could not get over 600/second. When running in the same DC as the ASB we were able to acheive 6k/second. This test was done with one instance writing to ASB and another instance reading from ASB, as adding consumption over the same AMQP connection killed throughput. diff --git a/docs/usage/transports/grpc.md b/docs/usage/transports/grpc.md deleted file mode 100644 index 7c8d277c5f1..00000000000 --- a/docs/usage/transports/grpc.md +++ /dev/null @@ -1,56 +0,0 @@ -# gRPC - -> [MassTransit.Grpc](https://www.nuget.org/packages/MassTransit.Grpc) - -A new gRPC transport, designed to be a peer-to-peer distributed non-durable message transport, is now included. It's entirely in-memory, has zero dependencies, and allows multiple service instances to exchange messages across a shared message fabric. - -::: warning -New, shiny, and very much in the early availability stage. There may be edge cases, so proceed with caution. The gRPC transport will be supported, and issues resolved as they're reported. -::: - -[Introduction Video (YouTube)](https://youtu.be/ChtpCM3N5a8) - -## Broker Topology - -The gRPC is modeled after RabbitMQ, and supports many of the same features. It uses exchanges and queues, and follows the same topology structure as the RabbitMQ transport. - -Fanout, Direct, and Topic exchanges are supported, along with routing key support. - -And, it, is fast. Using the server GC, message throughput is pretty impressive. - -On a single node (essentially in-memory, but serialized via protocol buffers): - -``` -Send: 253,774 msg/s -Consume: 172,996 msg/s -``` - -Across two nodes, load balanced via competing consumer: -``` -Send: 232,597 msg/s -Consume: 36,331 msg/s -``` - -> Consume rate is slower because the messages are evenly split across the local and remote node. - -## Examples - -### Minimal - -<<< @/docs/code/transports/GrpcConsoleListener.cs - -Full documentation is coming soon, but for now the host configuration is shown below. - -To configure the host using a complete address, such as `http://localhost:19796`, a `Uri` can be specified. The following configures a standalone instance, no servers are specified. Incoming connections are of course accepted. - -```cs -cfg.Host(new Uri("http://localhost:19796")); -``` - -### Multiple Nodes - -To configure a host that connects to other bus instances, use the _AddServer_ method in the host. In this example, the _host_ and _port_ are configured separately. The bus will not start until the server connections are established. - -<<< @/docs/code/transports/GrpcMultiConsoleListener.cs - -Check out [the discussion thread](https://github.com/MassTransit/MassTransit/discussions/2455) for more information. diff --git a/docs/usage/transports/in-memory.md b/docs/usage/transports/in-memory.md deleted file mode 100644 index 142db67e511..00000000000 --- a/docs/usage/transports/in-memory.md +++ /dev/null @@ -1,17 +0,0 @@ -# In Memory - -The in-memory transport is a great tool for testing, as it doesn't require a message broker to be installed or running. It's also very fast. But it isn't durable, and messages are gone if the bus is stopped or the process terminates. So, it's generally not a smart option for a production system. However, there are places where durability is not important so the cautionary tale is to proceed with caution. - -::: warning -The in-memory transport is intended for use within a single process only. It cannot be used to communicate between multiple processes (even if they are on the same machine). -::: - -## Broker Topology - -The in-memory transport uses an in-memory routing fabric, that replicates a lot of the behavior of RabbitMQ. - -## Examples - -### Minimal - -<<< @/docs/code/transports/InMemoryBus.cs diff --git a/docs/usage/transports/rabbitmq.md b/docs/usage/transports/rabbitmq.md deleted file mode 100644 index ae2bd48cc8c..00000000000 --- a/docs/usage/transports/rabbitmq.md +++ /dev/null @@ -1,114 +0,0 @@ -# RabbitMQ - -> [MassTransit.RabbitMQ](https://nuget.org/packages/MassTransit.RabbitMQ/) - -With tens of thousands of users, RabbitMQ is one of the most popular open source message brokers. RabbitMQ is lightweight and easy to deploy on premises and in the cloud. RabbitMQ can be deployed in distributed and federated configurations to meet high-scale, high-availability requirements. - -MassTransit fully supports RabbitMQ, including many of the advanced features and capabilities. - -::: tip Getting Started -To get started with RabbitMQ, refer to the [configuration](/usage/configuration) section which uses RabbitMQ in the examples. -::: - -## Minimal Example - -In the example below, which configures a receive endpoint, consumer, and message type, the bus is configured to use RabbitMQ. - -<<< @/docs/code/transports/RabbitMqConsoleListener.cs - -## Broker Topology - -With RabbitMQ, which supports exchanges and queues, messages are _sent_ or _published_ to exchanges and RabbitMQ routes those messages through exchanges to the appropriate queues. - -When the bus is started, MassTransit will create exchanges and queues on the virtual host for the receive endpoint. MassTransit creates durable, _fanout_ exchanges by default, and queues are also durable by default. - -## Configuration - -The configuration includes: - -* The RabbitMQ host - - Host name: `localhost` - - Virtual host: `/` - - User name and password used to connect to the virtual host (credentials are virtual-host specific) -* The receive endpoint - - Queue name: `order-events-listener` - - Consumer: `OrderSubmittedEventConsumer` - - Message type: `OrderSystem.Events.OrderSubmitted` - -| Name | Description | -|:-----|:------------| -| order-events-listener | Queue for the receive endpoint -| order-events-listener | An exchange, bound to the queue, used to _send_ messages -| OrderSystem.Events:OrderSubmitted | An exchange, named by the message-type, bound to the _order-events-listener_ exchange, used to _publish_ messages - -When a message is sent, the endpoint address can be one of two values: - -`exchange:order-events-listener` - -Send the message to the _order-events-listener_ exchange. If the exchange does not exist, it will be created. _MassTransit translates topic: to exchange: when using RabbitMQ, so that topic: addresses can be resolved – since RabbitMQ is the only supported transport that doesn't have topics._ - -`queue:order-events-listener` - -Send the message to the _order-events-listener_ exchange. If the exchange or queue does not exist, they will be created and the exchange will be bound to the queue. - -With either address, RabbitMQ will route the message from the _order-events-listener_ exchange to the _order-events-listener_ queue. - -When a message is published, the message is sent to the _OrderSystem.Events:OrderSubmitted_ exchange. If the exchange does not exist, it will be created. RabbitMQ will route the message from the _OrderSystem.Events:OrderSubmitted_ exchange to the _order-events-listener_ exchange, and subsequently to the _order-events-listener_ queue. If other receive endpoints connected to the same virtual host include consumers that consume the _OrderSubmitted_ message, a copy of the message would be routed to each of those endpoints as well. - -::: warning -If a message is published before starting the bus, so that MassTransit can create the exchanges and queues, the exchange _OrderSystem.Events:OrderSubmitted_ will be created. However, until the bus has been started at least once, there won't be a queue bound to the exchange and any published messages will be lost. Once the bus has been started, the queue will remain bound to the exchange even when the bus is stopped. -::: - -Durable exchanges and queues remain configured on the virtual host, so even if the bus is stopped messages will continue to be routed to the queue. When the bus is restarted, queued messages will be consumed. - -MassTransit includes several host-level configuration options that control the behavior for the entire bus. - -| Property | Type | Description -|-------|------------------------|--------|--- -| PublisherConfirmation | bool | MassTransit will wait until RabbitMQ confirms messages when publishing or sending messages (default: true) -| Heartbeat | TimeSpan |The heartbeat interval used by the RabbitMQ client to keep the connection alive -| RequestedChannelMax | ushort | The maximum number of channels allowed on the connection -| RequestedConnectionTimeout | TimeSpan | The connection timeout -| ContinuationTimeout | TImeSpan | Sets the time the client will wait for the broker to response to RPC requests. Increase this value if you are experiencing timeouts from RabbitMQ due to a slow broker instance. - -#### UseCluster - -MassTransit can connect to a cluster of RabbitMQ virtual hosts and treat them as a single virtual host. To configure a cluster, call the `UseCluster` methods, and add the cluster nodes, each of which becomes part of the virtual host identified by the host name. Each cluster node can specify either a `host` or a `host:port` combination. - -> While this exists, it's generally preferable to configure something like HAProxy in front of a RabbitMQ cluster, instead of using MassTransit's built-in cluster configuration. - -#### ConfigureBatchPublish - -MassTransit will briefly buffer messages before sending them to RabbitMQ, to increase message throughput. While use of the default values is recommended, the batch options can be configured. - -| Property | Type | Default |Description -|-------|------------------------|-----|--------|--- -| Enabled | bool | false | Enable or disable batch sends to RabbitMQ -| MessageLimit | int | 100 | Limit the number of messages per batch -| SizeLimit | int | 64K | A rough limit of the total message size -| Timeout | TimeSpan | 1ms | The time to wait for additional messages before sending - -<<< @/docs/code/transports/ConfigureBatchConsoleListener.cs - -MassTransit includes several receive endpoint level configuration options that control receive endpoint behavior. - -| Property | Type | Description -|-------------------------|--------|------------------ -| PrefetchCount | ushort | The number of unacknowledged messages that can be processed concurrently (default based on CPU count) -| PurgeOnStartup | bool | Removes all messages from the queue when the bus is started (default: false) -| AutoDelete | bool | If true, the queue will be automatically deleted when the bus is stopped (default: false) -| Durable | bool | If true, messages are persisted to disk before being acknowledged (default: true) - -## Additional Examples - -### CloudAMQP - -MassTransit can be used with CloudAMQP, which is a great SaaS-based solution to host your RabbitMQ broker. To configure MassTransit, the host and virtual host must be specified, and _UseSsl_ must be configured. - -<<< @/docs/code/transports/CloudAmqpConsoleListener.cs - -### AmazonMQ - RabbitMQ - -AmazonMQ now includes [RabbitMQ support](https://us-east-2.console.aws.amazon.com/amazon-mq/home), which means the best message broker can now be used easily on AWS. To configure MassTransit, the AMQPS endpoint address can be used to configure the host as shown below. - -<<< @/docs/code/transports/AmazonRabbitMqConsoleListener.cs diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index ab995fb59a8..00000000000 --- a/package-lock.json +++ /dev/null @@ -1,29739 +0,0 @@ -{ - "name": "masstransit-docs", - "version": "0.1.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "masstransit-docs", - "version": "0.1.0", - "license": "ISC", - "dependencies": { - "esm": "^3.2.25", - "follow-redirects": "^1.14.8", - "markdown-it-html5-embed": "^1.0.0", - "path-parse": "^1.0.7", - "prismjs": "^1.26.0", - "remove-markdown": "^0.3.0", - "vue-tweet-embed": "^2.4.0" - }, - "devDependencies": { - "@vuepress/plugin-active-header-links": "^1.9.7", - "@vuepress/plugin-back-to-top": "^1.9.7", - "@vuepress/plugin-google-analytics": "^1.9.7", - "vuepress": "^1.9.7" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, - "dependencies": { - "@babel/highlight": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz", - "integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.2", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-module-transforms": "^7.20.2", - "@babel/helpers": "^7.20.1", - "@babel/parser": "^7.20.2", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/generator": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz", - "integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.2", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", - "dev": true, - "dependencies": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", - "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.0", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.2.tgz", - "integrity": "sha512-k22GoYRAHPYr9I+Gvy2ZQlAe5mGy8BqWst2wRt8cwIufWTxrsVshhIBvYNqC80N0GSFWTsqRVexOtfzlgOEDvA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz", - "integrity": "sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0-0" - } - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", - "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", - "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", - "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/traverse": "^7.19.1", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", - "dev": true, - "dependencies": { - "@babel/types": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, - "dependencies": { - "@babel/types": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz", - "integrity": "sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==", - "dev": true, - "dependencies": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", - "dev": true, - "dependencies": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz", - "integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg==", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", - "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz", - "integrity": "sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==", - "dev": true, - "dependencies": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", - "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-proposal-decorators": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.2.tgz", - "integrity": "sha512-nkBH96IBmgKnbHQ5gXFrcmez+Z9S2EIDKDQGp005ROqBigc88Tky4rzCnlP/lnlj245dCEQl4/YyV0V1kYh5dw==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.20.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/plugin-syntax-decorators": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", - "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz", - "integrity": "sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", - "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "dev": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", - "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-decorators": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", - "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", - "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", - "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.2.tgz", - "integrity": "sha512-y5V15+04ry69OV2wULmwhEA6jwSWXO1TwAtIwiPXcvHcoOQUqpyMVd2bDsQJMW8AurjulIyUV8kDqtjSwHy1uQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz", - "integrity": "sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==", - "dev": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", - "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz", - "integrity": "sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", - "dev": true, - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", - "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz", - "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz", - "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-simple-access": "^7.19.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz", - "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==", - "dev": true, - "dependencies": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-identifier": "^7.19.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz", - "integrity": "sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.3.tgz", - "integrity": "sha512-oZg/Fpx0YDrj13KsLyO8I/CX3Zdw7z0O9qOd95SqcoIzuqy/WTGWvePeHAnZCN54SfdyjHcb1S30gc8zlzlHcA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.20.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", - "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", - "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", - "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.18.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", - "dev": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", - "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.20.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.20.2", - "@babel/plugin-transform-classes": "^7.20.2", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.20.2", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.19.6", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.6", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.20.1", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dev": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", - "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.13.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz", - "integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==", - "dev": true, - "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "node_modules/@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "dependencies": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true, - "engines": { - "node": ">= 6" - } - }, - "node_modules/@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dev": true, - "dependencies": { - "defer-to-connect": "^1.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", - "dev": true, - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", - "dev": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/highlight.js": { - "version": "9.12.4", - "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz", - "integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==", - "dev": true - }, - "node_modules/@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", - "dev": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "node_modules/@types/linkify-it": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", - "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", - "dev": true - }, - "node_modules/@types/markdown-it": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-10.0.3.tgz", - "integrity": "sha512-daHJk22isOUvNssVGF2zDnnSyxHhFYhtjeX4oQaKD6QzL3ZR1QSgiD1g+Q6/WSWYVogNXYDXODtbgW/WiFCtyw==", - "dev": true, - "dependencies": { - "@types/highlight.js": "^9.7.0", - "@types/linkify-it": "*", - "@types/mdurl": "*", - "highlight.js": "^9.7.0" - } - }, - "node_modules/@types/mdurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", - "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", - "dev": true - }, - "node_modules/@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", - "dev": true - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true - }, - "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", - "dev": true - }, - "node_modules/@types/q": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", - "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==", - "dev": true - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true - }, - "node_modules/@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "dev": true, - "dependencies": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "node_modules/@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", - "dev": true - }, - "node_modules/@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", - "dev": true - }, - "node_modules/@types/uglify-js": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz", - "integrity": "sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==", - "dev": true, - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/@types/webpack": { - "version": "4.41.33", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.33.tgz", - "integrity": "sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/@types/webpack-dev-server": { - "version": "3.11.6", - "resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz", - "integrity": "sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ==", - "dev": true, - "dependencies": { - "@types/connect-history-api-fallback": "*", - "@types/express": "*", - "@types/serve-static": "*", - "@types/webpack": "^4", - "http-proxy-middleware": "^1.0.0" - } - }, - "node_modules/@types/webpack-sources": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", - "dev": true, - "dependencies": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.7.3" - } - }, - "node_modules/@types/webpack-sources/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@vue/babel-helper-vue-jsx-merge-props": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", - "integrity": "sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==", - "dev": true - }, - "node_modules/@vue/babel-helper-vue-transform-on": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz", - "integrity": "sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==", - "dev": true - }, - "node_modules/@vue/babel-plugin-jsx": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz", - "integrity": "sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/template": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", - "@vue/babel-helper-vue-transform-on": "^1.0.2", - "camelcase": "^6.0.0", - "html-tags": "^3.1.0", - "svg-tags": "^1.0.0" - } - }, - "node_modules/@vue/babel-plugin-transform-vue-jsx": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.4.0.tgz", - "integrity": "sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "html-tags": "^2.0.0", - "lodash.kebabcase": "^4.1.1", - "svg-tags": "^1.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-plugin-transform-vue-jsx/node_modules/html-tags": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", - "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@vue/babel-preset-app": { - "version": "4.5.19", - "resolved": "https://registry.npmjs.org/@vue/babel-preset-app/-/babel-preset-app-4.5.19.tgz", - "integrity": "sha512-VCNRiAt2P/bLo09rYt3DLe6xXUMlhJwrvU18Ddd/lYJgC7s8+wvhgYs+MTx4OiAXdu58drGwSBO9SPx7C6J82Q==", - "dev": true, - "dependencies": { - "@babel/core": "^7.11.0", - "@babel/helper-compilation-targets": "^7.9.6", - "@babel/helper-module-imports": "^7.8.3", - "@babel/plugin-proposal-class-properties": "^7.8.3", - "@babel/plugin-proposal-decorators": "^7.8.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.11.0", - "@babel/preset-env": "^7.11.0", - "@babel/runtime": "^7.11.0", - "@vue/babel-plugin-jsx": "^1.0.3", - "@vue/babel-preset-jsx": "^1.2.4", - "babel-plugin-dynamic-import-node": "^2.3.3", - "core-js": "^3.6.5", - "core-js-compat": "^3.6.5", - "semver": "^6.1.0" - }, - "peerDependencies": { - "@babel/core": "*", - "core-js": "^3", - "vue": "^2 || ^3.0.0-0" - }, - "peerDependenciesMeta": { - "core-js": { - "optional": true - }, - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/babel-preset-jsx": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-preset-jsx/-/babel-preset-jsx-1.4.0.tgz", - "integrity": "sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==", - "dev": true, - "dependencies": { - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "@vue/babel-sugar-composition-api-inject-h": "^1.4.0", - "@vue/babel-sugar-composition-api-render-instance": "^1.4.0", - "@vue/babel-sugar-functional-vue": "^1.4.0", - "@vue/babel-sugar-inject-h": "^1.4.0", - "@vue/babel-sugar-v-model": "^1.4.0", - "@vue/babel-sugar-v-on": "^1.4.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0", - "vue": "*" - }, - "peerDependenciesMeta": { - "vue": { - "optional": true - } - } - }, - "node_modules/@vue/babel-sugar-composition-api-inject-h": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.4.0.tgz", - "integrity": "sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-composition-api-render-instance": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.4.0.tgz", - "integrity": "sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-functional-vue": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.4.0.tgz", - "integrity": "sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-inject-h": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.4.0.tgz", - "integrity": "sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-v-model": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.4.0.tgz", - "integrity": "sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "camelcase": "^5.0.0", - "html-tags": "^2.0.0", - "svg-tags": "^1.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-v-model/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@vue/babel-sugar-v-model/node_modules/html-tags": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", - "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@vue/babel-sugar-v-on": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.4.0.tgz", - "integrity": "sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==", - "dev": true, - "dependencies": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "camelcase": "^5.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@vue/babel-sugar-v-on/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/@vue/compiler-sfc": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", - "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", - "dependencies": { - "@babel/parser": "^7.18.4", - "postcss": "^8.4.14", - "source-map": "^0.6.1" - } - }, - "node_modules/@vue/component-compiler-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", - "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", - "dev": true, - "dependencies": { - "consolidate": "^0.15.1", - "hash-sum": "^1.0.2", - "lru-cache": "^4.1.2", - "merge-source-map": "^1.1.0", - "postcss": "^7.0.36", - "postcss-selector-parser": "^6.0.2", - "source-map": "~0.6.1", - "vue-template-es2015-compiler": "^1.9.0" - }, - "optionalDependencies": { - "prettier": "^1.18.2 || ^2.0.0" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "dependencies": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/@vue/component-compiler-utils/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/@vue/component-compiler-utils/node_modules/yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true - }, - "node_modules/@vuepress/core": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/core/-/core-1.9.7.tgz", - "integrity": "sha512-u5eb1mfNLV8uG2UuxlvpB/FkrABxeMHqymTsixOnsOg2REziv9puEIbqaZ5BjLPvbCDvSj6rn+DwjENmBU+frQ==", - "dev": true, - "dependencies": { - "@babel/core": "^7.8.4", - "@vue/babel-preset-app": "^4.1.2", - "@vuepress/markdown": "1.9.7", - "@vuepress/markdown-loader": "1.9.7", - "@vuepress/plugin-last-updated": "1.9.7", - "@vuepress/plugin-register-components": "1.9.7", - "@vuepress/shared-utils": "1.9.7", - "@vuepress/types": "1.9.7", - "autoprefixer": "^9.5.1", - "babel-loader": "^8.0.4", - "bundle-require": "2.1.8", - "cache-loader": "^3.0.0", - "chokidar": "^2.0.3", - "connect-history-api-fallback": "^1.5.0", - "copy-webpack-plugin": "^5.0.2", - "core-js": "^3.6.4", - "cross-spawn": "^6.0.5", - "css-loader": "^2.1.1", - "esbuild": "0.14.7", - "file-loader": "^3.0.1", - "js-yaml": "^3.13.1", - "lru-cache": "^5.1.1", - "mini-css-extract-plugin": "0.6.0", - "optimize-css-assets-webpack-plugin": "^5.0.1", - "portfinder": "^1.0.13", - "postcss-loader": "^3.0.0", - "postcss-safe-parser": "^4.0.1", - "toml": "^3.0.0", - "url-loader": "^1.0.1", - "vue": "^2.6.10", - "vue-loader": "^15.7.1", - "vue-router": "^3.4.5", - "vue-server-renderer": "^2.6.10", - "vue-template-compiler": "^2.6.10", - "vuepress-html-webpack-plugin": "^3.2.0", - "vuepress-plugin-container": "^2.0.2", - "webpack": "^4.8.1", - "webpack-chain": "^6.0.0", - "webpack-dev-server": "^3.5.1", - "webpack-merge": "^4.1.2", - "webpackbar": "3.2.0" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/@vuepress/markdown": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/markdown/-/markdown-1.9.7.tgz", - "integrity": "sha512-DFOjYkwV6fT3xXTGdTDloeIrT1AbwJ9pwefmrp0rMgC6zOz3XUJn6qqUwcYFO5mNBWpbiFQ3JZirCtgOe+xxBA==", - "dev": true, - "dependencies": { - "@vuepress/shared-utils": "1.9.7", - "markdown-it": "^8.4.1", - "markdown-it-anchor": "^5.0.2", - "markdown-it-chain": "^1.3.0", - "markdown-it-emoji": "^1.4.0", - "markdown-it-table-of-contents": "^0.4.0", - "prismjs": "^1.13.0" - } - }, - "node_modules/@vuepress/markdown-loader": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/markdown-loader/-/markdown-loader-1.9.7.tgz", - "integrity": "sha512-mxXF8FtX/QhOg/UYbe4Pr1j5tcf/aOEI502rycTJ3WF2XAtOmewjkGV4eAA6f6JmuM/fwzOBMZKDyy9/yo2I6Q==", - "dev": true, - "dependencies": { - "@vuepress/markdown": "1.9.7", - "loader-utils": "^1.1.0", - "lru-cache": "^5.1.1" - } - }, - "node_modules/@vuepress/plugin-active-header-links": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-active-header-links/-/plugin-active-header-links-1.9.7.tgz", - "integrity": "sha512-G1M8zuV9Og3z8WBiKkWrofG44NEXsHttc1MYreDXfeWh/NLjr9q1GPCEXtiCjrjnHZHB3cSQTKnTqAHDq35PGA==", - "dev": true, - "dependencies": { - "@vuepress/types": "1.9.7", - "lodash.debounce": "^4.0.8" - } - }, - "node_modules/@vuepress/plugin-back-to-top": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-back-to-top/-/plugin-back-to-top-1.9.7.tgz", - "integrity": "sha512-DM1S+Q8Xn/i+zhe4zThekxb1M2abfKLklg/NKtQloklHKdNdVfk+EcxWYNmNfSii+ymDWaaG8lmH0xjVhy0iXw==", - "dev": true, - "dependencies": { - "@vuepress/types": "1.9.7", - "lodash.debounce": "^4.0.8" - } - }, - "node_modules/@vuepress/plugin-google-analytics": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-google-analytics/-/plugin-google-analytics-1.9.7.tgz", - "integrity": "sha512-ZpsYrk23JdwbcJo9xArVcdqYHt5VyTX9UN9bLqNrLJRgRTV0X2jKUkM63dlKTJMpBf+0K1PQMJbGBXgOO7Yh0Q==", - "dev": true, - "dependencies": { - "@vuepress/types": "1.9.7" - } - }, - "node_modules/@vuepress/plugin-last-updated": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-last-updated/-/plugin-last-updated-1.9.7.tgz", - "integrity": "sha512-FiFBOl49dlFRjbLRnRAv77HDWfe+S/eCPtMQobq4/O3QWuL3Na5P4fCTTVzq1K7rWNO9EPsWNB2Jb26ndlQLKQ==", - "dev": true, - "dependencies": { - "@vuepress/types": "1.9.7", - "cross-spawn": "^6.0.5" - } - }, - "node_modules/@vuepress/plugin-nprogress": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-nprogress/-/plugin-nprogress-1.9.7.tgz", - "integrity": "sha512-sI148igbdRfLgyzB8PdhbF51hNyCDYXsBn8bBWiHdzcHBx974sVNFKtfwdIZcSFsNrEcg6zo8YIrQ+CO5vlUhQ==", - "dev": true, - "dependencies": { - "@vuepress/types": "1.9.7", - "nprogress": "^0.2.0" - } - }, - "node_modules/@vuepress/plugin-register-components": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-register-components/-/plugin-register-components-1.9.7.tgz", - "integrity": "sha512-l/w1nE7Dpl+LPMb8+AHSGGFYSP/t5j6H4/Wltwc2QcdzO7yqwC1YkwwhtTXvLvHOV8O7+rDg2nzvq355SFkfKA==", - "dev": true, - "dependencies": { - "@vuepress/shared-utils": "1.9.7", - "@vuepress/types": "1.9.7" - } - }, - "node_modules/@vuepress/plugin-search": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-search/-/plugin-search-1.9.7.tgz", - "integrity": "sha512-MLpbUVGLxaaHEwflFxvy0pF9gypFVUT3Q9Zc6maWE+0HDWAvzMxo6GBaj6mQPwjOqNQMf4QcN3hDzAZktA+DQg==", - "dev": true, - "dependencies": { - "@vuepress/types": "1.9.7" - } - }, - "node_modules/@vuepress/shared-utils": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/shared-utils/-/shared-utils-1.9.7.tgz", - "integrity": "sha512-lIkO/eSEspXgVHjYHa9vuhN7DuaYvkfX1+TTJDiEYXIwgwqtvkTv55C+IOdgswlt0C/OXDlJaUe1rGgJJ1+FTw==", - "dev": true, - "dependencies": { - "chalk": "^2.3.2", - "escape-html": "^1.0.3", - "fs-extra": "^7.0.1", - "globby": "^9.2.0", - "gray-matter": "^4.0.1", - "hash-sum": "^1.0.2", - "semver": "^6.0.0", - "toml": "^3.0.0", - "upath": "^1.1.0" - } - }, - "node_modules/@vuepress/theme-default": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/theme-default/-/theme-default-1.9.7.tgz", - "integrity": "sha512-NZzCLIl+bgJIibhkqVmk/NSku57XIuXugxAN3uiJrCw6Mu6sb3xOvbk0En3k+vS2BKHxAZ6Cx7dbCiyknDQnSA==", - "dev": true, - "dependencies": { - "@vuepress/plugin-active-header-links": "1.9.7", - "@vuepress/plugin-nprogress": "1.9.7", - "@vuepress/plugin-search": "1.9.7", - "@vuepress/types": "1.9.7", - "docsearch.js": "^2.5.2", - "lodash": "^4.17.15", - "stylus": "^0.54.8", - "stylus-loader": "^3.0.2", - "vuepress-plugin-container": "^2.0.2", - "vuepress-plugin-smooth-scroll": "^0.0.3" - } - }, - "node_modules/@vuepress/types": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/types/-/types-1.9.7.tgz", - "integrity": "sha512-moLQzkX3ED2o18dimLemUm7UVDKxhcrJmGt5C0Ng3xxrLPaQu7UqbROtEKB3YnMRt4P/CA91J+Ck+b9LmGabog==", - "dev": true, - "dependencies": { - "@types/markdown-it": "^10.0.0", - "@types/webpack-dev-server": "^3", - "webpack-chain": "^6.0.0" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", - "dev": true, - "dependencies": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", - "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", - "dev": true, - "dependencies": { - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "node_modules/@webassemblyjs/helper-fsm": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", - "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-module-context": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", - "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", - "dev": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", - "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", - "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", - "dev": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", - "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", - "dev": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", - "dev": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", - "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", - "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", - "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", - "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "node_modules/@webassemblyjs/wast-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", - "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/floating-point-hex-parser": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-code-frame": "1.9.0", - "@webassemblyjs/helper-fsm": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true, - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/agentkeepalive": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-2.2.0.tgz", - "integrity": "sha512-TnB6ziK363p7lR8QpeLC8aMr8EGYBKZTpgzQLfqTs3bR0Oo5VbKdwKf8h0dSzsYrB7lSCgfJnMZKqShvlq5Oyg==", - "dev": true, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true, - "peerDependencies": { - "ajv": ">=5.0.0" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/algoliasearch": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-3.35.1.tgz", - "integrity": "sha512-K4yKVhaHkXfJ/xcUnil04xiSrB8B8yHZoFEhWNpXg23eiCnqvTZw1tn/SqvdsANlYHLJlKl0qi3I/Q2Sqo7LwQ==", - "dev": true, - "dependencies": { - "agentkeepalive": "^2.2.0", - "debug": "^2.6.9", - "envify": "^4.0.0", - "es6-promise": "^4.1.0", - "events": "^1.1.0", - "foreach": "^2.0.5", - "global": "^4.3.2", - "inherits": "^2.0.1", - "isarray": "^2.0.1", - "load-script": "^1.0.0", - "object-keys": "^1.0.11", - "querystring-es3": "^0.2.1", - "reduce": "^1.0.1", - "semver": "^5.1.0", - "tunnel-agent": "^0.6.0" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/algoliasearch/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/algoliasearch/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/algoliasearch/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==", - "dev": true - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dev": true, - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "node_modules/array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dev": true, - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array.prototype.reduce": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", - "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "dependencies": { - "safer-buffer": "~2.1.0" - } - }, - "node_modules/asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "dependencies": { - "object-assign": "^4.1.1", - "util": "0.10.3" - } - }, - "node_modules/assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/assert/node_modules/inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", - "dev": true - }, - "node_modules/assert/node_modules/util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==", - "dev": true, - "dependencies": { - "inherits": "2.0.1" - } - }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "dependencies": { - "lodash": "^4.17.14" - } - }, - "node_modules/async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "node_modules/async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "node_modules/atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true, - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/autocomplete.js": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/autocomplete.js/-/autocomplete.js-0.36.0.tgz", - "integrity": "sha512-jEwUXnVMeCHHutUt10i/8ZiRaCb0Wo+ZyKxeGsYwBDtw6EJHqEeDrq4UwZRD8YBSvp3g6klP678il2eeiVXN2Q==", - "dev": true, - "dependencies": { - "immediate": "^3.2.3" - } - }, - "node_modules/autoprefixer": { - "version": "9.8.8", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", - "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", - "dev": true, - "dependencies": { - "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001109", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "picocolors": "^0.2.1", - "postcss": "^7.0.32", - "postcss-value-parser": "^4.1.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - }, - "node_modules/autoprefixer/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/autoprefixer/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, - "node_modules/babel-loader": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", - "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", - "dev": true, - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "dependencies": { - "object.assign": "^4.1.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true - }, - "node_modules/bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "dependencies": { - "tweetnacl": "^0.14.3" - } - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "node_modules/bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==", - "dev": true, - "dependencies": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "node_modules/boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", - "dev": true, - "dependencies": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/boxen/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/boxen/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/boxen/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/boxen/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/boxen/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/boxen/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "dependencies": { - "fill-range": "^7.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "dependencies": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - } - }, - "node_modules/browserify-sign/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/browserify-sign/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "node_modules/buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "node_modules/buffer-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/buffer-json/-/buffer-json-2.0.0.tgz", - "integrity": "sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==", - "dev": true - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true - }, - "node_modules/buffer/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true - }, - "node_modules/bundle-require": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-2.1.8.tgz", - "integrity": "sha512-oOEg3A0hy/YzvNWNowtKD0pmhZKseOFweCbgyMqTIih4gRY1nJWsvrOCT27L9NbIyL5jMjTFrAUpGxxpW68Puw==", - "dev": true, - "peerDependencies": { - "esbuild": ">=0.13" - } - }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", - "dev": true, - "dependencies": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "node_modules/cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/cache-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-3.0.1.tgz", - "integrity": "sha512-HzJIvGiGqYsFUrMjAJNDbVZoG7qQA+vy9AIoKs7s9DscNfki0I589mf2w6/tW+kkFH3zyiknoWV5Jdynu6b/zw==", - "dev": true, - "dependencies": { - "buffer-json": "^2.0.0", - "find-cache-dir": "^2.1.0", - "loader-utils": "^1.2.3", - "mkdirp": "^0.5.1", - "neo-async": "^2.6.1", - "schema-utils": "^1.0.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^4.0.0" - } - }, - "node_modules/cache-loader/node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cache-loader/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cache-loader/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cache-loader/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cache-loader/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cache-loader/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/cache-loader/node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cache-loader/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/cache-loader/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dev": true, - "dependencies": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cacheable-request/node_modules/lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/cacheable-request/node_modules/normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "dev": true - }, - "node_modules/caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "dev": true, - "dependencies": { - "callsites": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "dev": true, - "dependencies": { - "caller-callsite": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", - "dev": true, - "dependencies": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - } - ] - }, - "node_modules/caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true - }, - "node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "deprecated": "Chokidar 2 does not receive security updates since 2019. Upgrade to chokidar 3 with 15x fewer dependencies", - "dev": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } - }, - "node_modules/chokidar/node_modules/anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/chokidar/node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chokidar/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "node_modules/cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clean-css": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", - "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", - "dev": true, - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", - "dev": true, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "dependencies": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "node_modules/cliui/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/cliui/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/cliui/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "dependencies": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "dev": true, - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dev": true, - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true - }, - "node_modules/commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "node_modules/component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "dependencies": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "engines": [ - "node >= 0.8" - ], - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "dependencies": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "dev": true - }, - "node_modules/console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "node_modules/consolidate": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", - "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", - "dev": true, - "dependencies": { - "bluebird": "^3.1.1" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true - }, - "node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-disposition/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "node_modules/cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true - }, - "node_modules/copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "dev": true, - "dependencies": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/copy-webpack-plugin": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.2.tgz", - "integrity": "sha512-Uh7crJAco3AjBvgAy9Z75CjK8IG+gxaErro71THQ+vv/bl4HaQcpkexAY8KVW/T6D2W2IRr+couF/knIRkZMIQ==", - "dev": true, - "dependencies": { - "cacache": "^12.0.3", - "find-cache-dir": "^2.1.0", - "glob-parent": "^3.1.0", - "globby": "^7.1.1", - "is-glob": "^4.0.1", - "loader-utils": "^1.2.3", - "minimatch": "^3.0.4", - "normalize-path": "^3.0.0", - "p-limit": "^2.2.1", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "webpack-log": "^2.0.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/copy-webpack-plugin/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g==", - "dev": true, - "dependencies": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/copy-webpack-plugin/node_modules/globby/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/copy-webpack-plugin/node_modules/ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "node_modules/copy-webpack-plugin/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/copy-webpack-plugin/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/copy-webpack-plugin/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/copy-webpack-plugin/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/copy-webpack-plugin/node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/copy-webpack-plugin/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/core-js": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz", - "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==", - "dev": true, - "hasInstallScript": true, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz", - "integrity": "sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==", - "dev": true, - "dependencies": { - "browserslist": "^4.21.4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "node_modules/cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" - } - }, - "node_modules/cross-spawn/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "engines": { - "node": "*" - } - }, - "node_modules/crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/css": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", - "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "source-map": "^0.6.1", - "source-map-resolve": "^0.5.2", - "urix": "^0.1.0" - } - }, - "node_modules/css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/css-declaration-sorter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", - "dev": true, - "dependencies": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - }, - "engines": { - "node": ">4" - } - }, - "node_modules/css-declaration-sorter/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/css-declaration-sorter/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/css-loader": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.1.1.tgz", - "integrity": "sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==", - "dev": true, - "dependencies": { - "camelcase": "^5.2.0", - "icss-utils": "^4.1.0", - "loader-utils": "^1.2.3", - "normalize-path": "^3.0.0", - "postcss": "^7.0.14", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^2.0.6", - "postcss-modules-scope": "^2.1.0", - "postcss-modules-values": "^2.0.0", - "postcss-value-parser": "^3.3.0", - "schema-utils": "^1.0.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^4.0.0" - } - }, - "node_modules/css-loader/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/css-loader/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/css-loader/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/css-loader/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/css-loader/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/css-parse": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz", - "integrity": "sha512-UNIFik2RgSbiTwIW1IsFwXWn6vs+bYdq83LKTSOsx7NJR7WII9dxewkHLltfTLVppoUApHV0118a4RZRI9FLwA==", - "dev": true, - "dependencies": { - "css": "^2.0.0" - } - }, - "node_modules/css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "node_modules/css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, - "node_modules/css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true, - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz", - "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==", - "dev": true, - "dependencies": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.8", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-preset-default": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz", - "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==", - "dev": true, - "dependencies": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.3", - "postcss-unique-selectors": "^4.0.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-preset-default/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/cssnano-preset-default/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", - "integrity": "sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", - "integrity": "sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-util-raw-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-util-raw-cache/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/cssnano-util-raw-cache/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/cssnano/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "node_modules/csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, - "node_modules/cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==", - "dev": true - }, - "node_modules/dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true - }, - "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", - "dev": true, - "dependencies": { - "mimic-response": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "dependencies": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deepmerge": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", - "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-gateway": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", - "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", - "dev": true, - "dependencies": { - "execa": "^1.0.0", - "ip-regex": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true - }, - "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "dependencies": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/del/node_modules/globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", - "dev": true, - "dependencies": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true - }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "dependencies": { - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, - "node_modules/dns-packet": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", - "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", - "dev": true, - "dependencies": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/dns-txt": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ==", - "dev": true, - "dependencies": { - "buffer-indexof": "^1.0.0" - } - }, - "node_modules/docsearch.js": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/docsearch.js/-/docsearch.js-2.6.3.tgz", - "integrity": "sha512-GN+MBozuyz664ycpZY0ecdQE0ND/LSgJKhTLA0/v3arIS3S1Rpf2OJz6A35ReMsm91V5apcmzr5/kM84cvUg+A==", - "deprecated": "This package has been deprecated and is no longer maintained. Please use @docsearch/js.", - "dev": true, - "dependencies": { - "algoliasearch": "^3.24.5", - "autocomplete.js": "0.36.0", - "hogan.js": "^3.0.2", - "request": "^2.87.0", - "stack-utils": "^1.0.1", - "to-factory": "^1.0.0", - "zepto": "^1.2.0" - } - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/dom-serializer/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", - "dev": true - }, - "node_modules/domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true, - "engines": { - "node": ">=0.4", - "npm": ">=1.2" - } - }, - "node_modules/domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domhandler/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/duplexer3": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", - "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", - "dev": true - }, - "node_modules/duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dev": true, - "dependencies": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true - }, - "node_modules/electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", - "dev": true - }, - "node_modules/elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/enhanced-resolve/node_modules/memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, - "node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, - "node_modules/envify": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/envify/-/envify-4.1.0.tgz", - "integrity": "sha512-IKRVVoAYr4pIx4yIWNsz9mOsboxlNXiu7TNBnem/K/uTHdkyzXWDzHCK7UTolqBbgaBz0tQHsD3YNls0uIIjiw==", - "dev": true, - "dependencies": { - "esprima": "^4.0.0", - "through": "~2.3.4" - }, - "bin": { - "envify": "bin/envify" - } - }, - "node_modules/envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true, - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", - "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true - }, - "node_modules/esbuild": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.7.tgz", - "integrity": "sha512-+u/msd6iu+HvfysUPkZ9VHm83LImmSNnecYPfFI01pQ7TTcsFR+V0BkybZX7mPtIaI7LCrse6YRj+v3eraJSgw==", - "dev": true, - "hasInstallScript": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "optionalDependencies": { - "esbuild-android-arm64": "0.14.7", - "esbuild-darwin-64": "0.14.7", - "esbuild-darwin-arm64": "0.14.7", - "esbuild-freebsd-64": "0.14.7", - "esbuild-freebsd-arm64": "0.14.7", - "esbuild-linux-32": "0.14.7", - "esbuild-linux-64": "0.14.7", - "esbuild-linux-arm": "0.14.7", - "esbuild-linux-arm64": "0.14.7", - "esbuild-linux-mips64le": "0.14.7", - "esbuild-linux-ppc64le": "0.14.7", - "esbuild-netbsd-64": "0.14.7", - "esbuild-openbsd-64": "0.14.7", - "esbuild-sunos-64": "0.14.7", - "esbuild-windows-32": "0.14.7", - "esbuild-windows-64": "0.14.7", - "esbuild-windows-arm64": "0.14.7" - } - }, - "node_modules/esbuild-android-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.7.tgz", - "integrity": "sha512-9/Q1NC4JErvsXzJKti0NHt+vzKjZOgPIjX/e6kkuCzgfT/GcO3FVBcGIv4HeJG7oMznE6KyKhvLrFgt7CdU2/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "node_modules/esbuild-darwin-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.7.tgz", - "integrity": "sha512-Z9X+3TT/Xj+JiZTVlwHj2P+8GoiSmUnGVz0YZTSt8WTbW3UKw5Pw2ucuJ8VzbD2FPy0jbIKJkko/6CMTQchShQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/esbuild-darwin-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.7.tgz", - "integrity": "sha512-68e7COhmwIiLXBEyxUxZSSU0akgv8t3e50e2QOtKdBUE0F6KIRISzFntLe2rYlNqSsjGWsIO6CCc9tQxijjSkw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/esbuild-freebsd-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.7.tgz", - "integrity": "sha512-76zy5jAjPiXX/S3UvRgG85Bb0wy0zv/J2lel3KtHi4V7GUTBfhNUPt0E5bpSXJ6yMT7iThhnA5rOn+IJiUcslQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/esbuild-freebsd-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.7.tgz", - "integrity": "sha512-lSlYNLiqyzd7qCN5CEOmLxn7MhnGHPcu5KuUYOG1i+t5A6q7LgBmfYC9ZHJBoYyow3u4CNu79AWHbvVLpE/VQQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/esbuild-linux-32": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.7.tgz", - "integrity": "sha512-Vk28u409wVOXqTaT6ek0TnfQG4Ty1aWWfiysIaIRERkNLhzLhUf4i+qJBN8mMuGTYOkE40F0Wkbp6m+IidOp2A==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/esbuild-linux-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.7.tgz", - "integrity": "sha512-+Lvz6x+8OkRk3K2RtZwO+0a92jy9si9cUea5Zoru4yJ/6EQm9ENX5seZE0X9DTwk1dxJbjmLsJsd3IoowyzgVg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/esbuild-linux-arm": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.7.tgz", - "integrity": "sha512-OzpXEBogbYdcBqE4uKynuSn5YSetCvK03Qv1HcOY1VN6HmReuatjJ21dCH+YPHSpMEF0afVCnNfffvsGEkxGJQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/esbuild-linux-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.7.tgz", - "integrity": "sha512-kJd5beWSqteSAW086qzCEsH6uwpi7QRIpzYWHzEYwKKu9DiG1TwIBegQJmLpPsLp4v5RAFjea0JAmAtpGtRpqg==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/esbuild-linux-mips64le": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.7.tgz", - "integrity": "sha512-mFWpnDhZJmj/h7pxqn1GGDsKwRfqtV7fx6kTF5pr4PfXe8pIaTERpwcKkoCwZUkWAOmUEjMIUAvFM72A6hMZnA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/esbuild-linux-ppc64le": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.7.tgz", - "integrity": "sha512-wM7f4M0bsQXfDL4JbbYD0wsr8cC8KaQ3RPWc/fV27KdErPW7YsqshZZSjDV0kbhzwpNNdhLItfbaRT8OE8OaKA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/esbuild-netbsd-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.7.tgz", - "integrity": "sha512-J/afS7woKyzGgAL5FlgvMyqgt5wQ597lgsT+xc2yJ9/7BIyezeXutXqfh05vszy2k3kSvhLesugsxIA71WsqBw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ] - }, - "node_modules/esbuild-openbsd-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.7.tgz", - "integrity": "sha512-7CcxgdlCD+zAPyveKoznbgr3i0Wnh0L8BDGRCjE/5UGkm5P/NQko51tuIDaYof8zbmXjjl0OIt9lSo4W7I8mrw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ] - }, - "node_modules/esbuild-sunos-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.7.tgz", - "integrity": "sha512-GKCafP2j/KUljVC3nesw1wLFSZktb2FGCmoT1+730zIF5O6hNroo0bSEofm6ZK5mNPnLiSaiLyRB9YFgtkd5Xg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ] - }, - "node_modules/esbuild-windows-32": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.7.tgz", - "integrity": "sha512-5I1GeL/gZoUUdTPA0ws54bpYdtyeA2t6MNISalsHpY269zK8Jia/AXB3ta/KcDHv2SvNwabpImeIPXC/k0YW6A==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/esbuild-windows-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.7.tgz", - "integrity": "sha512-CIGKCFpQOSlYsLMbxt8JjxxvVw9MlF1Rz2ABLVfFyHUF5OeqHD5fPhGrCVNaVrhO8Xrm+yFmtjcZudUGr5/WYQ==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/esbuild-windows-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.7.tgz", - "integrity": "sha512-eOs1eSivOqN7cFiRIukEruWhaCf75V0N8P0zP7dh44LIhLl8y6/z++vv9qQVbkBm5/D7M7LfCfCTmt1f1wHOCw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "dependencies": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==", - "engines": { - "node": ">=6" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true, - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "node_modules/events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", - "dev": true, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "dependencies": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", - "dev": true, - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/expand-brackets/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dev": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - } - }, - "node_modules/express/node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/express/node_modules/qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "dependencies": { - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/express/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true, - "engines": [ - "node >=0.6.0" - ] - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "dependencies": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/fast-glob/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-glob/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/figgy-pudding": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", - "dev": true - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/file-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-3.0.1.tgz", - "integrity": "sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw==", - "dev": true, - "dependencies": { - "loader-utils": "^1.0.2", - "schema-utils": "^1.0.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^4.0.0" - } - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", - "dev": true - }, - "node_modules/forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "dev": true, - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "node_modules/fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, - "engines": { - "node": ">=6 <7 || >=8" - } - }, - "node_modules/fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, - "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "dependencies": { - "pump": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "dev": true, - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==", - "dev": true - }, - "node_modules/global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dev": true, - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "node_modules/global-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", - "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", - "dev": true, - "dependencies": { - "ini": "1.3.7" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/globby": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", - "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", - "dev": true, - "dependencies": { - "@types/glob": "^7.1.1", - "array-union": "^1.0.2", - "dir-glob": "^2.2.2", - "fast-glob": "^2.2.6", - "glob": "^7.1.3", - "ignore": "^4.0.3", - "pify": "^4.0.1", - "slash": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dev": true, - "dependencies": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "dev": true, - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true - }, - "node_modules/har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "deprecated": "this library is no longer supported", - "dev": true, - "dependencies": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "dev": true, - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/hash-base/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", - "dev": true - }, - "node_modules/hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "bin": { - "he": "bin/he" - } - }, - "node_modules/hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, - "node_modules/highlight.js": { - "version": "9.18.5", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz", - "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==", - "deprecated": "Support has ended for 9.x series. Upgrade to @latest", - "dev": true, - "hasInstallScript": true, - "engines": { - "node": "*" - } - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/hogan.js": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", - "integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==", - "dev": true, - "dependencies": { - "mkdirp": "0.3.0", - "nopt": "1.0.10" - }, - "bin": { - "hulk": "bin/hulk" - } - }, - "node_modules/hogan.js/node_modules/mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==", - "deprecated": "Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==", - "dev": true - }, - "node_modules/hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==", - "dev": true - }, - "node_modules/html-entities": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", - "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", - "dev": true - }, - "node_modules/html-minifier": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", - "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", - "dev": true, - "dependencies": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.2.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - }, - "bin": { - "html-minifier": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/htmlparser2/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/htmlparser2/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/htmlparser2/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/htmlparser2/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", - "dev": true - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-middleware": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", - "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", - "dev": true, - "dependencies": { - "@types/http-proxy": "^1.17.5", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dev": true, - "dependencies": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - }, - "engines": { - "node": ">=0.8", - "npm": ">=1.3.7" - } - }, - "node_modules/https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-replace-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", - "dev": true - }, - "node_modules/icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "dev": true, - "dependencies": { - "postcss": "^7.0.14" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/icss-utils/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/icss-utils/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==", - "dev": true - }, - "node_modules/ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/immediate": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", - "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", - "dev": true - }, - "node_modules/import-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", - "integrity": "sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg==", - "dev": true, - "dependencies": { - "import-from": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", - "dev": true, - "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-from": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", - "integrity": "sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w==", - "dev": true, - "dependencies": { - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "dependencies": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-local/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-local/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-local/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/import-local/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local/node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==", - "dev": true - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "node_modules/ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", - "dev": true - }, - "node_modules/internal-ip": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", - "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", - "dev": true, - "dependencies": { - "default-gateway": "^4.2.0", - "ipaddr.js": "^1.9.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", - "dev": true - }, - "node_modules/ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", - "dev": true, - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "dependencies": { - "ci-info": "^2.0.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==", - "dev": true, - "dependencies": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "dev": true, - "dependencies": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "dependencies": { - "is-path-inside": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-in-cwd/node_modules/is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "dependencies": { - "path-is-inside": "^1.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", - "dev": true - }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true - }, - "node_modules/javascript-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", - "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", - "dev": true - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true - }, - "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", - "dev": true - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "node_modules/json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "node_modules/json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, - "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dev": true, - "dependencies": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dev": true, - "dependencies": { - "json-buffer": "3.0.0" - } - }, - "node_modules/killable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", - "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", - "dev": true - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/last-call-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", - "dev": true, - "dependencies": { - "lodash": "^4.17.5", - "webpack-sources": "^1.1.0" - } - }, - "node_modules/latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "dev": true, - "dependencies": { - "package-json": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", - "dependencies": { - "uc.micro": "^1.0.1" - } - }, - "node_modules/load-script": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", - "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==", - "dev": true - }, - "node_modules/loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, - "node_modules/loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/loader-utils/node_modules/json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "node_modules/lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", - "dev": true - }, - "node_modules/lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, - "node_modules/lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", - "dev": true - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, - "node_modules/lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "node_modules/lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "dependencies": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true - }, - "node_modules/loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - }, - "funding": { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/loglevel" - } - }, - "node_modules/lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", - "dev": true - }, - "node_modules/lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", - "dev": true, - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", - "dependencies": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" - } - }, - "node_modules/markdown-it-anchor": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz", - "integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==", - "dev": true, - "peerDependencies": { - "markdown-it": "*" - } - }, - "node_modules/markdown-it-chain": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/markdown-it-chain/-/markdown-it-chain-1.3.0.tgz", - "integrity": "sha512-XClV8I1TKy8L2qsT9iX3qiV+50ZtcInGXI80CA+DP62sMs7hXlyV/RM3hfwy5O3Ad0sJm9xIwQELgANfESo8mQ==", - "dev": true, - "dependencies": { - "webpack-chain": "^4.9.0" - }, - "engines": { - "node": ">=6.9" - }, - "peerDependencies": { - "markdown-it": ">=5.0.0" - } - }, - "node_modules/markdown-it-chain/node_modules/javascript-stringify": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-1.6.0.tgz", - "integrity": "sha512-fnjC0up+0SjEJtgmmG+teeel68kutkvzfctO/KxE3qJlbunkJYAshgH3boU++gSBHP8z5/r0ts0qRIrHf0RTQQ==", - "dev": true - }, - "node_modules/markdown-it-chain/node_modules/webpack-chain": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/webpack-chain/-/webpack-chain-4.12.1.tgz", - "integrity": "sha512-BCfKo2YkDe2ByqkEWe1Rw+zko4LsyS75LVr29C6xIrxAg9JHJ4pl8kaIZ396SUSNp6b4815dRZPSTAS8LlURRQ==", - "dev": true, - "dependencies": { - "deepmerge": "^1.5.2", - "javascript-stringify": "^1.6.0" - } - }, - "node_modules/markdown-it-container": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-container/-/markdown-it-container-2.0.0.tgz", - "integrity": "sha512-IxPOaq2LzrGuFGyYq80zaorXReh2ZHGFOB1/Hen429EJL1XkPI3FJTpx9TsJeua+j2qTru4h3W1TiCRdeivMmA==", - "dev": true - }, - "node_modules/markdown-it-emoji": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", - "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==", - "dev": true - }, - "node_modules/markdown-it-html5-embed": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-html5-embed/-/markdown-it-html5-embed-1.0.0.tgz", - "integrity": "sha512-SPgugO/1+/9sZcgxoxijoTHSUpCUgFCNe1MSuTmDxDkV6NQrVzMclhRMFgE/rcHO+2rhIg3U7Oy80XA/E8ytpg==", - "dependencies": { - "markdown-it": "^8.4.0", - "mimoza": "~1.0.0" - } - }, - "node_modules/markdown-it-table-of-contents": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz", - "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", - "dev": true, - "engines": { - "node": ">6.4.0" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true - }, - "node_modules/mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==", - "dev": true, - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true - }, - "node_modules/merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "dependencies": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/mimoza": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mimoza/-/mimoza-1.0.0.tgz", - "integrity": "sha512-+j7SSye/hablu66K/jjeyPmk6WL8RoXfeZ+MMn37vSNDGuaWY/5wm10LpSpxAHX4kNoEwkTWYHba8ePVip+Hqg==", - "dependencies": { - "mime-db": "^1.6.0" - } - }, - "node_modules/min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", - "dev": true, - "dependencies": { - "dom-walk": "^0.1.0" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz", - "integrity": "sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw==", - "dev": true, - "dependencies": { - "loader-utils": "^1.1.0", - "normalize-url": "^2.0.1", - "schema-utils": "^1.0.0", - "webpack-sources": "^1.1.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^4.4.0" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "dependencies": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mixin-deep/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==", - "dev": true, - "dependencies": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "node_modules/ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "node_modules/multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "dependencies": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", - "dev": true - }, - "node_modules/nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", - "dev": true, - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanomatch/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "node_modules/nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "node_modules/no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "dev": true, - "dependencies": { - "lower-case": "^1.1.1" - } - }, - "node_modules/node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true, - "engines": { - "node": ">= 6.0.0" - } - }, - "node_modules/node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "dependencies": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - } - }, - "node_modules/node-libs-browser/node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true, - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/node-libs-browser/node_modules/punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true - }, - "node_modules/node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", - "dev": true - }, - "node_modules/nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "*" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", - "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", - "dev": true, - "dependencies": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "dependencies": { - "path-key": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/nprogress": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "dev": true - }, - "node_modules/nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "dependencies": { - "boolbase": "~1.0.0" - } - }, - "node_modules/num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", - "dev": true - }, - "node_modules/oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "dev": true, - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "dev": true, - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", - "dev": true, - "dependencies": { - "array.prototype.reduce": "^1.0.5", - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "dev": true, - "bin": { - "opencollective-postinstall": "index.js" - } - }, - "node_modules/opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", - "dev": true, - "dependencies": { - "is-wsl": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/optimize-css-assets-webpack-plugin": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.8.tgz", - "integrity": "sha512-mgFS1JdOtEGzD8l+EuISqL57cKO+We9GcoiQEmdCWRqqck+FGNmYJtx9qfAPzEz+lRrlThWMuGDaRkI/yWNx/Q==", - "dev": true, - "dependencies": { - "cssnano": "^4.1.10", - "last-call-webpack-plugin": "^3.0.0" - }, - "peerDependencies": { - "webpack": "^4.0.0" - } - }, - "node_modules/os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true - }, - "node_modules/p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-retry": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", - "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", - "dev": true, - "dependencies": { - "retry": "^0.12.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "dev": true, - "dependencies": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "node_modules/parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "dev": true, - "dependencies": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "node_modules/param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", - "dev": true, - "dependencies": { - "no-case": "^2.2.0" - } - }, - "node_modules/parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "dependencies": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "node_modules/path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", - "dev": true - }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true - }, - "node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "node_modules/path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true - }, - "node_modules/path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-type/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true - }, - "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dev": true, - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/portfinder": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", - "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", - "dev": true, - "dependencies": { - "async": "^2.6.4", - "debug": "^3.2.7", - "mkdirp": "^0.5.6" - }, - "engines": { - "node": ">= 0.12.0" - } - }, - "node_modules/portfinder/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postcss": { - "version": "8.4.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", - "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - } - ], - "dependencies": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-calc": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", - "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", - "dev": true, - "dependencies": { - "postcss": "^7.0.27", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/postcss-calc/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-calc/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-colormin": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-colormin/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-colormin/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-colormin/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-convert-values": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-convert-values/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-convert-values/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-convert-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-discard-comments": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-comments/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-discard-comments/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-duplicates/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-discard-duplicates/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-discard-empty": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-empty/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-discard-empty/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-overridden/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-discard-overridden/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-load-config": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", - "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", - "dev": true, - "dependencies": { - "cosmiconfig": "^5.0.0", - "import-cwd": "^2.0.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-loader": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", - "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", - "dev": true, - "dependencies": { - "loader-utils": "^1.1.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "schema-utils": "^1.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-loader/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-loader/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-loader/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", - "dev": true, - "dependencies": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-merge-longhand/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-merge-longhand/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-merge-longhand/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-merge-rules": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-merge-rules/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-merge-rules/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "dependencies": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-font-values/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-minify-font-values/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-minify-font-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", - "dev": true, - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-gradients/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-minify-gradients/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-minify-gradients/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-minify-params": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", - "dev": true, - "dependencies": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-params/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-minify-params/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-minify-params/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", - "dev": true, - "dependencies": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-selectors/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-minify-selectors/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "dependencies": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "dev": true, - "dependencies": { - "postcss": "^7.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-modules-extract-imports/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-modules-extract-imports/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz", - "integrity": "sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==", - "dev": true, - "dependencies": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0", - "postcss-value-parser": "^3.3.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-modules-local-by-default/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-modules-local-by-default/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-modules-local-by-default/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-modules-scope": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", - "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", - "dev": true, - "dependencies": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/postcss-modules-scope/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-modules-scope/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-modules-values": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz", - "integrity": "sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==", - "dev": true, - "dependencies": { - "icss-replace-symbols": "^1.1.0", - "postcss": "^7.0.6" - } - }, - "node_modules/postcss-modules-values/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-modules-values/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-charset/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-normalize-charset/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", - "dev": true, - "dependencies": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-display-values/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-normalize-display-values/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-display-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", - "dev": true, - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-positions/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-normalize-positions/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-positions/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", - "dev": true, - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-repeat-style/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-normalize-repeat-style/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-repeat-style/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-normalize-string": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", - "dev": true, - "dependencies": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-string/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-normalize-string/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-string/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", - "dev": true, - "dependencies": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-timing-functions/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-normalize-timing-functions/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-timing-functions/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-unicode/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-normalize-unicode/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-unicode/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-normalize-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", - "dev": true, - "dependencies": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-url/node_modules/normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/postcss-normalize-url/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-normalize-url/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-url/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-whitespace/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-normalize-whitespace/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-whitespace/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-ordered-values": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", - "dev": true, - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-ordered-values/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-ordered-values/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-ordered-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-reduce-initial/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-reduce-initial/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", - "dev": true, - "dependencies": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-reduce-transforms/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-reduce-transforms/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-reduce-transforms/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-safe-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", - "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", - "dev": true, - "dependencies": { - "postcss": "^7.0.26" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/postcss-safe-parser/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-safe-parser/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "dev": true, - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz", - "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==", - "dev": true, - "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-svgo/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-svgo/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-svgo/node_modules/postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "node_modules/postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", - "dev": true, - "dependencies": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-unique-selectors/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/postcss-unique-selectors/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "node_modules/prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true, - "optional": true, - "bin": { - "prettier": "bin-prettier.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "url": "https://github.com/prettier/prettier?sponsor=1" - } - }, - "node_modules/pretty-error": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", - "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", - "dev": true, - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^2.0.4" - } - }, - "node_modules/pretty-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", - "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==", - "engines": { - "node": ">=6" - } - }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true, - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true - }, - "node_modules/pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true - }, - "node_modules/psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - }, - "node_modules/pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "dependencies": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, - "node_modules/pumpify/node_modules/pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/pupa": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", - "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", - "dev": true, - "dependencies": { - "escape-goat": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "dev": true, - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", - "dev": true, - "dependencies": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", - "dev": true, - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readable-stream/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/readdirp/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/readdirp/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/reduce": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/reduce/-/reduce-1.0.2.tgz", - "integrity": "sha512-xX7Fxke/oHO5IfZSk77lvPa/7bjMh9BuCk4OOoX5XTXrM7s0Z+MkPfSDfz0q7r91BhhGSs8gii/VEN/7zhCPpQ==", - "dev": true, - "dependencies": { - "object-keys": "^1.1.0" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true - }, - "node_modules/regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regex-not/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", - "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", - "dev": true, - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/registry-auth-token": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", - "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", - "dev": true, - "dependencies": { - "rc": "1.2.8" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dev": true, - "dependencies": { - "rc": "^1.2.8" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/regjsgen": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", - "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==", - "dev": true - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/remove-markdown": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.3.0.tgz", - "integrity": "sha512-5392eIuy1mhjM74739VunOlsOYKjsH82rQcTBlJ1bkICVC3dQ3ksQzTHh4jGHQFnM+1xzLzcFOMH+BofqXhroQ==" - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true - }, - "node_modules/renderkid": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", - "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", - "dev": true, - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^3.0.1" - } - }, - "node_modules/renderkid/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/renderkid/node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/renderkid/node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/renderkid/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ] - }, - "node_modules/renderkid/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", - "dev": true, - "dependencies": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, - "node_modules/resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==", - "dev": true, - "dependencies": { - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "deprecated": "https://github.com/lydell/resolve-url#deprecated", - "dev": true - }, - "node_modules/responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", - "dev": true, - "dependencies": { - "lowercase-keys": "^1.0.0" - } - }, - "node_modules/ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "node_modules/rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==", - "dev": true - }, - "node_modules/rgba-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==", - "dev": true - }, - "node_modules/rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", - "dev": true, - "dependencies": { - "aproba": "^1.1.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "dev": true, - "dependencies": { - "ret": "~0.1.10" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "node_modules/sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "node_modules/schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true - }, - "node_modules/selfsigned": { - "version": "1.10.14", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.14.tgz", - "integrity": "sha512-lkjaiAye+wBZDCBsu5BGi0XiLRxeUlsGod5ZP924CRSEoGuZAw/f7y9RKu28rwTfiHVhdavhB0qH0INV6P1lEA==", - "dev": true, - "dependencies": { - "node-forge": "^0.10.0" - } - }, - "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "dev": true, - "dependencies": { - "semver": "^6.3.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/send/node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/send/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - }, - "node_modules/serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "dev": true, - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "dependencies": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, - "node_modules/set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "node_modules/sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "dependencies": { - "shebang-regex": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dev": true, - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true - }, - "node_modules/slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/smoothscroll-polyfill": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz", - "integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==", - "dev": true - }, - "node_modules/snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/snapdragon/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/snapdragon/node_modules/source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/sockjs-client": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", - "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", - "dev": true, - "dependencies": { - "debug": "^3.2.7", - "eventsource": "^2.0.2", - "faye-websocket": "^0.11.4", - "inherits": "^2.0.4", - "url-parse": "^1.5.10" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://tidelift.com/funding/github/npm/sockjs-client" - } - }, - "node_modules/sockjs-client/node_modules/debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/sockjs/node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true, - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", - "dev": true, - "dependencies": { - "is-plain-obj": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/sort-keys/node_modules/is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", - "dev": true, - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "deprecated": "See https://github.com/lydell/source-map-url#deprecated", - "dev": true - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/spdy-transport/node_modules/readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/split-string/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "node_modules/sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "dev": true, - "dependencies": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - }, - "bin": { - "sshpk-conv": "bin/sshpk-conv", - "sshpk-sign": "bin/sshpk-sign", - "sshpk-verify": "bin/sshpk-verify" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ssri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", - "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", - "dev": true, - "dependencies": { - "figgy-pudding": "^3.5.1" - } - }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", - "dev": true - }, - "node_modules/stack-utils": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz", - "integrity": "sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==", - "dev": true, - "dependencies": { - "escape-string-regexp": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/stack-utils/node_modules/escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", - "dev": true, - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-2.3.1.tgz", - "integrity": "sha512-eOsoKTWnr6C8aWrqJJ2KAReXoa7Vn5Ywyw6uCXgA/xDhxPoaIsBa5aNJmISY04dLwXPBnDHW4diGM7Sn5K4R/g==", - "dev": true, - "dependencies": { - "ci-info": "^3.1.1" - } - }, - "node_modules/std-env/node_modules/ci-info": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.2.tgz", - "integrity": "sha512-lVZdhvbEudris15CLytp2u6Y0p5EKfztae9Fqa189MfNmln9F33XuH69v5fvNfiRN5/0eAUz2yJL3mo+nhaRKg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "dependencies": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "node_modules/stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "dependencies": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "node_modules/strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stylehacks": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", - "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", - "dev": true, - "dependencies": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/stylehacks/node_modules/picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "node_modules/stylehacks/node_modules/postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/stylehacks/node_modules/postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "dependencies": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/stylus": { - "version": "0.54.8", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", - "integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", - "dev": true, - "dependencies": { - "css-parse": "~2.0.0", - "debug": "~3.1.0", - "glob": "^7.1.6", - "mkdirp": "~1.0.4", - "safer-buffer": "^2.1.2", - "sax": "~1.2.4", - "semver": "^6.3.0", - "source-map": "^0.7.3" - }, - "bin": { - "stylus": "bin/stylus" - }, - "engines": { - "node": "*" - } - }, - "node_modules/stylus-loader": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz", - "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==", - "dev": true, - "dependencies": { - "loader-utils": "^1.0.2", - "lodash.clonedeep": "^4.5.0", - "when": "~3.6.x" - }, - "peerDependencies": { - "stylus": ">=0.52.4" - } - }, - "node_modules/stylus/node_modules/debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/stylus/node_modules/mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/stylus/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "node_modules/stylus/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", - "dev": true - }, - "node_modules/svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", - "dev": true, - "dependencies": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/term-size": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", - "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/terser": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", - "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", - "dev": true, - "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", - "dev": true, - "dependencies": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^4.0.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser-webpack-plugin/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser-webpack-plugin/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser-webpack-plugin/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser-webpack-plugin/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser-webpack-plugin/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/terser-webpack-plugin/node_modules/pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/terser-webpack-plugin/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/terser-webpack-plugin/node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "node_modules/through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "node_modules/through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true - }, - "node_modules/timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "dependencies": { - "setimmediate": "^1.0.4" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", - "dev": true - }, - "node_modules/to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==", - "dev": true - }, - "node_modules/to-factory": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-factory/-/to-factory-1.0.0.tgz", - "integrity": "sha512-JVYrY42wMG7ddf+wBUQR/uHGbjUHZbLisJ8N62AMm0iTZ0p8YTcZLzdtomU0+H+wa99VbkyvQGB3zxB7NDzgIQ==", - "dev": true - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/to-regex/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, - "engines": { - "node": ">=0.6" - } - }, - "node_modules/toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "dev": true - }, - "node_modules/toposort": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", - "integrity": "sha512-FclLrw8b9bMWf4QlCJuHBEVhSRsqDj6u3nIjAzPeJvgl//1hBlffdlk0MALceL14+koWEdU4ofRAXofbODxQzg==", - "dev": true - }, - "node_modules/tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "dependencies": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - }, - "engines": { - "node": ">=0.8" - } - }, - "node_modules/tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==", - "dev": true - }, - "node_modules/tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "dependencies": { - "safe-buffer": "^5.0.1" - }, - "engines": { - "node": "*" - } - }, - "node_modules/tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true - }, - "node_modules/type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "node_modules/uglify-js": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", - "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", - "dev": true, - "dependencies": { - "commander": "~2.19.0", - "source-map": "~0.6.1" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/uglify-js/node_modules/commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", - "dev": true - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==", - "dev": true - }, - "node_modules/uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==", - "dev": true - }, - "node_modules/unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, - "node_modules/unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "dependencies": { - "crypto-random-string": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", - "dev": true - }, - "node_modules/unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", - "dev": true, - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "dev": true, - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - }, - "node_modules/upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true, - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - } - ], - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - }, - "bin": { - "browserslist-lint": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/update-notifier": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", - "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", - "dev": true, - "dependencies": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/update-notifier/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/update-notifier/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/update-notifier/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/update-notifier/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", - "dev": true - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "deprecated": "Please see https://github.com/lydell/urix#deprecated", - "dev": true - }, - "node_modules/url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", - "dev": true, - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url-loader": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", - "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", - "dev": true, - "dependencies": { - "loader-utils": "^1.1.0", - "mime": "^2.0.3", - "schema-utils": "^1.0.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^3.0.0 || ^4.0.0" - } - }, - "node_modules/url-loader/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "node_modules/url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", - "dev": true, - "dependencies": { - "prepend-http": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "dev": true - }, - "node_modules/use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "dependencies": { - "inherits": "2.0.3" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "node_modules/util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/util/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "dev": true - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", - "dev": true, - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vendors": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "engines": [ - "node >=0.6.0" - ], - "dependencies": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - } - }, - "node_modules/verror/node_modules/core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "node_modules/vue": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz", - "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", - "dependencies": { - "@vue/compiler-sfc": "2.7.14", - "csstype": "^3.1.0" - } - }, - "node_modules/vue-hot-reload-api": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", - "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", - "dev": true - }, - "node_modules/vue-loader": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz", - "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", - "dev": true, - "dependencies": { - "@vue/component-compiler-utils": "^3.1.0", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" - }, - "peerDependencies": { - "css-loader": "*", - "webpack": "^3.0.0 || ^4.1.0 || ^5.0.0-0" - }, - "peerDependenciesMeta": { - "cache-loader": { - "optional": true - }, - "vue-template-compiler": { - "optional": true - } - } - }, - "node_modules/vue-router": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", - "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==", - "dev": true - }, - "node_modules/vue-server-renderer": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.7.14.tgz", - "integrity": "sha512-NlGFn24tnUrj7Sqb8njhIhWREuCJcM3140aMunLNcx951BHG8j3XOrPP7psSCaFA8z6L4IWEjudztdwTp1CBVw==", - "dev": true, - "dependencies": { - "chalk": "^4.1.2", - "hash-sum": "^2.0.0", - "he": "^1.2.0", - "lodash.template": "^4.5.0", - "lodash.uniq": "^4.5.0", - "resolve": "^1.22.0", - "serialize-javascript": "^6.0.0", - "source-map": "0.5.6" - } - }, - "node_modules/vue-server-renderer/node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/vue-server-renderer/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/vue-server-renderer/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/vue-server-renderer/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/vue-server-renderer/node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/vue-server-renderer/node_modules/hash-sum": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", - "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", - "dev": true - }, - "node_modules/vue-server-renderer/node_modules/serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/vue-server-renderer/node_modules/source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vue-server-renderer/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/vue-style-loader": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", - "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", - "dev": true, - "dependencies": { - "hash-sum": "^1.0.2", - "loader-utils": "^1.0.2" - } - }, - "node_modules/vue-template-compiler": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", - "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", - "dev": true, - "dependencies": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "node_modules/vue-template-es2015-compiler": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", - "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", - "dev": true - }, - "node_modules/vue-tweet-embed": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/vue-tweet-embed/-/vue-tweet-embed-2.4.0.tgz", - "integrity": "sha512-bjViatv0priR1dTEPJpRyWigWGUTUC28VT/sWTaZE+RBWuj/XZvOU5Hzk+O8Mue2dBCAHJrRpoO1VKlcgmHohg==", - "peerDependencies": { - "vue": "^2.2.0" - } - }, - "node_modules/vuepress": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/vuepress/-/vuepress-1.9.7.tgz", - "integrity": "sha512-aSXpoJBGhgjaWUsT1Zs/ZO8JdDWWsxZRlVme/E7QYpn+ZB9iunSgPMozJQNFaHzcRq4kPx5A4k9UhzLRcvtdMg==", - "dev": true, - "hasInstallScript": true, - "dependencies": { - "@vuepress/core": "1.9.7", - "@vuepress/theme-default": "1.9.7", - "@vuepress/types": "1.9.7", - "cac": "^6.5.6", - "envinfo": "^7.2.0", - "opencollective-postinstall": "^2.0.2", - "update-notifier": "^4.0.0" - }, - "bin": { - "vuepress": "cli.js" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/vuepress-html-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vuepress-html-webpack-plugin/-/vuepress-html-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-BebAEl1BmWlro3+VyDhIOCY6Gef2MCBllEVAP3NUAtMguiyOwo/dClbwJ167WYmcxHJKLl7b0Chr9H7fpn1d0A==", - "dev": true, - "dependencies": { - "html-minifier": "^3.2.3", - "loader-utils": "^0.2.16", - "lodash": "^4.17.3", - "pretty-error": "^2.0.2", - "tapable": "^1.0.0", - "toposort": "^1.0.0", - "util.promisify": "1.0.0" - }, - "engines": { - "node": ">=6.9" - }, - "peerDependencies": { - "webpack": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" - } - }, - "node_modules/vuepress-html-webpack-plugin/node_modules/big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true, - "engines": { - "node": "*" - } - }, - "node_modules/vuepress-html-webpack-plugin/node_modules/emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng==", - "dev": true, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vuepress-html-webpack-plugin/node_modules/json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", - "dev": true, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/vuepress-html-webpack-plugin/node_modules/loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha512-tiv66G0SmiOx+pLWMtGEkfSEejxvb6N6uRrQjfWJIT79W9GMpgKeCAmm9aVBKtd4WEgntciI8CsGqjpDoCWJug==", - "dev": true, - "dependencies": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - }, - "node_modules/vuepress-html-webpack-plugin/node_modules/util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "dependencies": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "node_modules/vuepress-plugin-container": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/vuepress-plugin-container/-/vuepress-plugin-container-2.1.5.tgz", - "integrity": "sha512-TQrDX/v+WHOihj3jpilVnjXu9RcTm6m8tzljNJwYhxnJUW0WWQ0hFLcDTqTBwgKIFdEiSxVOmYE+bJX/sq46MA==", - "dev": true, - "dependencies": { - "@vuepress/shared-utils": "^1.2.0", - "markdown-it-container": "^2.0.0" - } - }, - "node_modules/vuepress-plugin-smooth-scroll": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/vuepress-plugin-smooth-scroll/-/vuepress-plugin-smooth-scroll-0.0.3.tgz", - "integrity": "sha512-qsQkDftLVFLe8BiviIHaLV0Ea38YLZKKonDGsNQy1IE0wllFpFIEldWD8frWZtDFdx6b/O3KDMgVQ0qp5NjJCg==", - "dev": true, - "dependencies": { - "smoothscroll-polyfill": "^0.4.3" - } - }, - "node_modules/watchpack": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", - "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", - "dev": true, - "dependencies": { - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - }, - "optionalDependencies": { - "chokidar": "^3.4.1", - "watchpack-chokidar2": "^2.0.1" - } - }, - "node_modules/watchpack-chokidar2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", - "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", - "dev": true, - "optional": true, - "dependencies": { - "chokidar": "^2.1.8" - } - }, - "node_modules/watchpack/node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/watchpack/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "optional": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/watchpack/node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/watchpack/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "optional": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/watchpack/node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "optional": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/watchpack/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "optional": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/webpack": { - "version": "4.46.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", - "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", - "dev": true, - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.5.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - }, - "webpack-command": { - "optional": true - } - } - }, - "node_modules/webpack-chain": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/webpack-chain/-/webpack-chain-6.5.1.tgz", - "integrity": "sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==", - "dev": true, - "dependencies": { - "deepmerge": "^1.5.2", - "javascript-stringify": "^2.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-dev-middleware": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", - "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", - "dev": true, - "dependencies": { - "memory-fs": "^0.4.1", - "mime": "^2.4.4", - "mkdirp": "^0.5.1", - "range-parser": "^1.2.1", - "webpack-log": "^2.0.0" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-server": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.3.tgz", - "integrity": "sha512-3x31rjbEQWKMNzacUZRE6wXvUFuGpH7vr0lIEbYpMAG9BOxi0928QU1BBswOAP3kg3H1O4hiS+sq4YyAn6ANnA==", - "dev": true, - "dependencies": { - "ansi-html-community": "0.0.8", - "bonjour": "^3.5.0", - "chokidar": "^2.1.8", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "debug": "^4.1.1", - "del": "^4.1.1", - "express": "^4.17.1", - "html-entities": "^1.3.1", - "http-proxy-middleware": "0.19.1", - "import-local": "^2.0.0", - "internal-ip": "^4.3.0", - "ip": "^1.1.5", - "is-absolute-url": "^3.0.3", - "killable": "^1.0.1", - "loglevel": "^1.6.8", - "opn": "^5.5.0", - "p-retry": "^3.0.1", - "portfinder": "^1.0.26", - "schema-utils": "^1.0.0", - "selfsigned": "^1.10.8", - "semver": "^6.3.0", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "sockjs-client": "^1.5.0", - "spdy": "^4.0.2", - "strip-ansi": "^3.0.1", - "supports-color": "^6.1.0", - "url": "^0.11.0", - "webpack-dev-middleware": "^3.7.2", - "webpack-log": "^2.0.0", - "ws": "^6.2.1", - "yargs": "^13.3.2" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 6.11.5" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", - "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", - "dev": true, - "dependencies": { - "http-proxy": "^1.17.0", - "is-glob": "^4.0.0", - "lodash": "^4.17.11", - "micromatch": "^3.1.10" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/webpack-dev-server/node_modules/is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpack-dev-server/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/webpack-dev-server/node_modules/strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/webpack-dev-server/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "dependencies": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/webpack-merge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", - "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", - "dev": true, - "dependencies": { - "lodash": "^4.17.15" - } - }, - "node_modules/webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "node_modules/webpack/node_modules/braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/micromatch/node_modules/extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/webpack/node_modules/to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpackbar": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-3.2.0.tgz", - "integrity": "sha512-PC4o+1c8gWWileUfwabe0gqptlXUDJd5E0zbpr2xHP1VSOVlZVPBZ8j6NCR8zM5zbKdxPhctHXahgpNK1qFDPw==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.1.0", - "chalk": "^2.4.1", - "consola": "^2.6.0", - "figures": "^3.0.0", - "pretty-time": "^1.1.0", - "std-env": "^2.2.1", - "text-table": "^0.2.0", - "wrap-ansi": "^5.1.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^3.0.0 || ^4.0.0" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/when": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", - "integrity": "sha512-d1VUP9F96w664lKINMGeElWdhhb5sC+thXM+ydZGU3ZnaE09Wv6FaS+mpM9570kcDs/xMfcXJBTLsMdHEFYY9Q==", - "dev": true - }, - "node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "which": "bin/which" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", - "dev": true - }, - "node_modules/widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "dev": true, - "dependencies": { - "string-width": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "dev": true, - "dependencies": { - "errno": "~0.1.7" - } - }, - "node_modules/wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrap-ansi/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/wrap-ansi/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", - "dev": true, - "dependencies": { - "async-limiter": "~1.0.0" - } - }, - "node_modules/xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true, - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "node_modules/yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "dependencies": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "node_modules/yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "dependencies": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - }, - "node_modules/yargs-parser/node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "node_modules/yargs/node_modules/find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/yargs/node_modules/string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "dependencies": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/yargs/node_modules/strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "dependencies": { - "ansi-regex": "^4.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/zepto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/zepto/-/zepto-1.2.0.tgz", - "integrity": "sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==", - "dev": true - } - }, - "dependencies": { - "@ampproject/remapping": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", - "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", - "dev": true, - "requires": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - } - }, - "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", - "dev": true, - "requires": { - "@babel/highlight": "^7.18.6" - } - }, - "@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", - "dev": true - }, - "@babel/core": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.2.tgz", - "integrity": "sha512-w7DbG8DtMrJcFOi4VrLm+8QM4az8Mo+PuLBKLp2zrYRCow8W/f9xiXm5sN53C8HksCyDQwCKha9JiDoIyPjT2g==", - "dev": true, - "requires": { - "@ampproject/remapping": "^2.1.0", - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.2", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-module-transforms": "^7.20.2", - "@babel/helpers": "^7.20.1", - "@babel/parser": "^7.20.2", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.1", - "semver": "^6.3.0" - } - }, - "@babel/generator": { - "version": "7.20.4", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.4.tgz", - "integrity": "sha512-luCf7yk/cm7yab6CAW1aiFnmEfBJplb/JojV56MYEK7ziWfGmFlTfmL9Ehwfy4gFhbjBfWO1wj7/TuSbVNEEtA==", - "dev": true, - "requires": { - "@babel/types": "^7.20.2", - "@jridgewell/gen-mapping": "^0.3.2", - "jsesc": "^2.5.1" - }, - "dependencies": { - "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" - } - } - } - }, - "@babel/helper-annotate-as-pure": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.18.6.tgz", - "integrity": "sha512-duORpUiYrEpzKIop6iNbjnwKLAKnJ47csTyRACyEmWj0QdUrm5aqNJGHSSEQSUAvNW0ojX0dOmK9dZduvkfeXA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.18.9.tgz", - "integrity": "sha512-yFQ0YCHoIqarl8BCRwBL8ulYUaZpz3bNsA7oFepAzee+8/+ImtADXNOmO5vJvsPff3qi+hvpkY/NYBTrBQgdNw==", - "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.18.6", - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-compilation-targets": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", - "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.20.0", - "@babel/helper-validator-option": "^7.18.6", - "browserslist": "^4.21.3", - "semver": "^6.3.0" - } - }, - "@babel/helper-create-class-features-plugin": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.20.2.tgz", - "integrity": "sha512-k22GoYRAHPYr9I+Gvy2ZQlAe5mGy8BqWst2wRt8cwIufWTxrsVshhIBvYNqC80N0GSFWTsqRVexOtfzlgOEDvA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6" - } - }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.19.0.tgz", - "integrity": "sha512-htnV+mHX32DF81amCDrwIDr8nrp1PTm+3wfBN9/v8QJOLEioOCOG7qNyq0nHeFiWbT3Eb7gsPwEmV64UCQ1jzw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "regexpu-core": "^5.1.0" - } - }, - "@babel/helper-define-polyfill-provider": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.3.3.tgz", - "integrity": "sha512-z5aQKU4IzbqCC1XH0nAqfsFLMVSo22SBKUc0BxGrLkolTdPTructy0ToNnlO2zA4j9Q/7pjMZf0DSY+DSTYzww==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.17.7", - "@babel/helper-plugin-utils": "^7.16.7", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - } - }, - "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", - "dev": true - }, - "@babel/helper-explode-assignable-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.18.6.tgz", - "integrity": "sha512-eyAYAsQmB80jNfg4baAtLeWAQHfHFiR483rzFK+BhETlGZaQC9bsfrugfXDCbRHLQbIA7U5NxhhOxN7p/dWIcg==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", - "dev": true, - "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" - } - }, - "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-member-expression-to-functions": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.18.9.tgz", - "integrity": "sha512-RxifAh2ZoVU67PyKIO4AMi1wTenGfMR/O/ae0CCRqwgBAt5v7xjdtRw7UoSbsreKrQn5t7r89eruK/9JjYHuDg==", - "dev": true, - "requires": { - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-module-imports": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", - "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-module-transforms": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", - "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.20.2", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.19.1", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.2" - } - }, - "@babel/helper-optimise-call-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.18.6.tgz", - "integrity": "sha512-HP59oD9/fEHQkdcbgFCnbmgH5vIQTJbxh2yf+CdM89/glUNnuzr87Q8GIjGEnOktTROemO0Pe0iPAYbqZuOUiA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-plugin-utils": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", - "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", - "dev": true - }, - "@babel/helper-remap-async-to-generator": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.18.9.tgz", - "integrity": "sha512-dI7q50YKd8BAv3VEfgg7PS7yD3Rtbi2J1XMXaalXO0W0164hYLnh8zpjRS0mte9MfVp/tltvr/cfdXPvJr1opA==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-wrap-function": "^7.18.9", - "@babel/types": "^7.18.9" - } - }, - "@babel/helper-replace-supers": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.19.1.tgz", - "integrity": "sha512-T7ahH7wV0Hfs46SFh5Jz3s0B6+o8g3c+7TMxu7xKfmHikg7EAZ3I2Qk9LFhjxXq8sL7UkP5JflezNwoZa8WvWw==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-member-expression-to-functions": "^7.18.9", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/traverse": "^7.19.1", - "@babel/types": "^7.19.0" - } - }, - "@babel/helper-simple-access": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", - "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", - "dev": true, - "requires": { - "@babel/types": "^7.20.2" - } - }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.20.0.tgz", - "integrity": "sha512-5y1JYeNKfvnT8sZcK9DVRtpTbGiomYIHviSP3OQWmDPU3DeH4a1ZlT/N2lyQ5P8egjcRaT/Y9aNqUxK0WsnIIg==", - "dev": true, - "requires": { - "@babel/types": "^7.20.0" - } - }, - "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", - "dev": true, - "requires": { - "@babel/types": "^7.18.6" - } - }, - "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", - "dev": true - }, - "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", - "dev": true - }, - "@babel/helper-validator-option": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", - "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", - "dev": true - }, - "@babel/helper-wrap-function": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.19.0.tgz", - "integrity": "sha512-txX8aN8CZyYGTwcLhlk87KRqncAzhh5TpQamZUa0/u3an36NtDpUP6bQgBCBcLeBs09R/OwQu3OjK0k/HwfNDg==", - "dev": true, - "requires": { - "@babel/helper-function-name": "^7.19.0", - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" - } - }, - "@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", - "dev": true, - "requires": { - "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" - } - }, - "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", - "dev": true, - "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - } - }, - "@babel/parser": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.3.tgz", - "integrity": "sha512-OP/s5a94frIPXwjzEcv5S/tpQfc6XhxYUnmWpgdqMWGgYCuErA3SzozaRAMQgSZWKeTJxht9aWAkUY+0UzvOFg==" - }, - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz", - "integrity": "sha512-Dgxsyg54Fx1d4Nge8UnvTrED63vrwOdPmyvPzlNN/boaliRP54pm3pGzZD1SJUwrBA+Cs/xdG8kXX6Mn/RfISQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.18.9.tgz", - "integrity": "sha512-AHrP9jadvH7qlOj6PINbgSuphjQUAK7AOT7DPjBo9EHoLhQTnnK5u45e1Hd4DbSQEO9nqPWtQ89r+XEOWFScKg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-proposal-optional-chaining": "^7.18.9" - } - }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.1.tgz", - "integrity": "sha512-Gh5rchzSwE4kC+o/6T8waD0WHEQIsDmjltY8WnWRXHUdH8axZhuH86Ov9M72YhJfDrZseQwuuWaaIT/TmePp3g==", - "dev": true, - "requires": { - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-remap-async-to-generator": "^7.18.9", - "@babel/plugin-syntax-async-generators": "^7.8.4" - } - }, - "@babel/plugin-proposal-class-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.18.6.tgz", - "integrity": "sha512-cumfXOF0+nzZrrN8Rf0t7M+tF6sZc7vhQwYQck9q1/5w2OExlD+b4v4RpMJFaV1Z7WcDRgO6FqvxqxGlwo+RHQ==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-class-static-block": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.18.6.tgz", - "integrity": "sha512-+I3oIiNxrCpup3Gi8n5IGMwj0gOCAjcJUSQEcotNnCCPMEnixawOQ+KeJPlgfjzx+FKQ1QSyZOWe7wmoJp7vhw==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - } - }, - "@babel/plugin-proposal-decorators": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.20.2.tgz", - "integrity": "sha512-nkBH96IBmgKnbHQ5gXFrcmez+Z9S2EIDKDQGp005ROqBigc88Tky4rzCnlP/lnlj245dCEQl4/YyV0V1kYh5dw==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.20.2", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/plugin-syntax-decorators": "^7.19.0" - } - }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.18.6.tgz", - "integrity": "sha512-1auuwmK+Rz13SJj36R+jqFPMJWyKEDd7lLSdOj4oJK0UTgGueSAtkrCvz9ewmgyU/P941Rv2fQwZJN8s6QruXw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - } - }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.18.9.tgz", - "integrity": "sha512-k1NtHyOMvlDDFeb9G5PhUXuGj8m/wiwojgQVEhJ/fsVsMCpLyOP4h0uGEjYJKrRI+EVPlb5Jk+Gt9P97lOGwtA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - } - }, - "@babel/plugin-proposal-json-strings": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.18.6.tgz", - "integrity": "sha512-lr1peyn9kOdbYc0xr0OdHTZ5FMqS6Di+H0Fz2I/JwMzGmzJETNeOFq2pBySw6X/KFL5EWDjlJuMsUGRFb8fQgQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-json-strings": "^7.8.3" - } - }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.18.9.tgz", - "integrity": "sha512-128YbMpjCrP35IOExw2Fq+x55LMP42DzhOhX2aNNIdI9avSWl2PI0yuBWarr3RYpZBSPtabfadkH2yeRiMD61Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - } - }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.18.6.tgz", - "integrity": "sha512-wQxQzxYeJqHcfppzBDnm1yAY0jSRkUXR2z8RePZYrKwMKgMlE8+Z6LUno+bd6LvbGh8Gltvy74+9pIYkr+XkKA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - } - }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.18.6.tgz", - "integrity": "sha512-ozlZFogPqoLm8WBr5Z8UckIoE4YQ5KESVcNudyXOR8uqIkliTEgJ3RoketfG6pmzLdeZF0H/wjE9/cCEitBl7Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - } - }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.20.2.tgz", - "integrity": "sha512-Ks6uej9WFK+fvIMesSqbAto5dD8Dz4VuuFvGJFKgIGSkJuRGcrwGECPA1fDgQK3/DbExBJpEkTeYeB8geIFCSQ==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.20.1" - } - }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.18.6.tgz", - "integrity": "sha512-Q40HEhs9DJQyaZfUjjn6vE8Cv4GmMHCYuMGIWUnlxH6400VGxOuwWsPt4FxXxJkC/5eOzgn0z21M9gMT4MOhbw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - } - }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.18.9.tgz", - "integrity": "sha512-v5nwt4IqBXihxGsW2QmCWMDS3B3bzGIk/EQVZz2ei7f3NJl8NzAJVvUmpDW5q1CRNY+Beb/k58UAH1Km1N411w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - } - }, - "@babel/plugin-proposal-private-methods": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.18.6.tgz", - "integrity": "sha512-nutsvktDItsNn4rpGItSNV2sz1XwS+nfU0Rg8aCx3W3NOKVzdMjJRu0O5OkgDp3ZGICSTbgRpxZoWsxoKRvbeA==", - "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.18.6.tgz", - "integrity": "sha512-9Rysx7FOctvT5ouj5JODjAFAkgGoudQuLPamZb0v1TGLpapdNaftzifU8NTWQm0IRjqoYypdrSmyWgkocDQ8Dw==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-create-class-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - } - }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", - "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", - "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", - "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.12.13" - } - }, - "@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", - "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-decorators": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.19.0.tgz", - "integrity": "sha512-xaBZUEDntt4faL1yN8oIFlhfXeQAWJW7CLKYsHTUqriCUbj8xOra8bfxxKGi/UwExPFBuPdH4XfHc9rGQhrVkQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", - "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.3" - } - }, - "@babel/plugin-syntax-import-assertions": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.20.0.tgz", - "integrity": "sha512-IUh1vakzNoWalR8ch/areW7qFopR2AEw03JlG7BbrDqmQ4X3q9uuipQwSGrUn7oGiemKjtSLDhNtQHzMHr1JdQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", - "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-jsx": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", - "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", - "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", - "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", - "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.10.4" - } - }, - "@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", - "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", - "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", - "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.8.0" - } - }, - "@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", - "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.18.6.tgz", - "integrity": "sha512-9S9X9RUefzrsHZmKMbDXxweEH+YlE8JJEuat9FdvW9Qh1cw7W64jELCtWNkPBPX5En45uy28KGvA/AySqUh8CQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.18.6.tgz", - "integrity": "sha512-ARE5wZLKnTgPW7/1ftQmSi1CmkqqHo2DNmtztFhvgtOWSDfq0Cq9/9L+KnZNYSNrydBekhW3rwShduf59RoXag==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-remap-async-to-generator": "^7.18.6" - } - }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.18.6.tgz", - "integrity": "sha512-ExUcOqpPWnliRcPqves5HJcJOvHvIIWfuS4sroBUenPuMdmW+SMHDakmtS7qOo13sVppmUijqeTv7qqGsvURpQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-block-scoping": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.20.2.tgz", - "integrity": "sha512-y5V15+04ry69OV2wULmwhEA6jwSWXO1TwAtIwiPXcvHcoOQUqpyMVd2bDsQJMW8AurjulIyUV8kDqtjSwHy1uQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-classes": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.20.2.tgz", - "integrity": "sha512-9rbPp0lCVVoagvtEyQKSo5L8oo0nQS/iif+lwlAz29MccX2642vWDlSZK+2T2buxbopotId2ld7zZAzRfz9j1g==", - "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.18.6", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-optimise-call-expression": "^7.18.6", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-replace-supers": "^7.19.1", - "@babel/helper-split-export-declaration": "^7.18.6", - "globals": "^11.1.0" - } - }, - "@babel/plugin-transform-computed-properties": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.18.9.tgz", - "integrity": "sha512-+i0ZU1bCDymKakLxn5srGHrsAPRELC2WIbzwjLhHW9SIE1cPYkLCL0NlnXMZaM1vhfgA2+M7hySk42VBvrkBRw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-destructuring": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.20.2.tgz", - "integrity": "sha512-mENM+ZHrvEgxLTBXUiQ621rRXZes3KWUv6NdQlrnr1TkWVw+hUjQBZuP2X32qKlrlG2BzgR95gkuCRSkJl8vIw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.18.6.tgz", - "integrity": "sha512-6S3jpun1eEbAxq7TdjLotAsl4WpQI9DxfkycRcKrjhQYzU87qpXdknpBg/e+TdcMehqGnLFi7tnFUBR02Vq6wg==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.18.9.tgz", - "integrity": "sha512-d2bmXCtZXYc59/0SanQKbiWINadaJXqtvIQIzd4+hNwkWBgyCd5F/2t1kXoUdvPMrxzPvhK6EMQRROxsue+mfw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.18.6.tgz", - "integrity": "sha512-wzEtc0+2c88FVR34aQmiz56dxEkxr2g8DQb/KfaFa1JYXOFVsbhvAonFN6PwVWj++fKmku8NP80plJ5Et4wqHw==", - "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-for-of": { - "version": "7.18.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.18.8.tgz", - "integrity": "sha512-yEfTRnjuskWYo0k1mHUqrVWaZwrdq8AYbfrpqULOJOaucGSp4mNMVps+YtA8byoevxS/urwU75vyhQIxcCgiBQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-function-name": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.18.9.tgz", - "integrity": "sha512-WvIBoRPaJQ5yVHzcnJFor7oS5Ls0PYixlTYE63lCj2RtdQEl15M68FXQlxnG6wdraJIXRdR7KI+hQ7q/9QjrCQ==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.18.9", - "@babel/helper-function-name": "^7.18.9", - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.18.9.tgz", - "integrity": "sha512-IFQDSRoTPnrAIrI5zoZv73IFeZu2dhu6irxQjY9rNjTT53VmKg9fenjvoiOWOkJ6mm4jKVPtdMzBY98Fp4Z4cg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.18.6.tgz", - "integrity": "sha512-qSF1ihLGO3q+/g48k85tUjD033C29TNTVB2paCwZPVmOsjn9pClvYYrM2VeJpBY2bcNkuny0YUyTNRyRxJ54KA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-modules-amd": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.19.6.tgz", - "integrity": "sha512-uG3od2mXvAtIFQIh0xrpLH6r5fpSQN04gIVovl+ODLdUMANokxQLZnPBHcjmv3GxRjnqwLuHvppjjcelqUFZvg==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.19.6.tgz", - "integrity": "sha512-8PIa1ym4XRTKuSsOUXqDG0YaOlEuTVvHMe5JCfgBMOtHvJKw/4NGovEGN33viISshG/rZNVrACiBmPQLvWN8xQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-simple-access": "^7.19.4" - } - }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.19.6.tgz", - "integrity": "sha512-fqGLBepcc3kErfR9R3DnVpURmckXP7gj7bAlrTQyBxrigFqszZCkFkcoxzCp2v32XmwXLvbw+8Yq9/b+QqksjQ==", - "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-validator-identifier": "^7.19.1" - } - }, - "@babel/plugin-transform-modules-umd": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.18.6.tgz", - "integrity": "sha512-dcegErExVeXcRqNtkRU/z8WlBLnvD4MRnHgNs3MytRO1Mn1sHRyhbcpYbVMGclAqOjdW+9cfkdZno9dFdfKLfQ==", - "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.19.1.tgz", - "integrity": "sha512-oWk9l9WItWBQYS4FgXD4Uyy5kq898lvkXpXQxoJEY1RnvPk4R/Dvu2ebXU9q8lP+rlMwUQTFf2Ok6d78ODa0kw==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.19.0", - "@babel/helper-plugin-utils": "^7.19.0" - } - }, - "@babel/plugin-transform-new-target": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.18.6.tgz", - "integrity": "sha512-DjwFA/9Iu3Z+vrAn+8pBUGcjhxKguSMlsFqeCKbhb9BAV756v0krzVK04CRDi/4aqmk8BsHb4a/gFcaA5joXRw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-object-super": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.18.6.tgz", - "integrity": "sha512-uvGz6zk+pZoS1aTZrOvrbj6Pp/kK2mp45t2B+bTDre2UgsZZ8EZLSJtUg7m/no0zOJUWgFONpB7Zv9W2tSaFlA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "@babel/helper-replace-supers": "^7.18.6" - } - }, - "@babel/plugin-transform-parameters": { - "version": "7.20.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.20.3.tgz", - "integrity": "sha512-oZg/Fpx0YDrj13KsLyO8I/CX3Zdw7z0O9qOd95SqcoIzuqy/WTGWvePeHAnZCN54SfdyjHcb1S30gc8zlzlHcA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.20.2" - } - }, - "@babel/plugin-transform-property-literals": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.18.6.tgz", - "integrity": "sha512-cYcs6qlgafTud3PAzrrRNbQtfpQ8+y/+M5tKmksS9+M1ckbH6kzY8MrexEM9mcA6JDsukE19iIRvAyYl463sMg==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-regenerator": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.18.6.tgz", - "integrity": "sha512-poqRI2+qiSdeldcz4wTSTXBRryoq3Gc70ye7m7UD5Ww0nE29IXqMl6r7Nd15WBgRd74vloEMlShtH6CKxVzfmQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6", - "regenerator-transform": "^0.15.0" - } - }, - "@babel/plugin-transform-reserved-words": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.18.6.tgz", - "integrity": "sha512-oX/4MyMoypzHjFrT1CdivfKZ+XvIPMFXwwxHp/r0Ddy2Vuomt4HDFGmft1TAY2yiTKiNSsh3kjBAzcM8kSdsjA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-runtime": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.19.6.tgz", - "integrity": "sha512-PRH37lz4JU156lYFW1p8OxE5i7d6Sl/zV58ooyr+q1J1lnQPyg5tIiXlIwNVhJaY4W3TmOtdc8jqdXQcB1v5Yw==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-plugin-utils": "^7.19.0", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "semver": "^6.3.0" - } - }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.18.6.tgz", - "integrity": "sha512-eCLXXJqv8okzg86ywZJbRn19YJHU4XUa55oz2wbHhaQVn/MM+XhukiT7SYqp/7o00dg52Rj51Ny+Ecw4oyoygw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-spread": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.19.0.tgz", - "integrity": "sha512-RsuMk7j6n+r752EtzyScnWkQyuJdli6LdO5Klv8Yx0OfPVTcQkIUfS8clx5e9yHXzlnhOZF3CbQ8C2uP5j074w==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.19.0", - "@babel/helper-skip-transparent-expression-wrappers": "^7.18.9" - } - }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.18.6.tgz", - "integrity": "sha512-kfiDrDQ+PBsQDO85yj1icueWMfGfJFKN1KCkndygtu/C9+XUfydLC8Iv5UYJqRwy4zk8EcplRxEOeLyjq1gm6Q==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/plugin-transform-template-literals": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.18.9.tgz", - "integrity": "sha512-S8cOWfT82gTezpYOiVaGHrCbhlHgKhQt8XH5ES46P2XWmX92yisoZywf5km75wv5sYcXDUCLMmMxOLCtthDgMA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.18.9.tgz", - "integrity": "sha512-SRfwTtF11G2aemAZWivL7PD+C9z52v9EvMqH9BuYbabyPuKUvSWks3oCg6041pT925L4zVFqaVBeECwsmlguEw==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.18.10.tgz", - "integrity": "sha512-kKAdAI+YzPgGY/ftStBFXTI1LZFju38rYThnfMykS+IXy8BVx+res7s2fxf1l8I35DV2T97ezo6+SGrXz6B3iQ==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.18.9" - } - }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.18.6.tgz", - "integrity": "sha512-gE7A6Lt7YLnNOL3Pb9BNeZvi+d8l7tcRrG4+pwJjK9hD2xX4mEvjlQW60G9EEmfXVYRPv9VRQcyegIVHCql/AA==", - "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - } - }, - "@babel/preset-env": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.20.2.tgz", - "integrity": "sha512-1G0efQEWR1EHkKvKHqbG+IN/QdgwfByUpM5V5QroDzGV2t3S/WXNQd693cHiHTlCFMpr9B6FkPFXDA2lQcKoDg==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.20.1", - "@babel/helper-compilation-targets": "^7.20.0", - "@babel/helper-plugin-utils": "^7.20.2", - "@babel/helper-validator-option": "^7.18.6", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.18.6", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-async-generator-functions": "^7.20.1", - "@babel/plugin-proposal-class-properties": "^7.18.6", - "@babel/plugin-proposal-class-static-block": "^7.18.6", - "@babel/plugin-proposal-dynamic-import": "^7.18.6", - "@babel/plugin-proposal-export-namespace-from": "^7.18.9", - "@babel/plugin-proposal-json-strings": "^7.18.6", - "@babel/plugin-proposal-logical-assignment-operators": "^7.18.9", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.18.6", - "@babel/plugin-proposal-numeric-separator": "^7.18.6", - "@babel/plugin-proposal-object-rest-spread": "^7.20.2", - "@babel/plugin-proposal-optional-catch-binding": "^7.18.6", - "@babel/plugin-proposal-optional-chaining": "^7.18.9", - "@babel/plugin-proposal-private-methods": "^7.18.6", - "@babel/plugin-proposal-private-property-in-object": "^7.18.6", - "@babel/plugin-proposal-unicode-property-regex": "^7.18.6", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.20.0", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.18.6", - "@babel/plugin-transform-async-to-generator": "^7.18.6", - "@babel/plugin-transform-block-scoped-functions": "^7.18.6", - "@babel/plugin-transform-block-scoping": "^7.20.2", - "@babel/plugin-transform-classes": "^7.20.2", - "@babel/plugin-transform-computed-properties": "^7.18.9", - "@babel/plugin-transform-destructuring": "^7.20.2", - "@babel/plugin-transform-dotall-regex": "^7.18.6", - "@babel/plugin-transform-duplicate-keys": "^7.18.9", - "@babel/plugin-transform-exponentiation-operator": "^7.18.6", - "@babel/plugin-transform-for-of": "^7.18.8", - "@babel/plugin-transform-function-name": "^7.18.9", - "@babel/plugin-transform-literals": "^7.18.9", - "@babel/plugin-transform-member-expression-literals": "^7.18.6", - "@babel/plugin-transform-modules-amd": "^7.19.6", - "@babel/plugin-transform-modules-commonjs": "^7.19.6", - "@babel/plugin-transform-modules-systemjs": "^7.19.6", - "@babel/plugin-transform-modules-umd": "^7.18.6", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.19.1", - "@babel/plugin-transform-new-target": "^7.18.6", - "@babel/plugin-transform-object-super": "^7.18.6", - "@babel/plugin-transform-parameters": "^7.20.1", - "@babel/plugin-transform-property-literals": "^7.18.6", - "@babel/plugin-transform-regenerator": "^7.18.6", - "@babel/plugin-transform-reserved-words": "^7.18.6", - "@babel/plugin-transform-shorthand-properties": "^7.18.6", - "@babel/plugin-transform-spread": "^7.19.0", - "@babel/plugin-transform-sticky-regex": "^7.18.6", - "@babel/plugin-transform-template-literals": "^7.18.9", - "@babel/plugin-transform-typeof-symbol": "^7.18.9", - "@babel/plugin-transform-unicode-escapes": "^7.18.10", - "@babel/plugin-transform-unicode-regex": "^7.18.6", - "@babel/preset-modules": "^0.1.5", - "@babel/types": "^7.20.2", - "babel-plugin-polyfill-corejs2": "^0.3.3", - "babel-plugin-polyfill-corejs3": "^0.6.0", - "babel-plugin-polyfill-regenerator": "^0.4.1", - "core-js-compat": "^3.25.1", - "semver": "^6.3.0" - } - }, - "@babel/preset-modules": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", - "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - } - }, - "@babel/runtime": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz", - "integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==", - "dev": true, - "requires": { - "regenerator-runtime": "^0.13.10" - } - }, - "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" - } - }, - "@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - } - }, - "@babel/types": { - "version": "7.20.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.2.tgz", - "integrity": "sha512-FnnvsNWgZCr232sqtXggapvlkk/tuwR/qhGzcmxI0GXLCjmPYQPzio2FbdlWuY6y1sHFfQKk+rRbUZ9VStQMog==", - "dev": true, - "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", - "to-fast-properties": "^2.0.0" - } - }, - "@jridgewell/gen-mapping": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", - "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", - "dev": true, - "requires": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - } - }, - "@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", - "dev": true - }, - "@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", - "dev": true - }, - "@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true - }, - "@jridgewell/trace-mapping": { - "version": "0.3.17", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", - "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", - "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" - } - }, - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "@sindresorhus/is": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz", - "integrity": "sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==", - "dev": true - }, - "@szmarczak/http-timer": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-1.1.2.tgz", - "integrity": "sha512-XIB2XbzHTN6ieIjfIMV9hlVcfPU26s2vafYWQcZHWXHOxiaRZYEDKEwdl129Zyg50+foYV2jCgtrqSA6qNuNSA==", - "dev": true, - "requires": { - "defer-to-connect": "^1.0.1" - } - }, - "@types/body-parser": { - "version": "1.19.2", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", - "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", - "dev": true, - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/connect-history-api-fallback": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.3.5.tgz", - "integrity": "sha512-h8QJa8xSb1WD4fpKBDcATDNGXghFj6/3GRWG6dhmRcu0RX1Ubasur2Uvx5aeEwlf0MwblEC2bMzzMQntxnw/Cw==", - "dev": true, - "requires": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "@types/express": { - "version": "4.17.14", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.14.tgz", - "integrity": "sha512-TEbt+vaPFQ+xpxFLFssxUDXj5cWCxZJjIcB7Yg0k0GMHGtgtQgpvx/MUQUeAkNbA9AAGrwkAsoeItdTgS7FMyg==", - "dev": true, - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.31", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.31.tgz", - "integrity": "sha512-DxMhY+NAsTwMMFHBTtJFNp5qiHKJ7TeqOo23zVEM9alT1Ml27Q3xcTH0xwxn7Q0BbMcVEJOs/7aQtUWupUQN3Q==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==", - "dev": true, - "requires": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "@types/highlight.js": { - "version": "9.12.4", - "resolved": "https://registry.npmjs.org/@types/highlight.js/-/highlight.js-9.12.4.tgz", - "integrity": "sha512-t2szdkwmg2JJyuCM20e8kR2X59WCE5Zkl4bzm1u1Oukjm79zpbiAv+QjnwLnuuV0WHEcX2NgUItu0pAMKuOPww==", - "dev": true - }, - "@types/http-proxy": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.9.tgz", - "integrity": "sha512-QsbSjA/fSk7xB+UXlCT3wHBy5ai9wOcNDWwZAtud+jXhwOM3l+EYZh8Lng4+/6n8uar0J7xILzqftJdJ/Wdfkw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, - "@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "@types/linkify-it": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", - "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", - "dev": true - }, - "@types/markdown-it": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-10.0.3.tgz", - "integrity": "sha512-daHJk22isOUvNssVGF2zDnnSyxHhFYhtjeX4oQaKD6QzL3ZR1QSgiD1g+Q6/WSWYVogNXYDXODtbgW/WiFCtyw==", - "dev": true, - "requires": { - "@types/highlight.js": "^9.7.0", - "@types/linkify-it": "*", - "@types/mdurl": "*", - "highlight.js": "^9.7.0" - } - }, - "@types/mdurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", - "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", - "dev": true - }, - "@types/mime": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-3.0.1.tgz", - "integrity": "sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==", - "dev": true - }, - "@types/minimatch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", - "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", - "dev": true - }, - "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", - "dev": true - }, - "@types/q": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.5.tgz", - "integrity": "sha512-L28j2FcJfSZOnL1WBjDYp2vUHCeIFlyYI/53EwD/rKUBQ7MtUUfbQWiyKJGpcnv4/WgrhWsFKrcPstcAt/J0tQ==", - "dev": true - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", - "dev": true - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", - "dev": true - }, - "@types/serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-z5xyF6uh8CbjAu9760KDKsH2FcDxZ2tFCsA4HIMWE6IkiYMXfVoa+4f9KX+FN0ZLsaMw1WNG2ETLA6N+/YA+cg==", - "dev": true, - "requires": { - "@types/mime": "*", - "@types/node": "*" - } - }, - "@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", - "dev": true - }, - "@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", - "dev": true - }, - "@types/uglify-js": { - "version": "3.17.1", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.1.tgz", - "integrity": "sha512-GkewRA4i5oXacU/n4MA9+bLgt5/L3F1mKrYvFGm7r2ouLXhRKjuWwo9XHNnbx6WF3vlGW21S3fCvgqxvxXXc5g==", - "dev": true, - "requires": { - "source-map": "^0.6.1" - } - }, - "@types/webpack": { - "version": "4.41.33", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.33.tgz", - "integrity": "sha512-PPajH64Ft2vWevkerISMtnZ8rTs4YmRbs+23c402J0INmxDKCrhZNvwZYtzx96gY2wAtXdrK1BS2fiC8MlLr3g==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" - } - }, - "@types/webpack-dev-server": { - "version": "3.11.6", - "resolved": "https://registry.npmjs.org/@types/webpack-dev-server/-/webpack-dev-server-3.11.6.tgz", - "integrity": "sha512-XCph0RiiqFGetukCTC3KVnY1jwLcZ84illFRMbyFzCcWl90B/76ew0tSqF46oBhnLC4obNDG7dMO0JfTN0MgMQ==", - "dev": true, - "requires": { - "@types/connect-history-api-fallback": "*", - "@types/express": "*", - "@types/serve-static": "*", - "@types/webpack": "^4", - "http-proxy-middleware": "^1.0.0" - } - }, - "@types/webpack-sources": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", - "dev": true, - "requires": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.7.3" - }, - "dependencies": { - "source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true - } - } - }, - "@vue/babel-helper-vue-jsx-merge-props": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-jsx-merge-props/-/babel-helper-vue-jsx-merge-props-1.4.0.tgz", - "integrity": "sha512-JkqXfCkUDp4PIlFdDQ0TdXoIejMtTHP67/pvxlgeY+u5k3LEdKuWZ3LK6xkxo52uDoABIVyRwqVkfLQJhk7VBA==", - "dev": true - }, - "@vue/babel-helper-vue-transform-on": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@vue/babel-helper-vue-transform-on/-/babel-helper-vue-transform-on-1.0.2.tgz", - "integrity": "sha512-hz4R8tS5jMn8lDq6iD+yWL6XNB699pGIVLk7WSJnn1dbpjaazsjZQkieJoRX6gW5zpYSCFqQ7jUquPNY65tQYA==", - "dev": true - }, - "@vue/babel-plugin-jsx": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-jsx/-/babel-plugin-jsx-1.1.1.tgz", - "integrity": "sha512-j2uVfZjnB5+zkcbc/zsOc0fSNGCMMjaEXP52wdwdIfn0qjFfEYpYZBFKFg+HHnQeJCVrjOeO0YxgaL7DMrym9w==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.0.0", - "@babel/template": "^7.0.0", - "@babel/traverse": "^7.0.0", - "@babel/types": "^7.0.0", - "@vue/babel-helper-vue-transform-on": "^1.0.2", - "camelcase": "^6.0.0", - "html-tags": "^3.1.0", - "svg-tags": "^1.0.0" - } - }, - "@vue/babel-plugin-transform-vue-jsx": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-plugin-transform-vue-jsx/-/babel-plugin-transform-vue-jsx-1.4.0.tgz", - "integrity": "sha512-Fmastxw4MMx0vlgLS4XBX0XiBbUFzoMGeVXuMV08wyOfXdikAFqBTuYPR0tlk+XskL19EzHc39SgjrPGY23JnA==", - "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "html-tags": "^2.0.0", - "lodash.kebabcase": "^4.1.1", - "svg-tags": "^1.0.0" - }, - "dependencies": { - "html-tags": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", - "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", - "dev": true - } - } - }, - "@vue/babel-preset-app": { - "version": "4.5.19", - "resolved": "https://registry.npmjs.org/@vue/babel-preset-app/-/babel-preset-app-4.5.19.tgz", - "integrity": "sha512-VCNRiAt2P/bLo09rYt3DLe6xXUMlhJwrvU18Ddd/lYJgC7s8+wvhgYs+MTx4OiAXdu58drGwSBO9SPx7C6J82Q==", - "dev": true, - "requires": { - "@babel/core": "^7.11.0", - "@babel/helper-compilation-targets": "^7.9.6", - "@babel/helper-module-imports": "^7.8.3", - "@babel/plugin-proposal-class-properties": "^7.8.3", - "@babel/plugin-proposal-decorators": "^7.8.3", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-jsx": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.11.0", - "@babel/preset-env": "^7.11.0", - "@babel/runtime": "^7.11.0", - "@vue/babel-plugin-jsx": "^1.0.3", - "@vue/babel-preset-jsx": "^1.2.4", - "babel-plugin-dynamic-import-node": "^2.3.3", - "core-js": "^3.6.5", - "core-js-compat": "^3.6.5", - "semver": "^6.1.0" - } - }, - "@vue/babel-preset-jsx": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-preset-jsx/-/babel-preset-jsx-1.4.0.tgz", - "integrity": "sha512-QmfRpssBOPZWL5xw7fOuHNifCQcNQC1PrOo/4fu6xlhlKJJKSA3HqX92Nvgyx8fqHZTUGMPHmFA+IDqwXlqkSA==", - "dev": true, - "requires": { - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "@vue/babel-sugar-composition-api-inject-h": "^1.4.0", - "@vue/babel-sugar-composition-api-render-instance": "^1.4.0", - "@vue/babel-sugar-functional-vue": "^1.4.0", - "@vue/babel-sugar-inject-h": "^1.4.0", - "@vue/babel-sugar-v-model": "^1.4.0", - "@vue/babel-sugar-v-on": "^1.4.0" - } - }, - "@vue/babel-sugar-composition-api-inject-h": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-inject-h/-/babel-sugar-composition-api-inject-h-1.4.0.tgz", - "integrity": "sha512-VQq6zEddJHctnG4w3TfmlVp5FzDavUSut/DwR0xVoe/mJKXyMcsIibL42wPntozITEoY90aBV0/1d2KjxHU52g==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-composition-api-render-instance": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-composition-api-render-instance/-/babel-sugar-composition-api-render-instance-1.4.0.tgz", - "integrity": "sha512-6ZDAzcxvy7VcnCjNdHJ59mwK02ZFuP5CnucloidqlZwVQv5CQLijc3lGpR7MD3TWFi78J7+a8J56YxbCtHgT9Q==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-functional-vue": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-functional-vue/-/babel-sugar-functional-vue-1.4.0.tgz", - "integrity": "sha512-lTEB4WUFNzYt2In6JsoF9sAYVTo84wC4e+PoZWSgM6FUtqRJz7wMylaEhSRgG71YF+wfLD6cc9nqVeXN2rwBvw==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-inject-h": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-inject-h/-/babel-sugar-inject-h-1.4.0.tgz", - "integrity": "sha512-muwWrPKli77uO2fFM7eA3G1lAGnERuSz2NgAxuOLzrsTlQl8W4G+wwbM4nB6iewlKbwKRae3nL03UaF5ffAPMA==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0" - } - }, - "@vue/babel-sugar-v-model": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-model/-/babel-sugar-v-model-1.4.0.tgz", - "integrity": "sha512-0t4HGgXb7WHYLBciZzN5s0Hzqan4Ue+p/3FdQdcaHAb7s5D9WZFGoSxEZHrR1TFVZlAPu1bejTKGeAzaaG3NCQ==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-helper-vue-jsx-merge-props": "^1.4.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "camelcase": "^5.0.0", - "html-tags": "^2.0.0", - "svg-tags": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "html-tags": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-2.0.0.tgz", - "integrity": "sha512-+Il6N8cCo2wB/Vd3gqy/8TZhTD3QvcVeQLCnZiGkGCH3JP28IgGAY41giccp2W4R3jfyJPAP318FQTa1yU7K7g==", - "dev": true - } - } - }, - "@vue/babel-sugar-v-on": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@vue/babel-sugar-v-on/-/babel-sugar-v-on-1.4.0.tgz", - "integrity": "sha512-m+zud4wKLzSKgQrWwhqRObWzmTuyzl6vOP7024lrpeJM4x2UhQtRDLgYjXAw9xBXjCwS0pP9kXjg91F9ZNo9JA==", - "dev": true, - "requires": { - "@babel/plugin-syntax-jsx": "^7.2.0", - "@vue/babel-plugin-transform-vue-jsx": "^1.4.0", - "camelcase": "^5.0.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } - } - }, - "@vue/compiler-sfc": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-2.7.14.tgz", - "integrity": "sha512-aNmNHyLPsw+sVvlQFQ2/8sjNuLtK54TC6cuKnVzAY93ks4ZBrvwQSnkkIh7bsbNhum5hJBS00wSDipQ937f5DA==", - "requires": { - "@babel/parser": "^7.18.4", - "postcss": "^8.4.14", - "source-map": "^0.6.1" - } - }, - "@vue/component-compiler-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@vue/component-compiler-utils/-/component-compiler-utils-3.3.0.tgz", - "integrity": "sha512-97sfH2mYNU+2PzGrmK2haqffDpVASuib9/w2/noxiFi31Z54hW+q3izKQXXQZSNhtiUpAI36uSuYepeBe4wpHQ==", - "dev": true, - "requires": { - "consolidate": "^0.15.1", - "hash-sum": "^1.0.2", - "lru-cache": "^4.1.2", - "merge-source-map": "^1.1.0", - "postcss": "^7.0.36", - "postcss-selector-parser": "^6.0.2", - "prettier": "^1.18.2 || ^2.0.0", - "source-map": "~0.6.1", - "vue-template-es2015-compiler": "^1.9.0" - }, - "dependencies": { - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", - "dev": true - } - } - }, - "@vuepress/core": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/core/-/core-1.9.7.tgz", - "integrity": "sha512-u5eb1mfNLV8uG2UuxlvpB/FkrABxeMHqymTsixOnsOg2REziv9puEIbqaZ5BjLPvbCDvSj6rn+DwjENmBU+frQ==", - "dev": true, - "requires": { - "@babel/core": "^7.8.4", - "@vue/babel-preset-app": "^4.1.2", - "@vuepress/markdown": "1.9.7", - "@vuepress/markdown-loader": "1.9.7", - "@vuepress/plugin-last-updated": "1.9.7", - "@vuepress/plugin-register-components": "1.9.7", - "@vuepress/shared-utils": "1.9.7", - "@vuepress/types": "1.9.7", - "autoprefixer": "^9.5.1", - "babel-loader": "^8.0.4", - "bundle-require": "2.1.8", - "cache-loader": "^3.0.0", - "chokidar": "^2.0.3", - "connect-history-api-fallback": "^1.5.0", - "copy-webpack-plugin": "^5.0.2", - "core-js": "^3.6.4", - "cross-spawn": "^6.0.5", - "css-loader": "^2.1.1", - "esbuild": "0.14.7", - "file-loader": "^3.0.1", - "js-yaml": "^3.13.1", - "lru-cache": "^5.1.1", - "mini-css-extract-plugin": "0.6.0", - "optimize-css-assets-webpack-plugin": "^5.0.1", - "portfinder": "^1.0.13", - "postcss-loader": "^3.0.0", - "postcss-safe-parser": "^4.0.1", - "toml": "^3.0.0", - "url-loader": "^1.0.1", - "vue": "^2.6.10", - "vue-loader": "^15.7.1", - "vue-router": "^3.4.5", - "vue-server-renderer": "^2.6.10", - "vue-template-compiler": "^2.6.10", - "vuepress-html-webpack-plugin": "^3.2.0", - "vuepress-plugin-container": "^2.0.2", - "webpack": "^4.8.1", - "webpack-chain": "^6.0.0", - "webpack-dev-server": "^3.5.1", - "webpack-merge": "^4.1.2", - "webpackbar": "3.2.0" - } - }, - "@vuepress/markdown": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/markdown/-/markdown-1.9.7.tgz", - "integrity": "sha512-DFOjYkwV6fT3xXTGdTDloeIrT1AbwJ9pwefmrp0rMgC6zOz3XUJn6qqUwcYFO5mNBWpbiFQ3JZirCtgOe+xxBA==", - "dev": true, - "requires": { - "@vuepress/shared-utils": "1.9.7", - "markdown-it": "^8.4.1", - "markdown-it-anchor": "^5.0.2", - "markdown-it-chain": "^1.3.0", - "markdown-it-emoji": "^1.4.0", - "markdown-it-table-of-contents": "^0.4.0", - "prismjs": "^1.13.0" - } - }, - "@vuepress/markdown-loader": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/markdown-loader/-/markdown-loader-1.9.7.tgz", - "integrity": "sha512-mxXF8FtX/QhOg/UYbe4Pr1j5tcf/aOEI502rycTJ3WF2XAtOmewjkGV4eAA6f6JmuM/fwzOBMZKDyy9/yo2I6Q==", - "dev": true, - "requires": { - "@vuepress/markdown": "1.9.7", - "loader-utils": "^1.1.0", - "lru-cache": "^5.1.1" - } - }, - "@vuepress/plugin-active-header-links": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-active-header-links/-/plugin-active-header-links-1.9.7.tgz", - "integrity": "sha512-G1M8zuV9Og3z8WBiKkWrofG44NEXsHttc1MYreDXfeWh/NLjr9q1GPCEXtiCjrjnHZHB3cSQTKnTqAHDq35PGA==", - "dev": true, - "requires": { - "@vuepress/types": "1.9.7", - "lodash.debounce": "^4.0.8" - } - }, - "@vuepress/plugin-back-to-top": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-back-to-top/-/plugin-back-to-top-1.9.7.tgz", - "integrity": "sha512-DM1S+Q8Xn/i+zhe4zThekxb1M2abfKLklg/NKtQloklHKdNdVfk+EcxWYNmNfSii+ymDWaaG8lmH0xjVhy0iXw==", - "dev": true, - "requires": { - "@vuepress/types": "1.9.7", - "lodash.debounce": "^4.0.8" - } - }, - "@vuepress/plugin-google-analytics": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-google-analytics/-/plugin-google-analytics-1.9.7.tgz", - "integrity": "sha512-ZpsYrk23JdwbcJo9xArVcdqYHt5VyTX9UN9bLqNrLJRgRTV0X2jKUkM63dlKTJMpBf+0K1PQMJbGBXgOO7Yh0Q==", - "dev": true, - "requires": { - "@vuepress/types": "1.9.7" - } - }, - "@vuepress/plugin-last-updated": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-last-updated/-/plugin-last-updated-1.9.7.tgz", - "integrity": "sha512-FiFBOl49dlFRjbLRnRAv77HDWfe+S/eCPtMQobq4/O3QWuL3Na5P4fCTTVzq1K7rWNO9EPsWNB2Jb26ndlQLKQ==", - "dev": true, - "requires": { - "@vuepress/types": "1.9.7", - "cross-spawn": "^6.0.5" - } - }, - "@vuepress/plugin-nprogress": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-nprogress/-/plugin-nprogress-1.9.7.tgz", - "integrity": "sha512-sI148igbdRfLgyzB8PdhbF51hNyCDYXsBn8bBWiHdzcHBx974sVNFKtfwdIZcSFsNrEcg6zo8YIrQ+CO5vlUhQ==", - "dev": true, - "requires": { - "@vuepress/types": "1.9.7", - "nprogress": "^0.2.0" - } - }, - "@vuepress/plugin-register-components": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-register-components/-/plugin-register-components-1.9.7.tgz", - "integrity": "sha512-l/w1nE7Dpl+LPMb8+AHSGGFYSP/t5j6H4/Wltwc2QcdzO7yqwC1YkwwhtTXvLvHOV8O7+rDg2nzvq355SFkfKA==", - "dev": true, - "requires": { - "@vuepress/shared-utils": "1.9.7", - "@vuepress/types": "1.9.7" - } - }, - "@vuepress/plugin-search": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/plugin-search/-/plugin-search-1.9.7.tgz", - "integrity": "sha512-MLpbUVGLxaaHEwflFxvy0pF9gypFVUT3Q9Zc6maWE+0HDWAvzMxo6GBaj6mQPwjOqNQMf4QcN3hDzAZktA+DQg==", - "dev": true, - "requires": { - "@vuepress/types": "1.9.7" - } - }, - "@vuepress/shared-utils": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/shared-utils/-/shared-utils-1.9.7.tgz", - "integrity": "sha512-lIkO/eSEspXgVHjYHa9vuhN7DuaYvkfX1+TTJDiEYXIwgwqtvkTv55C+IOdgswlt0C/OXDlJaUe1rGgJJ1+FTw==", - "dev": true, - "requires": { - "chalk": "^2.3.2", - "escape-html": "^1.0.3", - "fs-extra": "^7.0.1", - "globby": "^9.2.0", - "gray-matter": "^4.0.1", - "hash-sum": "^1.0.2", - "semver": "^6.0.0", - "toml": "^3.0.0", - "upath": "^1.1.0" - } - }, - "@vuepress/theme-default": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/theme-default/-/theme-default-1.9.7.tgz", - "integrity": "sha512-NZzCLIl+bgJIibhkqVmk/NSku57XIuXugxAN3uiJrCw6Mu6sb3xOvbk0En3k+vS2BKHxAZ6Cx7dbCiyknDQnSA==", - "dev": true, - "requires": { - "@vuepress/plugin-active-header-links": "1.9.7", - "@vuepress/plugin-nprogress": "1.9.7", - "@vuepress/plugin-search": "1.9.7", - "@vuepress/types": "1.9.7", - "docsearch.js": "^2.5.2", - "lodash": "^4.17.15", - "stylus": "^0.54.8", - "stylus-loader": "^3.0.2", - "vuepress-plugin-container": "^2.0.2", - "vuepress-plugin-smooth-scroll": "^0.0.3" - } - }, - "@vuepress/types": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@vuepress/types/-/types-1.9.7.tgz", - "integrity": "sha512-moLQzkX3ED2o18dimLemUm7UVDKxhcrJmGt5C0Ng3xxrLPaQu7UqbROtEKB3YnMRt4P/CA91J+Ck+b9LmGabog==", - "dev": true, - "requires": { - "@types/markdown-it": "^10.0.0", - "@types/webpack-dev-server": "^3", - "webpack-chain": "^6.0.0" - } - }, - "@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", - "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", - "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", - "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/helper-fsm": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", - "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", - "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0" - } - }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", - "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" - } - }, - "@webassemblyjs/ieee754": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", - "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", - "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "@webassemblyjs/leb128": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", - "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", - "dev": true, - "requires": { - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/utf8": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", - "dev": true - }, - "@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", - "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", - "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", - "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" - } - }, - "@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", - "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "@webassemblyjs/wast-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", - "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/floating-point-hex-parser": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-code-frame": "1.9.0", - "@webassemblyjs/helper-fsm": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@webassemblyjs/wast-printer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "requires": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - } - }, - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true - }, - "agentkeepalive": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-2.2.0.tgz", - "integrity": "sha512-TnB6ziK363p7lR8QpeLC8aMr8EGYBKZTpgzQLfqTs3bR0Oo5VbKdwKf8h0dSzsYrB7lSCgfJnMZKqShvlq5Oyg==", - "dev": true - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true, - "requires": {} - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true, - "requires": {} - }, - "algoliasearch": { - "version": "3.35.1", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-3.35.1.tgz", - "integrity": "sha512-K4yKVhaHkXfJ/xcUnil04xiSrB8B8yHZoFEhWNpXg23eiCnqvTZw1tn/SqvdsANlYHLJlKl0qi3I/Q2Sqo7LwQ==", - "dev": true, - "requires": { - "agentkeepalive": "^2.2.0", - "debug": "^2.6.9", - "envify": "^4.0.0", - "es6-promise": "^4.1.0", - "events": "^1.1.0", - "foreach": "^2.0.5", - "global": "^4.3.2", - "inherits": "^2.0.1", - "isarray": "^2.0.1", - "load-script": "^1.0.0", - "object-keys": "^1.0.11", - "querystring-es3": "^0.2.1", - "reduce": "^1.0.1", - "semver": "^5.1.0", - "tunnel-agent": "^0.6.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha512-0FcBfdcmaumGPQ0qPn7Q5qTgz/ooXgIyp1rf8ik5bGX8mpE2YHjC0P/eyQvxu1GURYQgq9ozf2mteQ5ZD9YiyQ==", - "dev": true - }, - "ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "dev": true, - "requires": { - "string-width": "^4.1.0" - } - }, - "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true - }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "dev": true, - "requires": { - "type-fest": "^0.21.3" - }, - "dependencies": { - "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true - } - } - }, - "ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true - }, - "ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true - }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "requires": { - "color-convert": "^1.9.0" - } - }, - "anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "dev": true, - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - } - }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", - "dev": true - }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "requires": { - "sprintf-js": "~1.0.2" - } - }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha512-YVIQ82gZPGBebQV/a8dar4AitzCQs0jjXwMPZllpXMaGjXPYVUawSxQrRsjhjupyVxEvbHgUmIhKVlND+j02kA==", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==", - "dev": true - }, - "array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", - "dev": true, - "requires": { - "array-uniq": "^1.0.1" - } - }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha512-SleRWjh9JUud2wH1hPs9rZBZ33H6T9HOiL0uwGnGx9FpE6wKGyfWugmbkEOIs6qWrZhg0LWeLziLrEwQJhs5mQ==", - "dev": true - }, - "array.prototype.reduce": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz", - "integrity": "sha512-kDdugMl7id9COE8R7MHF5jWk7Dqt/fs4Pv+JXoICnYwqpjjjbUurz6w5fT5IG6brLdJhv6/VoHB0H7oyIBXd+Q==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - } - }, - "asn1": { - "version": "0.2.6", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", - "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", - "dev": true, - "requires": { - "safer-buffer": "~2.1.0" - } - }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", - "dev": true, - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha512-8nWq2nLTAwd02jTqJExUYFSD/fKq6VH9Y/oG2accc/kdI0V98Bag8d5a4gi3XHz73rDWa2PvTtvcWYquKqSENA==", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha512-5KiHfsmkqacuKjkRkdV7SsfDJ2EGiPsK92s2MhNSY0craxjTdKTtqKsJaCWp4LW33ZZ0OPUv1WO/TFvNQRiQxQ==", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } - } - }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==", - "dev": true - }, - "async": { - "version": "2.6.4", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", - "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", - "dev": true, - "requires": { - "lodash": "^4.17.14" - } - }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", - "dev": true - }, - "autocomplete.js": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/autocomplete.js/-/autocomplete.js-0.36.0.tgz", - "integrity": "sha512-jEwUXnVMeCHHutUt10i/8ZiRaCb0Wo+ZyKxeGsYwBDtw6EJHqEeDrq4UwZRD8YBSvp3g6klP678il2eeiVXN2Q==", - "dev": true, - "requires": { - "immediate": "^3.2.3" - } - }, - "autoprefixer": { - "version": "9.8.8", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.8.tgz", - "integrity": "sha512-eM9d/swFopRt5gdJ7jrpCwgvEMIayITpojhkkSMRsFHYuH5bkSQ4p/9qTEHtmNudUZh22Tehu7I6CxAW0IXTKA==", - "dev": true, - "requires": { - "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001109", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "picocolors": "^0.2.1", - "postcss": "^7.0.32", - "postcss-value-parser": "^4.1.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", - "dev": true - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, - "babel-loader": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.3.0.tgz", - "integrity": "sha512-H8SvsMF+m9t15HNLMipppzkC+Y2Yq+v3SonZyU70RBL/h1gxPkH08Ot8pEE9Z4Kd+czyWJClmFS8qzIP9OZ04Q==", - "dev": true, - "requires": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "dependencies": { - "loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - } - } - }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-plugin-polyfill-corejs2": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.3.3.tgz", - "integrity": "sha512-8hOdmFYFSZhqg2C/JgLUQ+t52o5nirNwaWM2B9LWteozwIvM14VSwdsCAUET10qT+kmySAlseadmfeeSWFCy+Q==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.17.7", - "@babel/helper-define-polyfill-provider": "^0.3.3", - "semver": "^6.1.1" - } - }, - "babel-plugin-polyfill-corejs3": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.6.0.tgz", - "integrity": "sha512-+eHqR6OPcBhJOGgsIar7xoAB1GcSwVUA3XjAd7HJNzOXT4wv6/H7KIdA/Nc60cvUlDbKApmqNvD1B1bzOt4nyA==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3", - "core-js-compat": "^3.25.1" - } - }, - "babel-plugin-polyfill-regenerator": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.4.1.tgz", - "integrity": "sha512-NtQGmyQDXjQqQ+IzRkBVwEOz9lQ4zxAQZgoAYEtU9dJjnl1Oc98qnN7jcp+bE7O7aYzVpavXE3/VKXNzUbh7aw==", - "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.3.3" - } - }, - "balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", - "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true - }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", - "dev": true, - "requires": { - "tweetnacl": "^0.14.3" - } - }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" - } - }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "bn.js": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", - "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==", - "dev": true - }, - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dev": true, - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - } - } - }, - "bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha512-RaVTblr+OnEli0r/ud8InrU7D+G0y6aJhlxaLa6Pwty4+xoxboF1BsUI45tujvRpbj9dQVoglChqonGAsjEBYg==", - "dev": true, - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true - }, - "boxen": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-4.2.0.tgz", - "integrity": "sha512-eB4uT9RGzg2odpER62bBwSLvUeGC+WbRjjyyFhGsKnc8wp/m0+hQsMUvUe3H2V0D5vw0nBdO1hCJoZo5mKeuIQ==", - "dev": true, - "requires": { - "ansi-align": "^3.0.0", - "camelcase": "^5.3.1", - "chalk": "^3.0.0", - "cli-boxes": "^2.2.0", - "string-width": "^4.1.0", - "term-size": "^2.1.0", - "type-fest": "^0.8.1", - "widest-line": "^3.1.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==", - "dev": true - }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", - "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", - "dev": true, - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", - "dev": true, - "requires": { - "pako": "~1.0.5" - } - }, - "browserslist": { - "version": "4.21.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", - "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001400", - "electron-to-chromium": "^1.4.251", - "node-releases": "^2.0.6", - "update-browserslist-db": "^1.0.9" - } - }, - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - } - } - }, - "buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true - }, - "buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "buffer-json": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/buffer-json/-/buffer-json-2.0.0.tgz", - "integrity": "sha512-+jjPFVqyfF1esi9fvfUs3NqM0pH1ziZ36VP4hmA/y/Ssfo/5w5xHKfTw9BwQjoJ1w/oVtpLomqwUHKdefGyuHw==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==", - "dev": true - }, - "bundle-require": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-2.1.8.tgz", - "integrity": "sha512-oOEg3A0hy/YzvNWNowtKD0pmhZKseOFweCbgyMqTIih4gRY1nJWsvrOCT27L9NbIyL5jMjTFrAUpGxxpW68Puw==", - "dev": true, - "requires": {} - }, - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "dev": true - }, - "cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true - }, - "cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", - "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - } - }, - "cache-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/cache-loader/-/cache-loader-3.0.1.tgz", - "integrity": "sha512-HzJIvGiGqYsFUrMjAJNDbVZoG7qQA+vy9AIoKs7s9DscNfki0I589mf2w6/tW+kkFH3zyiknoWV5Jdynu6b/zw==", - "dev": true, - "requires": { - "buffer-json": "^2.0.0", - "find-cache-dir": "^2.1.0", - "loader-utils": "^1.2.3", - "mkdirp": "^0.5.1", - "neo-async": "^2.6.1", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "cacheable-request": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-6.1.0.tgz", - "integrity": "sha512-Oj3cAGPCqOZX7Rz64Uny2GYAZNliQSqfbePrgAQ1wKAihYmCUnraBtJtKcGR4xz7wF+LoJC+ssFZvv5BgF9Igg==", - "dev": true, - "requires": { - "clone-response": "^1.0.2", - "get-stream": "^5.1.0", - "http-cache-semantics": "^4.0.0", - "keyv": "^3.0.0", - "lowercase-keys": "^2.0.0", - "normalize-url": "^4.1.0", - "responselike": "^1.0.2" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "lowercase-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", - "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", - "dev": true - }, - "normalize-url": { - "version": "4.5.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.1.tgz", - "integrity": "sha512-9UZCFRHQdNrfTpGg8+1INIg93B6zE0aXMVFkw1WFwvO4SlZywU6aLg5Of0Ap/PgcbSw4LNxvMWXMeugwMCX0AA==", - "dev": true - } - } - }, - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "call-me-maybe": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.2.tgz", - "integrity": "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==", - "dev": true - }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha512-JuG3qI4QOftFsZyOn1qq87fq5grLIyk1JYd5lJmdA+fG7aQ9pA/i3JIJGcO3q0MrRcHlOt1U+ZeHW8Dq9axALQ==", - "dev": true, - "requires": { - "callsites": "^2.0.0" - } - }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha512-MCL3sf6nCSXOwCTzvPKhN18TU7AHTvdtam8DAogxcrJ8Rjfbbg7Lgng64H9Iy+vUV6VGFClN/TyxBkAebLRR4A==", - "dev": true, - "requires": { - "caller-callsite": "^2.0.0" - } - }, - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha512-ksWePWBloaWPxJYQ8TL0JHvtci6G5QTKwQ95RcWAa/lzoAKuAOflGdAK92hpHXjkwb8zLxoLNUoNYZgVsaJzvQ==", - "dev": true - }, - "camel-case": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-3.0.0.tgz", - "integrity": "sha512-+MbKztAYHXPr1jNTSKQF52VpcFjwY5RkR7fxksV8Doo4KAYc5Fl4UJRgthBbTmEx8C54DqahhbLJkDwjI3PI/w==", - "dev": true, - "requires": { - "no-case": "^2.2.0", - "upper-case": "^1.1.1" - } - }, - "camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "dev": true - }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "caniuse-lite": { - "version": "1.0.30001434", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001434.tgz", - "integrity": "sha512-aOBHrLmTQw//WFa2rcF1If9fa3ypkC1wzqqiKHgfdrXTWcU8C4gKVZT77eQAPWN1APys3+uQ0Df07rKauXGEYA==", - "dev": true - }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha512-3pKJwH184Xo/lnH6oyP1q2pMd7HcypqqmRs91/6/i2CGtWwIKGCkOOMTm/zXbgTEWHw1uNpNi/igc3ePOYHb6w==", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", - "dev": true - }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - } - }, - "clean-css": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-4.2.4.tgz", - "integrity": "sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==", - "dev": true, - "requires": { - "source-map": "~0.6.0" - } - }, - "cli-boxes": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-2.2.1.tgz", - "integrity": "sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==", - "dev": true - }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "dev": true, - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "clone-response": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", - "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", - "dev": true, - "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - } - }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha512-lNkKvzEeMBBjUGHZ+q6z9pSJla0KWAQPvtzhEV9+iGyQYG+pBpl7xKDhxoNSOZH2hhv0v5k0y2yAM4o4SjoSkw==", - "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - } - }, - "color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "dev": true, - "requires": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "requires": { - "color-name": "1.1.3" - } - }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "color-string": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", - "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", - "dev": true, - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, - "requires": { - "delayed-stream": "~1.0.0" - } - }, - "commander": { - "version": "2.17.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", - "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", - "dev": true - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "requires": { - "mime-db": ">= 1.43.0 < 2" - } - }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", - "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "configstore": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", - "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "graceful-fs": "^4.1.2", - "make-dir": "^3.0.0", - "unique-string": "^2.0.0", - "write-file-atomic": "^3.0.0", - "xdg-basedir": "^4.0.0" - } - }, - "connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true - }, - "consola": { - "version": "2.15.3", - "resolved": "https://registry.npmjs.org/consola/-/consola-2.15.3.tgz", - "integrity": "sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==", - "dev": true - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true - }, - "consolidate": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/consolidate/-/consolidate-0.15.1.tgz", - "integrity": "sha512-DW46nrsMJgy9kqAbPt5rKaCr7uFtpo4mSUvLHIUbJEjm0vo+aY5QLwBUq3FK4tRnJr/X0Psc0C4jf/h+HtXSMw==", - "dev": true, - "requires": { - "bluebird": "^3.1.1" - } - }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==", - "dev": true - }, - "content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "requires": { - "safe-buffer": "5.2.1" - }, - "dependencies": { - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", - "dev": true - }, - "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", - "dev": true - }, - "cookie": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", - "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true - }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw==", - "dev": true - }, - "copy-webpack-plugin": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.2.tgz", - "integrity": "sha512-Uh7crJAco3AjBvgAy9Z75CjK8IG+gxaErro71THQ+vv/bl4HaQcpkexAY8KVW/T6D2W2IRr+couF/knIRkZMIQ==", - "dev": true, - "requires": { - "cacache": "^12.0.3", - "find-cache-dir": "^2.1.0", - "glob-parent": "^3.1.0", - "globby": "^7.1.1", - "is-glob": "^4.0.1", - "loader-utils": "^1.2.3", - "minimatch": "^3.0.4", - "normalize-path": "^3.0.0", - "p-limit": "^2.2.1", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "webpack-log": "^2.0.0" - }, - "dependencies": { - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha512-yANWAN2DUcBtuus5Cpd+SKROzXHs2iVXFZt/Ykrfz6SAXqacLX25NZpltE+39ceMexYF4TtEadjuSTw8+3wX4g==", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true - } - } - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", - "dev": true - } - } - }, - "core-js": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.26.1.tgz", - "integrity": "sha512-21491RRQVzUn0GGM9Z1Jrpr6PNPxPi+Za8OM9q4tksTSnlbXXGKK1nXNg/QvwFYettXvSX6zWKCtHHfjN4puyA==", - "dev": true - }, - "core-js-compat": { - "version": "3.26.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.26.1.tgz", - "integrity": "sha512-622/KzTudvXCDLRw70iHW4KKs1aGpcRcowGWyYJr2DEBfRrd6hNJybxSWJFuZYD4ma86xhrwDDHxmDaIq4EA8A==", - "dev": true, - "requires": { - "browserslist": "^4.21.4" - } - }, - "core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "dev": true - }, - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - } - }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", - "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", - "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - } - }, - "crypto-random-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", - "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", - "dev": true - }, - "css": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/css/-/css-2.2.4.tgz", - "integrity": "sha512-oUnjmWpy0niI3x/mPL8dVEI1l7MnG3+HHyRPHf+YFSbK+svOhXpmSOcDURUh2aOCgl2grzrOPt1nHLuCVFULLw==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "source-map": "^0.6.1", - "source-map-resolve": "^0.5.2", - "urix": "^0.1.0" - } - }, - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha512-zj5D7X1U2h2zsXOAM8EyUREBnnts6H+Jm+d1M2DbiQQcUtnqgQsMrdo8JW9R80YFUmIdBZeMu5wvYM7hcgWP/Q==", - "dev": true - }, - "css-declaration-sorter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", - "dev": true, - "requires": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "css-loader": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-2.1.1.tgz", - "integrity": "sha512-OcKJU/lt232vl1P9EEDamhoO9iKY3tIjY5GU+XDLblAykTdgs6Ux9P1hTHve8nFKy5KPpOXOsVI/hIwi3841+w==", - "dev": true, - "requires": { - "camelcase": "^5.2.0", - "icss-utils": "^4.1.0", - "loader-utils": "^1.2.3", - "normalize-path": "^3.0.0", - "postcss": "^7.0.14", - "postcss-modules-extract-imports": "^2.0.0", - "postcss-modules-local-by-default": "^2.0.6", - "postcss-modules-scope": "^2.1.0", - "postcss-modules-values": "^2.0.0", - "postcss-value-parser": "^3.3.0", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "css-parse": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/css-parse/-/css-parse-2.0.0.tgz", - "integrity": "sha512-UNIFik2RgSbiTwIW1IsFwXWn6vs+bYdq83LKTSOsx7NJR7WII9dxewkHLltfTLVppoUApHV0118a4RZRI9FLwA==", - "dev": true, - "requires": { - "css": "^2.0.0" - } - }, - "css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true - }, - "css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", - "dev": true, - "requires": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - } - }, - "css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "cssnano": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz", - "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.8", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "cssnano-preset-default": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz", - "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==", - "dev": true, - "requires": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.3", - "postcss-unique-selectors": "^4.0.1" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", - "integrity": "sha512-6RIcwmV3/cBMG8Aj5gucQRsJb4vv4I4rn6YjPbVWd5+Pn/fuG+YseGvXGk00XLkoZkaj31QOD7vMUpNPC4FIuw==", - "dev": true - }, - "cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", - "integrity": "sha512-JPMZ1TSMRUPVIqEalIBNoBtAYbi8okvcFns4O0YIhcdGebeYZK7dMyHJiQ6GqNBA9kE0Hym4Aqym5rPdsV/4Cw==", - "dev": true - }, - "cssnano-util-raw-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", - "dev": true - }, - "csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "requires": { - "css-tree": "^1.1.2" - }, - "dependencies": { - "css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - } - }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - } - } - }, - "csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" - }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha512-NJGVKPS81XejHcLhaLJS7plab0fK3slPh11mESeeDq2W4ZI5kUKK/LRRdVDvjJseojbPB7ZwjnyOybg3Igea/A==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "de-indent": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/de-indent/-/de-indent-1.0.2.tgz", - "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", - "dev": true - }, - "debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha512-hjf+xovcEn31w/EUYdTXQh/8smFL/dzYjohQGEIgjyNavaJfBY2p5F527Bo1VPATxv0VYTUC2bOcXvqFwk78Og==", - "dev": true - }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha512-BzRPQuY1ip+qDonAOz42gRm/pg9F768C+npV/4JOsxRC2sq+Rlk+Q4ZCAsOhnIaMrgarILY+RMUIvMmmX1qAEA==", - "dev": true, - "requires": { - "mimic-response": "^1.0.0" - } - }, - "deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" - } - }, - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "deepmerge": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", - "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==", - "dev": true - }, - "default-gateway": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", - "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", - "dev": true, - "requires": { - "execa": "^1.0.0", - "ip-regex": "^2.1.0" - } - }, - "defer-to-connect": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-1.1.3.tgz", - "integrity": "sha512-0ISdNousHvZT2EiFlZeZAHBUvSxmKswVCEf8hW7KWgG4a8MVEu/3Vb6uWYozkjylyCxe0JBIiRB1jV45S70WVQ==", - "dev": true - }, - "define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", - "dev": true, - "requires": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "dependencies": { - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha512-KVbFv2TQtbzCoxAnfD6JcHZTYCzyliEaaeM/gH8qQdkKr5s0OP9scEgvdcngyk7AVdY6YVW/TJHd+lQ/Df3Daw==", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true - } - } - } - } - }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true - }, - "detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true - }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - } - }, - "dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", - "dev": true - }, - "dns-packet": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", - "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", - "dev": true, - "requires": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" - } - }, - "dns-txt": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha512-Ix5PrWjphuSoUXV/Zv5gaFHjnaJtb02F2+Si3Ht9dyJ87+Z/lMmy+dpNHtTGraNK958ndXq2i+GLkWsWHcKaBQ==", - "dev": true, - "requires": { - "buffer-indexof": "^1.0.0" - } - }, - "docsearch.js": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/docsearch.js/-/docsearch.js-2.6.3.tgz", - "integrity": "sha512-GN+MBozuyz664ycpZY0ecdQE0ND/LSgJKhTLA0/v3arIS3S1Rpf2OJz6A35ReMsm91V5apcmzr5/kM84cvUg+A==", - "dev": true, - "requires": { - "algoliasearch": "^3.24.5", - "autocomplete.js": "0.36.0", - "hogan.js": "^3.0.2", - "request": "^2.87.0", - "stack-utils": "^1.0.1", - "to-factory": "^1.0.0", - "zepto": "^1.2.0" - } - }, - "dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "dev": true, - "requires": { - "utila": "~0.4" - } - }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - }, - "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true - } - } - }, - "dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==", - "dev": true - }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true - }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true - }, - "domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "dev": true, - "requires": { - "domelementtype": "^2.2.0" - }, - "dependencies": { - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - } - } - }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", - "dev": true, - "requires": { - "is-obj": "^2.0.0" - } - }, - "duplexer3": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.5.tgz", - "integrity": "sha512-1A8za6ws41LQgv9HrE/66jyC5yuSjQ3L/KOpFtoBilsAK2iA2wuS5rTt1OCzIvtS2V7nVmedsUU+DGRcjBmOYA==", - "dev": true - }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", - "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", - "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" - } - }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.4.284", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", - "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", - "dev": true - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true - }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true - }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, - "requires": { - "once": "^1.4.0" - } - }, - "enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - } - } - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" - }, - "envify": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/envify/-/envify-4.1.0.tgz", - "integrity": "sha512-IKRVVoAYr4pIx4yIWNsz9mOsboxlNXiu7TNBnem/K/uTHdkyzXWDzHCK7UTolqBbgaBz0tQHsD3YNls0uIIjiw==", - "dev": true, - "requires": { - "esprima": "^4.0.0", - "through": "~2.3.4" - } - }, - "envinfo": { - "version": "7.8.1", - "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.8.1.tgz", - "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==", - "dev": true - }, - "errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "requires": { - "prr": "~1.0.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "dev": true, - "requires": { - "is-arrayish": "^0.2.1" - } - }, - "es-abstract": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", - "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, - "es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "es6-promise": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", - "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==", - "dev": true - }, - "esbuild": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.14.7.tgz", - "integrity": "sha512-+u/msd6iu+HvfysUPkZ9VHm83LImmSNnecYPfFI01pQ7TTcsFR+V0BkybZX7mPtIaI7LCrse6YRj+v3eraJSgw==", - "dev": true, - "requires": { - "esbuild-android-arm64": "0.14.7", - "esbuild-darwin-64": "0.14.7", - "esbuild-darwin-arm64": "0.14.7", - "esbuild-freebsd-64": "0.14.7", - "esbuild-freebsd-arm64": "0.14.7", - "esbuild-linux-32": "0.14.7", - "esbuild-linux-64": "0.14.7", - "esbuild-linux-arm": "0.14.7", - "esbuild-linux-arm64": "0.14.7", - "esbuild-linux-mips64le": "0.14.7", - "esbuild-linux-ppc64le": "0.14.7", - "esbuild-netbsd-64": "0.14.7", - "esbuild-openbsd-64": "0.14.7", - "esbuild-sunos-64": "0.14.7", - "esbuild-windows-32": "0.14.7", - "esbuild-windows-64": "0.14.7", - "esbuild-windows-arm64": "0.14.7" - } - }, - "esbuild-android-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.14.7.tgz", - "integrity": "sha512-9/Q1NC4JErvsXzJKti0NHt+vzKjZOgPIjX/e6kkuCzgfT/GcO3FVBcGIv4HeJG7oMznE6KyKhvLrFgt7CdU2/w==", - "dev": true, - "optional": true - }, - "esbuild-darwin-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.14.7.tgz", - "integrity": "sha512-Z9X+3TT/Xj+JiZTVlwHj2P+8GoiSmUnGVz0YZTSt8WTbW3UKw5Pw2ucuJ8VzbD2FPy0jbIKJkko/6CMTQchShQ==", - "dev": true, - "optional": true - }, - "esbuild-darwin-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.7.tgz", - "integrity": "sha512-68e7COhmwIiLXBEyxUxZSSU0akgv8t3e50e2QOtKdBUE0F6KIRISzFntLe2rYlNqSsjGWsIO6CCc9tQxijjSkw==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.7.tgz", - "integrity": "sha512-76zy5jAjPiXX/S3UvRgG85Bb0wy0zv/J2lel3KtHi4V7GUTBfhNUPt0E5bpSXJ6yMT7iThhnA5rOn+IJiUcslQ==", - "dev": true, - "optional": true - }, - "esbuild-freebsd-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.7.tgz", - "integrity": "sha512-lSlYNLiqyzd7qCN5CEOmLxn7MhnGHPcu5KuUYOG1i+t5A6q7LgBmfYC9ZHJBoYyow3u4CNu79AWHbvVLpE/VQQ==", - "dev": true, - "optional": true - }, - "esbuild-linux-32": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.14.7.tgz", - "integrity": "sha512-Vk28u409wVOXqTaT6ek0TnfQG4Ty1aWWfiysIaIRERkNLhzLhUf4i+qJBN8mMuGTYOkE40F0Wkbp6m+IidOp2A==", - "dev": true, - "optional": true - }, - "esbuild-linux-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.14.7.tgz", - "integrity": "sha512-+Lvz6x+8OkRk3K2RtZwO+0a92jy9si9cUea5Zoru4yJ/6EQm9ENX5seZE0X9DTwk1dxJbjmLsJsd3IoowyzgVg==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.14.7.tgz", - "integrity": "sha512-OzpXEBogbYdcBqE4uKynuSn5YSetCvK03Qv1HcOY1VN6HmReuatjJ21dCH+YPHSpMEF0afVCnNfffvsGEkxGJQ==", - "dev": true, - "optional": true - }, - "esbuild-linux-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.7.tgz", - "integrity": "sha512-kJd5beWSqteSAW086qzCEsH6uwpi7QRIpzYWHzEYwKKu9DiG1TwIBegQJmLpPsLp4v5RAFjea0JAmAtpGtRpqg==", - "dev": true, - "optional": true - }, - "esbuild-linux-mips64le": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.7.tgz", - "integrity": "sha512-mFWpnDhZJmj/h7pxqn1GGDsKwRfqtV7fx6kTF5pr4PfXe8pIaTERpwcKkoCwZUkWAOmUEjMIUAvFM72A6hMZnA==", - "dev": true, - "optional": true - }, - "esbuild-linux-ppc64le": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.7.tgz", - "integrity": "sha512-wM7f4M0bsQXfDL4JbbYD0wsr8cC8KaQ3RPWc/fV27KdErPW7YsqshZZSjDV0kbhzwpNNdhLItfbaRT8OE8OaKA==", - "dev": true, - "optional": true - }, - "esbuild-netbsd-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.7.tgz", - "integrity": "sha512-J/afS7woKyzGgAL5FlgvMyqgt5wQ597lgsT+xc2yJ9/7BIyezeXutXqfh05vszy2k3kSvhLesugsxIA71WsqBw==", - "dev": true, - "optional": true - }, - "esbuild-openbsd-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.7.tgz", - "integrity": "sha512-7CcxgdlCD+zAPyveKoznbgr3i0Wnh0L8BDGRCjE/5UGkm5P/NQko51tuIDaYof8zbmXjjl0OIt9lSo4W7I8mrw==", - "dev": true, - "optional": true - }, - "esbuild-sunos-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.14.7.tgz", - "integrity": "sha512-GKCafP2j/KUljVC3nesw1wLFSZktb2FGCmoT1+730zIF5O6hNroo0bSEofm6ZK5mNPnLiSaiLyRB9YFgtkd5Xg==", - "dev": true, - "optional": true - }, - "esbuild-windows-32": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.14.7.tgz", - "integrity": "sha512-5I1GeL/gZoUUdTPA0ws54bpYdtyeA2t6MNISalsHpY269zK8Jia/AXB3ta/KcDHv2SvNwabpImeIPXC/k0YW6A==", - "dev": true, - "optional": true - }, - "esbuild-windows-64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.14.7.tgz", - "integrity": "sha512-CIGKCFpQOSlYsLMbxt8JjxxvVw9MlF1Rz2ABLVfFyHUF5OeqHD5fPhGrCVNaVrhO8Xrm+yFmtjcZudUGr5/WYQ==", - "dev": true, - "optional": true - }, - "esbuild-windows-arm64": { - "version": "0.14.7", - "resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.7.tgz", - "integrity": "sha512-eOs1eSivOqN7cFiRIukEruWhaCf75V0N8P0zP7dh44LIhLl8y6/z++vv9qQVbkBm5/D7M7LfCfCTmt1f1wHOCw==", - "dev": true, - "optional": true - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "dev": true - }, - "escape-goat": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-2.1.1.tgz", - "integrity": "sha512-8/uIhbG12Csjy2JEW7D9pHbreaVaS/OpN3ycnyvElTdwM5n6GY6W6e2IPemfvGZeUMqZ9A/3GqIZMgKnBhAw/Q==", - "dev": true - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "dev": true - }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "esm": { - "version": "3.2.25", - "resolved": "https://registry.npmjs.org/esm/-/esm-3.2.25.tgz", - "integrity": "sha512-U1suiZ2oDVWv4zPO56S0NcR5QriEahGtdN2OR6FiOG4WJvcjBVFB0qI4+eKoWFH483PKGuLuu6V8Z4T5g63UVA==" - }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "requires": { - "estraverse": "^5.2.0" - }, - "dependencies": { - "estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true - } - } - }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true - }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "dev": true - }, - "eventsource": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-2.0.2.tgz", - "integrity": "sha512-IzUmBGPR3+oUG9dUeXynyNmf91/3zUSJg1lCktzKw47OXuhco54U3r9B7O4XX+Rb1Itm9OZ2b0RkTs10bICOxA==", - "dev": true - }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", - "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "execa": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", - "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha512-w/ozOKR9Obk3qoWeY/WDi6MFta9AoMR+zud60mdnbniMcBxRuFJyDt2LdX/14A1UABeqk+Uk+LDfUpvoGKppZA==", - "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "express": { - "version": "4.18.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", - "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", - "dev": true, - "requires": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.1", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.5.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.2.0", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.7", - "qs": "6.11.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.18.0", - "serve-static": "1.15.0", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "dependencies": { - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "qs": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", - "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", - "dev": true, - "requires": { - "side-channel": "^1.0.4" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", - "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", - "dev": true - }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "figgy-pudding": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", - "dev": true - }, - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "file-loader": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-3.0.1.tgz", - "integrity": "sha512-4sNIOXgtH/9WZq4NvlfU3Opn5ynUsqBwSLyM+I7UOwdGigTBYfVVQEwe/msZNX/j4pCJTIM14Fsw66Svo1oVrw==", - "dev": true, - "requires": { - "loader-utils": "^1.0.2", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "dev": true, - "optional": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "finalhandler": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", - "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" - }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", - "dev": true - }, - "foreach": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.6.tgz", - "integrity": "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg==", - "dev": true - }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", - "dev": true - }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", - "mime-types": "^2.1.12" - } - }, - "forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true - }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha512-GMBAbW9antB8iZRHLoGw0b3HANt57diZYFO/HL1JGIC1MjKrdmhxvrJbupnVvpys0zsz7yBApXdQyfepKly2kA==", - "dev": true, - "requires": { - "map-cache": "^0.2.2" - } - }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true - }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - } - }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha512-gehEzmPn2nAwr39eay+x3X34Ra+M2QlVUTLhkXPjWdeO8RF9kszk116avgBJM3ZyNHgHXBNx+VmPaFC36k0PzA==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true - }, - "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - } - }, - "functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", - "dev": true - }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true - }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true - }, - "get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - } - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", - "dev": true, - "requires": { - "assert-plus": "^1.0.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==", - "dev": true - }, - "global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dev": true, - "requires": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, - "global-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-2.1.0.tgz", - "integrity": "sha512-MG6kdOUh/xBnyo9cJFeIKkLEc1AyFq42QTU4XiX51i2NEdxLxLWXIjEjmqKeSuKR7pAZjTqUVoT2b2huxVLgYQ==", - "dev": true, - "requires": { - "ini": "1.3.7" - } - }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globby": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-9.2.0.tgz", - "integrity": "sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "array-union": "^1.0.2", - "dir-glob": "^2.2.2", - "fast-glob": "^2.2.6", - "glob": "^7.1.3", - "ignore": "^4.0.3", - "pify": "^4.0.1", - "slash": "^2.0.0" - } - }, - "got": { - "version": "9.6.0", - "resolved": "https://registry.npmjs.org/got/-/got-9.6.0.tgz", - "integrity": "sha512-R7eWptXuGYxwijs0eV+v3o6+XH1IqVK8dJOEecQfTmkncw9AV4dcw/Dhxi8MdlqPthxxpZyizMzyg8RTmEsG+Q==", - "dev": true, - "requires": { - "@sindresorhus/is": "^0.14.0", - "@szmarczak/http-timer": "^1.1.2", - "cacheable-request": "^6.0.0", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^4.1.0", - "lowercase-keys": "^1.0.1", - "mimic-response": "^1.0.1", - "p-cancelable": "^1.0.0", - "to-readable-stream": "^1.0.0", - "url-parse-lax": "^3.0.0" - } - }, - "graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "dev": true, - "requires": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - } - }, - "handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", - "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, - "requires": { - "function-bind": "^1.1.1" - } - }, - "has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", - "dev": true - }, - "has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true - }, - "has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.1" - } - }, - "has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", - "dev": true - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", - "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - } - }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "dependencies": { - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "has-yarn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-2.1.0.tgz", - "integrity": "sha512-UqBRqi4ju7T+TqGNdqAO0PaSVGsDGJUBQvk9eUWNGRY1CFGDzYhLWoM7JQEemnlvVcv/YEmc2wNW8BC24EnUsw==", - "dev": true - }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true - } - } - }, - "hash-sum": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-1.0.2.tgz", - "integrity": "sha512-fUs4B4L+mlt8/XAtSOGMUO1TXmAelItBPtJG7CyHJfYTdDjwisntGO2JQz7oUsatOY9o68+57eziUVNw/mRHmA==", - "dev": true - }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true - }, - "hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, - "highlight.js": { - "version": "9.18.5", - "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-9.18.5.tgz", - "integrity": "sha512-a5bFyofd/BHCX52/8i8uJkjr9DYwXIPnM/plwI6W7ezItLGqzt7X2G2nXuYSfsIJdkwwj/g9DG1LkcGJI/dDoA==", - "dev": true - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", - "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "hogan.js": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/hogan.js/-/hogan.js-3.0.2.tgz", - "integrity": "sha512-RqGs4wavGYJWE07t35JQccByczmNUXQT0E12ZYV1VKYu5UiAU9lsos/yBAcf840+zrUQQxgVduCR5/B8nNtibg==", - "dev": true, - "requires": { - "mkdirp": "0.3.0", - "nopt": "1.0.10" - }, - "dependencies": { - "mkdirp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", - "integrity": "sha512-OHsdUcVAQ6pOtg5JYWpCBo9W/GySVuwvP9hueRMW7UqshC0tbfzLv8wjySTPm3tfUZ/21CE9E1pJagOA91Pxew==", - "dev": true - } - } - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha512-M5ezZw4LzXbBKMruP+BNANf0k+19hDQMgpzBIYnya//Al+fjNct9Wf3b1WedLqdEs2hKBvxq/jh+DsHJLj0F9A==", - "dev": true - }, - "hsla-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha512-7Wn5GMLuHBjZCb2bTmnDOycho0p/7UVaAeqXZGbHrBCl6Yd/xDhQJAXe6Ga9AXJH2I5zY1dEdYw2u1UptnSBJA==", - "dev": true - }, - "html-entities": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", - "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", - "dev": true - }, - "html-minifier": { - "version": "3.5.21", - "resolved": "https://registry.npmjs.org/html-minifier/-/html-minifier-3.5.21.tgz", - "integrity": "sha512-LKUKwuJDhxNa3uf/LPR/KVjm/l3rBqtYeCOAekvG8F1vItxMUpueGd94i/asDDr8/1u7InxzFA5EeGjhhG5mMA==", - "dev": true, - "requires": { - "camel-case": "3.0.x", - "clean-css": "4.2.x", - "commander": "2.17.x", - "he": "1.2.x", - "param-case": "2.1.x", - "relateurl": "0.2.x", - "uglify-js": "3.4.x" - } - }, - "html-tags": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.2.0.tgz", - "integrity": "sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==", - "dev": true - }, - "htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - }, - "dependencies": { - "dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true - } - } - }, - "http-cache-semantics": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz", - "integrity": "sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==", - "dev": true - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "http-parser-js": { - "version": "0.5.8", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", - "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", - "dev": true - }, - "http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - } - }, - "http-proxy-middleware": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-1.3.1.tgz", - "integrity": "sha512-13eVVDYS4z79w7f1+NPllJtOQFx/FdUW4btIvVRMaRlUY9VGstAbo5MOhLEuUgZFRHn3x50ufn25zkj/boZnEg==", - "dev": true, - "requires": { - "@types/http-proxy": "^1.17.5", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - } - }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" - } - }, - "https-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==", - "dev": true - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "icss-replace-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz", - "integrity": "sha512-chIaY3Vh2mh2Q3RGXttaDIzeiPvaVXJ+C4DAh/w3c37SKZ/U6PGMmuicR2EQQp9bKG8zLMCl7I+PtIoOOPp8Gg==", - "dev": true - }, - "icss-utils": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-4.1.1.tgz", - "integrity": "sha512-4aFq7wvWyMHKgxsH8QQtGpvbASCf+eM3wPRLI6R+MgAnTCZ6STYsRvttLvRWK0Nfif5piF394St3HeJDaljGPA==", - "dev": true, - "requires": { - "postcss": "^7.0.14" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true - }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha512-DUNFN5j7Tln0D+TxzloUjKB+CtVu6myn0JEFak6dG18mNt9YkQ6lzGCdafwofISZ1lLF3xRHJ98VKy9ynkcFaA==", - "dev": true - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "immediate": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.3.0.tgz", - "integrity": "sha512-HR7EVodfFUdQCTIeySw+WDRFJlPcLOJbXfwwZ7Oom6tjsvZ3bOkCDJHehQC3nxJrv7+f9XecwazynjU8e4Vw3Q==", - "dev": true - }, - "import-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-cwd/-/import-cwd-2.1.0.tgz", - "integrity": "sha512-Ew5AZzJQFqrOV5BTW3EIoHAnoie1LojZLXKcCQ/yTRyVZosBhK1x1ViYjHGf5pAFOq8ZyChZp6m/fSN7pJyZtg==", - "dev": true, - "requires": { - "import-from": "^2.1.0" - } - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha512-eZ5H8rcgYazHbKC3PG4ClHNykCSxtAhxSSEM+2mb+7evD2CKF5V7c0dNum7AdpDh0ZdICwZY9sRSn8f+KH96sg==", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "import-from": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-from/-/import-from-2.1.0.tgz", - "integrity": "sha512-0vdnLL2wSGnhlRmzHJAg5JHjt1l2vYhzJ7tNLGbeVg0fse56tpGaH0uzH+r9Slej+BSXXEHvBKDEnVSLLE9/+w==", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "import-lazy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-2.1.0.tgz", - "integrity": "sha512-m7ZEHgtw69qOGw+jwxXkHlrlIPdTGkyh66zXZ1ajZbxkDBNjSY/LGbmjc7h0s2ELsUDTAhFr55TrPSSqJGPG0A==", - "dev": true - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - }, - "dependencies": { - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - } - } - }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true - }, - "indexes-of": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha512-bup+4tap3Hympa+JBJUG7XuOsdNQ6fxt0MHyXMKuLBKn0OqsTfvUxkUrroEX1+B2VsSHvCjiIcZVxRtYa4nllA==", - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", - "dev": true - }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "requires": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, - "ini": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.7.tgz", - "integrity": "sha512-iKpRpXP+CrP2jyrxvg1kMUpXDyRUFDWurxbnVT1vQPx+Wz9uCYsMIqYuSBLV+PAaZG/d7kRLKRFc9oDMsH+mFQ==", - "dev": true - }, - "internal-ip": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", - "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", - "dev": true, - "requires": { - "default-gateway": "^4.2.0", - "ipaddr.js": "^1.9.0" - } - }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, - "ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", - "dev": true - }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha512-58yWmlHpp7VYfcdTwMTvwMmqx/Elfxjd9RXTDyMsbL7lLWmhMylLEqiYVLKuLzOZqVgiWXD9MfR62Vv89VRxkw==", - "dev": true - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true - }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha512-vOx7VprsKyllwjSkLV79NIhpyLfr3jAp7VaTCMXOJHu4m0Ew1CZ2fcjASwmV1jI3BWuWHB013M48eyeldk9gYg==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha512-e1BM1qnDbMRG3ll2U9dSK0UMHuWOs3pY3AtcFsmvwPtKL3MML/Q86i+GilLfvqEs4GW+ExB91tQ3Ig9noDIZ+A==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true - }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "dev": true, - "requires": { - "has-bigints": "^1.0.1" - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha512-9fRVlXc0uCxEDj1nQzaWONSpbTfx0FmJfzHF7pwlI8DkWGoHBBea4Pg5Ky0ojwwxQmnSifgbKkI06Qv0Ljgj+Q==", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true - }, - "is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "dev": true - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", - "dev": true, - "requires": { - "ci-info": "^2.0.0" - } - }, - "is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha512-H1U8Vz0cfXNujrJzEcvvwMDW9Ra+biSYA3ThdQvAnMLJkEHQXn6bWzLkxHtVYJ+Sdbx0b6finn3jZiaVe7MAHA==", - "dev": true, - "requires": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "is-core-module": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", - "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha512-+w9D5ulSoBNlmw9OHn3U2v51SyoCd0he+bB3xMl62oijhrspxowjU+AIcDY0N3iEJbUEkB15IlMASQsxYigvXg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } - } - }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", - "dev": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "is-installed-globally": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.3.2.tgz", - "integrity": "sha512-wZ8x1js7Ia0kecP/CHM/3ABkAmujX7WPvQk6uu3Fly/Mk44pySulQpnHG46OMjHGXApINnV4QhY3SWnECO2z5g==", - "dev": true, - "requires": { - "global-dirs": "^2.0.1", - "is-path-inside": "^3.0.1" - } - }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true - }, - "is-npm": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-4.0.0.tgz", - "integrity": "sha512-96ECIfh9xtDDlPylNPXhzjsykHsMJZ18ASpaWzQyBr4YRTcVjUvzaHayDAES2oU/3KpljhHUjtSRNiDwi0F0ig==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", - "dev": true, - "requires": { - "is-path-inside": "^2.1.0" - }, - "dependencies": { - "is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", - "dev": true, - "requires": { - "path-is-inside": "^1.0.2" - } - } - } - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, - "is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", - "dev": true - }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", - "dev": true - }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "dev": true, - "requires": { - "has-tostringtag": "^1.0.0" - } - }, - "is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", - "dev": true, - "requires": { - "has-symbols": "^1.0.2" - } - }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "dev": true - }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, - "is-windows": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", - "dev": true - }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", - "dev": true - }, - "is-yarn-global": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.3.0.tgz", - "integrity": "sha512-VjSeb/lHmkoyd8ryPVIKvOCn4D1koMqY+vqyjjUfc3xyKtP4dYOxM44sZrnqQSzSds3xyOrUTLTC9LVCVgLngw==", - "dev": true - }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true - }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", - "dev": true - }, - "javascript-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-2.1.0.tgz", - "integrity": "sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==", - "dev": true - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true - }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", - "dev": true - }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", - "dev": true - }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", - "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", - "dev": true - }, - "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", - "dev": true - }, - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.6" - } - }, - "jsprim": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", - "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", - "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.4.0", - "verror": "1.10.0" - } - }, - "keyv": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz", - "integrity": "sha512-9ykJ/46SN/9KPM/sichzQ7OvXyGDYKGTaDlKMGCAlg2UK8KRy4jb0d8sFc+0Tt0YYnThq8X2RZgCg74RPxgcVA==", - "dev": true, - "requires": { - "json-buffer": "3.0.0" - } - }, - "killable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", - "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "last-call-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/last-call-webpack-plugin/-/last-call-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-7KI2l2GIZa9p2spzPIVZBYyNKkN+e/SQPpnjlTiPhdbDW3F86tdKKELxKpzJ5sgU19wQWsACULZmpTPYHeWO5w==", - "dev": true, - "requires": { - "lodash": "^4.17.5", - "webpack-sources": "^1.1.0" - } - }, - "latest-version": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-5.1.0.tgz", - "integrity": "sha512-weT+r0kTkRQdCdYCNtkMwWXQTMEswKrFBkm4ckQOMVhhqhIMI1UT2hMj+1iigIhgSZm5gTmrRXBNoGUgaTY1xA==", - "dev": true, - "requires": { - "package-json": "^6.3.0" - } - }, - "linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", - "requires": { - "uc.micro": "^1.0.1" - } - }, - "load-script": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/load-script/-/load-script-1.0.0.tgz", - "integrity": "sha512-kPEjMFtZvwL9TaZo0uZ2ml+Ye9HUMmPwbYRJ324qF9tqMejwykJ5ggTyvzmrbBeapCAbk98BSbTeovHEEP1uCA==", - "dev": true - }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true - }, - "loader-utils": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.2.tgz", - "integrity": "sha512-I5d00Pd/jwMD2QCduo657+YM/6L3KZu++pmX9VFncxaxvHcru9jx1lBaFft+r4Mt2jK0Yhp41XlRAihzPxHNCg==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "dependencies": { - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", - "dev": true, - "requires": { - "minimist": "^1.2.0" - } - } - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true - }, - "lodash._reinterpolate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz", - "integrity": "sha512-xYHt68QRoYGjeeM/XOE1uJtvXQAgvszfBhjV4yvsQH0u2i9I6cI6c6/eG4Hh3UAOVn0y/xAXwmTzEay49Q//HA==", - "dev": true - }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", - "dev": true - }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true - }, - "lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", - "dev": true - }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "dev": true - }, - "lodash.template": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz", - "integrity": "sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "lodash.templatesettings": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/lodash.templatesettings/-/lodash.templatesettings-4.2.0.tgz", - "integrity": "sha512-stgLz+i3Aa9mZgnjr/O+v9ruKZsPsndy7qPZOchbqk2cnTU1ZaldKK+v7m54WoKIyxiuMZTKT2H81F8BeAc3ZQ==", - "dev": true, - "requires": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "dev": true - }, - "loglevel": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.8.1.tgz", - "integrity": "sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==", - "dev": true - }, - "lower-case": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-1.1.4.tgz", - "integrity": "sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==", - "dev": true - }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", - "dev": true - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha512-8y/eV9QQZCiyn1SprXSrCmqJN0yNRATe+PO8ztwqrvrbdRLA3eYJF0yaR0YayLWkMbsQSKWS9N2gPcGEc4UsZg==", - "dev": true - }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha512-4y7uGv8bd2WdM9vpQsiQNo41Ln1NvhvDRuVt0k2JZQ+ezN2uaQes7lZeZ+QQUHOLQAtDaBJ+7wCbi+ab/KFs+w==", - "dev": true, - "requires": { - "object-visit": "^1.0.0" - } - }, - "markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", - "requires": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - } - }, - "markdown-it-anchor": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz", - "integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==", - "dev": true, - "requires": {} - }, - "markdown-it-chain": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/markdown-it-chain/-/markdown-it-chain-1.3.0.tgz", - "integrity": "sha512-XClV8I1TKy8L2qsT9iX3qiV+50ZtcInGXI80CA+DP62sMs7hXlyV/RM3hfwy5O3Ad0sJm9xIwQELgANfESo8mQ==", - "dev": true, - "requires": { - "webpack-chain": "^4.9.0" - }, - "dependencies": { - "javascript-stringify": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/javascript-stringify/-/javascript-stringify-1.6.0.tgz", - "integrity": "sha512-fnjC0up+0SjEJtgmmG+teeel68kutkvzfctO/KxE3qJlbunkJYAshgH3boU++gSBHP8z5/r0ts0qRIrHf0RTQQ==", - "dev": true - }, - "webpack-chain": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/webpack-chain/-/webpack-chain-4.12.1.tgz", - "integrity": "sha512-BCfKo2YkDe2ByqkEWe1Rw+zko4LsyS75LVr29C6xIrxAg9JHJ4pl8kaIZ396SUSNp6b4815dRZPSTAS8LlURRQ==", - "dev": true, - "requires": { - "deepmerge": "^1.5.2", - "javascript-stringify": "^1.6.0" - } - } - } - }, - "markdown-it-container": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-container/-/markdown-it-container-2.0.0.tgz", - "integrity": "sha512-IxPOaq2LzrGuFGyYq80zaorXReh2ZHGFOB1/Hen429EJL1XkPI3FJTpx9TsJeua+j2qTru4h3W1TiCRdeivMmA==", - "dev": true - }, - "markdown-it-emoji": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", - "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==", - "dev": true - }, - "markdown-it-html5-embed": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-html5-embed/-/markdown-it-html5-embed-1.0.0.tgz", - "integrity": "sha512-SPgugO/1+/9sZcgxoxijoTHSUpCUgFCNe1MSuTmDxDkV6NQrVzMclhRMFgE/rcHO+2rhIg3U7Oy80XA/E8ytpg==", - "requires": { - "markdown-it": "^8.4.0", - "mimoza": "~1.0.0" - } - }, - "markdown-it-table-of-contents": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/markdown-it-table-of-contents/-/markdown-it-table-of-contents-0.4.4.tgz", - "integrity": "sha512-TAIHTHPwa9+ltKvKPWulm/beozQU41Ab+FIefRaQV1NRnpzwcV9QOe6wXQS5WLivm5Q/nlo0rl6laGkMDZE7Gw==", - "dev": true - }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true - }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" - }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true - }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha512-cda4JKCxReDXFXRqOHPQscuIYg1PvxbE2S2GP45rnwfEK+vZaXC8C1OFvdHIbgw0DLzowXGVoxLaAmlgRy14GQ==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", - "dev": true - }, - "merge-source-map": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.1.0.tgz", - "integrity": "sha512-Qkcp7P2ygktpMPh2mCQZaf3jhN6D3Z/qVZHSdWvQ+2Ef5HgRAPBO57A77+ENm0CPx2+1Ce/MYKi3ymqdfuqibw==", - "dev": true, - "requires": { - "source-map": "^0.6.1" - } - }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true - }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true - }, - "micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", - "dev": true, - "requires": { - "braces": "^3.0.2", - "picomatch": "^2.3.1" - } - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", - "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "mime": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", - "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", - "dev": true - }, - "mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" - }, - "mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, - "requires": { - "mime-db": "1.52.0" - } - }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true - }, - "mimoza": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/mimoza/-/mimoza-1.0.0.tgz", - "integrity": "sha512-+j7SSye/hablu66K/jjeyPmk6WL8RoXfeZ+MMn37vSNDGuaWY/5wm10LpSpxAHX4kNoEwkTWYHba8ePVip+Hqg==", - "requires": { - "mime-db": "^1.6.0" - } - }, - "min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", - "dev": true, - "requires": { - "dom-walk": "^0.1.0" - } - }, - "mini-css-extract-plugin": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-0.6.0.tgz", - "integrity": "sha512-79q5P7YGI6rdnVyIAV4NXpBQJFWdkzJxCim3Kog4078fM0piAaFlwocqbejdWtLW1cEzCexPrh6EdyFsPgVdAw==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "normalize-url": "^2.0.1", - "schema-utils": "^1.0.0", - "webpack-sources": "^1.1.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true - }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==", - "dev": true - }, - "minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - }, - "minimist": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", - "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", - "dev": true - }, - "mississippi": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", - "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - } - }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", - "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", - "dev": true, - "requires": { - "minimist": "^1.2.6" - } - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha512-hdrFxZOycD/g6A6SoI2bB5NA/5NEqD0569+S47WZhPvm46sD50ZHdYaFmnua5lndde9rCHGjmfK7Z8BuCt/PcQ==", - "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", - "dev": true, - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" - } - }, - "multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", - "dev": true - }, - "nan": { - "version": "2.17.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.17.0.tgz", - "integrity": "sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==", - "dev": true, - "optional": true - }, - "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" - }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true - }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true - }, - "no-case": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-2.3.2.tgz", - "integrity": "sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==", - "dev": true, - "requires": { - "lower-case": "^1.1.1" - } - }, - "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "dev": true - }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", - "dev": true - } - } - }, - "node-releases": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", - "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", - "dev": true - }, - "nopt": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", - "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", - "dev": true, - "requires": { - "abbrev": "1" - } - }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "dev": true - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true - }, - "normalize-url": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", - "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", - "dev": true, - "requires": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" - } - }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", - "dev": true, - "requires": { - "path-key": "^2.0.0" - } - }, - "nprogress": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "dev": true - }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", - "dev": true, - "requires": { - "boolbase": "~1.0.0" - } - }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha512-Y1wZESM7VUThYY+4W+X4ySH2maqcA+p7UR+w8VWNWVAd6lwuXXWz/w/Cz43J/dI2I+PS6wD5N+bJUF+gjWvIqg==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true - }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha512-79LYn6VAb63zgtmAteVOWo9Vdj71ZVBy3Pbse+VqxDpEP83XuujMrGqHIwAXJ5I/aM0zU7dIyIAhifVTPrNItQ==", - "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "object-inspect": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", - "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", - "dev": true - }, - "object-is": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", - "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true - }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha512-GBaMwwAVK9qbQN3Scdo0OyvgPW7l3lnaVMj84uTOZlswkX0KpF6fyDBJhtTthf7pymztoN36/KEr1DyhF96zEA==", - "dev": true, - "requires": { - "isobject": "^3.0.0" - } - }, - "object.assign": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", - "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - } - }, - "object.getownpropertydescriptors": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.5.tgz", - "integrity": "sha512-yDNzckpM6ntyQiGTik1fKV1DcVDRS+w8bvpWNCBanvH5LfRX9O8WTHqQzG4RZwRAM4I0oU7TV11Lj5v0g20ibw==", - "dev": true, - "requires": { - "array.prototype.reduce": "^1.0.5", - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha512-tqa/UMy/CCoYmj+H5qc07qvSL9dqcs/WZENZ1JbtWBlATP+iVOe778gE6MSijnyCnORzDuX6hU+LA4SZ09YjFQ==", - "dev": true, - "requires": { - "isobject": "^3.0.1" - } - }, - "object.values": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.6.tgz", - "integrity": "sha512-FVVTkD1vENCsAcwNs9k6jea2uHC/X0+JcjG8YA60FN5CMaJmG95wT9jek/xX9nornqGRrBkKtzuAu2wuHpKqvw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, - "requires": { - "ee-first": "1.1.1" - } - }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true - }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "requires": { - "wrappy": "1" - } - }, - "opencollective-postinstall": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz", - "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==", - "dev": true - }, - "opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", - "dev": true, - "requires": { - "is-wsl": "^1.1.0" - } - }, - "optimize-css-assets-webpack-plugin": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/optimize-css-assets-webpack-plugin/-/optimize-css-assets-webpack-plugin-5.0.8.tgz", - "integrity": "sha512-mgFS1JdOtEGzD8l+EuISqL57cKO+We9GcoiQEmdCWRqqck+FGNmYJtx9qfAPzEz+lRrlThWMuGDaRkI/yWNx/Q==", - "dev": true, - "requires": { - "cssnano": "^4.1.10", - "last-call-webpack-plugin": "^3.0.0" - } - }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==", - "dev": true - }, - "p-cancelable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-1.1.0.tgz", - "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", - "dev": true - }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "dev": true - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true - }, - "p-retry": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", - "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", - "dev": true, - "requires": { - "retry": "^0.12.0" - } - }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", - "dev": true - }, - "package-json": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-6.5.0.tgz", - "integrity": "sha512-k3bdm2n25tkyxcjSKzB5x8kfVxlMdgsbPr0GkZcwHsLpba6cBjqCt1KlcChKEvxHIcTB1FVMuwoijZ26xex5MQ==", - "dev": true, - "requires": { - "got": "^9.6.0", - "registry-auth-token": "^4.0.0", - "registry-url": "^5.0.0", - "semver": "^6.2.0" - } - }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true - }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", - "dev": true, - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "param-case": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-2.1.1.tgz", - "integrity": "sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==", - "dev": true, - "requires": { - "no-case": "^2.2.0" - } - }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", - "dev": true, - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha512-XHXfu/yOQRy9vYOtUDVMN60OEJjW013GoObG1o+xwQTpB9eYJX/BjXMsdW13ZDPruFhYYn0AG22w0xgQMwl3Nw==", - "dev": true - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true - }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha512-ALzNPpyNq9AqXMBjeymIjFDAkAFH06mHJH/cSBHAgU0s4vfpBn6b2nf8tiRLvagKD8RbTpq2FKTBg7cl9l3c7Q==", - "dev": true - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", - "dev": true - }, - "path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", - "dev": true - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "dev": true - } - } - }, - "pbkdf2": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", - "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", - "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "performance-now": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", - "dev": true - }, - "picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" - }, - "picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", - "dev": true - }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", - "dev": true, - "requires": { - "pinkie": "^2.0.0" - } - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "portfinder": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.32.tgz", - "integrity": "sha512-on2ZJVVDXRADWE6jnQaX0ioEylzgBpQk8r55NE4wjXW1ZxO+BgDlY6DXwj20i0V8eB4SenDQ00WEaxfiIQPcxg==", - "dev": true, - "requires": { - "async": "^2.6.4", - "debug": "^3.2.7", - "mkdirp": "^0.5.6" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha512-xTgYBc3fuo7Yt7JbiuFxSYGToMoz8fLoE6TC9Wx1P/u+LfeThMOAqmuyECnlBaaJb+u1m9hHiXUEtwW4OzfUJg==", - "dev": true - }, - "postcss": { - "version": "8.4.19", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", - "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", - "requires": { - "nanoid": "^3.3.4", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - } - }, - "postcss-calc": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", - "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", - "dev": true, - "requires": { - "postcss": "^7.0.27", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.2" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-colormin": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-convert-values": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-discard-comments": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-discard-empty": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-load-config": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-2.1.2.tgz", - "integrity": "sha512-/rDeGV6vMUo3mwJZmeHfEDvwnTKKqQ0S7OHUi/kJvvtx3aWtyWG2/0ZWnzCt2keEclwN6Tf0DST2v9kITdOKYw==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "import-cwd": "^2.0.0" - } - }, - "postcss-loader": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-3.0.0.tgz", - "integrity": "sha512-cLWoDEY5OwHcAjDnkyRQzAXfs2jrKjXpO/HQFcc5b5u/r7aa471wdmChmwfnv7x2u840iat/wi0lQ5nbRgSkUA==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "postcss": "^7.0.0", - "postcss-load-config": "^2.0.0", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", - "dev": true, - "requires": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-merge-rules": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-params": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "postcss-modules-extract-imports": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-2.0.0.tgz", - "integrity": "sha512-LaYLDNS4SG8Q5WAWqIJgdHPJrDDr/Lv775rMBFUbgjTz6j34lUznACHcdRWroPvXANP2Vj7yNK57vp9eFqzLWQ==", - "dev": true, - "requires": { - "postcss": "^7.0.5" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-modules-local-by-default": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-2.0.6.tgz", - "integrity": "sha512-oLUV5YNkeIBa0yQl7EYnxMgy4N6noxmiwZStaEJUSe2xPMcdNc8WmBQuQCx18H5psYbVxz8zoHk0RAAYZXP9gA==", - "dev": true, - "requires": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0", - "postcss-value-parser": "^3.3.1" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-modules-scope": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-2.2.0.tgz", - "integrity": "sha512-YyEgsTMRpNd+HmyC7H/mh3y+MeFWevy7V1evVhJWewmMbjDHIbZbOXICC2y+m1xI1UVfIT1HMW/O04Hxyu9oXQ==", - "dev": true, - "requires": { - "postcss": "^7.0.6", - "postcss-selector-parser": "^6.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-modules-values": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-2.0.0.tgz", - "integrity": "sha512-Ki7JZa7ff1N3EIMlPnGTZfUMe69FFwiQPnVSXC9mnn3jozCRBYIxiZd44yJOV2AmabOo4qFf8s0dC/+lweG7+w==", - "dev": true, - "requires": { - "icss-replace-symbols": "^1.1.0", - "postcss": "^7.0.6" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", - "dev": true, - "requires": { - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-string": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", - "dev": true, - "requires": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", - "dev": true, - "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "dev": true - }, - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-ordered-values": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", - "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", - "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-safe-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", - "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", - "dev": true, - "requires": { - "postcss": "^7.0.26" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-selector-parser": { - "version": "6.0.11", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", - "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-svgo": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz", - "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==", - "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - } - } - }, - "postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - } - } - }, - "postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", - "dev": true - }, - "prettier": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz", - "integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==", - "dev": true, - "optional": true - }, - "pretty-error": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-2.1.2.tgz", - "integrity": "sha512-EY5oDzmsX5wvuynAByrmY0P0hcp+QpnAKbJng2A2MPjVKXCxrDSUkzghVJ4ZGPIv+JC4gX8fPUWscC0RtjsWGw==", - "dev": true, - "requires": { - "lodash": "^4.17.20", - "renderkid": "^2.0.4" - } - }, - "pretty-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", - "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", - "dev": true - }, - "prismjs": { - "version": "1.29.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", - "integrity": "sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==" - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", - "dev": true - }, - "proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, - "requires": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - } - }, - "prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true - }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==", - "dev": true - }, - "psl": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", - "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==", - "dev": true - }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } - } - }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", - "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } - } - }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true - }, - "pupa": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-2.1.1.tgz", - "integrity": "sha512-l1jNAspIBSFqbT+y+5FosojNpVpF94nlI+wDUpqP9enwOTfHx9f0gh5nB96vl+6yTpsJsypeNrwfzPrKuHB41A==", - "dev": true, - "requires": { - "escape-goat": "^2.0.0" - } - }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", - "dev": true - }, - "qs": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", - "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", - "dev": true - }, - "query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", - "dev": true, - "requires": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "dev": true - }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", - "dev": true - }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true - }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "requires": { - "safe-buffer": "^5.1.0" - } - }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", - "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true - }, - "raw-body": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", - "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", - "dev": true, - "requires": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true - } - } - }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - } - }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - }, - "dependencies": { - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - } - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - } - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "reduce": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/reduce/-/reduce-1.0.2.tgz", - "integrity": "sha512-xX7Fxke/oHO5IfZSk77lvPa/7bjMh9BuCk4OOoX5XTXrM7s0Z+MkPfSDfz0q7r91BhhGSs8gii/VEN/7zhCPpQ==", - "dev": true, - "requires": { - "object-keys": "^1.1.0" - } - }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true - }, - "regenerate-unicode-properties": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", - "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", - "dev": true, - "requires": { - "regenerate": "^1.4.2" - } - }, - "regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true - }, - "regenerator-transform": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", - "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" - } - }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "regexp.prototype.flags": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", - "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - } - }, - "regexpu-core": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.2.2.tgz", - "integrity": "sha512-T0+1Zp2wjF/juXMrMxHxidqGYn8U4R+zleSJhX9tQ1PUsS8a9UtYfbsF9LdiVgNX3kiX8RNaKM42nfSgvFJjmw==", - "dev": true, - "requires": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsgen": "^0.7.1", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - } - }, - "registry-auth-token": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-4.2.2.tgz", - "integrity": "sha512-PC5ZysNb42zpFME6D/XlIgtNGdTl8bBOCw90xQLVMpzuuubJKYDWFAEuUNc+Cn8Z8724tg2SDhDRrkVEsqfDMg==", - "dev": true, - "requires": { - "rc": "1.2.8" - } - }, - "registry-url": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-5.1.0.tgz", - "integrity": "sha512-8acYXXTI0AkQv6RAOjE3vOaIXZkT9wo4LOFbBKYQEEnnMNBpKqdUrI6S4NT0KPIo/WVvJ5tE/X5LF/TQUf0ekw==", - "dev": true, - "requires": { - "rc": "^1.2.8" - } - }, - "regjsgen": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.7.1.tgz", - "integrity": "sha512-RAt+8H2ZEzHeYWxZ3H2z6tF18zyyOnlcdaafLrm21Bguj7uZy6ULibiAFdXEtKQY4Sy7wDTwDiOazasMLc4KPA==", - "dev": true - }, - "regjsparser": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", - "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", - "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, - "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", - "dev": true - } - } - }, - "relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "dev": true - }, - "remove-markdown": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/remove-markdown/-/remove-markdown-0.3.0.tgz", - "integrity": "sha512-5392eIuy1mhjM74739VunOlsOYKjsH82rQcTBlJ1bkICVC3dQ3ksQzTHh4jGHQFnM+1xzLzcFOMH+BofqXhroQ==" - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha512-/hS+Y0u3aOfIETiaiirUFwDBDzmXPvO+jAfKTitUngIPzdKc6Z0LoFjM/CK5PL4C+eKwHohlHAb6H0VFfmmUsw==", - "dev": true - }, - "renderkid": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-2.0.7.tgz", - "integrity": "sha512-oCcFyxaMrKsKcTY59qnCAtmDVSLfPbrv6A3tVbPdFMMrv5jaK10V6m40cKsoPNhAqN6rmHW9sswW4o3ruSrwUQ==", - "dev": true, - "requires": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^3.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true - }, - "css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - } - }, - "css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", - "dev": true - }, - "dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "dev": true - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "dev": true - }, - "nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - } - } - }, - "repeat-element": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.4.tgz", - "integrity": "sha512-LFiNfRcSu7KK3evMyYOuCzv3L10TW7yC1G2/+StMjK8Y6Vqd2MG7r/Qjw4ghtuCOjFvlnms/iMmLqpvW/ES/WQ==", - "dev": true - }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "dev": true - }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", - "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - } - }, - "require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", - "dev": true - }, - "requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "dev": true - }, - "resolve": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", - "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", - "dev": true, - "requires": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", - "dev": true - }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha512-ZuF55hVUQaaczgOIwqWzkEcEidmlD/xl44x1UZnhOXcYuFN2S6+rcxpG+C1N3So0wvNI3DmJICUFfu2SxhBmvg==", - "dev": true - }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha512-/Fpe5guzJk1gPqdJLJR5u7eG/gNY4nImjbRDaVWVMRhne55TCmj2i9Q+54PBRfatRC8v/rIiv9BN0pMd9OV5EQ==", - "dev": true, - "requires": { - "lowercase-keys": "^1.0.0" - } - }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "dev": true - }, - "rgb-regex": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha512-gDK5mkALDFER2YLqH6imYvK6g02gpNGM4ILDZ472EwWfXZnC2ZEpoB2ECXTyOVUKuk/bPJZMzwQPBYICzP+D3w==", - "dev": true - }, - "rgba-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha512-zgn5OjNQXLUTdq8m17KdaicF6w89TZs8ZU8y0AYENIU6wG8GG6LLm0yLSiPY8DmaYmHdgRW8rnApjoT0fQRfMg==", - "dev": true - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", - "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha512-ntymy489o0/QQplUDnpYAYUsO50K9SBrIVaKCWDOJzYJts0f9WH9RFJkyagebkw5+y1oi00R7ynNW/d12GBumg==", - "dev": true, - "requires": { - "aproba": "^1.1.1" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha512-aJXcif4xnaNUzvUuC5gcb46oTS7zvg4jpMTnuqtrEPlR3vFr4pxtdTwaF1Qs3Enjn9HK+ZlwQui+a7z0SywIzg==", - "dev": true, - "requires": { - "ret": "~0.1.10" - } - }, - "safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - } - }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true - }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true - }, - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - }, - "section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - } - }, - "select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true - }, - "selfsigned": { - "version": "1.10.14", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.14.tgz", - "integrity": "sha512-lkjaiAye+wBZDCBsu5BGi0XiLRxeUlsGod5ZP924CRSEoGuZAw/f7y9RKu28rwTfiHVhdavhB0qH0INV6P1lEA==", - "dev": true, - "requires": { - "node-forge": "^0.10.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "semver-diff": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-3.1.1.tgz", - "integrity": "sha512-GX0Ix/CJcHyB8c4ykpHGIAvLyOwOobtM/8d+TQkAd81/bEjgPHrfba41Vpesr7jX/t8Uh+R3EX9eAS5be+jQYg==", - "dev": true, - "requires": { - "semver": "^6.3.0" - } - }, - "send": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", - "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", - "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - } - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true - } - } - }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "dev": true, - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true - } - } - }, - "serve-static": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", - "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", - "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.18.0" - } - }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", - "dev": true - }, - "set-value": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - } - }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==", - "dev": true - }, - "setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", - "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" - } - }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", - "dev": true - }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "dev": true, - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - } - }, - "signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", - "dev": true, - "requires": { - "is-arrayish": "^0.3.1" - }, - "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true - } - } - }, - "slash": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-2.0.0.tgz", - "integrity": "sha512-ZYKh3Wh2z1PpEXWr0MpSBZ0V6mZHAQfYevttO11c51CaWjGTaadiKZ+wVt1PbMlDV5qhMFslpZCemhwOK7C89A==", - "dev": true - }, - "smoothscroll-polyfill": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/smoothscroll-polyfill/-/smoothscroll-polyfill-0.4.4.tgz", - "integrity": "sha512-TK5ZA9U5RqCwMpfoMq/l1mrH0JAR7y7KRvOBx0n2869aLxch+gT9GhN3yUfjiw+d/DiF1mKo14+hd62JyMmoBg==", - "dev": true - }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", - "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", - "dev": true - } - } - }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", - "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - } - } - }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", - "dev": true, - "requires": { - "kind-of": "^3.2.0" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "requires": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - }, - "dependencies": { - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "dev": true - } - } - }, - "sockjs-client": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.6.1.tgz", - "integrity": "sha512-2g0tjOR+fRs0amxENLi/q5TiJTqY+WXFOzb5UwXndlK6TO3U/mirZznpx6w34HVMoc3g7cY24yC/ZMIYnDlfkw==", - "dev": true, - "requires": { - "debug": "^3.2.7", - "eventsource": "^2.0.2", - "faye-websocket": "^0.11.4", - "inherits": "^2.0.4", - "url-parse": "^1.5.10" - }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } - } - }, - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", - "dev": true, - "requires": { - "is-plain-obj": "^1.0.0" - }, - "dependencies": { - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", - "dev": true - } - } - }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" - }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", - "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "source-map-url": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.1.tgz", - "integrity": "sha512-cPiFOTLUKvJFIg4SKVScy4ilPPW6rFgMgfuZJPNoDuMs3nC1HbMUycBoJw77xFIp6z1UJQJOfx6C9GMH80DiTw==", - "dev": true - }, - "spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - } - }, - "spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", - "dev": true, - "requires": { - "extend-shallow": "^3.0.0" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" - }, - "sshpk": { - "version": "1.17.0", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", - "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", - "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" - } - }, - "ssri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", - "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "stack-utils": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-1.0.5.tgz", - "integrity": "sha512-KZiTzuV3CnSnSvgMRrARVCj+Ht7rMbauGDK0LdVFRGyenwdylpajAp4Q0i6SX8rEmbTpMMf6ryq2gb8pPq2WgQ==", - "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } - } - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha512-72E9+uLc27Mt718pMHt9VMNiAL4LMsmDbBva8mxWUCkT07fSzEGMYUCk0XWY6lp0j6RBAG4cJ3mWuZv2OE3s0g==", - "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true - }, - "std-env": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-2.3.1.tgz", - "integrity": "sha512-eOsoKTWnr6C8aWrqJJ2KAReXoa7Vn5Ywyw6uCXgA/xDhxPoaIsBa5aNJmISY04dLwXPBnDHW4diGM7Sn5K4R/g==", - "dev": true, - "requires": { - "ci-info": "^3.1.1" - }, - "dependencies": { - "ci-info": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.6.2.tgz", - "integrity": "sha512-lVZdhvbEudris15CLytp2u6Y0p5EKfztae9Fqa189MfNmln9F33XuH69v5fvNfiRN5/0eAUz2yJL3mo+nhaRKg==", - "dev": true - } - } - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", - "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", - "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha512-R3f198pcvnB+5IpnBlRkphuE9n46WyVl8I39W/ZUTZLz4nqSP/oLYUrcnJrw462Ds8he4YKMov2efsTIw1BDGQ==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - } - }, - "string.prototype.trimend": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.6.tgz", - "integrity": "sha512-JySq+4mrPf9EsDBEDYMOb/lM7XQLulwg5R/m1r0PXEFqrV0qHvl58sdTilSXtKOflCsK2E8jxf+GKC0T07RWwQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "string.prototype.trimstart": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.6.tgz", - "integrity": "sha512-omqjMDaY92pbn5HOX7f9IccLA+U1tA9GvtU4JrodiXFfYB7jPzzHpRzpglLAjtUV6bB557zwClJezTqnAiYnQA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - } - }, - "strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.1" - } - }, - "strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "dev": true - }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha512-7FCwGGmx8mD5xQd3RPUvnSpUXHM3BWuzjtpD4TXsfcZ9EL4azvVVUscFYwD9nx8Kh+uCBC00XBtAykoMHwTh8Q==", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true - }, - "stylehacks": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", - "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", - "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "picocolors": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-0.2.1.tgz", - "integrity": "sha512-cMlDqaLEqfSaW8Z7N5Jw+lyIW869EzT73/F5lhtY9cLGoVxSXznfgfXMO0Z5K0o0Q2TkTXq+0KFsdnSe3jDViA==", - "dev": true - }, - "postcss": { - "version": "7.0.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.39.tgz", - "integrity": "sha512-yioayjNbHn6z1/Bywyb2Y4s3yvDAeXGOyxqD+LnVOinq6Mdmd++SW2wUNVzavyyHxd6+DxzWGIuosg6P1Rj8uA==", - "dev": true, - "requires": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - } - }, - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } - } - }, - "stylus": { - "version": "0.54.8", - "resolved": "https://registry.npmjs.org/stylus/-/stylus-0.54.8.tgz", - "integrity": "sha512-vr54Or4BZ7pJafo2mpf0ZcwA74rpuYCZbxrHBsH8kbcXOwSfvBFwsRfpGO5OD5fhG5HDCFW737PKaawI7OqEAg==", - "dev": true, - "requires": { - "css-parse": "~2.0.0", - "debug": "~3.1.0", - "glob": "^7.1.6", - "mkdirp": "~1.0.4", - "safer-buffer": "^2.1.2", - "sax": "~1.2.4", - "semver": "^6.3.0", - "source-map": "^0.7.3" - }, - "dependencies": { - "debug": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true - }, - "source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true - } - } - }, - "stylus-loader": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/stylus-loader/-/stylus-loader-3.0.2.tgz", - "integrity": "sha512-+VomPdZ6a0razP+zinir61yZgpw2NfljeSsdUF5kJuEzlo3khXhY19Fn6l8QQz1GRJGtMCo8nG5C04ePyV7SUA==", - "dev": true, - "requires": { - "loader-utils": "^1.0.2", - "lodash.clonedeep": "^4.5.0", - "when": "~3.6.x" - } - }, - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "dev": true - }, - "svg-tags": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", - "dev": true - }, - "svgo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", - "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - } - }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true - }, - "term-size": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", - "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", - "dev": true - }, - "terser": { - "version": "4.8.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.1.tgz", - "integrity": "sha512-4GnLC0x667eJG0ewJTa6z/yXrbLGv80D9Ru6HIpCQmO+Q4PfEtBFi0ObSckqwL6VyQv/7ENJieXHo2ANmdQwgw==", - "dev": true, - "requires": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "dependencies": { - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - } - } - }, - "terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - }, - "dependencies": { - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } - } - }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, - "through": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true - }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true - }, - "timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" - } - }, - "timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", - "dev": true - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==", - "dev": true - }, - "to-factory": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-factory/-/to-factory-1.0.0.tgz", - "integrity": "sha512-JVYrY42wMG7ddf+wBUQR/uHGbjUHZbLisJ8N62AMm0iTZ0p8YTcZLzdtomU0+H+wa99VbkyvQGB3zxB7NDzgIQ==", - "dev": true - }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", - "dev": true - }, - "to-object-path": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", - "integrity": "sha512-9mWHdnGRuh3onocaHzukyvCZhzvr6tiflAy/JRFXcJX0TjgfWA9pk9t8CMbzmBE4Jfw58pXbkngtBtqYxzNEyg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "to-readable-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/to-readable-stream/-/to-readable-stream-1.0.0.tgz", - "integrity": "sha512-Iq25XBt6zD5npPhlLVXGFN3/gyR2/qODcKNNyTMd4vbm39HUaOiAM4PMq0eMVC/Tkxz+Zjdsc55g9yyz+Yq00Q==", - "dev": true - }, - "to-regex": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", - "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", - "dev": true, - "requires": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "dependencies": { - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true - }, - "toml": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/toml/-/toml-3.0.0.tgz", - "integrity": "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==", - "dev": true - }, - "toposort": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/toposort/-/toposort-1.0.7.tgz", - "integrity": "sha512-FclLrw8b9bMWf4QlCJuHBEVhSRsqDj6u3nIjAzPeJvgl//1hBlffdlk0MALceL14+koWEdU4ofRAXofbODxQzg==", - "dev": true - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==", - "dev": true - }, - "tunnel-agent": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", - "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", - "dev": true, - "requires": { - "safe-buffer": "^5.0.1" - } - }, - "tweetnacl": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", - "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", - "dev": true - }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - }, - "type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "requires": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - } - }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", - "dev": true - }, - "typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "dev": true, - "requires": { - "is-typedarray": "^1.0.0" - } - }, - "uc.micro": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", - "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" - }, - "uglify-js": { - "version": "3.4.10", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.4.10.tgz", - "integrity": "sha512-Y2VsbPVs0FIshJztycsO2SfPk7/KAF/T72qzv9u5EpQ4kB2hQoHlhNQTsNyy6ul7lQtqJN/AoWeS23OzEiEFxw==", - "dev": true, - "requires": { - "commander": "~2.19.0", - "source-map": "~0.6.1" - }, - "dependencies": { - "commander": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", - "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", - "dev": true - } - } - }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, - "unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", - "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - } - }, - "unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", - "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", - "dev": true - }, - "union-value": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", - "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", - "dev": true, - "requires": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - } - }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==", - "dev": true - }, - "uniqs": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha512-mZdDpf3vBV5Efh29kMw5tXoup/buMgxLzOt/XKFKcVmi+15ManNQWr6HfZ2aiZTYlYixbdNJ0KFmIZIv52tHSQ==", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unique-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", - "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", - "dev": true, - "requires": { - "crypto-random-string": "^2.0.0" - } - }, - "universalify": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", - "dev": true - }, - "unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true - }, - "unquote": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", - "integrity": "sha512-vRCqFv6UhXpWxZPyGDh/F3ZpNv8/qo7w6iufLpQg9aKnQ71qM4B5KiI7Mia9COcjEhrO9LueHpMYjYzsWH3OIg==", - "dev": true - }, - "unset-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", - "integrity": "sha512-PcA2tsuGSF9cnySLHTLSh2qrQiJ70mn+r+Glzxv2TWZblxsxCC52BDlZoPCsz7STd9pN7EZetkWZBAvk4cgZdQ==", - "dev": true, - "requires": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } - }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", - "dev": true - }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "dev": true - } - } - }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true - }, - "update-browserslist-db": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", - "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", - "dev": true, - "requires": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" - } - }, - "update-notifier": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz", - "integrity": "sha512-Yld6Z0RyCYGB6ckIjffGOSOmHXj1gMeE7aROz4MG+XMkmixBX4jUngrGXNYz7wPKBmtoD4MnBa2Anu7RSKht/A==", - "dev": true, - "requires": { - "boxen": "^4.2.0", - "chalk": "^3.0.0", - "configstore": "^5.0.1", - "has-yarn": "^2.1.0", - "import-lazy": "^2.1.0", - "is-ci": "^2.0.0", - "is-installed-globally": "^0.3.1", - "is-npm": "^4.0.0", - "is-yarn-global": "^0.3.0", - "latest-version": "^5.0.0", - "pupa": "^2.0.1", - "semver-diff": "^3.1.1", - "xdg-basedir": "^4.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "upper-case": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-1.1.3.tgz", - "integrity": "sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==", - "dev": true - }, - "uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "requires": { - "punycode": "^2.1.0" - } - }, - "urix": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", - "integrity": "sha512-Am1ousAhSLBeB9cG/7k7r2R0zj50uDRlZHPGbazid5s9rlF1F/QKYObEKSIunSjIOkJZqwRRLpvewjEkM7pSqg==", - "dev": true - }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha512-kbailJa29QrtXnxgq+DdCEGlbTeYM2eJUxsz6vjZavrCYPMIFHMKQmSKYAIuUK2i7hgPm28a8piX5NTUtM/LKQ==", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "dev": true - } - } - }, - "url-loader": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-1.1.2.tgz", - "integrity": "sha512-dXHkKmw8FhPqu8asTc1puBfe3TehOCo2+RmOOev5suNCIYBcT626kxiWg1NBVkwc4rO8BGa7gP70W7VXuqHrjg==", - "dev": true, - "requires": { - "loader-utils": "^1.1.0", - "mime": "^2.0.3", - "schema-utils": "^1.0.0" - }, - "dependencies": { - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - } - } - }, - "url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", - "dev": true, - "requires": { - "prepend-http": "^2.0.0" - } - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", - "dev": true, - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "dev": true - } - } - }, - "util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true - }, - "util.promisify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", - "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", - "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.2", - "has-symbols": "^1.0.1", - "object.getownpropertydescriptors": "^2.1.0" - } - }, - "utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "dev": true - }, - "utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - }, - "vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true - }, - "vendors": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", - "dev": true - }, - "verror": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", - "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", - "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "core-util-is": "1.0.2", - "extsprintf": "^1.2.0" - }, - "dependencies": { - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", - "dev": true - } - } - }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true - }, - "vue": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue/-/vue-2.7.14.tgz", - "integrity": "sha512-b2qkFyOM0kwqWFuQmgd4o+uHGU7T+2z3T+WQp8UBjADfEv2n4FEMffzBmCKNP0IGzOEEfYjvtcC62xaSKeQDrQ==", - "requires": { - "@vue/compiler-sfc": "2.7.14", - "csstype": "^3.1.0" - } - }, - "vue-hot-reload-api": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz", - "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog==", - "dev": true - }, - "vue-loader": { - "version": "15.10.1", - "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.10.1.tgz", - "integrity": "sha512-SaPHK1A01VrNthlix6h1hq4uJu7S/z0kdLUb6klubo738NeQoLbS6V9/d8Pv19tU0XdQKju3D1HSKuI8wJ5wMA==", - "dev": true, - "requires": { - "@vue/component-compiler-utils": "^3.1.0", - "hash-sum": "^1.0.2", - "loader-utils": "^1.1.0", - "vue-hot-reload-api": "^2.3.0", - "vue-style-loader": "^4.1.0" - } - }, - "vue-router": { - "version": "3.6.5", - "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.6.5.tgz", - "integrity": "sha512-VYXZQLtjuvKxxcshuRAwjHnciqZVoXAjTjcqBTz4rKc8qih9g9pI3hbDjmqXaHdgL3v8pV6P8Z335XvHzESxLQ==", - "dev": true - }, - "vue-server-renderer": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-server-renderer/-/vue-server-renderer-2.7.14.tgz", - "integrity": "sha512-NlGFn24tnUrj7Sqb8njhIhWREuCJcM3140aMunLNcx951BHG8j3XOrPP7psSCaFA8z6L4IWEjudztdwTp1CBVw==", - "dev": true, - "requires": { - "chalk": "^4.1.2", - "hash-sum": "^2.0.0", - "he": "^1.2.0", - "lodash.template": "^4.5.0", - "lodash.uniq": "^4.5.0", - "resolve": "^1.22.0", - "serialize-javascript": "^6.0.0", - "source-map": "0.5.6" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "hash-sum": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/hash-sum/-/hash-sum-2.0.0.tgz", - "integrity": "sha512-WdZTbAByD+pHfl/g9QSsBIIwy8IT+EsPiKDs0KNX+zSHhdDLFKdZu0BQHljvO+0QI/BasbMSUa8wYNCZTvhslg==", - "dev": true - }, - "serialize-javascript": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", - "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", - "dev": true, - "requires": { - "randombytes": "^2.1.0" - } - }, - "source-map": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.6.tgz", - "integrity": "sha512-MjZkVp0NHr5+TPihLcadqnlVoGIoWo4IBHptutGh9wI3ttUYvCG26HkSuDi+K6lsZ25syXJXcctwgyVCt//xqA==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } - } - }, - "vue-style-loader": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/vue-style-loader/-/vue-style-loader-4.1.3.tgz", - "integrity": "sha512-sFuh0xfbtpRlKfm39ss/ikqs9AbKCoXZBpHeVZ8Tx650o0k0q/YCM7FRvigtxpACezfq6af+a7JeqVTWvncqDg==", - "dev": true, - "requires": { - "hash-sum": "^1.0.2", - "loader-utils": "^1.0.2" - } - }, - "vue-template-compiler": { - "version": "2.7.14", - "resolved": "https://registry.npmjs.org/vue-template-compiler/-/vue-template-compiler-2.7.14.tgz", - "integrity": "sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==", - "dev": true, - "requires": { - "de-indent": "^1.0.2", - "he": "^1.2.0" - } - }, - "vue-template-es2015-compiler": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/vue-template-es2015-compiler/-/vue-template-es2015-compiler-1.9.1.tgz", - "integrity": "sha512-4gDntzrifFnCEvyoO8PqyJDmguXgVPxKiIxrBKjIowvL9l+N66196+72XVYR8BBf1Uv1Fgt3bGevJ+sEmxfZzw==", - "dev": true - }, - "vue-tweet-embed": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/vue-tweet-embed/-/vue-tweet-embed-2.4.0.tgz", - "integrity": "sha512-bjViatv0priR1dTEPJpRyWigWGUTUC28VT/sWTaZE+RBWuj/XZvOU5Hzk+O8Mue2dBCAHJrRpoO1VKlcgmHohg==", - "requires": {} - }, - "vuepress": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/vuepress/-/vuepress-1.9.7.tgz", - "integrity": "sha512-aSXpoJBGhgjaWUsT1Zs/ZO8JdDWWsxZRlVme/E7QYpn+ZB9iunSgPMozJQNFaHzcRq4kPx5A4k9UhzLRcvtdMg==", - "dev": true, - "requires": { - "@vuepress/core": "1.9.7", - "@vuepress/theme-default": "1.9.7", - "@vuepress/types": "1.9.7", - "cac": "^6.5.6", - "envinfo": "^7.2.0", - "opencollective-postinstall": "^2.0.2", - "update-notifier": "^4.0.0" - } - }, - "vuepress-html-webpack-plugin": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/vuepress-html-webpack-plugin/-/vuepress-html-webpack-plugin-3.2.0.tgz", - "integrity": "sha512-BebAEl1BmWlro3+VyDhIOCY6Gef2MCBllEVAP3NUAtMguiyOwo/dClbwJ167WYmcxHJKLl7b0Chr9H7fpn1d0A==", - "dev": true, - "requires": { - "html-minifier": "^3.2.3", - "loader-utils": "^0.2.16", - "lodash": "^4.17.3", - "pretty-error": "^2.0.2", - "tapable": "^1.0.0", - "toposort": "^1.0.0", - "util.promisify": "1.0.0" - }, - "dependencies": { - "big.js": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-3.2.0.tgz", - "integrity": "sha512-+hN/Zh2D08Mx65pZ/4g5bsmNiZUuChDiQfTUQ7qJr4/kuopCr88xZsAXv6mBoZEsUI4OuGHlX59qE94K2mMW8Q==", - "dev": true - }, - "emojis-list": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-2.1.0.tgz", - "integrity": "sha512-knHEZMgs8BB+MInokmNTg/OyPlAddghe1YBgNwJBc5zsJi/uyIcXoSDsL/W9ymOsBoBGdPIHXYJ9+qKFwRwDng==", - "dev": true - }, - "json5": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-0.5.1.tgz", - "integrity": "sha512-4xrs1aW+6N5DalkqSVA8fxh458CXvR99WU8WLKmq4v8eWAL86Xo3BVqyd3SkA9wEVjCMqyvvRRkshAdOnBp5rw==", - "dev": true - }, - "loader-utils": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-0.2.17.tgz", - "integrity": "sha512-tiv66G0SmiOx+pLWMtGEkfSEejxvb6N6uRrQjfWJIT79W9GMpgKeCAmm9aVBKtd4WEgntciI8CsGqjpDoCWJug==", - "dev": true, - "requires": { - "big.js": "^3.1.3", - "emojis-list": "^2.0.0", - "json5": "^0.5.0", - "object-assign": "^4.0.1" - } - }, - "util.promisify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.0.tgz", - "integrity": "sha512-i+6qA2MPhvoKLuxnJNpXAGhg7HphQOSUq2LKMZD0m15EiskXUkMvKdF4Uui0WYeCUGea+o2cw/ZuwehtfsrNkA==", - "dev": true, - "requires": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - } - } - }, - "vuepress-plugin-container": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/vuepress-plugin-container/-/vuepress-plugin-container-2.1.5.tgz", - "integrity": "sha512-TQrDX/v+WHOihj3jpilVnjXu9RcTm6m8tzljNJwYhxnJUW0WWQ0hFLcDTqTBwgKIFdEiSxVOmYE+bJX/sq46MA==", - "dev": true, - "requires": { - "@vuepress/shared-utils": "^1.2.0", - "markdown-it-container": "^2.0.0" - } - }, - "vuepress-plugin-smooth-scroll": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/vuepress-plugin-smooth-scroll/-/vuepress-plugin-smooth-scroll-0.0.3.tgz", - "integrity": "sha512-qsQkDftLVFLe8BiviIHaLV0Ea38YLZKKonDGsNQy1IE0wllFpFIEldWD8frWZtDFdx6b/O3KDMgVQ0qp5NjJCg==", - "dev": true, - "requires": { - "smoothscroll-polyfill": "^0.4.3" - } - }, - "watchpack": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", - "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", - "dev": true, - "requires": { - "chokidar": "^3.4.1", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0", - "watchpack-chokidar2": "^2.0.1" - }, - "dependencies": { - "binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", - "dev": true, - "optional": true - }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "dev": true, - "optional": true, - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, - "fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", - "dev": true, - "optional": true - }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "optional": true, - "requires": { - "is-glob": "^4.0.1" - } - }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "dev": true, - "optional": true, - "requires": { - "binary-extensions": "^2.0.0" - } - }, - "readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "optional": true, - "requires": { - "picomatch": "^2.2.1" - } - } - } - }, - "watchpack-chokidar2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", - "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", - "dev": true, - "optional": true, - "requires": { - "chokidar": "^2.1.8" - } - }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" - } - }, - "webpack": { - "version": "4.46.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", - "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.5.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "dependencies": { - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - } - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "webpack-chain": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/webpack-chain/-/webpack-chain-6.5.1.tgz", - "integrity": "sha512-7doO/SRtLu8q5WM0s7vPKPWX580qhi0/yBHkOxNkv50f6qB76Zy9o2wRTrrPULqYTvQlVHuvbA8v+G5ayuUDsA==", - "dev": true, - "requires": { - "deepmerge": "^1.5.2", - "javascript-stringify": "^2.0.1" - } - }, - "webpack-dev-middleware": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", - "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", - "dev": true, - "requires": { - "memory-fs": "^0.4.1", - "mime": "^2.4.4", - "mkdirp": "^0.5.1", - "range-parser": "^1.2.1", - "webpack-log": "^2.0.0" - } - }, - "webpack-dev-server": { - "version": "3.11.3", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.3.tgz", - "integrity": "sha512-3x31rjbEQWKMNzacUZRE6wXvUFuGpH7vr0lIEbYpMAG9BOxi0928QU1BBswOAP3kg3H1O4hiS+sq4YyAn6ANnA==", - "dev": true, - "requires": { - "ansi-html-community": "0.0.8", - "bonjour": "^3.5.0", - "chokidar": "^2.1.8", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "debug": "^4.1.1", - "del": "^4.1.1", - "express": "^4.17.1", - "html-entities": "^1.3.1", - "http-proxy-middleware": "0.19.1", - "import-local": "^2.0.0", - "internal-ip": "^4.3.0", - "ip": "^1.1.5", - "is-absolute-url": "^3.0.3", - "killable": "^1.0.1", - "loglevel": "^1.6.8", - "opn": "^5.5.0", - "p-retry": "^3.0.1", - "portfinder": "^1.0.26", - "schema-utils": "^1.0.0", - "selfsigned": "^1.10.8", - "semver": "^6.3.0", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "sockjs-client": "^1.5.0", - "spdy": "^4.0.2", - "strip-ansi": "^3.0.1", - "supports-color": "^6.1.0", - "url": "^0.11.0", - "webpack-dev-middleware": "^3.7.2", - "webpack-log": "^2.0.0", - "ws": "^6.2.1", - "yargs": "^13.3.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", - "dev": true - }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", - "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - } - }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", - "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - } - }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", - "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - } - }, - "http-proxy-middleware": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", - "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", - "dev": true, - "requires": { - "http-proxy": "^1.17.0", - "is-glob": "^4.0.0", - "lodash": "^4.17.11", - "micromatch": "^3.1.10" - } - }, - "is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", - "dev": true, - "requires": { - "kind-of": "^3.0.2" - }, - "dependencies": { - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } - } - }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", - "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "dependencies": { - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", - "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - } - } - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "to-regex-range": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", - "dev": true, - "requires": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - } - } - } - }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", - "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - } - }, - "webpack-merge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-4.2.2.tgz", - "integrity": "sha512-TUE1UGoTX2Cd42j3krGYqObZbOD+xF7u28WB7tfUordytSjbWTIjK/8V0amkBfTYN4/pB/GIDlJZZ657BGG19g==", - "dev": true, - "requires": { - "lodash": "^4.17.15" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "webpackbar": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-3.2.0.tgz", - "integrity": "sha512-PC4o+1c8gWWileUfwabe0gqptlXUDJd5E0zbpr2xHP1VSOVlZVPBZ8j6NCR8zM5zbKdxPhctHXahgpNK1qFDPw==", - "dev": true, - "requires": { - "ansi-escapes": "^4.1.0", - "chalk": "^2.4.1", - "consola": "^2.6.0", - "figures": "^3.0.0", - "pretty-time": "^1.1.0", - "std-env": "^2.2.1", - "text-table": "^0.2.0", - "wrap-ansi": "^5.1.0" - } - }, - "websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "requires": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - } - }, - "websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true - }, - "when": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/when/-/when-3.6.4.tgz", - "integrity": "sha512-d1VUP9F96w664lKINMGeElWdhhb5sC+thXM+ydZGU3ZnaE09Wv6FaS+mpM9570kcDs/xMfcXJBTLsMdHEFYY9Q==", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "dev": true, - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - } - }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha512-B+enWhmw6cjfVC7kS8Pj9pCrKSc5txArRyaYGe088shv/FGWH+0Rjx/xPgtsWfsUtS27FkP697E4DDhgrgoc0Q==", - "dev": true - }, - "widest-line": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-3.1.0.tgz", - "integrity": "sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==", - "dev": true, - "requires": { - "string-width": "^4.0.0" - } - }, - "worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", - "dev": true, - "requires": { - "errno": "~0.1.7" - } - }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - } - }, - "xdg-basedir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", - "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", - "dev": true - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", - "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", - "dev": true - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - }, - "dependencies": { - "ansi-regex": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", - "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==", - "dev": true - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", - "dev": true - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "dev": true, - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" - } - }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "dev": true, - "requires": { - "ansi-regex": "^4.1.0" - } - } - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - }, - "dependencies": { - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", - "dev": true - } - } - }, - "zepto": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/zepto/-/zepto-1.2.0.tgz", - "integrity": "sha512-C1x6lfvBICFTQIMgbt3JqMOno3VOtkWat/xEakLTOurskYIHPmzJrzd1e8BnmtdDVJlGuk5D+FxyCA8MPmkIyA==", - "dev": true - } - } -} diff --git a/package.json b/package.json deleted file mode 100644 index 88b693e5309..00000000000 --- a/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "masstransit-docs", - "version": "0.1.0", - "description": "MassTransit Documentation", - "main": "index.js", - "devDependencies": { - "@vuepress/plugin-active-header-links": "^1.9.7", - "@vuepress/plugin-back-to-top": "^1.9.7", - "@vuepress/plugin-google-analytics": "^1.9.7", - "vuepress": "^1.9.7" - }, - "dependencies": { - "esm": "^3.2.25", - "follow-redirects": "^1.14.8", - "markdown-it-html5-embed": "^1.0.0", - "path-parse": "^1.0.7", - "prismjs": "^1.26.0", - "remove-markdown": "^0.3.0", - "vue-tweet-embed": "^2.4.0" - }, - "scripts": { - "docs:build": "bash -c \"rm -rf docs/.vuepress/dist\" && vuepress build docs", - "docs:publish": "cd docs/.vuepress/dist && git init && git fetch https://github.com/MassTransit/masstransit.github.io.git && git checkout 220443cd2ab45d486fcee10a65669aff0bda31ab && git checkout -b master && git add . && git commit -am \"Deploy Documentation\" && git push --force --set-upstream https://github.com/MassTransit/masstransit.github.io.git master", - "docs:dev": "vuepress dev docs" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/MassTransit/MassTransit.git" - }, - "author": "", - "license": "ISC", - "bugs": { - "url": "https://github.com/MassTransit/MassTransit/issues" - }, - "homepage": "https://github.com/MassTransit/MassTransit#readme" -} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 546366db3f8..63e5a964a61 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -3,7 +3,7 @@ MassTransit - Copyright 2007-2022 Chris Patterson + Copyright 2007-2024 Chris Patterson Chris Patterson true true @@ -20,8 +20,8 @@ mt-logo-small.png NuGet.README.md Apache-2.0 - https://github.com/MassTransit/MassTransit - MassTransit is a message-based distributed application framework for .NET http://masstransit-project.com + https://masstransit.io + MassTransit provides a developer-focused, modern platform for creating distributed applications without complexity. True true diff --git a/src/MassTransit.Abstractions/Attributes/MessageUrnAttribute.cs b/src/MassTransit.Abstractions/Attributes/MessageUrnAttribute.cs new file mode 100644 index 00000000000..483233a3eea --- /dev/null +++ b/src/MassTransit.Abstractions/Attributes/MessageUrnAttribute.cs @@ -0,0 +1,43 @@ +namespace MassTransit +{ + using System; + + + /// + /// Specify the message type name for this message type + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface)] + public class MessageUrnAttribute : + Attribute + { + /// + /// + /// The urn value to use for this message type. + /// Prefixes with default scheme and namespace if true. + public MessageUrnAttribute(string urn, bool useDefaultPrefix = true) + { + if (urn == null) + throw new ArgumentNullException(nameof(urn)); + + if (string.IsNullOrWhiteSpace(urn)) + throw new ArgumentException("Value cannot be empty or whitespace only string.", nameof(urn)); + + if (urn.StartsWith(MessageUrn.Prefix)) + throw new ArgumentException($"Value should not contain the default prefix '{MessageUrn.Prefix}'.", nameof(urn)); + + Urn = FormatUrn(urn, useDefaultPrefix); + } + + public Uri Urn { get; } + + static Uri FormatUrn(string urn, bool useDefaultPrefix) + { + var fullValue = useDefaultPrefix ? MessageUrn.Prefix + urn : urn; + + if (Uri.TryCreate(fullValue, UriKind.Absolute, out var uri)) + return uri; + + throw new UriFormatException($"Invalid URN: {fullValue}"); + } + } +} diff --git a/src/MassTransit.Abstractions/Attributes/NullableAttributes.cs b/src/MassTransit.Abstractions/Attributes/NullableAttributes.cs index 6a67eb9c43e..1d36960845a 100644 --- a/src/MassTransit.Abstractions/Attributes/NullableAttributes.cs +++ b/src/MassTransit.Abstractions/Attributes/NullableAttributes.cs @@ -1,8 +1,8 @@ -namespace MassTransit -{ - using System; - +using System; +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ [AttributeUsage(AttributeTargets.Parameter)] sealed class NotNullWhenAttribute : Attribute @@ -20,3 +20,22 @@ public NotNullWhenAttribute(bool returnValue) public bool ReturnValue { get; } } } + + +[AttributeUsage(AttributeTargets.Parameter)] +sealed class MaybeNullWhenAttribute : + Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public MaybeNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } +} +#endif diff --git a/src/MassTransit.Abstractions/Clients/RequestTimeout.cs b/src/MassTransit.Abstractions/Clients/RequestTimeout.cs index ae964876278..e7d40a5db46 100644 --- a/src/MassTransit.Abstractions/Clients/RequestTimeout.cs +++ b/src/MassTransit.Abstractions/Clients/RequestTimeout.cs @@ -6,9 +6,10 @@ namespace MassTransit /// /// A timeout, which can be a default (none) or a valid TimeSpan > 0, includes factory methods to make it "cute" /// - public struct RequestTimeout + public readonly struct RequestTimeout : + IEquatable { - TimeSpan? _timeout; + readonly TimeSpan? _timeout; RequestTimeout(TimeSpan timeout) { @@ -28,6 +29,31 @@ public struct RequestTimeout public static RequestTimeout None { get; } = new RequestTimeout(); public static RequestTimeout Default { get; } = new RequestTimeout(TimeSpan.FromSeconds(30)); + public bool Equals(RequestTimeout other) + { + return Nullable.Equals(_timeout, other._timeout); + } + + public override bool Equals(object? obj) + { + return obj is RequestTimeout other && Equals(other); + } + + public override int GetHashCode() + { + return _timeout.GetHashCode(); + } + + public static bool operator ==(RequestTimeout left, RequestTimeout right) + { + return left.Equals(right); + } + + public static bool operator !=(RequestTimeout left, RequestTimeout right) + { + return !left.Equals(right); + } + public static implicit operator RequestTimeout(TimeSpan timeout) { if (timeout <= TimeSpan.Zero) diff --git a/src/MassTransit.Abstractions/Clients/Response.cs b/src/MassTransit.Abstractions/Clients/Response.cs index 9a394201039..2e1c104e68b 100644 --- a/src/MassTransit.Abstractions/Clients/Response.cs +++ b/src/MassTransit.Abstractions/Clients/Response.cs @@ -1,6 +1,7 @@ namespace MassTransit { using System; + using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; diff --git a/src/MassTransit.Abstractions/Configuration/Configuration/ActivityConfigurationObservable.cs b/src/MassTransit.Abstractions/Configuration/Configuration/ActivityConfigurationObservable.cs index f1e8c833871..c8af19a1d2d 100644 --- a/src/MassTransit.Abstractions/Configuration/Configuration/ActivityConfigurationObservable.cs +++ b/src/MassTransit.Abstractions/Configuration/Configuration/ActivityConfigurationObservable.cs @@ -28,5 +28,17 @@ public void CompensateActivityConfigured(ICompensateActivityCon { ForEach(observer => observer.CompensateActivityConfigured(configurator)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Configuration/Configuration/ConsumerConfigurationObservable.cs b/src/MassTransit.Abstractions/Configuration/Configuration/ConsumerConfigurationObservable.cs index b4fb439f280..9bcab69c0d3 100644 --- a/src/MassTransit.Abstractions/Configuration/Configuration/ConsumerConfigurationObservable.cs +++ b/src/MassTransit.Abstractions/Configuration/Configuration/ConsumerConfigurationObservable.cs @@ -19,5 +19,17 @@ public void ConsumerMessageConfigured(IConsumerMessageConfi { ForEach(observer => observer.ConsumerMessageConfigured(configurator)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Configuration/Configuration/EndpointConfigurationObservable.cs b/src/MassTransit.Abstractions/Configuration/Configuration/EndpointConfigurationObservable.cs index f12a297c77c..25435111eea 100644 --- a/src/MassTransit.Abstractions/Configuration/Configuration/EndpointConfigurationObservable.cs +++ b/src/MassTransit.Abstractions/Configuration/Configuration/EndpointConfigurationObservable.cs @@ -12,5 +12,17 @@ public void EndpointConfigured(T configurator) { ForEach(observer => observer.EndpointConfigured(configurator)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Configuration/Configuration/HandlerConfigurationObservable.cs b/src/MassTransit.Abstractions/Configuration/Configuration/HandlerConfigurationObservable.cs index d22e0fedd49..716d67cbb34 100644 --- a/src/MassTransit.Abstractions/Configuration/Configuration/HandlerConfigurationObservable.cs +++ b/src/MassTransit.Abstractions/Configuration/Configuration/HandlerConfigurationObservable.cs @@ -12,5 +12,17 @@ public void HandlerConfigured(IHandlerConfigurator configura { ForEach(observer => observer.HandlerConfigured(configurator)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Configuration/Configuration/SagaConfigurationObservable.cs b/src/MassTransit.Abstractions/Configuration/Configuration/SagaConfigurationObservable.cs index f0a72676b54..3d2ccb622c7 100644 --- a/src/MassTransit.Abstractions/Configuration/Configuration/SagaConfigurationObservable.cs +++ b/src/MassTransit.Abstractions/Configuration/Configuration/SagaConfigurationObservable.cs @@ -25,5 +25,17 @@ public void SagaMessageConfigured(ISagaMessageConfigurator observer.SagaMessageConfigured(configurator)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Configuration/Configuration/ValueTypeGroupKeyProvider.cs b/src/MassTransit.Abstractions/Configuration/Configuration/ValueTypeGroupKeyProvider.cs index 7999b157ab2..36042b39938 100644 --- a/src/MassTransit.Abstractions/Configuration/Configuration/ValueTypeGroupKeyProvider.cs +++ b/src/MassTransit.Abstractions/Configuration/Configuration/ValueTypeGroupKeyProvider.cs @@ -17,7 +17,7 @@ public ValueTypeGroupKeyProvider(Func, TKey?> provider) public bool TryGetKey(ConsumeContext context, out TKey key) { - var property = _provider(context); + TKey? property = _provider(context); if (property.HasValue) { diff --git a/src/MassTransit.Abstractions/Configuration/Consumers/BatchOptions.cs b/src/MassTransit.Abstractions/Configuration/Consumers/BatchOptions.cs index e705d282ffb..1f3a1c75c25 100644 --- a/src/MassTransit.Abstractions/Configuration/Consumers/BatchOptions.cs +++ b/src/MassTransit.Abstractions/Configuration/Consumers/BatchOptions.cs @@ -14,12 +14,22 @@ public class BatchOptions : IConfigureReceiveEndpoint, ISpecification { + /// + /// Override the default receive endpoint configuration done by the batch options + /// + public delegate void ConfigurationCallback(string name, IReceiveEndpointConfigurator configurator); + + + ConfigurationCallback _configurationCallback; + public BatchOptions() { ConcurrencyLimit = 1; MessageLimit = 10; TimeLimit = TimeSpan.FromSeconds(1); TimeLimitStart = BatchTimeLimitStart.FromFirst; + + _configurationCallback = DefaultConfigurationCallback; } /// @@ -49,12 +59,7 @@ public BatchOptions() public void Configure(string name, IReceiveEndpointConfigurator configurator) { - var messageCapacity = ConcurrencyLimit * MessageLimit; - - configurator.PrefetchCount = Math.Max(messageCapacity, configurator.PrefetchCount); - - if (configurator.ConcurrentMessageLimit < messageCapacity) - configurator.ConcurrentMessageLimit = messageCapacity; + _configurationCallback(name, configurator); } public IEnumerable Validate() @@ -67,6 +72,25 @@ public IEnumerable Validate() yield return this.Failure("Batch", "ConcurrencyLimit", "Must be > 0"); } + public BatchOptions SetConfigurationCallback(ConfigurationCallback callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + _configurationCallback = callback; + return this; + } + + void DefaultConfigurationCallback(string name, IReceiveEndpointConfigurator configurator) + { + var messageCapacity = ConcurrencyLimit * MessageLimit; + + configurator.PrefetchCount = Math.Max(messageCapacity, configurator.PrefetchCount); + + if (configurator.ConcurrentMessageLimit < messageCapacity) + configurator.ConcurrentMessageLimit = messageCapacity; + } + /// /// Sets the maximum number of messages in a single batch /// @@ -99,7 +123,7 @@ public BatchOptions SetTimeLimit(TimeSpan limit) } /// - /// Sets the starting point for the + /// Sets the starting point for the /// /// The starting point public BatchOptions SetTimeLimitStart(BatchTimeLimitStart timeLimitStart) diff --git a/src/MassTransit.Abstractions/Configuration/Consumers/IBatchConfigurator.cs b/src/MassTransit.Abstractions/Configuration/Consumers/IBatchConfigurator.cs index 8c932c265e7..55b04412c2b 100644 --- a/src/MassTransit.Abstractions/Configuration/Consumers/IBatchConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/Consumers/IBatchConfigurator.cs @@ -37,7 +37,8 @@ public interface IBatchConfigurator : /// /// Configure the consumer pipe /// - void Consumer(IConsumerFactory consumerFactory, Action>>? configure = null) + void Consumer(IConsumerFactory consumerFactory, + Action>>? configure = null) where TConsumer : class, IConsumer>; } } diff --git a/src/MassTransit.Abstractions/Configuration/Consumers/IServiceInstanceConfigurator.cs b/src/MassTransit.Abstractions/Configuration/Consumers/IServiceInstanceConfigurator.cs index 0b0faa3eea6..80c4434f418 100644 --- a/src/MassTransit.Abstractions/Configuration/Consumers/IServiceInstanceConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/Consumers/IServiceInstanceConfigurator.cs @@ -15,7 +15,7 @@ public interface IServiceInstanceConfigurator : /// Uri InstanceAddress { get; } - IBusFactoryConfigurator BusConfigurator { get; } + IReceiveConfigurator BusConfigurator { get; } IReceiveEndpointConfigurator InstanceEndpointConfigurator { get; } /// @@ -31,7 +31,7 @@ public interface IServiceInstanceConfigurator : IReceiveConfigurator where TEndpointConfigurator : IReceiveEndpointConfigurator { - new IBusFactoryConfigurator BusConfigurator { get; } + new IReceiveConfigurator BusConfigurator { get; } new TEndpointConfigurator InstanceEndpointConfigurator { get; } } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/ActivityDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/ActivityDefinition.cs index fbea60705e2..b35215493b6 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/ActivityDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/ActivityDefinition.cs @@ -28,12 +28,15 @@ protected string CompensateEndpointName IEndpointDefinition? IActivityDefinition.CompensateEndpointDefinition => CompensateEndpointDefinition; void IActivityDefinition.Configure(IReceiveEndpointConfigurator endpointConfigurator, - ICompensateActivityConfigurator compensateActivityConfigurator) + ICompensateActivityConfigurator compensateActivityConfigurator, IRegistrationContext context) { if (ConcurrentMessageLimit.HasValue) compensateActivityConfigurator.ConcurrentMessageLimit = ConcurrentMessageLimit; + #pragma warning disable CS0618 ConfigureCompensateActivity(endpointConfigurator, compensateActivityConfigurator); + #pragma warning restore CS0618 + ConfigureCompensateActivity(endpointConfigurator, compensateActivityConfigurator, context); } string IActivityDefinition.GetCompensateEndpointName(IEndpointNameFormatter formatter) @@ -63,9 +66,21 @@ protected void CompensateEndpoint(Action? con /// /// The receive endpoint configurator for the consumer /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] protected virtual void ConfigureCompensateActivity(IReceiveEndpointConfigurator endpointConfigurator, ICompensateActivityConfigurator compensateActivityConfigurator) { } + + /// + /// Called when the compensate activity is being configured on the endpoint. + /// + /// The receive endpoint configurator for the consumer + /// + /// + protected virtual void ConfigureCompensateActivity(IReceiveEndpointConfigurator endpointConfigurator, + ICompensateActivityConfigurator compensateActivityConfigurator, IRegistrationContext context) + { + } } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/EndpointRegistrationConfigurator.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/EndpointRegistrationConfigurator.cs index 96d1c93e648..387f64bf879 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/EndpointRegistrationConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/EndpointRegistrationConfigurator.cs @@ -1,5 +1,8 @@ namespace MassTransit.Configuration { + using System; + + public class EndpointRegistrationConfigurator : IEndpointRegistrationConfigurator where T : class @@ -42,5 +45,10 @@ public string InstanceId { set => _settings.InstanceId = value; } + + public void AddConfigureEndpointCallback(Action? callback) + { + _settings.AddConfigureEndpointCallback(callback); + } } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/EndpointSettings.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/EndpointSettings.cs index 17bfd97d3ac..923f97dec54 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/EndpointSettings.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/EndpointSettings.cs @@ -1,9 +1,15 @@ namespace MassTransit.Configuration { + using System; + using System.Collections.Generic; + + public class EndpointSettings : IEndpointSettings where T : class { + List>? _callbacks; + public EndpointSettings() { ConfigureConsumeTopology = true; @@ -20,5 +26,24 @@ public EndpointSettings() public bool ConfigureConsumeTopology { get; set; } public string? InstanceId { get; set; } + + public void ConfigureEndpoint(IReceiveEndpointConfigurator configurator) + { + if (_callbacks != null) + { + foreach (Action callback in _callbacks) + callback(configurator); + } + } + + public void AddConfigureEndpointCallback(Action? callback) + { + if (callback == null) + return; + + _callbacks ??= new List>(1); + + _callbacks.Add(callback); + } } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/SettingsEndpointDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/SettingsEndpointDefinition.cs index 367e28edc57..034f73c4721 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/SettingsEndpointDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/Configuration/SettingsEndpointDefinition.cs @@ -34,6 +34,7 @@ string FormatName() public void Configure(T configurator) where T : IReceiveEndpointConfigurator { + _settings.ConfigureEndpoint(configurator); } protected abstract string FormatEndpointName(IEndpointNameFormatter formatter); diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/ConsumerDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/ConsumerDefinition.cs index d8911c79b9c..23e88f485ea 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/ConsumerDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/ConsumerDefinition.cs @@ -42,12 +42,16 @@ public int? ConcurrentMessageLimit protected set => _concurrentMessageLimit = value; } - void IConsumerDefinition.Configure(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + void IConsumerDefinition.Configure(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { if (_concurrentMessageLimit.HasValue) consumerConfigurator.ConcurrentMessageLimit = _concurrentMessageLimit; + #pragma warning disable CS0618 ConfigureConsumer(endpointConfigurator, consumerConfigurator); + #pragma warning restore CS0618 + ConfigureConsumer(endpointConfigurator, consumerConfigurator, context); } Type IConsumerDefinition.ConsumerType => typeof(TConsumer); @@ -78,8 +82,21 @@ protected void Endpoint(Action? configure = n /// /// The receive endpoint configurator for the consumer /// The consumer configurator + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] protected virtual void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) { } + + /// + /// Called when the consumer is being configured on the endpoint. Configuration only applies to this consumer, and does not apply to + /// the endpoint. + /// + /// The receive endpoint configurator for the consumer + /// The consumer configurator + /// + protected virtual void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) + { + } } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/ExecuteActivityDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/ExecuteActivityDefinition.cs index 7c5045004cd..cf108b95a45 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/ExecuteActivityDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/ExecuteActivityDefinition.cs @@ -40,12 +40,15 @@ public int? ConcurrentMessageLimit } void IExecuteActivityDefinition.Configure(IReceiveEndpointConfigurator endpointConfigurator, - IExecuteActivityConfigurator executeActivityConfigurator) + IExecuteActivityConfigurator executeActivityConfigurator, IRegistrationContext context) { if (_concurrentMessageLimit.HasValue) executeActivityConfigurator.ConcurrentMessageLimit = _concurrentMessageLimit; + #pragma warning disable CS0618 ConfigureExecuteActivity(endpointConfigurator, executeActivityConfigurator); + #pragma warning restore CS0618 + ConfigureExecuteActivity(endpointConfigurator, executeActivityConfigurator, context); } string IExecuteActivityDefinition.GetExecuteEndpointName(IEndpointNameFormatter formatter) @@ -76,9 +79,21 @@ protected void ExecuteEndpoint(Action? config /// /// The receive endpoint configurator for the consumer /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] protected virtual void ConfigureExecuteActivity(IReceiveEndpointConfigurator endpointConfigurator, IExecuteActivityConfigurator executeActivityConfigurator) { } + + /// + /// Called when the compensate activity is being configured on the endpoint. + /// + /// The receive endpoint configurator for the consumer + /// + /// + protected virtual void ConfigureExecuteActivity(IReceiveEndpointConfigurator endpointConfigurator, + IExecuteActivityConfigurator executeActivityConfigurator, IRegistrationContext context) + { + } } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/FutureDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/FutureDefinition.cs index f800f9b4e0d..d0e15bcf94d 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/FutureDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/FutureDefinition.cs @@ -43,12 +43,16 @@ public int? ConcurrentMessageLimit protected set => _concurrentMessageLimit = value; } - void IFutureDefinition.Configure(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + void IFutureDefinition.Configure(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) { if (_concurrentMessageLimit.HasValue) sagaConfigurator.ConcurrentMessageLimit = _concurrentMessageLimit; + #pragma warning disable CS0618 ConfigureSaga(endpointConfigurator, sagaConfigurator); + #pragma warning restore CS0618 + ConfigureSaga(endpointConfigurator, sagaConfigurator, context); } Type IFutureDefinition.FutureType => typeof(TFuture); @@ -66,10 +70,23 @@ string IFutureDefinition.GetEndpointName(IEndpointNameFormatter formatter) /// /// The receive endpoint configurator for the consumer /// The saga configurator + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] protected virtual void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) { } + /// + /// Called when configuring the saga on the endpoint. Configuration only applies to this saga, and does not apply to + /// the endpoint. + /// + /// The receive endpoint configurator for the consumer + /// The saga configurator + /// + protected virtual void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) + { + } + /// /// Configure the saga endpoint /// diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IActivityDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IActivityDefinition.cs index 7d36d620a01..3c145a8055f 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IActivityDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IActivityDefinition.cs @@ -39,6 +39,8 @@ public interface IActivityDefinition : /// /// /// - void Configure(IReceiveEndpointConfigurator endpointConfigurator, ICompensateActivityConfigurator compensateActivityConfigurator); + /// + void Configure(IReceiveEndpointConfigurator endpointConfigurator, ICompensateActivityConfigurator compensateActivityConfigurator, + IRegistrationContext context); } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IConsumerDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IConsumerDefinition.cs index 8fc74521750..2d74cc8e82b 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IConsumerDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IConsumerDefinition.cs @@ -36,6 +36,8 @@ public interface IConsumerDefinition : /// /// The receive endpoint configurator for the consumer /// The consumer configurator - void Configure(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator); + /// + void Configure(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator, + IRegistrationContext context); } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IEndpointRegistrationConfigurator.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IEndpointRegistrationConfigurator.cs index f6930e08697..9c45ecb96f1 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IEndpointRegistrationConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IEndpointRegistrationConfigurator.cs @@ -1,5 +1,6 @@ namespace MassTransit { + using System; using Courier.Contracts; @@ -39,5 +40,11 @@ public interface IEndpointRegistrationConfigurator /// end of the endpoint name. /// string InstanceId { set; } + + /// + /// Add an endpoint configuration callback to the registration + /// + /// + void AddConfigureEndpointCallback(Action? callback); } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IEndpointSettings.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IEndpointSettings.cs index d53c7f747b6..abaed8f6979 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IEndpointSettings.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IEndpointSettings.cs @@ -13,6 +13,8 @@ public interface IEndpointSettings bool ConfigureConsumeTopology { get; } - string? InstanceId { get; set; } + string? InstanceId { get; } + + void ConfigureEndpoint(IReceiveEndpointConfigurator configurator); } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IExecuteActivityDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IExecuteActivityDefinition.cs index 9b09d85e2c0..6c55952f2bf 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IExecuteActivityDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IExecuteActivityDefinition.cs @@ -42,6 +42,8 @@ public interface IExecuteActivityDefinition : /// /// /// - void Configure(IReceiveEndpointConfigurator endpointConfigurator, IExecuteActivityConfigurator executeActivityConfigurator); + /// + void Configure(IReceiveEndpointConfigurator endpointConfigurator, IExecuteActivityConfigurator executeActivityConfigurator, + IRegistrationContext context); } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IFutureDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IFutureDefinition.cs index fb7a702ec0d..dc4166298f2 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/IFutureDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IFutureDefinition.cs @@ -13,7 +13,9 @@ public interface IFutureDefinition : /// Configure the future on the receive endpoint /// The receive endpoint configurator for the consumer /// The consumer configurator - void Configure(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator); + /// + void Configure(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context); } diff --git a/src/MassTransit/Configuration/DependencyInjection/IRegistrationContext.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IRegistrationContext.cs similarity index 97% rename from src/MassTransit/Configuration/DependencyInjection/IRegistrationContext.cs rename to src/MassTransit.Abstractions/Configuration/DependencyInjection/IRegistrationContext.cs index bf313d54b48..2d0218df257 100644 --- a/src/MassTransit/Configuration/DependencyInjection/IRegistrationContext.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/IRegistrationContext.cs @@ -23,7 +23,7 @@ public interface IRegistrationContext : /// /// /// The consumer type - void ConfigureConsumer(IReceiveEndpointConfigurator configurator, Action> configure = null) + void ConfigureConsumer(IReceiveEndpointConfigurator configurator, Action>? configure = null) where T : class, IConsumer; /// @@ -45,7 +45,7 @@ void ConfigureConsumer(IReceiveEndpointConfigurator configurator, Action /// /// The saga type - void ConfigureSaga(IReceiveEndpointConfigurator configurator, Action> configure = null) + void ConfigureSaga(IReceiveEndpointConfigurator configurator, Action>? configure = null) where T : class, ISaga; /// diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/ISagaDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/ISagaDefinition.cs index 2edb498122a..49304185d4b 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/ISagaDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/ISagaDefinition.cs @@ -36,6 +36,7 @@ public interface ISagaDefinition : /// /// The receive endpoint configurator for the consumer /// The consumer configurator - void Configure(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator); + /// + void Configure(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, IRegistrationContext context); } } diff --git a/src/MassTransit.Abstractions/Configuration/DependencyInjection/SagaDefinition.cs b/src/MassTransit.Abstractions/Configuration/DependencyInjection/SagaDefinition.cs index bc69a56f20f..ee8afd48d7f 100644 --- a/src/MassTransit.Abstractions/Configuration/DependencyInjection/SagaDefinition.cs +++ b/src/MassTransit.Abstractions/Configuration/DependencyInjection/SagaDefinition.cs @@ -43,12 +43,16 @@ public int? ConcurrentMessageLimit protected set => _concurrentMessageLimit = value; } - void ISagaDefinition.Configure(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + void ISagaDefinition.Configure(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) { if (_concurrentMessageLimit.HasValue) sagaConfigurator.ConcurrentMessageLimit = _concurrentMessageLimit; + #pragma warning disable CS0618 ConfigureSaga(endpointConfigurator, sagaConfigurator); + #pragma warning restore CS0618 + ConfigureSaga(endpointConfigurator, sagaConfigurator, context); } Type ISagaDefinition.SagaType => typeof(TSaga); @@ -66,10 +70,23 @@ string ISagaDefinition.GetEndpointName(IEndpointNameFormatter formatter) /// /// The receive endpoint configurator for the consumer /// The saga configurator + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] protected virtual void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) { } + /// + /// Called when configuring the saga on the endpoint. Configuration only applies to this saga, and does not apply to + /// the endpoint. + /// + /// The receive endpoint configurator for the consumer + /// The saga configurator + /// + protected virtual void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) + { + } + /// /// Configure the saga endpoint /// diff --git a/src/MassTransit.Abstractions/Configuration/IMessageFilterConfigurator.cs b/src/MassTransit.Abstractions/Configuration/IMessageFilterConfigurator.cs index c33101c83d2..649d3f80721 100644 --- a/src/MassTransit.Abstractions/Configuration/IMessageFilterConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/IMessageFilterConfigurator.cs @@ -6,21 +6,9 @@ /// /// Configures a message filter, for including and excluding message types /// - public interface IMessageFilterConfigurator + public interface IMessageFilterConfigurator : + IMessageTypeFilterConfigurator { - /// - /// Include the message if it is any of the specified message types - /// - /// - void Include(params Type[] messageTypes); - - /// - /// Include the message if it is the specified message type - /// - /// The message type - void Include() - where T : class; - /// /// Include the message if it is the specified message type and matches the specified filter expression /// @@ -29,19 +17,6 @@ void Include() void Include(Func filter) where T : class; - /// - /// Exclude the message if it is any of the specified message types - /// - /// - void Exclude(params Type[] messageTypes); - - /// - /// Exclude the message if it is the specified message type - /// - /// The message type - void Exclude() - where T : class; - /// /// Exclude the message if it is the specified message type and matches the specified filter expression /// diff --git a/src/MassTransit.Abstractions/Configuration/IMessageTypeFilterConfigurator.cs b/src/MassTransit.Abstractions/Configuration/IMessageTypeFilterConfigurator.cs new file mode 100644 index 00000000000..9d26c412e40 --- /dev/null +++ b/src/MassTransit.Abstractions/Configuration/IMessageTypeFilterConfigurator.cs @@ -0,0 +1,49 @@ +namespace MassTransit +{ + using System; + + + /// + /// Configures a message filter, for including and excluding message types + /// + public interface IMessageTypeFilterConfigurator + { + /// + /// Include the message if it is any of the specified message types + /// + /// + void Include(params Type[] messageTypes); + + /// + /// Include the type matches the specified filter expression + /// + /// The filter expression + void Include(Func filter); + + /// + /// Include the message if it is the specified message type + /// + /// The message type + void Include() + where T : class; + + /// + /// Exclude the message if it is any of the specified message types + /// + /// + void Exclude(params Type[] messageTypes); + + /// + /// Exclude the type matches the specified filter expression + /// + /// The filter expression + void Exclude(Func filter); + + /// + /// Exclude the message if it is the specified message type + /// + /// The message type + void Exclude() + where T : class; + } +} diff --git a/src/MassTransit.Abstractions/Configuration/MassTransitHostOptions.cs b/src/MassTransit.Abstractions/Configuration/MassTransitHostOptions.cs index ca212bf373c..f791970182f 100644 --- a/src/MassTransit.Abstractions/Configuration/MassTransitHostOptions.cs +++ b/src/MassTransit.Abstractions/Configuration/MassTransitHostOptions.cs @@ -23,5 +23,11 @@ public class MassTransitHostOptions /// The bus is still stopped, only the wait is canceled. /// public TimeSpan? StopTimeout { get; set; } + + /// + /// If specified, the timeout will be used to wait for Consumers to complete their work + /// After this timeout ConsumeContext.CancellationToken will be cancelled + /// + public TimeSpan? ConsumerStopTimeout { get; set; } } } diff --git a/src/MassTransit.Abstractions/Configuration/Middleware/FilterConfigurationExtensions.cs b/src/MassTransit.Abstractions/Configuration/Middleware/FilterConfigurationExtensions.cs index 351481d71a3..adc9341fef7 100644 --- a/src/MassTransit.Abstractions/Configuration/Middleware/FilterConfigurationExtensions.cs +++ b/src/MassTransit.Abstractions/Configuration/Middleware/FilterConfigurationExtensions.cs @@ -138,7 +138,9 @@ public static void UseFilter(this IPipeConfigurator var filterSpecification = new FilterPipeSpecification(filter); - var pipeBuilderConfigurator = new SplitFilterPipeSpecification(filterSpecification, contextProvider, inputContextProvider); + var pipeBuilderConfigurator = new PipeConfigurator.SplitFilterPipeSpecification(filterSpecification, + contextProvider, + inputContextProvider); configurator.AddPipeSpecification(pipeBuilderConfigurator); } diff --git a/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointConfigurator.cs b/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointConfigurator.cs index f796ecce15d..b4231ba427c 100644 --- a/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointConfigurator.cs @@ -10,6 +10,7 @@ namespace MassTransit /// public interface IReceiveEndpointConfigurator : IEndpointConfigurator, + IReceiveEndpointObserverConnector, IReceiveEndpointDependencyConnector, IReceiveEndpointDependentConnector { @@ -59,6 +60,12 @@ public interface IReceiveEndpointConfigurator : void ConfigureMessageTopology(bool enabled = true) where T : class; + /// + /// Configures whether the broker topology is configured for the specified message type. Related to + /// , but for an individual message type. + /// + void ConfigureMessageTopology(Type messageType, bool enabled = true); + [EditorBrowsable(EditorBrowsableState.Never)] void AddEndpointSpecification(IReceiveEndpointSpecification configurator); diff --git a/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointDependencyConnector.cs b/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointDependencyConnector.cs index 372b0f61f6e..d11df5abb5c 100644 --- a/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointDependencyConnector.cs +++ b/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointDependencyConnector.cs @@ -1,11 +1,14 @@ namespace MassTransit { + using Transports; + + public interface IReceiveEndpointDependencyConnector { /// - /// Add the observable receive endpoint as a dependent + /// Add receive endpoint dependency. Endpoint will be started when dependency is Ready /// - /// - void AddDependency(IReceiveEndpointDependentConnector dependent); + /// + void AddDependency(IReceiveEndpointDependency dependency); } } diff --git a/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointDependentConnector.cs b/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointDependentConnector.cs index 410d110f786..bec2c780c4d 100644 --- a/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointDependentConnector.cs +++ b/src/MassTransit.Abstractions/Configuration/Middleware/IReceiveEndpointDependentConnector.cs @@ -1,12 +1,14 @@ namespace MassTransit { - public interface IReceiveEndpointDependentConnector : - IReceiveEndpointObserverConnector + using Transports; + + + public interface IReceiveEndpointDependentConnector { /// - /// Add the observable receive endpoint as a dependency + /// Add the dependent to receive endpoint. Receive endpoint will be stopped when dependent is Completed /// - /// - void AddDependent(IReceiveEndpointObserverConnector dependency); + /// + void AddDependent(IReceiveEndpointDependent dependent); } } diff --git a/src/MassTransit.Abstractions/Configuration/Middleware/ReceiveEndpointConfiguratorDependencyExtensions.cs b/src/MassTransit.Abstractions/Configuration/Middleware/ReceiveEndpointConfiguratorDependencyExtensions.cs new file mode 100644 index 00000000000..ed2a5bd2c3e --- /dev/null +++ b/src/MassTransit.Abstractions/Configuration/Middleware/ReceiveEndpointConfiguratorDependencyExtensions.cs @@ -0,0 +1,99 @@ +namespace MassTransit +{ + using System.Threading.Tasks; + using Transports; + + + public static class ReceiveEndpointConfiguratorDependencyExtensions + { + public static void AddDependency(this IReceiveEndpointConfigurator connector, IReceiveEndpointConfigurator dependency) + { + connector.AddDependency(new ReceiveEndpointDependency(dependency)); + dependency.AddDependent(new ReceiveEndpointDependent(connector)); + } + + + class ReceiveEndpointDependency : + IReceiveEndpointDependency, + IReceiveEndpointObserver + { + readonly ConnectHandle _handle; + readonly TaskCompletionSource _ready; + + public ReceiveEndpointDependency(IReceiveEndpointObserverConnector connector) + { + _ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _handle = connector.ConnectReceiveEndpointObserver(this); + } + + public Task Ready => _ready.Task; + + Task IReceiveEndpointObserver.Ready(ReceiveEndpointReady ready) + { + _handle.Disconnect(); + + _ready.TrySetResult(ready); + + return Task.CompletedTask; + } + + Task IReceiveEndpointObserver.Stopping(ReceiveEndpointStopping stopping) + { + return Task.CompletedTask; + } + + Task IReceiveEndpointObserver.Completed(ReceiveEndpointCompleted completed) + { + return Task.CompletedTask; + } + + Task IReceiveEndpointObserver.Faulted(ReceiveEndpointFaulted faulted) + { + return Task.CompletedTask; + } + } + + + class ReceiveEndpointDependent : + IReceiveEndpointDependent, + IReceiveEndpointObserver + { + readonly TaskCompletionSource _completed; + readonly ConnectHandle _handle; + + public ReceiveEndpointDependent(IReceiveEndpointObserverConnector connector) + { + _completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _handle = connector.ConnectReceiveEndpointObserver(this); + } + + public Task Completed => _completed.Task; + + Task IReceiveEndpointObserver.Ready(ReceiveEndpointReady ready) + { + return Task.CompletedTask; + } + + Task IReceiveEndpointObserver.Stopping(ReceiveEndpointStopping stopping) + { + return Task.CompletedTask; + } + + Task IReceiveEndpointObserver.Completed(ReceiveEndpointCompleted completed) + { + _handle.Disconnect(); + + _completed.TrySetResult(completed); + + return Task.CompletedTask; + } + + Task IReceiveEndpointObserver.Faulted(ReceiveEndpointFaulted faulted) + { + return Task.CompletedTask; + } + } + } +} diff --git a/src/MassTransit.Abstractions/Configuration/Topology/IConsumeTopologyConfigurator.cs b/src/MassTransit.Abstractions/Configuration/Topology/IConsumeTopologyConfigurator.cs index 5149dc1fd80..4ff33747245 100644 --- a/src/MassTransit.Abstractions/Configuration/Topology/IConsumeTopologyConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/Topology/IConsumeTopologyConfigurator.cs @@ -1,5 +1,6 @@ namespace MassTransit { + using System; using Configuration; @@ -15,19 +16,17 @@ public interface IConsumeTopologyConfigurator : new IMessageConsumeTopologyConfigurator GetMessageTopology() where T : class; + /// + /// Returns the specification for the message type + /// + /// + IMessageConsumeTopologyConfigurator GetMessageTopology(Type messageType); + /// /// Adds a convention to the topology, which will be applied to every message type /// requested, to determine if a convention for the message type is available. /// /// The Consume topology convention bool TryAddConvention(IConsumeTopologyConvention convention); - - /// - /// Add a Consume topology for a specific message type - /// - /// The message type - /// The topology - void AddMessageConsumeTopology(IMessageConsumeTopology topology) - where T : class; } } diff --git a/src/MassTransit.Abstractions/Configuration/Topology/IMessageConsumeTopologyConfigurator.cs b/src/MassTransit.Abstractions/Configuration/Topology/IMessageConsumeTopologyConfigurator.cs index 15a4841f5f2..71b3180f072 100644 --- a/src/MassTransit.Abstractions/Configuration/Topology/IMessageConsumeTopologyConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/Topology/IMessageConsumeTopologyConfigurator.cs @@ -14,12 +14,6 @@ public interface IMessageConsumeTopologyConfigurator : IMessageConsumeTopology where TMessage : class { - /// - /// Specify whether the broker topology should be configured for this message type - /// (defaults to true) - /// - bool ConfigureConsumeTopology { get; set; } - void Add(IMessageConsumeTopology consumeTopology); /// @@ -60,6 +54,12 @@ void AddOrUpdateConvention(Func add, Func + /// Specify whether the broker topology should be configured for this message type + /// (defaults to true) + /// + bool ConfigureConsumeTopology { get; set; } + bool TryAddConvention(IConsumeTopologyConvention convention); } } diff --git a/src/MassTransit.Abstractions/Configuration/Topology/IMessagePublishTopologyConfigurator.cs b/src/MassTransit.Abstractions/Configuration/Topology/IMessagePublishTopologyConfigurator.cs index 0e399b0f39c..fd2a3bac811 100644 --- a/src/MassTransit.Abstractions/Configuration/Topology/IMessagePublishTopologyConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/Topology/IMessagePublishTopologyConfigurator.cs @@ -50,5 +50,7 @@ public interface IMessagePublishTopologyConfigurator : /// Exclude the message type from being created as a topic/exchange. /// new bool Exclude { set; } + + bool TryAddConvention(IPublishTopologyConvention convention); } } diff --git a/src/MassTransit.Abstractions/Configuration/Topology/IMessageSendTopologyConfigurator.cs b/src/MassTransit.Abstractions/Configuration/Topology/IMessageSendTopologyConfigurator.cs index 018d92c36bb..fb14d58c691 100644 --- a/src/MassTransit.Abstractions/Configuration/Topology/IMessageSendTopologyConfigurator.cs +++ b/src/MassTransit.Abstractions/Configuration/Topology/IMessageSendTopologyConfigurator.cs @@ -1,6 +1,7 @@ namespace MassTransit { using System; + using System.Diagnostics.CodeAnalysis; using Configuration; @@ -55,7 +56,7 @@ void AddOrUpdateConvention(Func add, Func /// /// - bool TryGetConvention(out TConvention? convention) + bool TryGetConvention([NotNullWhen(true)] out TConvention? convention) where TConvention : class, IMessageSendTopologyConvention; } @@ -63,5 +64,6 @@ bool TryGetConvention(out TConvention? convention) public interface IMessageSendTopologyConfigurator : ISpecification { + bool TryAddConvention(ISendTopologyConvention convention); } } diff --git a/src/MassTransit.Abstractions/Contexts/ConsumeContext.cs b/src/MassTransit.Abstractions/Contexts/ConsumeContext.cs index c780cf8ad7e..ad4785f01da 100644 --- a/src/MassTransit.Abstractions/Contexts/ConsumeContext.cs +++ b/src/MassTransit.Abstractions/Contexts/ConsumeContext.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; diff --git a/src/MassTransit.Abstractions/Contexts/Context/MissingConsumeContext.cs b/src/MassTransit.Abstractions/Contexts/Context/MissingConsumeContext.cs index aabcb899bc4..5f05841d94d 100644 --- a/src/MassTransit.Abstractions/Contexts/Context/MissingConsumeContext.cs +++ b/src/MassTransit.Abstractions/Contexts/Context/MissingConsumeContext.cs @@ -2,6 +2,7 @@ namespace MassTransit.Context { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -16,7 +17,7 @@ public bool HasPayloadType(Type payloadType) throw new ConsumeContextNotAvailableException(); } - public bool TryGetPayload(out T? payload) + public bool TryGetPayload([NotNullWhen(true)] out T? payload) where T : class { throw new ConsumeContextNotAvailableException(); diff --git a/src/MassTransit.Abstractions/Contexts/Context/PendingFaultCollection.cs b/src/MassTransit.Abstractions/Contexts/Context/PendingFaultCollection.cs index da2ebff770e..2120172d3d4 100644 --- a/src/MassTransit.Abstractions/Contexts/Context/PendingFaultCollection.cs +++ b/src/MassTransit.Abstractions/Contexts/Context/PendingFaultCollection.cs @@ -8,7 +8,7 @@ namespace MassTransit.Context public class PendingFaultCollection { - readonly IList _pendingFaults; + readonly List _pendingFaults; public PendingFaultCollection() { diff --git a/src/MassTransit.Abstractions/Contexts/Context/PublishContextProxy.cs b/src/MassTransit.Abstractions/Contexts/Context/PublishContextProxy.cs index 919ebf55e5b..6465ed634bb 100644 --- a/src/MassTransit.Abstractions/Contexts/Context/PublishContextProxy.cs +++ b/src/MassTransit.Abstractions/Contexts/Context/PublishContextProxy.cs @@ -132,6 +132,12 @@ public ISerialization Serialization set => _context.Serialization = value; } + public string[] SupportedMessageTypes + { + get => _context.SupportedMessageTypes; + set => _context.SupportedMessageTypes = value; + } + public long? BodyLength => _context.BodyLength; public SendContext CreateProxy(T message) diff --git a/src/MassTransit.Abstractions/Contexts/Context/PublishEndpointConverterCache.cs b/src/MassTransit.Abstractions/Contexts/Context/PublishEndpointConverterCache.cs index bbf1b68a079..82cb63a6346 100644 --- a/src/MassTransit.Abstractions/Contexts/Context/PublishEndpointConverterCache.cs +++ b/src/MassTransit.Abstractions/Contexts/Context/PublishEndpointConverterCache.cs @@ -128,7 +128,7 @@ public Task PublishInitializer(IPublishEndpoint endpoint, object values, IPipe

Converters = - new Lazy(() => new PublishEndpointConverterCache(), LazyThreadSafetyMode.PublicationOnly); + new Lazy(() => new PublishEndpointConverterCache()); } } } diff --git a/src/MassTransit.Abstractions/Contexts/Context/ResponseEndpointConverterCache.cs b/src/MassTransit.Abstractions/Contexts/Context/ResponseEndpointConverterCache.cs index f68df12cc8d..78b577db316 100644 --- a/src/MassTransit.Abstractions/Contexts/Context/ResponseEndpointConverterCache.cs +++ b/src/MassTransit.Abstractions/Contexts/Context/ResponseEndpointConverterCache.cs @@ -2,7 +2,6 @@ namespace MassTransit.Context { using System; using System.Collections.Concurrent; - using System.Threading; using System.Threading.Tasks; @@ -93,7 +92,7 @@ Task IResponseEndpointConverter.Respond(ConsumeContext consumeContext, object me static class Cached { internal static readonly Lazy Converters = - new Lazy(() => new ResponseEndpointConverterCache(), LazyThreadSafetyMode.PublicationOnly); + new Lazy(() => new ResponseEndpointConverterCache()); } } } diff --git a/src/MassTransit.Abstractions/Contexts/Context/SendContextProxy.cs b/src/MassTransit.Abstractions/Contexts/Context/SendContextProxy.cs index 2cfca1b34de..aec04c019a6 100644 --- a/src/MassTransit.Abstractions/Contexts/Context/SendContextProxy.cs +++ b/src/MassTransit.Abstractions/Contexts/Context/SendContextProxy.cs @@ -131,6 +131,12 @@ public ISerialization Serialization set => _context.Serialization = value; } + public string[] SupportedMessageTypes + { + get => _context.SupportedMessageTypes; + set => _context.SupportedMessageTypes = value; + } + public long? BodyLength => _context.BodyLength; public SendContext CreateProxy(T message) diff --git a/src/MassTransit.Abstractions/Contexts/Context/SendContextScope.cs b/src/MassTransit.Abstractions/Contexts/Context/SendContextScope.cs index 949a8405c2d..ba5b98fd41e 100644 --- a/src/MassTransit.Abstractions/Contexts/Context/SendContextScope.cs +++ b/src/MassTransit.Abstractions/Contexts/Context/SendContextScope.cs @@ -1,7 +1,7 @@ namespace MassTransit.Context { using System; - using System.Reflection; + using System.Diagnostics.CodeAnalysis; using System.Threading; using Payloads; @@ -44,10 +44,10 @@ IPayloadCache PayloadCache public override bool HasPayloadType(Type payloadType) { - return payloadType.GetTypeInfo().IsInstanceOfType(this) || PayloadCache.HasPayloadType(payloadType) || _context.HasPayloadType(payloadType); + return payloadType.IsInstanceOfType(this) || PayloadCache.HasPayloadType(payloadType) || _context.HasPayloadType(payloadType); } - public override bool TryGetPayload(out T? payload) + public override bool TryGetPayload([NotNullWhen(true)] out T? payload) where T : class { if (this is T context) diff --git a/src/MassTransit.Abstractions/Contexts/Context/SendEndpointConverterCache.cs b/src/MassTransit.Abstractions/Contexts/Context/SendEndpointConverterCache.cs index 07ae296e228..d6bca7b3db3 100644 --- a/src/MassTransit.Abstractions/Contexts/Context/SendEndpointConverterCache.cs +++ b/src/MassTransit.Abstractions/Contexts/Context/SendEndpointConverterCache.cs @@ -128,8 +128,7 @@ public Task SendInitializer(ISendEndpoint endpoint, object values, IPipe Converters = - new Lazy(() => new SendEndpointConverterCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy Converters = new Lazy(() => new SendEndpointConverterCache()); } } } diff --git a/src/MassTransit.Abstractions/Contexts/HeaderValue.cs b/src/MassTransit.Abstractions/Contexts/HeaderValue.cs index f1bbcded1d1..5f6df87a261 100644 --- a/src/MassTransit.Abstractions/Contexts/HeaderValue.cs +++ b/src/MassTransit.Abstractions/Contexts/HeaderValue.cs @@ -2,6 +2,7 @@ namespace MassTransit { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; public readonly struct HeaderValue diff --git a/src/MassTransit.Abstractions/Contexts/Headers.cs b/src/MassTransit.Abstractions/Contexts/Headers.cs index 0cfab9b27fb..20be5f90f4d 100644 --- a/src/MassTransit.Abstractions/Contexts/Headers.cs +++ b/src/MassTransit.Abstractions/Contexts/Headers.cs @@ -1,7 +1,7 @@ -#nullable enable -namespace MassTransit +namespace MassTransit { using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; ///

diff --git a/src/MassTransit.Abstractions/Contexts/INotifyJobContext.cs b/src/MassTransit.Abstractions/Contexts/INotifyJobContext.cs new file mode 100644 index 00000000000..0e0acf8465f --- /dev/null +++ b/src/MassTransit.Abstractions/Contexts/INotifyJobContext.cs @@ -0,0 +1,15 @@ +namespace MassTransit; + +using System; +using System.Threading.Tasks; +using Contracts.JobService; + + +public interface INotifyJobContext +{ + Task NotifyCanceled(); + Task NotifyStarted(); + Task NotifyCompleted(); + Task NotifyFaulted(Exception exception, TimeSpan? delay = default); + Task NotifyJobProgress(SetJobProgress progress); +} diff --git a/src/MassTransit.Abstractions/Contexts/ITransportSequenceNumber.cs b/src/MassTransit.Abstractions/Contexts/ITransportSequenceNumber.cs new file mode 100644 index 00000000000..523f3ba7a02 --- /dev/null +++ b/src/MassTransit.Abstractions/Contexts/ITransportSequenceNumber.cs @@ -0,0 +1,7 @@ +namespace MassTransit +{ + public interface ITransportSequenceNumber + { + ulong? SequenceNumber { get; } + } +} diff --git a/src/MassTransit.Abstractions/Contexts/JobContext.cs b/src/MassTransit.Abstractions/Contexts/JobContext.cs index 87d7d1e7ad2..f7c6542a8bc 100644 --- a/src/MassTransit.Abstractions/Contexts/JobContext.cs +++ b/src/MassTransit.Abstractions/Contexts/JobContext.cs @@ -1,35 +1,82 @@ -namespace MassTransit +namespace MassTransit; + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; + + +public interface JobContext : + PipeContext, + MessageContext, + ISendEndpointProvider, + IPublishEndpoint +{ + Guid JobId { get; } + Guid AttemptId { get; } + + /// + /// If previously attempted, this value is > 0 + /// + int RetryAttempt { get; } + + /// + /// The last reported progress value for this job + /// + long? LastProgressValue { get; } + + /// + /// The last reported progress limit for this job + /// + long? LastProgressLimit { get; } + + /// + /// How long the job has been running + /// + TimeSpan ElapsedTime { get; } + + /// + /// The job properties that were supplied when the job was submitted + /// + IPropertyCollection JobProperties { get; } + + /// + /// Properties that were configured for this job type + /// + IPropertyCollection JobTypeProperties { get; } + + /// + /// Properties that were configured for this job consumer instance + /// + IPropertyCollection InstanceProperties { get; } + + /// + /// Sets the job's progress, which gets reported back to the job saga + /// + /// + /// + /// + Task SetJobProgress(long value, long? limit); + + /// + /// Save job state, typically when canceling or faulting, so that subsequent retries can resume from the saved state + /// + /// + /// + /// + Task SaveJobState(T? jobState) + where T : class; + + bool TryGetJobState([NotNullWhen(true)] out T? jobState) + where T : class; +} + + +public interface JobContext : + JobContext + where TMessage : class { - using System; - using System.Threading.Tasks; - - - public interface JobContext : - PipeContext, - MessageContext, - ISendEndpointProvider, - IPublishEndpoint - { - Guid JobId { get; } - Guid AttemptId { get; } - int RetryAttempt { get; } - - TimeSpan ElapsedTime { get; } - - Task NotifyCanceled(string? reason = null); - Task NotifyStarted(); - Task NotifyCompleted(); - Task NotifyFaulted(Exception exception, TimeSpan? delay = default); - } - - - public interface JobContext : - JobContext - where TMessage : class - { - /// - /// The message that initiated the job - /// - TMessage Job { get; } - } + /// + /// The message that initiated the job + /// + TMessage Job { get; } } diff --git a/src/MassTransit.Abstractions/Contexts/MessageBody.cs b/src/MassTransit.Abstractions/Contexts/MessageBody.cs index eec051a569b..e5656de5589 100644 --- a/src/MassTransit.Abstractions/Contexts/MessageBody.cs +++ b/src/MassTransit.Abstractions/Contexts/MessageBody.cs @@ -1,4 +1,3 @@ -#nullable enable namespace MassTransit { using System.IO; diff --git a/src/MassTransit.Abstractions/Contexts/PartitionKeyConsumeContext.cs b/src/MassTransit.Abstractions/Contexts/PartitionKeyConsumeContext.cs new file mode 100644 index 00000000000..812dee5a785 --- /dev/null +++ b/src/MassTransit.Abstractions/Contexts/PartitionKeyConsumeContext.cs @@ -0,0 +1,9 @@ +namespace MassTransit; + +public interface PartitionKeyConsumeContext +{ + /// + /// The partition key for the message (defaults to "") + /// + string? PartitionKey { get; } +} diff --git a/src/MassTransit.Abstractions/Contexts/PartitionKeyExtensions.cs b/src/MassTransit.Abstractions/Contexts/PartitionKeyExtensions.cs new file mode 100644 index 00000000000..14f50305c5e --- /dev/null +++ b/src/MassTransit.Abstractions/Contexts/PartitionKeyExtensions.cs @@ -0,0 +1,44 @@ +namespace MassTransit; + +using System; + + +public static class PartitionKeyExtensions +{ + public static string? PartitionKey(this ConsumeContext context) + { + return context.TryGetPayload(out PartitionKeyConsumeContext? consumeContext) ? consumeContext.PartitionKey : string.Empty; + } + + public static string? PartitionKey(this SendContext context) + { + return context.TryGetPayload(out PartitionKeySendContext? sendContext) ? sendContext.PartitionKey : string.Empty; + } + + /// + /// Sets the routing key for this message + /// + /// + /// The routing key for this message + public static void SetPartitionKey(this SendContext context, string? routingKey) + { + if (!context.TryGetPayload(out PartitionKeySendContext? sendContext)) + throw new ArgumentException("The SendPartitionKeyContext was not available"); + + sendContext.PartitionKey = routingKey; + } + + /// + /// Sets the routing key for this message + /// + /// + /// The routing key for this message + public static bool TrySetPartitionKey(this SendContext context, string? routingKey) + { + if (!context.TryGetPayload(out PartitionKeySendContext? sendContext)) + return false; + + sendContext.PartitionKey = routingKey; + return true; + } +} diff --git a/src/MassTransit.Abstractions/Contexts/PartitionKeySendContext.cs b/src/MassTransit.Abstractions/Contexts/PartitionKeySendContext.cs new file mode 100644 index 00000000000..2c712239675 --- /dev/null +++ b/src/MassTransit.Abstractions/Contexts/PartitionKeySendContext.cs @@ -0,0 +1,9 @@ +namespace MassTransit; + +public interface PartitionKeySendContext +{ + /// + /// The partition key for the message (defaults to "") + /// + string? PartitionKey { get; set; } +} diff --git a/src/MassTransit.Abstractions/Contexts/PipeContext.cs b/src/MassTransit.Abstractions/Contexts/PipeContext.cs index 3986244e98c..29e398097f6 100644 --- a/src/MassTransit.Abstractions/Contexts/PipeContext.cs +++ b/src/MassTransit.Abstractions/Contexts/PipeContext.cs @@ -1,6 +1,7 @@ namespace MassTransit { using System; + using System.Diagnostics.CodeAnalysis; using System.Threading; @@ -35,7 +36,7 @@ bool TryGetPayload([NotNullWhen(true)] out T? payload) /// Returns an existing payload or creates the payload using the factory method provided /// /// The payload type - /// The payload factory is the payload is not present + /// The payload factory to use if the payload is not already present /// The payload T GetOrAddPayload(PayloadFactory payloadFactory) where T : class; diff --git a/src/MassTransit.Abstractions/Contexts/ReceiveContextExtensions.cs b/src/MassTransit.Abstractions/Contexts/ReceiveContextExtensions.cs index 365bd75c3eb..8998b97d36d 100644 --- a/src/MassTransit.Abstractions/Contexts/ReceiveContextExtensions.cs +++ b/src/MassTransit.Abstractions/Contexts/ReceiveContextExtensions.cs @@ -1,4 +1,3 @@ -#nullable enable namespace MassTransit { using System; @@ -98,6 +97,16 @@ public static Guid GetMessageId(this ReceiveContext context, Guid defaultValue) return context.TransportHeaders.GetEndpointAddress(MessageHeaders.FaultAddress); } + /// + /// Returns the message sent timestamp from the transport (not message headers) + /// + /// + /// + public static DateTime? GetSentTime(this ReceiveContext context) + { + return context.TransportHeaders.GetTimestamp(MessageHeaders.TransportSentTime); + } + public static string[] GetMessageTypes(this ReceiveContext context) { if (context.TransportHeaders.TryGetHeader(MessageHeaders.MessageType, out var value) && value is string text && !string.IsNullOrWhiteSpace(text)) @@ -247,7 +256,22 @@ public static Guid GetHeaderId(this Headers headers, string key, Guid defaultVal { Guid guid => guid, string text when Guid.TryParse(text, out var guid) => guid, - _ => default + _ => default(Guid?) + }; + } + + return default; + } + + static DateTime? GetTimestamp(this Headers headers, string key) + { + if (headers.TryGetHeader(key, out var value)) + { + return value switch + { + DateTime dateTime => dateTime, + string text when DateTime.TryParse(text, out var dateTime) => dateTime, + _ => default(DateTime?) }; } diff --git a/src/MassTransit.Abstractions/Contexts/RespondAsyncExecuteExtensions.cs b/src/MassTransit.Abstractions/Contexts/RespondAsyncExecuteExtensions.cs index 3a088d9b80d..875e33bcfa6 100644 --- a/src/MassTransit.Abstractions/Contexts/RespondAsyncExecuteExtensions.cs +++ b/src/MassTransit.Abstractions/Contexts/RespondAsyncExecuteExtensions.cs @@ -46,7 +46,6 @@ public static Task RespondAsync(this ConsumeContext context, T message, Func< /// The context to send the message /// The message /// The callback for the send context - // To cancel the send from happening /// The task which is completed once the Send is acknowledged by the broker public static Task RespondAsync(this ConsumeContext context, object message, Action callback) { diff --git a/src/MassTransit.Abstractions/Contexts/RoutingKeyExtensions.cs b/src/MassTransit.Abstractions/Contexts/RoutingKeyExtensions.cs index c6145a284e7..146000433ef 100644 --- a/src/MassTransit.Abstractions/Contexts/RoutingKeyExtensions.cs +++ b/src/MassTransit.Abstractions/Contexts/RoutingKeyExtensions.cs @@ -1,45 +1,44 @@ -namespace MassTransit +namespace MassTransit; + +using System; + + +public static class RoutingKeyExtensions { - using System; + public static string? RoutingKey(this ConsumeContext context) + { + return context.TryGetPayload(out RoutingKeyConsumeContext? consumeContext) ? consumeContext.RoutingKey : string.Empty; + } + + public static string? RoutingKey(this SendContext context) + { + return context.TryGetPayload(out RoutingKeySendContext? sendContext) ? sendContext.RoutingKey : string.Empty; + } + + /// + /// Sets the routing key for this message + /// + /// + /// The routing key for this message + public static void SetRoutingKey(this SendContext context, string? routingKey) + { + if (!context.TryGetPayload(out RoutingKeySendContext? sendContext)) + throw new ArgumentException("The SendRoutingKeyContext was not available"); + sendContext.RoutingKey = routingKey; + } - public static class RoutingKeyExtensions + /// + /// Sets the routing key for this message + /// + /// + /// The routing key for this message + public static bool TrySetRoutingKey(this SendContext context, string? routingKey) { - public static string? RoutingKey(this ConsumeContext context) - { - return context.TryGetPayload(out RoutingKeyConsumeContext? consumeContext) ? consumeContext!.RoutingKey : string.Empty; - } - - public static string? RoutingKey(this SendContext context) - { - return context.TryGetPayload(out RoutingKeySendContext? sendContext) ? sendContext!.RoutingKey : string.Empty; - } - - /// - /// Sets the routing key for this message - /// - /// - /// The routing key for this message - public static void SetRoutingKey(this SendContext context, string? routingKey) - { - if (!context.TryGetPayload(out RoutingKeySendContext? sendContext)) - throw new ArgumentException("The SendRoutingKeyContext was not available"); - - sendContext!.RoutingKey = routingKey; - } - - /// - /// Sets the routing key for this message - /// - /// - /// The routing key for this message - public static bool TrySetRoutingKey(this SendContext context, string? routingKey) - { - if (!context.TryGetPayload(out RoutingKeySendContext? sendContext)) - return false; - - sendContext!.RoutingKey = routingKey; - return true; - } + if (!context.TryGetPayload(out RoutingKeySendContext? sendContext)) + return false; + + sendContext.RoutingKey = routingKey; + return true; } } diff --git a/src/MassTransit.Abstractions/Contexts/SendContext.cs b/src/MassTransit.Abstractions/Contexts/SendContext.cs index 0cdbb690727..9060d467192 100644 --- a/src/MassTransit.Abstractions/Contexts/SendContext.cs +++ b/src/MassTransit.Abstractions/Contexts/SendContext.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace MassTransit +namespace MassTransit { using System; using System.Net.Mime; @@ -69,6 +68,11 @@ public interface SendContext : /// ISerialization Serialization { get; set; } + /// + /// The supported message types for the message being sent/published. For internal use only. + /// + string[] SupportedMessageTypes { get; set; } + /// /// After serialization, should return the length of the message body /// diff --git a/src/MassTransit.Abstractions/Contexts/SerializerContext.cs b/src/MassTransit.Abstractions/Contexts/SerializerContext.cs index e3a697004a7..3be2b9d4b11 100644 --- a/src/MassTransit.Abstractions/Contexts/SerializerContext.cs +++ b/src/MassTransit.Abstractions/Contexts/SerializerContext.cs @@ -1,8 +1,8 @@ -#nullable enable namespace MassTransit { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using Serialization; diff --git a/src/MassTransit.Abstractions/Contexts/TimeSpanContextScheduleExtensions.cs b/src/MassTransit.Abstractions/Contexts/TimeSpanContextScheduleExtensions.cs index d391593ecb3..2ec46fb3dfb 100644 --- a/src/MassTransit.Abstractions/Contexts/TimeSpanContextScheduleExtensions.cs +++ b/src/MassTransit.Abstractions/Contexts/TimeSpanContextScheduleExtensions.cs @@ -44,6 +44,44 @@ public static Task> ScheduleSend(this MessageSchedulerCon return scheduler.ScheduleSend(scheduledTime, message, pipe, cancellationToken); } + /// + /// Send a message, using a callback to modify the send context instead of building a pipe from scratch + /// + /// The message type + /// The message scheduler + /// The time at which the message should be delivered to the queue + /// The message + /// The callback for the send context + /// To cancel the send from happening + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, T message, + Action> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + + /// + /// Send a message, using a callback to modify the send context instead of building a pipe from scratch + /// + /// The message type + /// The message scheduler + /// The time at which the message should be delivered to the queue + /// The message + /// The callback for the send context + /// To cancel the send from happening + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, T message, + Func, Task> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + /// /// Send a message /// @@ -63,6 +101,44 @@ public static Task> ScheduleSend(this MessageSchedulerCon return scheduler.ScheduleSend(scheduledTime, message, pipe, cancellationToken); } + /// + /// Send a message, using a callback to modify the send context instead of building a pipe from scratch + /// + /// The message type + /// The message scheduler + /// The time at which the message should be delivered to the queue + /// The message + /// The callback for the send context + /// To cancel the send from happening + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, T message, + Action callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + + /// + /// Send a message, using a callback to modify the send context instead of building a pipe from scratch + /// + /// The message type + /// The message scheduler + /// The time at which the message should be delivered to the queue + /// The message + /// The callback for the send context + /// To cancel the send from happening + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, T message, + Func callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + /// /// Sends an object as a message, using the type of the message instance. /// @@ -98,8 +174,7 @@ public static Task ScheduleSend(this MessageSchedulerContext s } /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The message scheduler /// The message object @@ -115,6 +190,40 @@ public static Task ScheduleSend(this MessageSchedulerContext s return scheduler.ScheduleSend(scheduledTime, message, pipe, cancellationToken); } + /// + /// Sends an object as a message. + /// + /// The message scheduler + /// The message object + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, object message, + Func callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + + /// + /// Sends an object as a message. + /// + /// The message scheduler + /// The message object + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, object message, + Action callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + /// /// Sends an object as a message, using the message type specified. If the object cannot be cast /// to the specified message type, an exception will be thrown. @@ -134,6 +243,40 @@ public static Task ScheduleSend(this MessageSchedulerContext s return scheduler.ScheduleSend(scheduledTime, message, messageType, pipe, cancellationToken); } + /// + /// Sends an object as a message, using the message type specified. If the object cannot be cast + /// to the specified message type, an exception will be thrown. + /// + /// The message scheduler + /// The message object + /// The type of the message (use message.GetType() if desired) + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, object message, Type messageType, + Action callback, CancellationToken cancellationToken = default) + { + return scheduler.ScheduleSend(delay, message, messageType, callback.ToPipe(), cancellationToken); + } + + /// + /// Sends an object as a message, using the message type specified. If the object cannot be cast + /// to the specified message type, an exception will be thrown. + /// + /// The message scheduler + /// The message object + /// The type of the message (use message.GetType() if desired) + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, object message, Type messageType, + Func callback, CancellationToken cancellationToken = default) + { + return scheduler.ScheduleSend(delay, message, messageType, callback.ToPipe(), cancellationToken); + } + /// /// Sends an interface message, initializing the properties of the interface using the anonymous /// object specified @@ -173,6 +316,46 @@ public static Task> ScheduleSend(this MessageSchedulerCon return scheduler.ScheduleSend(scheduledTime, values, pipe, cancellationToken); } + /// + /// Sends an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, object values, + Action> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, values, callback.ToPipe(), cancellationToken); + } + + /// + /// Sends an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, object values, + Func, Task> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, values, callback.ToPipe(), cancellationToken); + } + /// /// Sends an interface message, initializing the properties of the interface using the anonymous /// object specified @@ -192,5 +375,45 @@ public static Task> ScheduleSend(this MessageSchedulerCon return scheduler.ScheduleSend(scheduledTime, values, pipe, cancellationToken); } + + /// + /// Sends an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, object values, + Action callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, values, callback.ToPipe(), cancellationToken); + } + + /// + /// Sends an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this MessageSchedulerContext scheduler, TimeSpan delay, object values, + Func callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(scheduledTime, values, callback.ToPipe(), cancellationToken); + } } } diff --git a/src/MassTransit.Abstractions/Contexts/TimeSpanScheduleExtensions.cs b/src/MassTransit.Abstractions/Contexts/TimeSpanScheduleExtensions.cs index b5b8a7331f3..3bd03075d7c 100644 --- a/src/MassTransit.Abstractions/Contexts/TimeSpanScheduleExtensions.cs +++ b/src/MassTransit.Abstractions/Contexts/TimeSpanScheduleExtensions.cs @@ -46,6 +46,46 @@ public static Task> ScheduleSend(this IMessageScheduler s return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, pipe, cancellationToken); } + /// + /// Send a message + /// + /// The message type + /// The message scheduler + /// The message + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, T message, + Action> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, callback.ToPipe(), cancellationToken); + } + + /// + /// Send a message + /// + /// The message type + /// The message scheduler + /// The message + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, T message, + Func, Task> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, callback.ToPipe(), cancellationToken); + } + /// /// Send a message /// @@ -66,6 +106,46 @@ public static Task> ScheduleSend(this IMessageScheduler s return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, pipe, cancellationToken); } + /// + /// Send a message + /// + /// The message type + /// The message scheduler + /// The message + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// The cancellation token + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, T message, + Action callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, callback.ToPipe(), cancellationToken); + } + + /// + /// Send a message + /// + /// The message type + /// The message scheduler + /// The message + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// The cancellation token + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, T message, + Func callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, callback.ToPipe(), cancellationToken); + } + /// /// Sends an object as a message, using the type of the message instance. /// @@ -103,8 +183,7 @@ public static Task ScheduleSend(this IMessageScheduler schedul } /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The message scheduler /// The message object @@ -121,6 +200,42 @@ public static Task ScheduleSend(this IMessageScheduler schedul return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, pipe, cancellationToken); } + /// + /// Sends an object as a message. + /// + /// The message scheduler + /// The message object + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// The token used to cancel the operation + /// The task which is completed once the Send is acknowledged by the broker + public static Task ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, object message, + Action callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, callback.ToPipe(), cancellationToken); + } + + /// + /// Sends an object as a message. + /// + /// The message scheduler + /// The message object + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The callback for the send context + /// The token used to cancel the operation + /// The task which is completed once the Send is acknowledged by the broker + public static Task ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, object message, + Func callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, callback.ToPipe(), cancellationToken); + } + /// /// Sends an object as a message, using the message type specified. If the object cannot be cast /// to the specified message type, an exception will be thrown. @@ -141,6 +256,46 @@ public static Task ScheduleSend(this IMessageScheduler schedul return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, messageType, pipe, cancellationToken); } + /// + /// Sends an object as a message, using the message type specified. If the object cannot be cast + /// to the specified message type, an exception will be thrown. + /// + /// The message scheduler + /// The message object + /// The type of the message (use message.GetType() if desired) + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, object message, + Type messageType, Action callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, messageType, callback.ToPipe(), cancellationToken); + } + + /// + /// Sends an object as a message, using the message type specified. If the object cannot be cast + /// to the specified message type, an exception will be thrown. + /// + /// The message scheduler + /// The message object + /// The type of the message (use message.GetType() if desired) + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, object message, + Type messageType, Func callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, message, messageType, callback.ToPipe(), cancellationToken); + } + /// /// Sends an interface message, initializing the properties of the interface using the anonymous /// object specified @@ -182,6 +337,48 @@ public static Task> ScheduleSend(this IMessageScheduler s return scheduler.ScheduleSend(destinationAddress, scheduledTime, values, pipe, cancellationToken); } + /// + /// Sends an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The property values to initialize on the interface + /// The send callback + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, object values, + Action> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, values, callback.ToPipe(), cancellationToken); + } + + /// + /// Sends an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The property values to initialize on the interface + /// The send callback + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, object values, + Func, Task> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, values, callback.ToPipe(), cancellationToken); + } + /// /// Sends an interface message, initializing the properties of the interface using the anonymous /// object specified @@ -202,5 +399,47 @@ public static Task> ScheduleSend(this IMessageScheduler s return scheduler.ScheduleSend(destinationAddress, scheduledTime, values, pipe, cancellationToken); } + + /// + /// Sends an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, object values, + Action callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, values, callback.ToPipe(), cancellationToken); + } + + /// + /// Sends an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The destination address where the schedule message should be sent + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Send is acknowledged by the broker + public static Task> ScheduleSend(this IMessageScheduler scheduler, Uri destinationAddress, TimeSpan delay, object values, + Func callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.ScheduleSend(destinationAddress, scheduledTime, values, callback.ToPipe(), cancellationToken); + } } } diff --git a/src/MassTransit.Abstractions/Contexts/TimeSpanSchedulePublishExtensions.cs b/src/MassTransit.Abstractions/Contexts/TimeSpanSchedulePublishExtensions.cs index ab3cefc85ba..22f47f0bc47 100644 --- a/src/MassTransit.Abstractions/Contexts/TimeSpanSchedulePublishExtensions.cs +++ b/src/MassTransit.Abstractions/Contexts/TimeSpanSchedulePublishExtensions.cs @@ -44,6 +44,44 @@ public static Task> SchedulePublish(this IMessageSchedule return scheduler.SchedulePublish(scheduledTime, message, pipe, cancellationToken); } + /// + /// Publish a message + /// + /// The message type + /// The message scheduler + /// The message + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task> SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, T message, + Action> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + + /// + /// Publish a message + /// + /// The message type + /// The message scheduler + /// The message + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task> SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, T message, + Func, Task> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + /// /// Publish a message /// @@ -63,6 +101,44 @@ public static Task> SchedulePublish(this IMessageSchedule return scheduler.SchedulePublish(scheduledTime, message, pipe, cancellationToken); } + /// + /// Publish a message + /// + /// The message type + /// The message scheduler + /// The message + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task> SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, T message, + Action callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + + /// + /// Publish a message + /// + /// The message type + /// The message scheduler + /// The message + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task> SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, T message, + Func callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + /// /// Publishes an object as a message, using the type of the message instance. /// @@ -98,8 +174,7 @@ public static Task SchedulePublish(this IMessageScheduler sche } /// - /// Publishes an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Publishes an object as a message. /// /// The message scheduler /// The message object @@ -115,6 +190,40 @@ public static Task SchedulePublish(this IMessageScheduler sche return scheduler.SchedulePublish(scheduledTime, message, pipe, cancellationToken); } + /// + /// Publishes an object as a message. + /// + /// The message scheduler + /// The message object + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, object message, + Action callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + + /// + /// Publishes an object as a message. + /// + /// The message scheduler + /// The message object + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, object message, + Func callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, message, callback.ToPipe(), cancellationToken); + } + /// /// Publishes an object as a message, using the message type specified. If the object cannot be cast /// to the specified message type, an exception will be thrown. @@ -134,6 +243,44 @@ public static Task SchedulePublish(this IMessageScheduler sche return scheduler.SchedulePublish(scheduledTime, message, messageType, pipe, cancellationToken); } + /// + /// Publishes an object as a message, using the message type specified. If the object cannot be cast + /// to the specified message type, an exception will be thrown. + /// + /// The message scheduler + /// The message object + /// The type of the message (use message.GetType() if desired) + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, object message, + Type messageType, Action callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, message, messageType, callback.ToPipe(), cancellationToken); + } + + /// + /// Publishes an object as a message, using the message type specified. If the object cannot be cast + /// to the specified message type, an exception will be thrown. + /// + /// The message scheduler + /// The message object + /// The type of the message (use message.GetType() if desired) + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, object message, + Type messageType, Func callback, CancellationToken cancellationToken = default) + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, message, messageType, callback.ToPipe(), cancellationToken); + } + /// /// Publishes an interface message, initializing the properties of the interface using the anonymous /// object specified @@ -173,6 +320,46 @@ public static Task> SchedulePublish(this IMessageSchedule return scheduler.SchedulePublish(scheduledTime, values, pipe, cancellationToken); } + /// + /// Publishes an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task> SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, object values, + Action> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, values, callback.ToPipe(), cancellationToken); + } + + /// + /// Publishes an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task> SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, object values, + Func, Task> callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, values, callback.ToPipe(), cancellationToken); + } + /// /// Publishes an interface message, initializing the properties of the interface using the anonymous /// object specified @@ -192,5 +379,45 @@ public static Task> SchedulePublish(this IMessageSchedule return scheduler.SchedulePublish(scheduledTime, values, pipe, cancellationToken); } + + /// + /// Publishes an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task> SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, object values, + Action callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, values, callback.ToPipe(), cancellationToken); + } + + /// + /// Publishes an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The message scheduler + /// The property values to initialize on the interface + /// The time at which the message should be delivered to the queue + /// The send callback + /// + /// The task which is completed once the Publish is acknowledged by the broker + public static Task> SchedulePublish(this IMessageScheduler scheduler, TimeSpan delay, object values, + Func callback, CancellationToken cancellationToken = default) + where T : class + { + var scheduledTime = DateTime.UtcNow + delay; + + return scheduler.SchedulePublish(scheduledTime, values, callback.ToPipe(), cancellationToken); + } } } diff --git a/src/MassTransit.Abstractions/Courier/Courier/RoutingSlipEventPublisher.cs b/src/MassTransit.Abstractions/Courier/Courier/RoutingSlipEventPublisher.cs index dd0a729a016..5fa9e7b9dd8 100644 --- a/src/MassTransit.Abstractions/Courier/Courier/RoutingSlipEventPublisher.cs +++ b/src/MassTransit.Abstractions/Courier/Courier/RoutingSlipEventPublisher.cs @@ -3,6 +3,7 @@ namespace MassTransit.Courier using System; using System.Collections.Generic; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Contracts; using Messages; @@ -14,26 +15,30 @@ public class RoutingSlipEventPublisher : IRoutingSlipEventPublisher { static IDictionary? _emptyObject; + readonly CancellationToken _cancellationToken; readonly CourierContext? _context; readonly HostInfo _host; readonly IPublishEndpoint _publishEndpoint; readonly RoutingSlip _routingSlip; readonly ISendEndpointProvider _sendEndpointProvider; - public RoutingSlipEventPublisher(CourierContext context, RoutingSlip routingSlip) + public RoutingSlipEventPublisher(CourierContext context, RoutingSlip routingSlip, CancellationToken cancellationToken) { _sendEndpointProvider = context; _publishEndpoint = context; _routingSlip = routingSlip; + _cancellationToken = cancellationToken; _host = context.Host; _context = context; } - public RoutingSlipEventPublisher(ISendEndpointProvider sendEndpointProvider, IPublishEndpoint publishEndpoint, RoutingSlip routingSlip) + public RoutingSlipEventPublisher(ISendEndpointProvider sendEndpointProvider, IPublishEndpoint publishEndpoint, RoutingSlip routingSlip, + CancellationToken cancellationToken) { _sendEndpointProvider = sendEndpointProvider; _publishEndpoint = publishEndpoint; _routingSlip = routingSlip; + _cancellationToken = cancellationToken; _host = HostMetadataCache.Host; } @@ -206,7 +211,7 @@ async Task PublishEvent(RoutingSlipEvents eventFlag, Func sub.Events.HasFlag(RoutingSlipEvents.Supplemental))) - await _publishEndpoint.Publish(messageFactory(RoutingSlipEventContents.All)).ConfigureAwait(false); + await _publishEndpoint.Publish(messageFactory(RoutingSlipEventContents.All), _cancellationToken).ConfigureAwait(false); } async Task PublishSubscriptionEvent(RoutingSlipEvents eventFlag, Func messageFactory, Subscription subscription) @@ -226,10 +231,10 @@ async Task PublishSubscriptionEvent(RoutingSlipEvents eventFlag, Func(_context.SerializerContext, subscription.Message); - await endpoint.Send(message, adapter).ConfigureAwait(false); + await endpoint.Send(message, adapter, _cancellationToken).ConfigureAwait(false); } else - await endpoint.Send(message).ConfigureAwait(false); + await endpoint.Send(message, _cancellationToken).ConfigureAwait(false); } } } diff --git a/src/MassTransit.Abstractions/Courier/Courier/RoutingSlipExecutor.cs b/src/MassTransit.Abstractions/Courier/Courier/RoutingSlipExecutor.cs new file mode 100644 index 00000000000..7bbcbbdc6e4 --- /dev/null +++ b/src/MassTransit.Abstractions/Courier/Courier/RoutingSlipExecutor.cs @@ -0,0 +1,42 @@ +namespace MassTransit.Courier +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Contracts; + + + public class RoutingSlipExecutor : + IRoutingSlipExecutor + { + readonly IPublishEndpoint _publishEndpoint; + readonly ISendEndpointProvider _sendEndpointProvider; + + public RoutingSlipExecutor(ISendEndpointProvider sendEndpointProvider, IPublishEndpoint publishEndpoint) + { + _sendEndpointProvider = sendEndpointProvider; + _publishEndpoint = publishEndpoint; + } + + public async Task Execute(RoutingSlip routingSlip, CancellationToken cancellationToken = default) + { + if (routingSlip.RanToCompletion()) + { + var timestamp = DateTime.UtcNow; + var duration = timestamp - routingSlip.CreateTimestamp; + + IRoutingSlipEventPublisher publisher = new RoutingSlipEventPublisher(_sendEndpointProvider, _publishEndpoint, routingSlip, cancellationToken); + + await publisher.PublishRoutingSlipCompleted(timestamp, duration, routingSlip.Variables).ConfigureAwait(false); + } + else + { + var address = routingSlip.GetNextExecuteAddress() ?? throw new RoutingSlipException("Activity execute address was not specified."); + + var endpoint = await _sendEndpointProvider.GetSendEndpoint(address).ConfigureAwait(false); + + await endpoint.Send(routingSlip, cancellationToken).ConfigureAwait(false); + } + } + } +} diff --git a/src/MassTransit.Abstractions/Courier/RoutingSlipExtensions.cs b/src/MassTransit.Abstractions/Courier/RoutingSlipExtensions.cs index b306758a136..4db44cf49ae 100644 --- a/src/MassTransit.Abstractions/Courier/RoutingSlipExtensions.cs +++ b/src/MassTransit.Abstractions/Courier/RoutingSlipExtensions.cs @@ -2,6 +2,7 @@ { using System; using System.Linq; + using System.Threading; using System.Threading.Tasks; using Courier; using Courier.Contracts; @@ -29,26 +30,16 @@ public static bool RanToCompletion(this RoutingSlip routingSlip) return routingSlip.CompensateLogs.Select(x => x.Address).Last(); } - public static async Task Execute(this T source, RoutingSlip routingSlip) + public static Task Execute(this T source, RoutingSlip routingSlip) where T : IPublishEndpoint, ISendEndpointProvider { - if (routingSlip.RanToCompletion()) - { - var timestamp = DateTime.UtcNow; - var duration = timestamp - routingSlip.CreateTimestamp; - - IRoutingSlipEventPublisher publisher = new RoutingSlipEventPublisher(source, source, routingSlip); - - await publisher.PublishRoutingSlipCompleted(timestamp, duration, routingSlip.Variables).ConfigureAwait(false); - } - else - { - var address = routingSlip.GetNextExecuteAddress() ?? throw new RoutingSlipException("Activity execute address was not specified."); - - var endpoint = await source.GetSendEndpoint(address).ConfigureAwait(false); + return new RoutingSlipExecutor(source, source).Execute(routingSlip, CancellationToken.None); + } - await endpoint.Send(routingSlip).ConfigureAwait(false); - } + public static Task Execute(this T source, RoutingSlip routingSlip, CancellationToken cancellationToken) + where T : IPublishEndpoint, ISendEndpointProvider + { + return new RoutingSlipExecutor(source, source).Execute(routingSlip, cancellationToken); } } } diff --git a/src/MassTransit.Abstractions/Exceptions/AbstractUriException.cs b/src/MassTransit.Abstractions/Exceptions/AbstractUriException.cs index df664f7ae51..ac3c8c2f6d5 100644 --- a/src/MassTransit.Abstractions/Exceptions/AbstractUriException.cs +++ b/src/MassTransit.Abstractions/Exceptions/AbstractUriException.cs @@ -18,17 +18,20 @@ protected AbstractUriException(Uri uri) } protected AbstractUriException(Uri uri, string message) - : base(uri + " => " + message) + : base($"{uri} => {message}") { Uri = uri; } protected AbstractUriException(Uri uri, string message, Exception innerException) - : base(uri + " => " + message, innerException) + : base($"{uri} => {message}", innerException) { Uri = uri; } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected AbstractUriException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ActivityCompensationException.cs b/src/MassTransit.Abstractions/Exceptions/ActivityCompensationException.cs index 9a1d4edb68d..83b9807512b 100644 --- a/src/MassTransit.Abstractions/Exceptions/ActivityCompensationException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ActivityCompensationException.cs @@ -22,6 +22,9 @@ public ActivityCompensationException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ActivityCompensationException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ActivityExecutionException.cs b/src/MassTransit.Abstractions/Exceptions/ActivityExecutionException.cs index dc4bf0338b4..68d779d80aa 100644 --- a/src/MassTransit.Abstractions/Exceptions/ActivityExecutionException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ActivityExecutionException.cs @@ -22,6 +22,9 @@ public ActivityExecutionException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ActivityExecutionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ActivityExecutionFaultedException.cs b/src/MassTransit.Abstractions/Exceptions/ActivityExecutionFaultedException.cs index 00efc9e2857..200ba7cc175 100644 --- a/src/MassTransit.Abstractions/Exceptions/ActivityExecutionFaultedException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ActivityExecutionFaultedException.cs @@ -23,6 +23,9 @@ public ActivityExecutionFaultedException(string message, Exception innerExceptio { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ActivityExecutionFaultedException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/CommandException.cs b/src/MassTransit.Abstractions/Exceptions/CommandException.cs index fdbf37009ce..571cd9e731b 100644 --- a/src/MassTransit.Abstractions/Exceptions/CommandException.cs +++ b/src/MassTransit.Abstractions/Exceptions/CommandException.cs @@ -22,6 +22,9 @@ public CommandException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected CommandException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ConfigurationException.cs b/src/MassTransit.Abstractions/Exceptions/ConfigurationException.cs index 38c6ff9d039..b5e05f2b6e0 100644 --- a/src/MassTransit.Abstractions/Exceptions/ConfigurationException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ConfigurationException.cs @@ -35,6 +35,9 @@ public ConfigurationException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ConfigurationException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ConnectionException.cs b/src/MassTransit.Abstractions/Exceptions/ConnectionException.cs index 6bd754b41b1..caf6ad169b3 100644 --- a/src/MassTransit.Abstractions/Exceptions/ConnectionException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ConnectionException.cs @@ -29,6 +29,9 @@ public ConnectionException(string message, Exception innerException, bool isTran IsTransient = isTransient; } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ConnectionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ConsumeContextNotAvailableException.cs b/src/MassTransit.Abstractions/Exceptions/ConsumeContextNotAvailableException.cs index b7e88b8f440..367375ece59 100644 --- a/src/MassTransit.Abstractions/Exceptions/ConsumeContextNotAvailableException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ConsumeContextNotAvailableException.cs @@ -23,6 +23,9 @@ public ConsumeContextNotAvailableException(string message, Exception innerExcept { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ConsumeContextNotAvailableException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ConsumerCanceledException.cs b/src/MassTransit.Abstractions/Exceptions/ConsumerCanceledException.cs index f6fdd3bb4e8..663e2885158 100644 --- a/src/MassTransit.Abstractions/Exceptions/ConsumerCanceledException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ConsumerCanceledException.cs @@ -22,6 +22,9 @@ public ConsumerCanceledException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ConsumerCanceledException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ConsumerException.cs b/src/MassTransit.Abstractions/Exceptions/ConsumerException.cs index c40f67aa371..fd877d94260 100644 --- a/src/MassTransit.Abstractions/Exceptions/ConsumerException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ConsumerException.cs @@ -22,6 +22,9 @@ public ConsumerException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ConsumerException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ConsumerMessageException.cs b/src/MassTransit.Abstractions/Exceptions/ConsumerMessageException.cs index da5d6cc9193..157da3566f3 100644 --- a/src/MassTransit.Abstractions/Exceptions/ConsumerMessageException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ConsumerMessageException.cs @@ -22,6 +22,9 @@ public ConsumerMessageException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ConsumerMessageException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ConventionException.cs b/src/MassTransit.Abstractions/Exceptions/ConventionException.cs index 194d3041dd6..062a116f1d1 100644 --- a/src/MassTransit.Abstractions/Exceptions/ConventionException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ConventionException.cs @@ -22,6 +22,9 @@ public ConventionException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ConventionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/CourierException.cs b/src/MassTransit.Abstractions/Exceptions/CourierException.cs index b8cc712143d..67f42ee1734 100644 --- a/src/MassTransit.Abstractions/Exceptions/CourierException.cs +++ b/src/MassTransit.Abstractions/Exceptions/CourierException.cs @@ -22,6 +22,9 @@ public CourierException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected CourierException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/DuplicateKeyPipeConfigurationException.cs b/src/MassTransit.Abstractions/Exceptions/DuplicateKeyPipeConfigurationException.cs index 2d37829ce3c..9992de9dcf4 100644 --- a/src/MassTransit.Abstractions/Exceptions/DuplicateKeyPipeConfigurationException.cs +++ b/src/MassTransit.Abstractions/Exceptions/DuplicateKeyPipeConfigurationException.cs @@ -22,6 +22,9 @@ public DuplicateKeyPipeConfigurationException(string message, Exception innerExc { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected DuplicateKeyPipeConfigurationException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/EndpointException.cs b/src/MassTransit.Abstractions/Exceptions/EndpointException.cs index f310d9b9cbc..a4e007b42db 100644 --- a/src/MassTransit.Abstractions/Exceptions/EndpointException.cs +++ b/src/MassTransit.Abstractions/Exceptions/EndpointException.cs @@ -27,6 +27,9 @@ public EndpointException(Uri uri, string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected EndpointException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/EndpointNotFoundException.cs b/src/MassTransit.Abstractions/Exceptions/EndpointNotFoundException.cs index 3b35df161e6..44f387b0ea2 100644 --- a/src/MassTransit.Abstractions/Exceptions/EndpointNotFoundException.cs +++ b/src/MassTransit.Abstractions/Exceptions/EndpointNotFoundException.cs @@ -22,6 +22,9 @@ public EndpointNotFoundException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected EndpointNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/EventExecutionException.cs b/src/MassTransit.Abstractions/Exceptions/EventExecutionException.cs index 9081d94b015..5266671d549 100644 --- a/src/MassTransit.Abstractions/Exceptions/EventExecutionException.cs +++ b/src/MassTransit.Abstractions/Exceptions/EventExecutionException.cs @@ -18,6 +18,9 @@ public EventExecutionException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected EventExecutionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/FutureNotFoundException.cs b/src/MassTransit.Abstractions/Exceptions/FutureNotFoundException.cs index bdc4497e50f..88910150fed 100644 --- a/src/MassTransit.Abstractions/Exceptions/FutureNotFoundException.cs +++ b/src/MassTransit.Abstractions/Exceptions/FutureNotFoundException.cs @@ -27,6 +27,9 @@ public FutureNotFoundException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected FutureNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/InvalidCompensationAddressException.cs b/src/MassTransit.Abstractions/Exceptions/InvalidCompensationAddressException.cs index bb70e06ce0e..675052d0a65 100644 --- a/src/MassTransit.Abstractions/Exceptions/InvalidCompensationAddressException.cs +++ b/src/MassTransit.Abstractions/Exceptions/InvalidCompensationAddressException.cs @@ -27,6 +27,9 @@ public InvalidCompensationAddressException(string message, Exception innerExcept { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected InvalidCompensationAddressException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/JobAlreadyExistsException.cs b/src/MassTransit.Abstractions/Exceptions/JobAlreadyExistsException.cs index c178e6700cb..09e7c2ea4a3 100644 --- a/src/MassTransit.Abstractions/Exceptions/JobAlreadyExistsException.cs +++ b/src/MassTransit.Abstractions/Exceptions/JobAlreadyExistsException.cs @@ -17,6 +17,9 @@ public JobAlreadyExistsException(Guid jobId) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected JobAlreadyExistsException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/MassTransitException.cs b/src/MassTransit.Abstractions/Exceptions/MassTransitException.cs index 6bbcb1bb13a..22fad794a7b 100644 --- a/src/MassTransit.Abstractions/Exceptions/MassTransitException.cs +++ b/src/MassTransit.Abstractions/Exceptions/MassTransitException.cs @@ -22,6 +22,9 @@ public MassTransitException(string? message, Exception? innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MassTransitException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/MessageDataException.cs b/src/MassTransit.Abstractions/Exceptions/MessageDataException.cs index d20cbb8313e..0a44a35781a 100644 --- a/src/MassTransit.Abstractions/Exceptions/MessageDataException.cs +++ b/src/MassTransit.Abstractions/Exceptions/MessageDataException.cs @@ -22,6 +22,9 @@ public MessageDataException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MessageDataException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/MessageInitializerException.cs b/src/MassTransit.Abstractions/Exceptions/MessageInitializerException.cs index 9c656769e96..ba17db3109b 100644 --- a/src/MassTransit.Abstractions/Exceptions/MessageInitializerException.cs +++ b/src/MassTransit.Abstractions/Exceptions/MessageInitializerException.cs @@ -22,6 +22,9 @@ public MessageInitializerException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MessageInitializerException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/MessageNotConsumedException.cs b/src/MassTransit.Abstractions/Exceptions/MessageNotConsumedException.cs index b297b6a74e3..1204a534e66 100644 --- a/src/MassTransit.Abstractions/Exceptions/MessageNotConsumedException.cs +++ b/src/MassTransit.Abstractions/Exceptions/MessageNotConsumedException.cs @@ -12,6 +12,9 @@ public MessageNotConsumedException() { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MessageNotConsumedException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/MessageRetryLimitExceededException.cs b/src/MassTransit.Abstractions/Exceptions/MessageRetryLimitExceededException.cs index 1b8b2cb3f6d..04785d3476d 100644 --- a/src/MassTransit.Abstractions/Exceptions/MessageRetryLimitExceededException.cs +++ b/src/MassTransit.Abstractions/Exceptions/MessageRetryLimitExceededException.cs @@ -12,6 +12,9 @@ public MessageRetryLimitExceededException() { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MessageRetryLimitExceededException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/NinjectCantHandleThis.cs b/src/MassTransit.Abstractions/Exceptions/NinjectCantHandleThis.cs index 800e4aa9a4b..84dd9891535 100644 --- a/src/MassTransit.Abstractions/Exceptions/NinjectCantHandleThis.cs +++ b/src/MassTransit.Abstractions/Exceptions/NinjectCantHandleThis.cs @@ -23,6 +23,9 @@ public NinjectCantHandleThis(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected NinjectCantHandleThis(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/NotImplementedByDesignException.cs b/src/MassTransit.Abstractions/Exceptions/NotImplementedByDesignException.cs index 0a89947fc2b..e6dece5e962 100644 --- a/src/MassTransit.Abstractions/Exceptions/NotImplementedByDesignException.cs +++ b/src/MassTransit.Abstractions/Exceptions/NotImplementedByDesignException.cs @@ -23,6 +23,9 @@ public NotImplementedByDesignException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected NotImplementedByDesignException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/PayloadException.cs b/src/MassTransit.Abstractions/Exceptions/PayloadException.cs index de25070eb9d..76ab164a757 100644 --- a/src/MassTransit.Abstractions/Exceptions/PayloadException.cs +++ b/src/MassTransit.Abstractions/Exceptions/PayloadException.cs @@ -22,6 +22,9 @@ public PayloadException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected PayloadException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/PayloadFactoryException.cs b/src/MassTransit.Abstractions/Exceptions/PayloadFactoryException.cs index 35142857a0c..003de3aa904 100644 --- a/src/MassTransit.Abstractions/Exceptions/PayloadFactoryException.cs +++ b/src/MassTransit.Abstractions/Exceptions/PayloadFactoryException.cs @@ -22,6 +22,9 @@ public PayloadFactoryException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected PayloadFactoryException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/PayloadNotFoundException.cs b/src/MassTransit.Abstractions/Exceptions/PayloadNotFoundException.cs index c83b744b480..e0919b3a5b5 100644 --- a/src/MassTransit.Abstractions/Exceptions/PayloadNotFoundException.cs +++ b/src/MassTransit.Abstractions/Exceptions/PayloadNotFoundException.cs @@ -22,6 +22,9 @@ public PayloadNotFoundException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected PayloadNotFoundException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/PipeBuilderException.cs b/src/MassTransit.Abstractions/Exceptions/PipeBuilderException.cs index 533eec9d14c..c381afe969e 100644 --- a/src/MassTransit.Abstractions/Exceptions/PipeBuilderException.cs +++ b/src/MassTransit.Abstractions/Exceptions/PipeBuilderException.cs @@ -22,6 +22,9 @@ public PipeFactoryException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected PipeFactoryException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/PipeConfigurationException.cs b/src/MassTransit.Abstractions/Exceptions/PipeConfigurationException.cs index 087bbec4d9c..d3770f134e9 100644 --- a/src/MassTransit.Abstractions/Exceptions/PipeConfigurationException.cs +++ b/src/MassTransit.Abstractions/Exceptions/PipeConfigurationException.cs @@ -22,6 +22,9 @@ public PipeConfigurationException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected PipeConfigurationException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/PipelineException.cs b/src/MassTransit.Abstractions/Exceptions/PipelineException.cs index bbe3416f1c8..f2f0ebafa3a 100644 --- a/src/MassTransit.Abstractions/Exceptions/PipelineException.cs +++ b/src/MassTransit.Abstractions/Exceptions/PipelineException.cs @@ -23,6 +23,9 @@ public PipelineException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected PipelineException(SerializationInfo info, StreamingContext context) : base(info, context) diff --git a/src/MassTransit.Abstractions/Exceptions/ProduceException.cs b/src/MassTransit.Abstractions/Exceptions/ProduceException.cs index 648b97edee0..ae8cd8a3bf9 100644 --- a/src/MassTransit.Abstractions/Exceptions/ProduceException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ProduceException.cs @@ -22,6 +22,9 @@ public ProduceException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ProduceException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/PublishException.cs b/src/MassTransit.Abstractions/Exceptions/PublishException.cs index 0b835612aa4..97d617c9c54 100644 --- a/src/MassTransit.Abstractions/Exceptions/PublishException.cs +++ b/src/MassTransit.Abstractions/Exceptions/PublishException.cs @@ -22,6 +22,9 @@ public PublishException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected PublishException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/RecurringJobException.cs b/src/MassTransit.Abstractions/Exceptions/RecurringJobException.cs new file mode 100644 index 00000000000..aec5984b724 --- /dev/null +++ b/src/MassTransit.Abstractions/Exceptions/RecurringJobException.cs @@ -0,0 +1,18 @@ +namespace MassTransit; + +using System; + + +[Serializable] +public class RecurringJobException : + MassTransitException +{ + public RecurringJobException() + { + } + + public RecurringJobException(string message) + : base(message) + { + } +} diff --git a/src/MassTransit.Abstractions/Exceptions/RequestCanceledException.cs b/src/MassTransit.Abstractions/Exceptions/RequestCanceledException.cs index 2ead3bd9713..34f83a6b015 100644 --- a/src/MassTransit.Abstractions/Exceptions/RequestCanceledException.cs +++ b/src/MassTransit.Abstractions/Exceptions/RequestCanceledException.cs @@ -23,6 +23,9 @@ public RequestCanceledException(string requestId, Exception innerException, Canc { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected RequestCanceledException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/RequestException.cs b/src/MassTransit.Abstractions/Exceptions/RequestException.cs index 0aa86f5285b..b4885f2c518 100644 --- a/src/MassTransit.Abstractions/Exceptions/RequestException.cs +++ b/src/MassTransit.Abstractions/Exceptions/RequestException.cs @@ -28,6 +28,9 @@ public RequestException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected RequestException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/RequestFaultException.cs b/src/MassTransit.Abstractions/Exceptions/RequestFaultException.cs index 7f0a202d9cd..c6c5fc6f255 100644 --- a/src/MassTransit.Abstractions/Exceptions/RequestFaultException.cs +++ b/src/MassTransit.Abstractions/Exceptions/RequestFaultException.cs @@ -20,6 +20,9 @@ public RequestFaultException() { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected RequestFaultException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -30,6 +33,9 @@ protected RequestFaultException(SerializationInfo info, StreamingContext context public string? RequestType { get; private set; } public Fault? Fault { get; private set; } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); diff --git a/src/MassTransit.Abstractions/Exceptions/RequestTimeoutException.cs b/src/MassTransit.Abstractions/Exceptions/RequestTimeoutException.cs index c38c97f39a5..c696ecaa40a 100644 --- a/src/MassTransit.Abstractions/Exceptions/RequestTimeoutException.cs +++ b/src/MassTransit.Abstractions/Exceptions/RequestTimeoutException.cs @@ -22,6 +22,9 @@ public RequestTimeoutException(string requestId, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected RequestTimeoutException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/RoutingSlipArgumentException.cs b/src/MassTransit.Abstractions/Exceptions/RoutingSlipArgumentException.cs index 16628d6637a..3d697d0bdc1 100644 --- a/src/MassTransit.Abstractions/Exceptions/RoutingSlipArgumentException.cs +++ b/src/MassTransit.Abstractions/Exceptions/RoutingSlipArgumentException.cs @@ -22,6 +22,9 @@ public RoutingSlipArgumentException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected RoutingSlipArgumentException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/RoutingSlipException.cs b/src/MassTransit.Abstractions/Exceptions/RoutingSlipException.cs index 93e1718ecbd..87ae5e50948 100644 --- a/src/MassTransit.Abstractions/Exceptions/RoutingSlipException.cs +++ b/src/MassTransit.Abstractions/Exceptions/RoutingSlipException.cs @@ -22,6 +22,9 @@ public RoutingSlipException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected RoutingSlipException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/RoutingSlipRequestFaultedException.cs b/src/MassTransit.Abstractions/Exceptions/RoutingSlipRequestFaultedException.cs new file mode 100644 index 00000000000..36b05e45003 --- /dev/null +++ b/src/MassTransit.Abstractions/Exceptions/RoutingSlipRequestFaultedException.cs @@ -0,0 +1,17 @@ +namespace MassTransit +{ + using Courier.Contracts; + + + public class RoutingSlipRequestFaultedException : + RoutingSlipException + { + public RoutingSlipRequestFaultedException(RoutingSlipFaulted faulted) + : base("The routing slip request faulted") + { + Faulted = faulted; + } + + public RoutingSlipFaulted Faulted { get; } + } +} diff --git a/src/MassTransit.Abstractions/Exceptions/SagaStateMachineException.cs b/src/MassTransit.Abstractions/Exceptions/SagaStateMachineException.cs index 913279ef971..8ceca957883 100644 --- a/src/MassTransit.Abstractions/Exceptions/SagaStateMachineException.cs +++ b/src/MassTransit.Abstractions/Exceptions/SagaStateMachineException.cs @@ -32,6 +32,9 @@ public SagaStateMachineException(Type machineType, string message, Exception inn { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected SagaStateMachineException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/SendException.cs b/src/MassTransit.Abstractions/Exceptions/SendException.cs index dab48e4988c..d145faef126 100644 --- a/src/MassTransit.Abstractions/Exceptions/SendException.cs +++ b/src/MassTransit.Abstractions/Exceptions/SendException.cs @@ -30,6 +30,9 @@ public SendException(Type messageType, Uri uri, string message, Exception innerE MessageType = messageType; } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected SendException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/TransportException.cs b/src/MassTransit.Abstractions/Exceptions/TransportException.cs index c8cc52cf6f2..209b058ca5f 100644 --- a/src/MassTransit.Abstractions/Exceptions/TransportException.cs +++ b/src/MassTransit.Abstractions/Exceptions/TransportException.cs @@ -27,6 +27,9 @@ public TransportException(Uri uri, string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected TransportException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/TransportUnavailableException.cs b/src/MassTransit.Abstractions/Exceptions/TransportUnavailableException.cs index 93f33187c68..9b41c2ed066 100644 --- a/src/MassTransit.Abstractions/Exceptions/TransportUnavailableException.cs +++ b/src/MassTransit.Abstractions/Exceptions/TransportUnavailableException.cs @@ -22,6 +22,9 @@ public TransportUnavailableException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected TransportUnavailableException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/UnhandledEventException.cs b/src/MassTransit.Abstractions/Exceptions/UnhandledEventException.cs index a39e2620694..fb93af60849 100644 --- a/src/MassTransit.Abstractions/Exceptions/UnhandledEventException.cs +++ b/src/MassTransit.Abstractions/Exceptions/UnhandledEventException.cs @@ -17,6 +17,9 @@ public UnhandledEventException(string machineType, string eventName, string stat { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected UnhandledEventException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/UnknownEventException.cs b/src/MassTransit.Abstractions/Exceptions/UnknownEventException.cs index f691079f958..ad31b84db18 100644 --- a/src/MassTransit.Abstractions/Exceptions/UnknownEventException.cs +++ b/src/MassTransit.Abstractions/Exceptions/UnknownEventException.cs @@ -17,6 +17,9 @@ public UnknownEventException(string machineType, string eventName) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected UnknownEventException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/UnknownStateException.cs b/src/MassTransit.Abstractions/Exceptions/UnknownStateException.cs index dd0acebcd0e..814694cd608 100644 --- a/src/MassTransit.Abstractions/Exceptions/UnknownStateException.cs +++ b/src/MassTransit.Abstractions/Exceptions/UnknownStateException.cs @@ -17,6 +17,9 @@ public UnknownStateException(string machineType, string stateName) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected UnknownStateException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Exceptions/ValueFactoryException.cs b/src/MassTransit.Abstractions/Exceptions/ValueFactoryException.cs index a809a5ce940..55f8e98cd08 100644 --- a/src/MassTransit.Abstractions/Exceptions/ValueFactoryException.cs +++ b/src/MassTransit.Abstractions/Exceptions/ValueFactoryException.cs @@ -17,6 +17,9 @@ public ValueFactoryException(string message) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ValueFactoryException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/ILoadSagaRepository.cs b/src/MassTransit.Abstractions/ILoadSagaRepository.cs index 0f58c9180cd..63c66111659 100644 --- a/src/MassTransit.Abstractions/ILoadSagaRepository.cs +++ b/src/MassTransit.Abstractions/ILoadSagaRepository.cs @@ -4,7 +4,8 @@ namespace MassTransit using System.Threading.Tasks; - public interface ILoadSagaRepository + public interface ILoadSagaRepository : + IProbeSite where TSaga : class, ISaga { Task Load(Guid correlationId); diff --git a/src/MassTransit.Abstractions/IPublishEndpoint.cs b/src/MassTransit.Abstractions/IPublishEndpoint.cs index c60cc632c2b..4b67c46c3af 100644 --- a/src/MassTransit.Abstractions/IPublishEndpoint.cs +++ b/src/MassTransit.Abstractions/IPublishEndpoint.cs @@ -54,16 +54,14 @@ Task Publish(T message, IPipe publishPipe, CancellationToken where T : class; /// - /// Publishes an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Publishes an object as a message. /// /// The message object /// Task Publish(object message, CancellationToken cancellationToken = default); /// - /// Publishes an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Publishes an object as a message. /// /// The message object /// diff --git a/src/MassTransit.Abstractions/IRoutingSlipExecutor.cs b/src/MassTransit.Abstractions/IRoutingSlipExecutor.cs new file mode 100644 index 00000000000..56d040d4b5c --- /dev/null +++ b/src/MassTransit.Abstractions/IRoutingSlipExecutor.cs @@ -0,0 +1,18 @@ +namespace MassTransit +{ + using System.Threading; + using System.Threading.Tasks; + using Courier.Contracts; + + + public interface IRoutingSlipExecutor + { + /// + /// Execute a routing slip + /// + /// + /// + /// + Task Execute(RoutingSlip routingSlip, CancellationToken cancellationToken = default); + } +} diff --git a/src/MassTransit.Abstractions/ISendEndpoint.cs b/src/MassTransit.Abstractions/ISendEndpoint.cs index b8fcfc3c0b4..c2ace619b5d 100644 --- a/src/MassTransit.Abstractions/ISendEndpoint.cs +++ b/src/MassTransit.Abstractions/ISendEndpoint.cs @@ -59,8 +59,7 @@ Task Send(T message, IPipe pipe, CancellationToken cancellationT Task Send(object message, Type messageType, CancellationToken cancellationToken = default); /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The message object /// diff --git a/src/MassTransit.Abstractions/Internals/Extensions/DateTimeConstants.cs b/src/MassTransit.Abstractions/Internals/Extensions/DateTimeConstants.cs new file mode 100644 index 00000000000..249203afbbe --- /dev/null +++ b/src/MassTransit.Abstractions/Internals/Extensions/DateTimeConstants.cs @@ -0,0 +1,9 @@ +namespace MassTransit.Internals; + +using System; + + +public static class DateTimeConstants +{ + public static readonly DateTime Epoch = new DateTime(1970, 1, 1); +} diff --git a/src/MassTransit.Abstractions/Internals/Extensions/DictionaryExtensions.cs b/src/MassTransit.Abstractions/Internals/Extensions/DictionaryExtensions.cs index 32fc4e916e6..a64019c7ff2 100644 --- a/src/MassTransit.Abstractions/Internals/Extensions/DictionaryExtensions.cs +++ b/src/MassTransit.Abstractions/Internals/Extensions/DictionaryExtensions.cs @@ -38,5 +38,52 @@ public static IDictionary MergeLeft(this IDictionary SetValue(this Dictionary dictionary, string key, string? value) + { + if (dictionary == null) + throw new ArgumentNullException(nameof(dictionary)); + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (value == null) + dictionary.Remove(key); + else + dictionary[key] = value; + + return dictionary; + } + + public static Dictionary SetValue(this Dictionary dictionary, string key, object? value, bool overwrite = true) + { + if (dictionary == null) + throw new ArgumentNullException(nameof(dictionary)); + if (key == null) + throw new ArgumentNullException(nameof(key)); + + if (overwrite) + { + if (value == null) + dictionary.Remove(key); + else + dictionary[key] = value; + } + else if (!dictionary.ContainsKey(key) && value != null) + dictionary.Add(key, value); + + return dictionary; + } + + public static Dictionary SetValues(this Dictionary dictionary, IEnumerable>? properties, + bool overwrite = true) + { + if (properties != null) + { + foreach (KeyValuePair header in properties) + SetValue(dictionary, header.Key, header.Value, overwrite); + } + + return dictionary; + } } } diff --git a/src/MassTransit.Abstractions/Internals/Extensions/InterfaceExtensions.cs b/src/MassTransit.Abstractions/Internals/Extensions/InterfaceExtensions.cs index bf3b44feb98..7362a955daf 100644 --- a/src/MassTransit.Abstractions/Internals/Extensions/InterfaceExtensions.cs +++ b/src/MassTransit.Abstractions/Internals/Extensions/InterfaceExtensions.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Linq; - using System.Reflection; using System.Threading.Tasks; @@ -29,14 +28,13 @@ public static bool HasInterface(this Type type, Type interfaceType) if (interfaceType == null) throw new ArgumentNullException(nameof(interfaceType)); - var interfaceTypeInfo = interfaceType.GetTypeInfo(); - if (!interfaceTypeInfo.IsInterface) + if (!interfaceType.IsInterface) throw new ArgumentException("The interface type must be an interface: " + interfaceType.Name); - if (interfaceTypeInfo.IsGenericTypeDefinition) + if (interfaceType.IsGenericTypeDefinition) return _cache.GetGenericInterface(type, interfaceType) != null; - return interfaceTypeInfo.IsAssignableFrom(type.GetTypeInfo()); + return interfaceType.IsAssignableFrom(type); } public static Type? GetInterface(this Type type) @@ -52,8 +50,7 @@ public static bool HasInterface(this Type type, Type interfaceType) if (interfaceType == null) throw new ArgumentNullException(nameof(interfaceType)); - var interfaceTypeInfo = interfaceType.GetTypeInfo(); - if (!interfaceTypeInfo.IsInterface) + if (!interfaceType.IsInterface) throw new ArgumentException("The interface type must be an interface: " + interfaceType.Name); return _cache.Get(type, interfaceType); @@ -106,7 +103,7 @@ public static bool ClosesType(this Type type, Type openType, out Type? closedTyp if (!openType.IsOpenGeneric()) throw new ArgumentException("The interface type must be an open generic interface: " + openType.Name); - if (openType.GetTypeInfo().IsInterface) + if (openType.IsInterface) { if (!openType.IsOpenGeneric()) throw new ArgumentException("The interface type must be an open generic interface: " + openType.Name); @@ -118,10 +115,9 @@ public static bool ClosesType(this Type type, Type openType, out Type? closedTyp return false; } - var typeInfo = interfaceType.GetTypeInfo(); - if (!typeInfo.IsGenericTypeDefinition && !typeInfo.ContainsGenericParameters) + if (interfaceType is { IsGenericTypeDefinition: false, ContainsGenericParameters: false }) { - closedType = typeInfo; + closedType = interfaceType; return true; } @@ -132,12 +128,11 @@ public static bool ClosesType(this Type type, Type openType, out Type? closedTyp var baseType = type; while (baseType != null && baseType != typeof(object)) { - var baseTypeInfo = baseType.GetTypeInfo(); - if (baseTypeInfo.IsGenericType && baseTypeInfo.GetGenericTypeDefinition() == openType) + if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == openType) { - if (!baseTypeInfo.IsGenericTypeDefinition && !baseTypeInfo.ContainsGenericParameters) + if (!baseType.IsGenericTypeDefinition && !baseType.ContainsGenericParameters) { - closedType = baseTypeInfo; + closedType = baseType; return true; } @@ -145,13 +140,13 @@ public static bool ClosesType(this Type type, Type openType, out Type? closedTyp return false; } - if (!baseTypeInfo.IsGenericType && baseType == openType) + if (!baseType.IsGenericType && baseType == openType) { - closedType = baseTypeInfo; + closedType = baseType; return true; } - baseType = baseTypeInfo.BaseType; + baseType = baseType.BaseType; } closedType = default; @@ -174,7 +169,7 @@ public static IEnumerable GetClosingArguments(this Type type, Type openTyp if (!openType.IsOpenGeneric()) throw new ArgumentException("The interface type must be an open generic interface: " + openType.Name); - if (openType.GetTypeInfo().IsInterface) + if (openType.IsInterface) { if (!openType.IsOpenGeneric()) throw new ArgumentException("The interface type must be an open generic interface: " + openType.Name); @@ -183,20 +178,19 @@ public static IEnumerable GetClosingArguments(this Type type, Type openTyp if (interfaceType == null) throw new ArgumentException("The interface type is not implemented by: " + type.Name); - return interfaceType.GetTypeInfo().GetGenericArguments().Where(x => !x.IsGenericParameter); + return interfaceType.GetGenericArguments().Where(x => !x.IsGenericParameter); } var baseType = type; while (baseType != null && baseType != typeof(object)) { - var baseTypeInfo = baseType.GetTypeInfo(); - if (baseTypeInfo.IsGenericType && baseType.GetGenericTypeDefinition() == openType) - return baseTypeInfo.GetGenericArguments().Where(x => !x.IsGenericParameter); + if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == openType) + return baseType.GetGenericArguments().Where(x => !x.IsGenericParameter); - if (!baseTypeInfo.IsGenericType && baseType == openType) - return baseTypeInfo.GetGenericArguments().Where(x => !x.IsGenericParameter); + if (!baseType.IsGenericType && baseType == openType) + return baseType.GetGenericArguments().Where(x => !x.IsGenericParameter); - baseType = baseTypeInfo.BaseType; + baseType = baseType.BaseType; } throw new ArgumentException("Could not find open type in type: " + type.Name); diff --git a/src/MassTransit.Abstractions/Internals/Extensions/QueryStringExtensions.cs b/src/MassTransit.Abstractions/Internals/Extensions/QueryStringExtensions.cs index 36fda156aee..f2aac49b5fc 100644 --- a/src/MassTransit.Abstractions/Internals/Extensions/QueryStringExtensions.cs +++ b/src/MassTransit.Abstractions/Internals/Extensions/QueryStringExtensions.cs @@ -106,6 +106,9 @@ public static void ParseHostPathAndEntityName(this Uri address, out string? host hostPath = "/"; entityName = path.Substring(1); } + + if (entityName.Contains('%')) + entityName = Uri.UnescapeDataString(entityName); } /// diff --git a/src/MassTransit.Abstractions/Internals/Extensions/TaskExtensions.cs b/src/MassTransit.Abstractions/Internals/Extensions/TaskExtensions.cs index 1d37078c932..b78d50326eb 100644 --- a/src/MassTransit.Abstractions/Internals/Extensions/TaskExtensions.cs +++ b/src/MassTransit.Abstractions/Internals/Extensions/TaskExtensions.cs @@ -105,9 +105,10 @@ static Task OrTimeoutInternal(this Task task, TimeSpan timeout, CancellationToke async Task WaitAsync() { - var delayTask = Task.Delay(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : timeout, cancellationToken); - + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var delayTask = Task.Delay(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : timeout, cts.Token); var completed = await Task.WhenAny(task, delayTask).ConfigureAwait(false); + if (completed == delayTask) { task.IgnoreUnobservedExceptions(); @@ -115,6 +116,8 @@ async Task WaitAsync() throw new TimeoutException(FormatTimeoutMessage(memberName, filePath, lineNumber)); } + cts.Cancel(); + task.GetAwaiter().GetResult(); } @@ -153,9 +156,10 @@ static Task OrTimeoutInternal(this Task task, TimeSpan timeout, Cancell async Task WaitAsync() { - var delayTask = Task.Delay(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : timeout, cancellationToken); - + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + var delayTask = Task.Delay(Debugger.IsAttached ? Timeout.InfiniteTimeSpan : timeout, cts.Token); var completed = await Task.WhenAny(task, delayTask).ConfigureAwait(false); + if (completed == delayTask) { task.IgnoreUnobservedExceptions(); @@ -163,6 +167,8 @@ async Task WaitAsync() throw new TimeoutException(FormatTimeoutMessage(memberName, filePath, lineNumber)); } + cts.Cancel(); + return task.GetAwaiter().GetResult(); } @@ -204,6 +210,50 @@ public static void IgnoreUnobservedExceptions(this Task task) }, TaskContinuationOptions.OnlyOnFaulted | TaskContinuationOptions.ExecuteSynchronously); } + public static void TrySetFromTask(this TaskCompletionSource source, Task task, T value) + { + switch (task) + { + case { IsCanceled: true }: + source.TrySetCanceled(); + break; + case { IsFaulted: true, Exception.InnerExceptions: not null }: + source.TrySetException(task.Exception.InnerExceptions); + break; + case { IsFaulted: true, Exception: not null }: + source.TrySetException(task.Exception); + break; + case { IsFaulted: true, Exception: null }: + source.TrySetException(new InvalidOperationException("The context faulted but no exception was present.")); + break; + default: + source.TrySetResult(value); + break; + } + } + + public static void TrySetFromTask(this TaskCompletionSource source, Task task) + { + switch (task) + { + case { IsCanceled: true }: + source.TrySetCanceled(); + break; + case { IsFaulted: true, Exception.InnerExceptions: not null }: + source.TrySetException(task.Exception.InnerExceptions); + break; + case { IsFaulted: true, Exception: not null }: + source.TrySetException(task.Exception); + break; + case { IsFaulted: true, Exception: null }: + source.TrySetException(new InvalidOperationException("The context faulted but no exception was present.")); + break; + default: + source.TrySetResult(task.Result); + break; + } + } + /// /// Register a callback on the which completes the resulting task. /// diff --git a/src/MassTransit.Abstractions/Internals/Extensions/TypeExtensions.cs b/src/MassTransit.Abstractions/Internals/Extensions/TypeExtensions.cs index 198213ac92c..44603b6fc1c 100644 --- a/src/MassTransit.Abstractions/Internals/Extensions/TypeExtensions.cs +++ b/src/MassTransit.Abstractions/Internals/Extensions/TypeExtensions.cs @@ -23,9 +23,7 @@ public static string GetTypeName(this Type type) public static IEnumerable GetAllProperties(this Type type) { - var typeInfo = type.GetTypeInfo(); - - return GetAllProperties(typeInfo); + return GetAllProperties(type.GetTypeInfo()); } public static IEnumerable GetAllProperties(this TypeInfo typeInfo) @@ -36,7 +34,7 @@ public static IEnumerable GetAllProperties(this TypeInfo typeInfo) yield return prop; } - var specialGetPropertyNames = typeInfo.DeclaredMethods + IEnumerable? specialGetPropertyNames = typeInfo.DeclaredMethods .Where(x => x.IsSpecialName && x.Name.StartsWith("get_") && !x.IsStatic) .Select(x => x.Name.Substring("get_".Length)).Distinct(); @@ -47,7 +45,9 @@ public static IEnumerable GetAllProperties(this TypeInfo typeInfo) if (typeInfo.IsInterface) { IEnumerable sourceProperties = properties - .Concat(typeInfo.ImplementedInterfaces.SelectMany(x => x.GetTypeInfo().DeclaredProperties)); + .Concat(typeInfo.ImplementedInterfaces.SelectMany(x => x.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | + BindingFlags.Static | BindingFlags.Public | + BindingFlags.NonPublic))); foreach (var prop in sourceProperties) yield return prop; @@ -61,17 +61,10 @@ public static IEnumerable GetAllProperties(this TypeInfo typeInfo) public static IEnumerable GetAllInterfaces(this Type type) { - var typeInfo = type.GetTypeInfo(); - - return GetAllInterfaces(typeInfo); - } - - public static IEnumerable GetAllInterfaces(this TypeInfo typeInfo) - { - if (typeInfo.IsInterface) - yield return typeInfo; + if (type.IsInterface) + yield return type; - foreach (var interfaceType in typeInfo.GetInterfaces()) + foreach (var interfaceType in type.GetInterfaces()) yield return interfaceType; } @@ -79,29 +72,28 @@ public static IEnumerable GetAllStaticProperties(this Type type) { var info = type.GetTypeInfo(); - if (info.BaseType != null) + if (type.BaseType != null) { - foreach (var prop in GetAllStaticProperties(info.BaseType)) + foreach (var prop in GetAllStaticProperties(type.BaseType)) yield return prop; } - IEnumerable props = info.DeclaredMethods + IEnumerable props = info.DeclaredMethods .Where(x => x.IsSpecialName && x.Name.StartsWith("get_") && x.IsStatic) - .Select(x => info.GetDeclaredProperty(x.Name.Substring("get_".Length))) - .Cast(); + .Select(x => info.GetDeclaredProperty(x.Name.Substring("get_".Length))); foreach (var propertyInfo in props) - yield return propertyInfo; + if (propertyInfo != null) + yield return propertyInfo; } - public static IEnumerable GetStaticProperties(this Type type) + public static IEnumerable GetStaticProperties(this Type type) { var info = type.GetTypeInfo(); return info.DeclaredMethods .Where(x => x.IsSpecialName && x.Name.StartsWith("get_") && x.IsStatic) - .Select(x => info.GetDeclaredProperty(x.Name.Substring("get_".Length))) - .Cast(); + .Select(x => info.GetDeclaredProperty(x.Name.Substring("get_".Length))); } /// @@ -111,17 +103,15 @@ public static IEnumerable GetStaticProperties(this Type type) /// True if the type can be constructed, otherwise false. public static bool IsConcrete(this Type type) { - var typeInfo = type.GetTypeInfo(); - return !typeInfo.IsAbstract && !typeInfo.IsInterface; + return type is { IsAbstract: false, IsInterface: false }; } public static bool IsInterfaceOrConcreteClass(this Type type) { - var typeInfo = type.GetTypeInfo(); - if (typeInfo.IsInterface) + if (type.IsInterface) return true; - return typeInfo.IsClass && !typeInfo.IsAbstract; + return type is { IsClass: true, IsAbstract: false }; } /// @@ -135,7 +125,7 @@ public static bool IsInterfaceOrConcreteClass(this Type type) /// public static bool IsConcreteAndAssignableTo(this Type type, Type assignableType) { - return IsConcrete(type) && assignableType.GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()); + return IsConcrete(type) && assignableType.IsAssignableFrom(type); } /// @@ -149,7 +139,7 @@ public static bool IsConcreteAndAssignableTo(this Type type, Type assignableType /// public static bool IsConcreteAndAssignableTo(this Type type) { - return IsConcrete(type) && typeof(T).GetTypeInfo().IsAssignableFrom(type.GetTypeInfo()); + return IsConcrete(type) && typeof(T).IsAssignableFrom(type); } /// @@ -160,8 +150,7 @@ public static bool IsConcreteAndAssignableTo(this Type type) /// True if the type can be null public static bool IsNullable(this Type type, out Type? underlyingType) { - var typeInfo = type.GetTypeInfo(); - var isNullable = typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>); + var isNullable = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); underlyingType = isNullable ? Nullable.GetUnderlyingType(type) : null; return isNullable; @@ -174,17 +163,7 @@ public static bool IsNullable(this Type type, out Type? underlyingType) /// True if the type is an open generic public static bool IsOpenGeneric(this Type type) { - return type.GetTypeInfo().IsOpenGeneric(); - } - - /// - /// Determines if the TypeInfo is an open generic with at least one unspecified generic argument - /// - /// The TypeInfo - /// True if the TypeInfo is an open generic - public static bool IsOpenGeneric(this TypeInfo typeInfo) - { - return typeInfo.IsGenericTypeDefinition || typeInfo.ContainsGenericParameters; + return type.IsGenericTypeDefinition || type.ContainsGenericParameters; } /// @@ -194,11 +173,9 @@ public static bool IsOpenGeneric(this TypeInfo typeInfo) /// True if the type can be null public static bool CanBeNull(this Type type) { - var typeInfo = type.GetTypeInfo(); - - return !typeInfo.IsValueType + return !type.IsValueType || type == typeof(string) - || typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(Nullable<>); + || (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>)); } /// @@ -233,27 +210,17 @@ public static bool HasAttribute(this ICustomAttributeProvider provider) /// public static bool IsAnonymousType(this Type type) { - return type.GetTypeInfo().IsAnonymousType(); - } - - /// - /// Returns true if the TypeInfo is an anonymous type - /// - /// - /// - public static bool IsAnonymousType(this TypeInfo typeInfo) - { - return typeInfo.FullName != null && typeInfo.HasAttribute() && typeInfo.FullName.Contains("AnonymousType"); + return type.FullName != null && type.HasAttribute() && type.FullName.Contains("AnonymousType"); } /// /// Returns true if the type is an FSharp type (maybe?) /// - /// + /// /// - public static bool IsFSharpType(this TypeInfo typeInfo) + public static bool IsFSharpType(this Type type) { - IEnumerable attributes = typeInfo.GetCustomAttributes(); + IEnumerable attributes = type.GetCustomAttributes(); return attributes.Any(attribute => attribute.GetType().FullName == "Microsoft.FSharp.Core.CompilationMappingAttribute"); } @@ -277,7 +244,7 @@ public static bool IsInNamespace(this Type type, string nameSpace) /// public static bool IsValueTypeOrObject(this Type valueType) { - if (valueType.GetTypeInfo().IsValueType + if (valueType.IsValueType || valueType == typeof(string) || valueType == typeof(Uri) || valueType == typeof(Version) diff --git a/src/MassTransit.Abstractions/Internals/GraphValidation/CyclicGraphException.cs b/src/MassTransit.Abstractions/Internals/GraphValidation/CyclicGraphException.cs index a91cd18ec6d..f89d573ec5b 100644 --- a/src/MassTransit.Abstractions/Internals/GraphValidation/CyclicGraphException.cs +++ b/src/MassTransit.Abstractions/Internals/GraphValidation/CyclicGraphException.cs @@ -22,6 +22,9 @@ public CyclicGraphException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected CyclicGraphException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.Abstractions/Internals/GraphValidation/NodeList.cs b/src/MassTransit.Abstractions/Internals/GraphValidation/NodeList.cs index 629227175d3..33c40f99cd1 100644 --- a/src/MassTransit.Abstractions/Internals/GraphValidation/NodeList.cs +++ b/src/MassTransit.Abstractions/Internals/GraphValidation/NodeList.cs @@ -16,7 +16,7 @@ public class NodeList : where T : notnull { readonly Func _nodeFactory; - readonly IList _nodes; + readonly List _nodes; readonly NodeTable _nodeTable; public NodeList(Func nodeFactory, int capacity) diff --git a/src/MassTransit.Abstractions/Internals/GraphValidation/TopologicalSort.cs b/src/MassTransit.Abstractions/Internals/GraphValidation/TopologicalSort.cs index 082997353c1..e8fc385cc19 100644 --- a/src/MassTransit.Abstractions/Internals/GraphValidation/TopologicalSort.cs +++ b/src/MassTransit.Abstractions/Internals/GraphValidation/TopologicalSort.cs @@ -9,7 +9,7 @@ public class TopologicalSort where T : notnull { readonly AdjacencyList _list; - readonly IList _results; + readonly List _results; readonly IEnumerable _sourceNodes; public TopologicalSort(AdjacencyList list) diff --git a/src/MassTransit.Abstractions/Internals/IMessageTypeCache.cs b/src/MassTransit.Abstractions/Internals/IMessageTypeCache.cs index 522633b47e0..f318541b796 100644 --- a/src/MassTransit.Abstractions/Internals/IMessageTypeCache.cs +++ b/src/MassTransit.Abstractions/Internals/IMessageTypeCache.cs @@ -12,16 +12,6 @@ interface IMessageTypeCache /// string DiagnosticAddress { get; } - /// - /// True if the type implements any known saga interfaces - /// - bool HasConsumerInterfaces { get; } - - /// - /// True if the type implements any known saga interfaces - /// - bool HasSagaInterfaces { get; } - /// /// True if the message type is a valid message type /// diff --git a/src/MassTransit.Abstractions/Internals/Reflection/IReadOnlyPropertyCache.cs b/src/MassTransit.Abstractions/Internals/Reflection/IReadOnlyPropertyCache.cs index ab3399b5064..8f7aa1aab32 100644 --- a/src/MassTransit.Abstractions/Internals/Reflection/IReadOnlyPropertyCache.cs +++ b/src/MassTransit.Abstractions/Internals/Reflection/IReadOnlyPropertyCache.cs @@ -1,6 +1,7 @@ namespace MassTransit.Internals { using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; public interface IReadOnlyPropertyCache : diff --git a/src/MassTransit.Abstractions/Internals/Reflection/IReadWritePropertyCache.cs b/src/MassTransit.Abstractions/Internals/Reflection/IReadWritePropertyCache.cs index f73e0c178a4..64ce1736e84 100644 --- a/src/MassTransit.Abstractions/Internals/Reflection/IReadWritePropertyCache.cs +++ b/src/MassTransit.Abstractions/Internals/Reflection/IReadWritePropertyCache.cs @@ -1,6 +1,7 @@ namespace MassTransit.Internals { using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; public interface IReadWritePropertyCache : diff --git a/src/MassTransit.Abstractions/Internals/Reflection/InterfaceReflectionCache.cs b/src/MassTransit.Abstractions/Internals/Reflection/InterfaceReflectionCache.cs index 472894de635..94f780c827e 100644 --- a/src/MassTransit.Abstractions/Internals/Reflection/InterfaceReflectionCache.cs +++ b/src/MassTransit.Abstractions/Internals/Reflection/InterfaceReflectionCache.cs @@ -17,7 +17,7 @@ public InterfaceReflectionCache() public Type? GetGenericInterface(Type type, Type interfaceType) { - if (!interfaceType.GetTypeInfo().IsGenericTypeDefinition) + if (!interfaceType.IsGenericTypeDefinition) { throw new ArgumentException($"The interface must be a generic interface definition: {TypeCache.GetShortName(interfaceType)}", nameof(interfaceType)); @@ -26,7 +26,7 @@ public InterfaceReflectionCache() // our contract states that we will not return generic interface definitions without generic type arguments if (type == interfaceType) return null; - if (type.GetTypeInfo().IsGenericType) + if (type.IsGenericType) { if (type.GetGenericTypeDefinition() == interfaceType) return type; @@ -34,7 +34,7 @@ public InterfaceReflectionCache() Type[] interfaces = type.GetTypeInfo().ImplementedInterfaces.ToArray(); - return interfaces.Where(t => t.GetTypeInfo().IsGenericType) + return interfaces.Where(t => t.IsGenericType) .FirstOrDefault(t => t.GetGenericTypeDefinition() == interfaceType); } @@ -47,7 +47,7 @@ public InterfaceReflectionCache() Type? GetInterfaceInternal(Type type, Type interfaceType) { - if (interfaceType.GetTypeInfo().IsGenericTypeDefinition) + if (interfaceType.IsGenericTypeDefinition) return GetGenericInterface(type, interfaceType); Type[] interfaces = type.GetTypeInfo().ImplementedInterfaces.ToArray(); diff --git a/src/MassTransit.Abstractions/Internals/Reflection/ReadOnlyProperty.cs b/src/MassTransit.Abstractions/Internals/Reflection/ReadOnlyProperty.cs index 248abe3311e..0ffa047ffc3 100644 --- a/src/MassTransit.Abstractions/Internals/Reflection/ReadOnlyProperty.cs +++ b/src/MassTransit.Abstractions/Internals/Reflection/ReadOnlyProperty.cs @@ -30,7 +30,7 @@ static Func GetGetMethod(PropertyInfo property) return _ => throw new InvalidOperationException("No GetMethod available on " + property.Name); var instance = Expression.Parameter(typeof(object), "instance"); - var instanceCast = property.DeclaringType.GetTypeInfo().IsValueType + var instanceCast = property.DeclaringType.IsValueType ? Expression.Convert(instance, property.DeclaringType) : Expression.TypeAs(instance, property.DeclaringType); diff --git a/src/MassTransit.Abstractions/Internals/Reflection/ReadOnlyPropertyCache.cs b/src/MassTransit.Abstractions/Internals/Reflection/ReadOnlyPropertyCache.cs index 892ffcde168..2d1517f846c 100644 --- a/src/MassTransit.Abstractions/Internals/Reflection/ReadOnlyPropertyCache.cs +++ b/src/MassTransit.Abstractions/Internals/Reflection/ReadOnlyPropertyCache.cs @@ -3,6 +3,7 @@ namespace MassTransit.Internals using System; using System.Collections; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; diff --git a/src/MassTransit.Abstractions/Internals/Reflection/ReadWriteProperty.cs b/src/MassTransit.Abstractions/Internals/Reflection/ReadWriteProperty.cs index 62ace889816..b7c59a52da9 100644 --- a/src/MassTransit.Abstractions/Internals/Reflection/ReadWriteProperty.cs +++ b/src/MassTransit.Abstractions/Internals/Reflection/ReadWriteProperty.cs @@ -32,11 +32,11 @@ static Action GetSetMethod(PropertyInfo property) var value = Expression.Parameter(typeof(object), "value"); // value as T is slightly faster than (T)value, so if it's not a value type, use that - var instanceCast = property.DeclaringType.GetTypeInfo().IsValueType + var instanceCast = property.DeclaringType.IsValueType ? Expression.Convert(instance, property.DeclaringType) : Expression.TypeAs(instance, property.DeclaringType); - var valueCast = property.PropertyType.GetTypeInfo().IsValueType + var valueCast = property.PropertyType.IsValueType ? Expression.Convert(value, property.PropertyType) : Expression.TypeAs(value, property.PropertyType); @@ -83,7 +83,7 @@ static Action GetSetMethod(PropertyInfo property) var instance = Expression.Parameter(typeof(T), "instance"); var value = Expression.Parameter(typeof(object), "value"); - var valueCast = property.PropertyType.GetTypeInfo().IsValueType + var valueCast = property.PropertyType.IsValueType ? Expression.Convert(value, property.PropertyType) : Expression.TypeAs(value, property.PropertyType); var call = Expression.Call(instance, property.SetMethod, valueCast); diff --git a/src/MassTransit.Abstractions/Internals/Reflection/ReadWritePropertyCache.cs b/src/MassTransit.Abstractions/Internals/Reflection/ReadWritePropertyCache.cs index 471267881d5..1df57d1943d 100644 --- a/src/MassTransit.Abstractions/Internals/Reflection/ReadWritePropertyCache.cs +++ b/src/MassTransit.Abstractions/Internals/Reflection/ReadWritePropertyCache.cs @@ -3,6 +3,7 @@ namespace MassTransit.Internals using System; using System.Collections; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; diff --git a/src/MassTransit.Abstractions/Internals/Reflection/TypeNameFormatter.cs b/src/MassTransit.Abstractions/Internals/Reflection/TypeNameFormatter.cs index 55c517ca2f1..4fb85ed3c1f 100644 --- a/src/MassTransit.Abstractions/Internals/Reflection/TypeNameFormatter.cs +++ b/src/MassTransit.Abstractions/Internals/Reflection/TypeNameFormatter.cs @@ -2,7 +2,6 @@ namespace MassTransit.Internals { using System; using System.Collections.Concurrent; - using System.Reflection; using System.Text; @@ -39,7 +38,7 @@ public string GetTypeName(Type type) string FormatTypeName(Type type) { - if (type.GetTypeInfo().IsGenericTypeDefinition) + if (type.IsGenericTypeDefinition) throw new ArgumentException("An open generic type cannot be used as a message name"); var sb = new StringBuilder(""); @@ -68,7 +67,7 @@ string FormatTypeName(StringBuilder sb, Type type, string? scope) sb.Append(_nestedTypeSeparator); } - if (type.GetTypeInfo().IsGenericType) + if (type.IsGenericType) { var name = type.GetGenericTypeDefinition().Name; @@ -79,7 +78,7 @@ string FormatTypeName(StringBuilder sb, Type type, string? scope) sb.Append(name); sb.Append(_genericOpen); - Type[] arguments = type.GetTypeInfo().GenericTypeArguments; + Type[] arguments = type.GenericTypeArguments; for (var i = 0; i < arguments.Length; i++) { if (i > 0) diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/AllocateJobSlot.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/AllocateJobSlot.cs index 65287df38f1..e5d5a081ece 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/AllocateJobSlot.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/AllocateJobSlot.cs @@ -1,6 +1,7 @@ namespace MassTransit.Contracts.JobService { using System; + using System.Collections.Generic; public interface AllocateJobSlot @@ -10,5 +11,10 @@ public interface AllocateJobSlot TimeSpan JobTimeout { get; } Guid JobId { get; } + + /// + /// The job properties + /// + Dictionary? JobProperties { get; } } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/CancelJob.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/CancelJob.cs index 4e4b5dad881..74576c24caf 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/CancelJob.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/CancelJob.cs @@ -10,14 +10,9 @@ public interface CancelJob /// Guid JobId { get; } - /// - /// The time the job was started - /// - DateTime Timestamp { get; } - /// /// The reason for cancelling the job /// - string Reason { get; } + string? Reason { get; } } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/CancelJobAttempt.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/CancelJobAttempt.cs new file mode 100644 index 00000000000..f4055584649 --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/CancelJobAttempt.cs @@ -0,0 +1,23 @@ +namespace MassTransit.Contracts.JobService; + +using System; + + +[ConfigureConsumeTopology(false)] +public interface CancelJobAttempt +{ + /// + /// The job identifier + /// + Guid JobId { get; } + + /// + /// Identifies this attempt to run the job + /// + Guid AttemptId { get; } + + /// + /// The supplied reason for the job cancellation + /// + string? Reason { get; } +} diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/CompleteJob.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/CompleteJob.cs index eedb2a08ba7..601a93dd1f5 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/CompleteJob.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/CompleteJob.cs @@ -16,12 +16,12 @@ public interface CompleteJob /// /// The job, as an object dictionary /// - IDictionary Job { get; } + Dictionary Job { get; } /// /// The result of the job /// - IDictionary Result { get; } + Dictionary Result { get; } /// /// The JobTypeId, to ensure the proper job type is started diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/FaultJob.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/FaultJob.cs index 3ca6e75e42c..162d0536dc8 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/FaultJob.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/FaultJob.cs @@ -29,7 +29,7 @@ public interface FaultJob /// /// The job, as an object dictionary /// - IDictionary Job { get; } + Dictionary Job { get; } /// /// The JobTypeId, to ensure the proper job type is started diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/FinalizeJob.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/FinalizeJob.cs new file mode 100644 index 00000000000..2313a9a564c --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/FinalizeJob.cs @@ -0,0 +1,12 @@ +namespace MassTransit.Contracts.JobService; + +using System; + + +public interface FinalizeJob +{ + /// + /// The job identifier + /// + Guid JobId { get; } +} diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/FinalizeJobAttempt.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/FinalizeJobAttempt.cs new file mode 100644 index 00000000000..a648a6f9063 --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/FinalizeJobAttempt.cs @@ -0,0 +1,17 @@ +namespace MassTransit.Contracts.JobService; + +using System; + + +public interface FinalizeJobAttempt +{ + /// + /// The job identifier + /// + Guid JobId { get; } + + /// + /// Identifies this attempt to run the job + /// + Guid AttemptId { get; } +} diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobAttemptCanceled.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobAttemptCanceled.cs index aec03da6be2..4b776146c54 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobAttemptCanceled.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobAttemptCanceled.cs @@ -7,7 +7,7 @@ public interface JobAttemptCanceled { Guid JobId { get; } Guid AttemptId { get; } - int RetryAttempt { get; } DateTime Timestamp { get; } + string Reason { get; } } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobAttemptCreated.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobAttemptCreated.cs deleted file mode 100644 index 5997aee9202..00000000000 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobAttemptCreated.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MassTransit.Contracts.JobService -{ - using System; - - - public interface JobAttemptCreated - { - Guid JobId { get; } - - Guid AttemptId { get; } - - int RetryAttempt { get; } - } -} diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobCancellationReasons.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobCancellationReasons.cs new file mode 100644 index 00000000000..d51d455e561 --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobCancellationReasons.cs @@ -0,0 +1,8 @@ +namespace MassTransit.Contracts.JobService; + +public static class JobCancellationReasons +{ + public static readonly string Shutdown = "Job Service Shutdown"; + public static readonly string CancellationRequested = "Cancellation Requested"; + public static readonly string ConsumerInitiated = "Consumer Initiated"; +} diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobCompleted.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobCompleted.cs index 33ab9429718..c3e6ecbd5e7 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobCompleted.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobCompleted.cs @@ -21,19 +21,19 @@ public interface JobCompleted /// /// The arguments used to start the job /// - IDictionary Job { get; } + Dictionary Job { get; } /// /// The result of the job /// - IDictionary Result { get; } + Dictionary Result { get; } } /// /// Published when a job completes (separately from ) /// - public interface JobCompleted + public interface JobCompleted where T : class { Guid JobId { get; } @@ -42,14 +42,11 @@ public interface JobCompleted TimeSpan Duration { get; } - /// - /// The arguments used to start the job - /// - IDictionary Job { get; } + T Job { get; } /// /// The result of the job /// - IDictionary Result { get; } + Dictionary Result { get; } } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobFaulted.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobFaulted.cs index ffff2583b10..6200cdfb33b 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobFaulted.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobFaulted.cs @@ -15,7 +15,7 @@ public interface JobFaulted TimeSpan? Duration { get; } - IDictionary Job { get; } + Dictionary Job { get; } ExceptionInfo Exceptions { get; } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobStarted.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobStarted.cs index 39d337cfed1..ab5879d737e 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobStarted.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobStarted.cs @@ -28,4 +28,32 @@ public interface JobStarted /// DateTime Timestamp { get; } } + + + /// + /// Event published when a node starts processing a job (separately from ) + /// + public interface JobStarted + where T : class + { + /// + /// The job identifier + /// + Guid JobId { get; } + + /// + /// Identifies this attempt to run the job + /// + Guid AttemptId { get; } + + /// + /// Zero if the job is being started for the first time, otherwise, the number of previous failures + /// + int RetryAttempt { get; } + + /// + /// The time the job was started + /// + DateTime Timestamp { get; } + } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobState.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobState.cs index ae37b2f34e3..2487570af23 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobState.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobState.cs @@ -1,6 +1,7 @@ namespace MassTransit.Contracts.JobService { using System; + using System.Collections.Generic; public interface JobState @@ -25,6 +26,11 @@ public interface JobState /// DateTime? Completed { get; } + /// + /// If the job has completed, the duration of the job + /// + TimeSpan? Duration { get; } + /// /// When the job faulted, if it faulted /// @@ -33,7 +39,7 @@ public interface JobState /// /// The fault reason, if it faulted /// - string Reason { get; } + string? Reason { get; } /// /// If the job has been retried, will be > 0 @@ -44,5 +50,51 @@ public interface JobState /// The current job state /// string CurrentState { get; } + + /// + /// The last reported progress value, if it's actually reported + /// + long? ProgressValue { get; } + + /// + /// The last reported progress limit, if it's actually reported + /// + long? ProgressLimit { get; } + + /// + /// The state of the job, as a dictionary. Use GetJobState{T} to get the job state + /// + Dictionary? JobState { get; } + + /// + /// If present, the next scheduled time for the job to run + /// + DateTime? NextStartDate { get; } + + /// + /// True if the job is a recurring job + /// + bool IsRecurring { get; } + + /// + /// If specified, the start date or the start of the data range (for recurring jobs) when the job should be run + /// + DateTime? StartDate { get; } + + /// + /// If specified, the end of the data range (for recurring jobs) when the job should no longer be run + /// + DateTime? EndDate { get; } + } + + + public interface JobState : + JobState + where T : class + { + /// + /// The job state, if available + /// + new T? JobState { get; } } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobSubmitted.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobSubmitted.cs index 414043fc6b6..4fbd8c8b4f0 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobSubmitted.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/JobSubmitted.cs @@ -26,6 +26,16 @@ public interface JobSubmitted /// /// The job, as an object dictionary /// - IDictionary Job { get; } + Dictionary Job { get; } + + /// + /// The job properties + /// + Dictionary? JobProperties { get; } + + /// + /// If the job is a recurring job, the schedule for the job + /// + RecurringJobSchedule? Schedule { get; } } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/RecurringJobSchedule.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/RecurringJobSchedule.cs new file mode 100644 index 00000000000..fffa51a8ffb --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/RecurringJobSchedule.cs @@ -0,0 +1,27 @@ +namespace MassTransit.Contracts.JobService; + +using System; + + +public interface RecurringJobSchedule +{ + /// + /// A valid cron expression specifying the job schedule + /// + string? CronExpression { get; } + + /// + /// If specified, the time zone in which the cron expression should be evaluated, otherwise UTC is used. + /// + string? TimeZoneId { get; } + + /// + /// If specified, the start date for the job. Otherwise, the current date/time will be used. + /// + DateTimeOffset? Start { get; } + + /// + /// If specified, the end date for the job after which it will be removed from the job scheduler + /// + DateTimeOffset? End { get; } +} diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/RunJob.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/RunJob.cs new file mode 100644 index 00000000000..c6cc88ed616 --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/RunJob.cs @@ -0,0 +1,12 @@ +namespace MassTransit.Contracts.JobService; + +using System; + + +/// +/// Run a scheduled job immediately, vs waiting for the next scheduled job time +/// +public interface RunJob +{ + Guid JobId { get; } +} diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/SaveJobState.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/SaveJobState.cs new file mode 100644 index 00000000000..e58164a0861 --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/SaveJobState.cs @@ -0,0 +1,17 @@ +namespace MassTransit.Contracts.JobService; + +using System; +using System.Collections.Generic; + + +public interface SaveJobState +{ + Guid JobId { get; } + + Guid AttemptId { get; } + + /// + /// The state of the job, as a dictionary, or null to clear the state + /// + Dictionary? JobState { get; } +} diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/SetConcurrentJobLimit.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/SetConcurrentJobLimit.cs index 859e3c0d32e..c757cfce6b2 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/SetConcurrentJobLimit.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/SetConcurrentJobLimit.cs @@ -1,6 +1,7 @@ namespace MassTransit.Contracts.JobService { using System; + using System.Collections.Generic; /// @@ -20,5 +21,25 @@ public interface SetConcurrentJobLimit /// How long a overridden limit should be in effect /// TimeSpan? Duration { get; } + + /// + /// If present, the job type name + /// + string? JobTypeName { get; } + + /// + /// Allows properties to be submitted by the job service instance that can be used by the job distribution strategy + /// + Dictionary? JobTypeProperties { get; } + + /// + /// Allows properties to be submitted by the job service instance that can be used by the job distribution strategy + /// + Dictionary? InstanceProperties { get; } + + /// + /// If configured, specifies a global limit across all job consumer instances + /// + int? GlobalConcurrentJobLimit { get; } } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/SetJobProgress.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/SetJobProgress.cs new file mode 100644 index 00000000000..e98d8f000bf --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/SetJobProgress.cs @@ -0,0 +1,23 @@ +namespace MassTransit.Contracts.JobService; + +using System; + + +public interface SetJobProgress +{ + Guid JobId { get; } + + Guid AttemptId { get; } + + long SequenceNumber { get; } + + /// + /// The current job progress value + /// + long Value { get; } + + /// + /// The maximum value of job progress (optional) + /// + long? Limit { get; } +} diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/StartJob.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/StartJob.cs index 16a9c415f33..c0b11099f42 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/StartJob.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/StartJob.cs @@ -25,11 +25,31 @@ public interface StartJob /// /// The job, as an object dictionary /// - IDictionary Job { get; } + Dictionary Job { get; } /// /// The JobTypeId, to ensure the proper job type is started /// Guid JobTypeId { get; } + + /// + /// The last reported progress value from a previous job execution + /// + long? LastProgressValue { get; } + + /// + /// The last reported progress limit + /// + long? LastProgressLimit { get; } + + /// + /// The job state, as an object dictionary + /// + Dictionary? JobState { get; } + + /// + /// The job properties, supplied when the job was submitted + /// + Dictionary? JobProperties { get; } } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/StartJobAttempt.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/StartJobAttempt.cs index 1bc7a91e889..e5dc857d07f 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/StartJobAttempt.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/StartJobAttempt.cs @@ -34,11 +34,31 @@ public interface StartJobAttempt /// /// The job, as an object dictionary /// - IDictionary Job { get; } + Dictionary Job { get; } /// /// The JobTypeId, to ensure the proper job type is started /// Guid JobTypeId { get; } + + /// + /// The last reported progress value from a previous job execution + /// + long? LastProgressValue { get; } + + /// + /// The last reported progress limit + /// + long? LastProgressLimit { get; } + + /// + /// The job state, as a dictionary + /// + Dictionary? JobState { get; } + + /// + /// The job properties, supplied when the job was submitted + /// + Dictionary? JobProperties { get; } } } diff --git a/src/MassTransit.Abstractions/JobService/Contracts/JobService/SubmitJob.cs b/src/MassTransit.Abstractions/JobService/Contracts/JobService/SubmitJob.cs index f40748c4eeb..33d96fc3e97 100644 --- a/src/MassTransit.Abstractions/JobService/Contracts/JobService/SubmitJob.cs +++ b/src/MassTransit.Abstractions/JobService/Contracts/JobService/SubmitJob.cs @@ -1,13 +1,17 @@ -namespace MassTransit.Contracts.JobService +namespace MassTransit.Contracts.JobService; + +using System; +using System.Collections.Generic; + + +public interface SubmitJob + where TJob : class { - using System; + Guid JobId { get; } + TJob Job { get; } - public interface SubmitJob - where TJob : class - { - Guid JobId { get; } + RecurringJobSchedule? Schedule { get; } - TJob Job { get; } - } + Dictionary? Properties { get; } } diff --git a/src/MassTransit.Abstractions/JobService/IPropertyCollection.cs b/src/MassTransit.Abstractions/JobService/IPropertyCollection.cs new file mode 100644 index 00000000000..6c111ccd139 --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/IPropertyCollection.cs @@ -0,0 +1,37 @@ +namespace MassTransit; + +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + + +public interface IPropertyCollection : + IReadOnlyDictionary +{ + /// + /// If the specified property name is found, returns the value of the property as an object + /// + /// The property name + /// The output property value + /// True if the property is present, otherwise false + bool TryGet(string key, [NotNullWhen(true)] out object? value); + + /// + /// Returns the specified property as the type, returning a default value is the property is not found + /// + /// The result type + /// The property name + /// The default value of the property if not found + /// The property value + T? Get(string key, T? defaultValue = default) + where T : class; + + /// + /// Returns the specified property as the type, returning a default value is the property is not found + /// + /// The result type + /// The property name + /// The default value of the property if not found + /// The property value + T? Get(string key, T? defaultValue = default) + where T : struct; +} diff --git a/src/MassTransit.Abstractions/JobService/ISetPropertyCollection.cs b/src/MassTransit.Abstractions/JobService/ISetPropertyCollection.cs new file mode 100644 index 00000000000..d7ba8aa0197 --- /dev/null +++ b/src/MassTransit.Abstractions/JobService/ISetPropertyCollection.cs @@ -0,0 +1,34 @@ +namespace MassTransit; + +using System; +using System.Collections.Generic; + + +public interface ISetPropertyCollection : + IPropertyCollection +{ + /// + /// Sets a property + /// + /// + /// The new value, or null to remove the property + /// + ISetPropertyCollection Set(string key, string? value); + + /// + /// Sets a property, overwriting an existing value if is true + /// + /// + /// The new value, or null to remove the property + /// + /// + ISetPropertyCollection Set(string key, object? value, bool overwrite = true); + + /// + /// Set multiple properties from an existing collection, any null values a removed from the property collection + /// + /// + /// + /// + ISetPropertyCollection SetMany(IEnumerable>? properties, bool overwrite = true); +} diff --git a/src/MassTransit.Abstractions/Licensing/LicenseContact.cs b/src/MassTransit.Abstractions/Licensing/LicenseContact.cs new file mode 100644 index 00000000000..49d4171eb03 --- /dev/null +++ b/src/MassTransit.Abstractions/Licensing/LicenseContact.cs @@ -0,0 +1,8 @@ +namespace MassTransit.Licensing +{ + public class LicenseContact + { + public string? Name { get; set; } + public string? Email { get; set; } + } +} diff --git a/src/MassTransit.Abstractions/Licensing/LicenseCustomer.cs b/src/MassTransit.Abstractions/Licensing/LicenseCustomer.cs new file mode 100644 index 00000000000..6bca763b3bd --- /dev/null +++ b/src/MassTransit.Abstractions/Licensing/LicenseCustomer.cs @@ -0,0 +1,8 @@ +namespace MassTransit.Licensing +{ + public class LicenseCustomer + { + public string? Id { get; set; } + public string? Name { get; set; } + } +} diff --git a/src/MassTransit.Abstractions/Licensing/LicenseFeature.cs b/src/MassTransit.Abstractions/Licensing/LicenseFeature.cs new file mode 100644 index 00000000000..9d32e5ba151 --- /dev/null +++ b/src/MassTransit.Abstractions/Licensing/LicenseFeature.cs @@ -0,0 +1,8 @@ +namespace MassTransit.Licensing +{ + public class LicenseFeature + { + public string? Name { get; set; } + public string? Description { get; set; } + } +} diff --git a/src/MassTransit.Abstractions/Licensing/LicenseInfo.cs b/src/MassTransit.Abstractions/Licensing/LicenseInfo.cs new file mode 100644 index 00000000000..e30b4890465 --- /dev/null +++ b/src/MassTransit.Abstractions/Licensing/LicenseInfo.cs @@ -0,0 +1,17 @@ +namespace MassTransit.Licensing +{ + using System; + using System.Collections.Generic; + + + public class LicenseInfo + { + public LicenseContact? Contact { get; set; } + public LicenseCustomer? Customer { get; set; } + + public Dictionary? Products { get; set; } + + public DateTime Created { get; set; } + public DateTime Expires { get; set; } + } +} diff --git a/src/MassTransit.Abstractions/Licensing/LicenseProduct.cs b/src/MassTransit.Abstractions/Licensing/LicenseProduct.cs new file mode 100644 index 00000000000..da5d015aec0 --- /dev/null +++ b/src/MassTransit.Abstractions/Licensing/LicenseProduct.cs @@ -0,0 +1,14 @@ +namespace MassTransit.Licensing +{ + using System; + using System.Collections.Generic; + + + public class LicenseProduct + { + public string? Name { get; set; } + public string? Description { get; set; } + public DateTime? Expires { get; set; } + public Dictionary? Features { get; set; } + } +} diff --git a/src/MassTransit.Abstractions/MassTransit.Abstractions.csproj b/src/MassTransit.Abstractions/MassTransit.Abstractions.csproj index beca52e7792..5d19ee14ac2 100644 --- a/src/MassTransit.Abstractions/MassTransit.Abstractions.csproj +++ b/src/MassTransit.Abstractions/MassTransit.Abstractions.csproj @@ -1,12 +1,12 @@ - + - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -19,15 +19,11 @@ $(Description) - - + + - - - - - + diff --git a/src/MassTransit.Abstractions/MassTransit.Abstractions.csproj.DotSettings b/src/MassTransit.Abstractions/MassTransit.Abstractions.csproj.DotSettings index 3153a9494ac..4754c7c3105 100644 --- a/src/MassTransit.Abstractions/MassTransit.Abstractions.csproj.DotSettings +++ b/src/MassTransit.Abstractions/MassTransit.Abstractions.csproj.DotSettings @@ -1,41 +1,47 @@ - + True - True - True - False - True - True - True - True - True - True - True - True - True - True - True - False - True - True - True - True - True - True - True - True - False - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True \ No newline at end of file + True + True + False + True + True + True + True + True + True + True + True + True + True + True + False + True + True + True + True + True + True + True + True + False + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True diff --git a/src/MassTransit.Abstractions/Mediator/MediatorRequestHandler.cs b/src/MassTransit.Abstractions/Mediator/MediatorRequestHandler.cs index 740a86cd86c..92f03ee19bc 100644 --- a/src/MassTransit.Abstractions/Mediator/MediatorRequestHandler.cs +++ b/src/MassTransit.Abstractions/Mediator/MediatorRequestHandler.cs @@ -5,7 +5,7 @@ namespace MassTransit.Mediator /// - /// A Mediator request handler base class, that provides a simplified overridable method with + /// A Mediator request handler base class that provides a simplified overridable method with /// a Task (void) return type /// /// @@ -23,8 +23,8 @@ public Task Consume(ConsumeContext context) /// - /// A Mediator request handler base class, that provides a simplified overridable method with - /// a Task<typeparamref name="TResponse"/>> return type + /// A Mediator request handler base class that provides a simplified overridable method with + /// a Task<> return type /// /// /// diff --git a/src/MassTransit.Abstractions/MediatorRequestExtensions.cs b/src/MassTransit.Abstractions/MediatorRequestExtensions.cs index fcaa50113c1..8d9702c712d 100644 --- a/src/MassTransit.Abstractions/MediatorRequestExtensions.cs +++ b/src/MassTransit.Abstractions/MediatorRequestExtensions.cs @@ -14,14 +14,18 @@ public static class MediatorRequestExtensions /// /// The request message /// + /// /// The response type /// The response object - public static async Task SendRequest(this IMediator mediator, Request request, CancellationToken cancellationToken = default) + public static async Task SendRequest(this IMediator mediator, Request request, CancellationToken cancellationToken = default, + RequestTimeout timeout = default) where T : class { try { - Response response = await mediator.CreateRequest(request, cancellationToken).GetResponse().ConfigureAwait(false); + using RequestHandle> handle = mediator.CreateRequest(request, cancellationToken, timeout); + + Response response = await handle.GetResponse().ConfigureAwait(false); return response.Message; } diff --git a/src/MassTransit.Abstractions/MessageDefaults.cs b/src/MassTransit.Abstractions/MessageDefaults.cs index 9877c54cdd6..378694aa6d0 100644 --- a/src/MassTransit.Abstractions/MessageDefaults.cs +++ b/src/MassTransit.Abstractions/MessageDefaults.cs @@ -2,12 +2,11 @@ namespace MassTransit { using System; using System.Text; - using System.Threading; public static class MessageDefaults { - static readonly Lazy _encoding = new Lazy(() => new UTF8Encoding(false, true), LazyThreadSafetyMode.PublicationOnly); + static readonly Lazy _encoding = new Lazy(() => new UTF8Encoding(false, true)); public static Encoding Encoding => _encoding.Value; } diff --git a/src/MassTransit.Abstractions/MessageHeaders.cs b/src/MassTransit.Abstractions/MessageHeaders.cs index bf0445818dc..d66bd23a052 100644 --- a/src/MassTransit.Abstractions/MessageHeaders.cs +++ b/src/MassTransit.Abstractions/MessageHeaders.cs @@ -140,6 +140,11 @@ public static class MessageHeaders /// public const string TransportMessageId = "TransportMessageId"; + /// + /// The Transport sent time (not supported by all, but hopefully enough) + /// + public const string TransportSentTime = "TransportSentTime"; + /// /// When the message is redelivered or scheduled, and a new MessageId was generated, the original messageId /// @@ -173,6 +178,11 @@ public static class Host public static class Request { public const string Accept = "MT-Request-AcceptType"; + + /// + /// Tracks routing slip retries when using the RoutingSlipRequestProxy + /// + public const string RoutingSlipRetryCount = "MT-RoutingSlip-RetryCount"; } diff --git a/src/MassTransit.Abstractions/MessageTypeCache.cs b/src/MassTransit.Abstractions/MessageTypeCache.cs index b4070567ff2..f54668fdeaf 100644 --- a/src/MassTransit.Abstractions/MessageTypeCache.cs +++ b/src/MassTransit.Abstractions/MessageTypeCache.cs @@ -5,16 +5,15 @@ namespace MassTransit using System.Collections.Generic; using System.Linq; using System.Reflection; - using System.Threading; using Internals; + using Metadata; public static class MessageTypeCache { static CachedType GetOrAdd(Type type) { - return Cached.Instance.GetOrAdd(type, _ => Activator.CreateInstance(typeof(CachedType<>).MakeGenericType(type)) as CachedType - ?? throw new InvalidOperationException("Failed to create cached message type")); + return Cached.Instance.GetOrAdd(type, _ => Activation.Activate(type, new Factory())); } public static IEnumerable GetProperties(Type type) @@ -37,16 +36,6 @@ public static bool IsTemporaryMessageType(Type type) return GetOrAdd(type).IsTemporaryMessageType; } - public static bool HasConsumerInterfaces(Type type) - { - return GetOrAdd(type).HasConsumerInterfaces; - } - - public static bool HasSagaInterfaces(Type type) - { - return GetOrAdd(type).HasSagaInterfaces; - } - public static Type[] GetMessageTypes(Type type) { return GetOrAdd(type).MessageTypes; @@ -58,6 +47,17 @@ public static string[] GetMessageTypeNames(Type type) } + readonly struct Factory : + IActivationType + { + public CachedType ActivateType() + where T : class + { + return new CachedType(); + } + } + + static class Cached { internal static readonly ConcurrentDictionary Instance = new ConcurrentDictionary(); @@ -66,8 +66,6 @@ static class Cached interface CachedType { - bool HasConsumerInterfaces { get; } - bool HasSagaInterfaces { get; } bool IsTemporaryMessageType { get; } bool IsValidMessageType { get; } string? InvalidMessageTypeReason { get; } @@ -80,8 +78,6 @@ interface CachedType class CachedType : CachedType { - bool CachedType.HasConsumerInterfaces => MessageTypeCache.HasConsumerInterfaces; - bool CachedType.HasSagaInterfaces => MessageTypeCache.HasSagaInterfaces; bool CachedType.IsTemporaryMessageType => MessageTypeCache.IsTemporaryMessageType; bool CachedType.IsValidMessageType => MessageTypeCache.IsValidMessageType; string? CachedType.InvalidMessageTypeReason => MessageTypeCache.InvalidMessageTypeReason; @@ -89,6 +85,14 @@ class CachedType : public string[] MessageTypeNames => MessageTypeCache.MessageTypeNames; public IEnumerable Properties => MessageTypeCache.Properties; + + public void Method1() + { + } + + public void Method2() + { + } } } @@ -97,40 +101,22 @@ public class MessageTypeCache : IMessageTypeCache { readonly Lazy _diagnosticAddress; - readonly Lazy _hasConsumerInterfaces; - readonly Lazy _hasSagaInterfaces; readonly Lazy _isTemporaryMessageType; readonly Lazy _isValidMessageType; readonly Lazy _messageTypeNames; - readonly Lazy _messageTypes; - readonly Lazy> _properties; string? _invalidMessageTypeReason; + Type[]? _messageTypes; + List? _properties; MessageTypeCache() { - _hasSagaInterfaces = new Lazy(ScanForSagaInterfaces, LazyThreadSafetyMode.PublicationOnly); - _hasConsumerInterfaces = new Lazy(() => !_hasSagaInterfaces.Value && ScanForConsumerInterfaces(), LazyThreadSafetyMode.PublicationOnly); - - static List PropertyListFactory() - { - return typeof(T).GetAllProperties() - .GroupBy(x => x.Name) - .Select(x => x.Last()) - .ToList(); - } - - _properties = new Lazy>(PropertyListFactory); - _isValidMessageType = new Lazy(CheckIfValidMessageType); - _isTemporaryMessageType = new Lazy(() => CheckIfTemporaryMessageType(typeof(T).GetTypeInfo())); - _messageTypes = new Lazy(() => GetMessageTypes().ToArray()); + _isTemporaryMessageType = new Lazy(() => CheckIfTemporaryMessageType(typeof(T))); _messageTypeNames = new Lazy(() => GetMessageTypeNames().ToArray()); _diagnosticAddress = new Lazy(GetDiagnosticAddress); } public static string DiagnosticAddress => Cached.Metadata.Value.DiagnosticAddress; - public static bool HasSagaInterfaces => Cached.Metadata.Value.HasSagaInterfaces; - public static bool HasConsumerInterfaces => Cached.Metadata.Value.HasConsumerInterfaces; public static IEnumerable Properties => Cached.Metadata.Value.Properties; public static bool IsValidMessageType => Cached.Metadata.Value.IsValidMessageType; public static string? InvalidMessageTypeReason => Cached.Metadata.Value.InvalidMessageTypeReason; @@ -139,19 +125,27 @@ static List PropertyListFactory() public static string[] MessageTypeNames => Cached.Metadata.Value.MessageTypeNames; bool IMessageTypeCache.IsTemporaryMessageType => _isTemporaryMessageType.Value; + string[] IMessageTypeCache.MessageTypeNames => _messageTypeNames.Value; string IMessageTypeCache.DiagnosticAddress => _diagnosticAddress.Value; - IEnumerable IMessageTypeCache.Properties => _properties.Value; + IEnumerable IMessageTypeCache.Properties => _properties ??= PropertyListFactory(); bool IMessageTypeCache.IsValidMessageType => _isValidMessageType.Value; string? IMessageTypeCache.InvalidMessageTypeReason => _invalidMessageTypeReason; - Type[] IMessageTypeCache.MessageTypes => _messageTypes.Value; - bool IMessageTypeCache.HasConsumerInterfaces => _hasConsumerInterfaces.Value; - bool IMessageTypeCache.HasSagaInterfaces => _hasSagaInterfaces.Value; + + Type[] IMessageTypeCache.MessageTypes => _messageTypes ??= GetMessageTypes().ToArray(); + + static List PropertyListFactory() + { + return typeof(T).GetAllProperties() + .GroupBy(x => x.Name) + .Select(x => x.Last()) + .ToList(); + } static bool CheckIfTemporaryMessageType(Type messageTypeInfo) { return (!messageTypeInfo.IsVisible && messageTypeInfo.IsClass) - || (messageTypeInfo.IsGenericType && messageTypeInfo.GetGenericArguments().Any(x => CheckIfTemporaryMessageType(x.GetTypeInfo()))); + || (messageTypeInfo.IsGenericType && messageTypeInfo.GetGenericArguments().Any(x => CheckIfTemporaryMessageType(x))); } /// @@ -175,16 +169,15 @@ static IEnumerable GetMessageTypes() } } - var baseType = typeof(T).GetTypeInfo().BaseType; + var baseType = typeof(T).BaseType; while (baseType != null && MessageTypeCache.IsValidMessageType(baseType)) { yield return baseType; - baseType = baseType.GetTypeInfo().BaseType; + baseType = baseType.BaseType; } - IEnumerable interfaces = typeof(T) - .GetTypeInfo() + IEnumerable? interfaces = typeof(T) .GetInterfaces() .Where(MessageTypeCache.IsValidMessageType); @@ -200,54 +193,51 @@ static IEnumerable GetMessageTypes() /// True if the message can be sent, otherwise false bool CheckIfValidMessageType() { - var typeInfo = typeof(T).GetTypeInfo(); + var type = typeof(T); - if (typeInfo.IsAnonymousType()) + var ns = type.Namespace; + if (ns == null) { - _invalidMessageTypeReason = $"Message types must not be anonymous types: {TypeCache.ShortName}"; - return false; - } + if (type.IsAnonymousType()) + { + _invalidMessageTypeReason = $"Message types must not be anonymous types: {TypeCache.ShortName}"; + return false; + } - if (typeInfo.Namespace == null) - { _invalidMessageTypeReason = $"Messages types must have a valid namespace: {TypeCache.ShortName}"; return false; } - if (typeof(object).GetTypeInfo().Assembly.Equals(typeInfo.Assembly)) - { - _invalidMessageTypeReason = $"Messages types must not be System types: {TypeCache.ShortName}"; - return false; - } + if (type is { Name: "JsonObject", Namespace: "System.Text.Json.Nodes" }) + return true; - if (typeInfo.Namespace == "System") + if (ns == "System" || ns.StartsWith("System.")) { _invalidMessageTypeReason = $"Messages types must not be in the System namespace: {TypeCache.ShortName}"; return false; } - var ns = typeInfo.Namespace; - if (ns != null && ns.StartsWith("System.")) + if (typeof(object).Assembly.Equals(type.Assembly)) { - _invalidMessageTypeReason = $"Messages types must not be in the System namespace: {TypeCache.ShortName}"; + _invalidMessageTypeReason = $"Messages types must not be System types: {TypeCache.ShortName}"; return false; } - if (typeInfo.HasInterface() - || typeInfo.HasInterface() - || typeInfo.HasInterface()) + if (type.HasInterface() + || type.HasInterface() + || type.HasInterface()) { _invalidMessageTypeReason = $"ConsumeContext, ReceiveContext, and SendContext are not valid message types: {TypeCache.ShortName}"; return false; } - if (typeInfo.IsGenericType) + if (type.IsGenericType) { - var typeDefinition = typeInfo.GetGenericTypeDefinition(); + var typeDefinition = type.GetGenericTypeDefinition(); if (typeDefinition == typeof(CorrelatedBy<>)) { _invalidMessageTypeReason = - $"CorrelatedBy<{typeof(T).GetClosingArgument(typeof(CorrelatedBy<>)).Name}> is not a valid message type"; + $"CorrelatedBy<{type.GetClosingArgument(typeof(CorrelatedBy<>)).Name}> is not a valid message type"; return false; } @@ -255,7 +245,7 @@ bool CheckIfValidMessageType() if (typeDefinition == typeof(Orchestrates<>)) { _invalidMessageTypeReason = - $"Orchestrates<{typeof(T).GetClosingArgument(typeof(Orchestrates<>)).Name}> is not a valid message type"; + $"Orchestrates<{type.GetClosingArgument(typeof(Orchestrates<>)).Name}> is not a valid message type"; return false; } @@ -263,7 +253,7 @@ bool CheckIfValidMessageType() if (typeDefinition == typeof(InitiatedBy<>)) { _invalidMessageTypeReason = - $"InitiatedBy<{typeof(T).GetClosingArgument(typeof(InitiatedBy<>)).Name}> is not a valid message type"; + $"InitiatedBy<{type.GetClosingArgument(typeof(InitiatedBy<>)).Name}> is not a valid message type"; return false; } @@ -271,19 +261,19 @@ bool CheckIfValidMessageType() if (typeDefinition == typeof(InitiatedByOrOrchestrates<>)) { _invalidMessageTypeReason = - $"InitiatedByOrOrchestrates<{typeof(T).GetClosingArgument(typeof(InitiatedByOrOrchestrates<>)).Name}> is not a valid message type"; + $"InitiatedByOrOrchestrates<{type.GetClosingArgument(typeof(InitiatedByOrOrchestrates<>)).Name}> is not a valid message type"; return false; } if (typeDefinition == typeof(Observes<,>)) { - Type[] closingArguments = typeof(T).GetClosingArguments(typeof(Observes<,>)).ToArray(); + Type[]? closingArguments = type.GetClosingArguments(typeof(Observes<,>)).ToArray(); _invalidMessageTypeReason = $"Observes<{closingArguments[0].Name},{closingArguments[1].Name}> is not a valid message type"; return false; } - if (typeInfo.IsOpenGeneric()) + if (type.IsOpenGeneric()) { _invalidMessageTypeReason = $"Message types must not be open generic types: {TypeCache.ShortName}"; return false; @@ -315,32 +305,10 @@ static string GetDiagnosticAddress() return $"{type}/{ns}"; } - static bool ScanForConsumerInterfaces() - { - Type[] interfaces = typeof(T).GetTypeInfo().GetInterfaces(); - - return interfaces.Any(t => t.HasInterface(typeof(IConsumer<>)) - || t.HasInterface(typeof(IJobConsumer<>))); - } - - static bool ScanForSagaInterfaces() - { - Type[] interfaces = typeof(T).GetTypeInfo().GetInterfaces(); - - if (interfaces.Contains(typeof(ISaga))) - return true; - - return interfaces.Any(t => t.HasInterface(typeof(InitiatedBy<>)) - || t.HasInterface(typeof(Orchestrates<>)) - || t.HasInterface(typeof(InitiatedByOrOrchestrates<>)) - || t.HasInterface(typeof(Observes<,>))); - } - static class Cached { - internal static readonly Lazy Metadata = - new Lazy(() => new MessageTypeCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy Metadata = new Lazy(() => new MessageTypeCache()); } } } diff --git a/src/MassTransit.Abstractions/MessageUrn.cs b/src/MassTransit.Abstractions/MessageUrn.cs index f61bf19935a..96f832608db 100644 --- a/src/MassTransit.Abstractions/MessageUrn.cs +++ b/src/MassTransit.Abstractions/MessageUrn.cs @@ -5,12 +5,15 @@ namespace MassTransit using System.Reflection; using System.Runtime.Serialization; using System.Text; + using Metadata; [Serializable] public class MessageUrn : Uri { + public const string Prefix = "urn:message:"; + static readonly ConcurrentDictionary _cache = new ConcurrentDictionary(); MessageUrn(string uriString) @@ -18,6 +21,9 @@ public class MessageUrn : { } + #if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] + #endif protected MessageUrn(SerializationInfo serializationInfo, StreamingContext streamingContext) : base(serializationInfo, streamingContext) { @@ -48,10 +54,21 @@ public static string ForTypeString(Type type) static Cached ValueFactory(Type type) { - return Activator.CreateInstance(typeof(Cached<>).MakeGenericType(type)) as Cached - ?? throw new InvalidOperationException($"MessageUrn creation failed for type: {TypeCache.GetShortName(type)} "); + return Activation.Activate(type, new Factory()); } + + readonly struct Factory : + IActivationType + { + public Cached ActivateType() + where T : class + { + return new Cached(); + } + } + + public void Deconstruct(out string? name, out string? ns, out string? assemblyName) { name = null; @@ -82,34 +99,54 @@ public void Deconstruct(out string? name, out string? ns, out string? assemblyNa static string GetUrnForType(Type type) { - var sb = new StringBuilder("urn:message:"); + return GetMessageName(type, true); + } + + static string GetMessageName(Type type, bool includeScope) + { + var messageName = GetMessageNameFromAttribute(type); + + return string.IsNullOrWhiteSpace(messageName) + ? GetMessageNameFromType(new StringBuilder(Prefix), type, includeScope) + : messageName!; + } + + static string? GetMessageNameFromAttribute(Type? type) + { + if (type is { IsArray: true, HasElementType: true }) + { + var elementType = type.GetElementType(); + var elementName = GetMessageNameFromAttribute(elementType); + + if (!string.IsNullOrWhiteSpace(elementName)) + return elementName + "[]"; + } - return GetMessageName(sb, type, true); + return type?.GetCustomAttribute()?.Urn.ToString(); } - static string GetMessageName(StringBuilder sb, Type type, bool includeScope) + static string GetMessageNameFromType(StringBuilder sb, Type type, bool includeScope) { - var typeInfo = type.GetTypeInfo(); - if (typeInfo.IsGenericParameter) - return ""; + if (type.IsGenericParameter) + return string.Empty; - if (includeScope && typeInfo.Namespace != null) + var ns = type.Namespace; + if (includeScope && ns != null) { - var ns = typeInfo.Namespace; sb.Append(ns); sb.Append(':'); } - if (typeInfo.IsNested && typeInfo.DeclaringType != null) + if (type is { IsNested: true, DeclaringType: { } }) { - GetMessageName(sb, typeInfo.DeclaringType, false); + GetMessageNameFromType(sb, type.DeclaringType, false); sb.Append('+'); } - if (typeInfo.IsGenericType) + if (type.IsGenericType) { - var name = typeInfo.GetGenericTypeDefinition().Name; + var name = type.GetGenericTypeDefinition().Name; //remove `1 var index = name.IndexOf('`'); @@ -120,21 +157,21 @@ static string GetMessageName(StringBuilder sb, Type type, bool includeScope) sb.Append(name); sb.Append('['); - Type[] arguments = typeInfo.GetGenericArguments(); + Type[] arguments = type.GetGenericArguments(); for (var i = 0; i < arguments.Length; i++) { if (i > 0) sb.Append(','); sb.Append('['); - GetMessageName(sb, arguments[i], true); + GetMessageNameFromType(sb, arguments[i], true); sb.Append(']'); } sb.Append(']'); } else - sb.Append(typeInfo.Name); + sb.Append(type.Name); return sb.ToString(); } diff --git a/src/MassTransit.Abstractions/Metadata/Activation.cs b/src/MassTransit.Abstractions/Metadata/Activation.cs new file mode 100644 index 00000000000..7c03eda4fab --- /dev/null +++ b/src/MassTransit.Abstractions/Metadata/Activation.cs @@ -0,0 +1,71 @@ +namespace MassTransit.Metadata +{ + using System; + using System.Collections.Concurrent; + + + public static class Activation + { + public static TResult Activate(Type type, IActivationType activationType) + { + return Cached.Instance.Value + .GetOrAdd(type, + _ => (CachedType)(Activator.CreateInstance(typeof(TypeAdapter<>).MakeGenericType(type)) ?? throw new InvalidOperationException())) + .ActivateType(activationType); + } + + public static TResult Activate(Type type, IActivationType activationType, T1 arg1) + { + return Cached.Instance.Value + .GetOrAdd(type, + _ => (CachedType)(Activator.CreateInstance(typeof(TypeAdapter<>).MakeGenericType(type)) ?? throw new InvalidOperationException())) + .ActivateType(activationType, arg1); + } + + public static TResult Activate(Type type, IActivationType activationType, T1 arg1, T2 arg2) + { + return Cached.Instance.Value + .GetOrAdd(type, + _ => (CachedType)(Activator.CreateInstance(typeof(TypeAdapter<>).MakeGenericType(type)) ?? throw new InvalidOperationException())) + .ActivateType(activationType, arg1, arg2); + } + + + interface CachedType + { + Type Type { get; } + TResult ActivateType(IActivationType activationType); + TResult ActivateType(IActivationType activationType, T1 arg1); + TResult ActivateType(IActivationType activationType, T1 arg1, T2 arg2); + } + + + static class Cached + { + internal static readonly Lazy> Instance = new(() => new ConcurrentDictionary()); + } + + + class TypeAdapter : + CachedType + where TAdapter : class + { + public Type Type => typeof(TAdapter); + + public TResult ActivateType(IActivationType activationType) + { + return activationType.ActivateType(); + } + + public TResult ActivateType(IActivationType activationType, T1 arg1) + { + return activationType.ActivateType(arg1); + } + + public TResult ActivateType(IActivationType activationType, T1 arg1, T2 arg2) + { + return activationType.ActivateType(arg1, arg2); + } + } + } +} diff --git a/src/MassTransit.Abstractions/Metadata/BusHostInfo.cs b/src/MassTransit.Abstractions/Metadata/BusHostInfo.cs index 600cc0e8c4e..f79b209c217 100644 --- a/src/MassTransit.Abstractions/Metadata/BusHostInfo.cs +++ b/src/MassTransit.Abstractions/Metadata/BusHostInfo.cs @@ -20,7 +20,7 @@ public BusHostInfo(bool initialize) OperatingSystemVersion = Environment.OSVersion.ToString(); var entryAssembly = System.Reflection.Assembly.GetEntryAssembly() ?? System.Reflection.Assembly.GetCallingAssembly(); MachineName = Environment.MachineName; - MassTransitVersion = typeof(HostInfo).GetTypeInfo().Assembly.GetName().Version?.ToString(); + MassTransitVersion = typeof(HostInfo).Assembly.GetName().Version?.ToString(); try { @@ -30,7 +30,7 @@ public BusHostInfo(bool initialize) if ("dotnet".Equals(ProcessName, StringComparison.OrdinalIgnoreCase)) ProcessName = GetUsefulProcessName(ProcessName); } - catch (PlatformNotSupportedException) + catch (NotSupportedException) { ProcessId = 0; ProcessName = GetUsefulProcessName("UWP"); diff --git a/src/MassTransit.Abstractions/Metadata/IActivationType.cs b/src/MassTransit.Abstractions/Metadata/IActivationType.cs new file mode 100644 index 00000000000..dbd9008c123 --- /dev/null +++ b/src/MassTransit.Abstractions/Metadata/IActivationType.cs @@ -0,0 +1,22 @@ +namespace MassTransit.Metadata +{ + public interface IActivationType + { + TResult ActivateType() + where T : class; + } + + + public interface IActivationType + { + TResult ActivateType(T1 arg1) + where T : class; + } + + + public interface IActivationType + { + TResult ActivateType(T1 arg1, T2 arg2) + where T : class; + } +} diff --git a/src/MassTransit.Abstractions/Metadata/IImplementedMessageTypeCache.cs b/src/MassTransit.Abstractions/Metadata/IImplementedMessageTypeCache.cs index 95acf7b9e29..e1f3eef05a0 100644 --- a/src/MassTransit.Abstractions/Metadata/IImplementedMessageTypeCache.cs +++ b/src/MassTransit.Abstractions/Metadata/IImplementedMessageTypeCache.cs @@ -7,7 +7,6 @@ public interface IImplementedMessageTypeCache /// Invokes the interface for each implemented type of the message /// /// - /// - void EnumerateImplementedTypes(IImplementedMessageType implementedMessageType, bool includeActualType); + void EnumerateImplementedTypes(IImplementedMessageType implementedMessageType); } } diff --git a/src/MassTransit.Abstractions/Metadata/ImplementedMessageTypeCache.cs b/src/MassTransit.Abstractions/Metadata/ImplementedMessageTypeCache.cs index 482bef13bb8..df60b868ad7 100644 --- a/src/MassTransit.Abstractions/Metadata/ImplementedMessageTypeCache.cs +++ b/src/MassTransit.Abstractions/Metadata/ImplementedMessageTypeCache.cs @@ -3,8 +3,6 @@ namespace MassTransit.Metadata using System; using System.Collections.Generic; using System.Linq; - using System.Reflection; - using System.Threading; using Internals; @@ -17,87 +15,104 @@ public class ImplementedMessageTypeCache : ImplementedMessageTypeCache() { _implementedTypes = GetMessageTypes() - .Where(x => x.Type != typeof(TMessage)) - .Select(x => Activator.CreateInstance(typeof(TypeAdapter<>).MakeGenericType(typeof(TMessage), x.Type), (object)x.Direct)) - .Cast() + .Select(x => Activation.Activate(x.Type, new Factory(), x.Direct)) .ToArray(); } - void IImplementedMessageTypeCache.EnumerateImplementedTypes(IImplementedMessageType implementedMessageType, bool includeActualType) + void IImplementedMessageTypeCache.EnumerateImplementedTypes(IImplementedMessageType implementedMessageType) { for (var i = 0; i < _implementedTypes.Length; i++) { - if (_implementedTypes[i].MessageType == typeof(TMessage) && !includeActualType) + if (_implementedTypes[i].MessageType == typeof(TMessage)) continue; _implementedTypes[i].ImplementsType(implementedMessageType); } } + public void Method1() + { + } + + public void Method2() + { + } + + public void Method3() + { + } + /// /// Enumerate the implemented message types /// /// The interface reference to invoke for each type - /// Include the actual message type first, before any implemented types - public static void EnumerateImplementedTypes(IImplementedMessageType implementedMessageType, bool includeActualType = false) + public static void EnumerateImplementedTypes(IImplementedMessageType implementedMessageType) { - Cached.Instance.Value.EnumerateImplementedTypes(implementedMessageType, includeActualType); + Cached.Instance.Value.EnumerateImplementedTypes(implementedMessageType); } static IEnumerable GetMessageTypes() { - if (MessageTypeCache.IsValidMessageType) - yield return new ImplementedType(typeof(TMessage), true); + return GetMessageTypes(new HashSet(), typeof(TMessage), true); + } - if (typeof(TMessage).ClosesType(typeof(Fault<>), out Type[] arguments)) + static IEnumerable GetMessageTypes(HashSet used, Type messageType, bool direct) + { + if (messageType.ClosesType(typeof(Fault<>), out Type[] arguments)) { - foreach (var faultMessageType in MessageTypeCache.GetMessageTypes(arguments[0])) + var directFault = direct; + foreach (var faultMessageType in GetMessageTypes(used, arguments[0], false)) { - var faultInterfaceType = typeof(Fault<>).MakeGenericType(faultMessageType); - if (faultInterfaceType != typeof(TMessage)) - yield return new ImplementedType(faultInterfaceType, true); + var faultInterfaceType = typeof(Fault<>).MakeGenericType(faultMessageType.Type); + if (faultInterfaceType != typeof(TMessage) && used.Add(faultInterfaceType)) + yield return new ImplementedType(faultInterfaceType, directFault); + + directFault = false; } } - Type[] implementedInterfaces = GetImplementedInterfaces(typeof(TMessage)).ToArray(); + var baseType = messageType.BaseType; + if (baseType != null && MessageTypeCache.IsValidMessageType(baseType)) + { + foreach (var baseMessageType in GetMessageTypes(used, baseType, false)) + { + if (used.Add(baseMessageType.Type)) + yield return new ImplementedType(baseMessageType.Type, direct); + } - foreach (var baseInterface in implementedInterfaces.Except(implementedInterfaces.SelectMany(x => x.GetInterfaces())) - .Where(MessageTypeCache.IsValidMessageType)) - yield return new ImplementedType(baseInterface, true); + if (used.Add(baseType)) + yield return new ImplementedType(baseType, direct); + } - foreach (var baseInterface in implementedInterfaces.SelectMany(x => x.GetInterfaces()).Distinct().Where(MessageTypeCache.IsValidMessageType)) - yield return new ImplementedType(baseInterface, false); + Type[]? interfaces = messageType.GetInterfaces(); - var baseType = typeof(TMessage).GetTypeInfo().BaseType; - while (baseType != null && MessageTypeCache.IsValidMessageType(baseType)) + for (var index = 0; index < interfaces.Length; index++) { - yield return new ImplementedType(baseType, typeof(TMessage).GetTypeInfo().BaseType == baseType); - - foreach (var baseInterface in GetImplementedInterfaces(baseType).Where(MessageTypeCache.IsValidMessageType)) - yield return new ImplementedType(baseInterface, false); + var interfaceType = interfaces[index]; - baseType = baseType.GetTypeInfo().BaseType; + if (MessageTypeCache.IsValidMessageType(interfaceType)) + { + foreach (var baseInterfaceType in GetMessageTypes(used, interfaceType, false)) + { + if (used.Add(baseInterfaceType.Type)) + yield return new ImplementedType(baseInterfaceType.Type, direct); + } + + if (used.Add(interfaceType)) + yield return new ImplementedType(interfaceType, direct); + } } } - static IEnumerable GetImplementedInterfaces(Type baseType) - { - var baseTypeInfo = baseType.GetTypeInfo(); - - IEnumerable baseInterfaces = baseTypeInfo - .GetInterfaces() - .Where(MessageTypeCache.IsValidMessageType) - .ToArray(); - if (baseTypeInfo.BaseType != null && baseTypeInfo.BaseType != typeof(object)) + readonly struct Factory : + IActivationType + { + public CachedType ActivateType(bool direct) + where T : class { - baseInterfaces = baseInterfaces - .Except(baseTypeInfo.BaseType.GetInterfaces()) - .Except(baseInterfaces.SelectMany(x => x.GetInterfaces())) - .ToArray(); + return new TypeAdapter(direct); } - - return baseInterfaces; } @@ -131,8 +146,7 @@ interface CachedType static class Cached { - internal static readonly Lazy> Instance = new Lazy>( - () => new ImplementedMessageTypeCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy> Instance = new(() => new ImplementedMessageTypeCache()); } @@ -146,12 +160,24 @@ public TypeAdapter(bool direct) } public bool Direct { get; } - Type CachedType.MessageType => typeof(TAdapter); + public Type MessageType => typeof(TAdapter); - void CachedType.ImplementsType(IImplementedMessageType implementedMessageType) + public void ImplementsType(IImplementedMessageType implementedMessageType) { implementedMessageType.ImplementsMessageType(Direct); } + + public void Method1() + { + } + + public void Method2() + { + } + + public void Method3() + { + } } } } diff --git a/src/MassTransit.Abstractions/Middleware/ConcurrencyLimitExtensions.cs b/src/MassTransit.Abstractions/Middleware/ConcurrencyLimitExtensions.cs index f0c66ebb772..be6325f3ca1 100644 --- a/src/MassTransit.Abstractions/Middleware/ConcurrencyLimitExtensions.cs +++ b/src/MassTransit.Abstractions/Middleware/ConcurrencyLimitExtensions.cs @@ -1,5 +1,4 @@ -#nullable enable -namespace MassTransit +namespace MassTransit { using System; using System.Threading.Tasks; diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/ChildSpecificationPipeBuilder.cs b/src/MassTransit.Abstractions/Middleware/Configuration/ChildSpecificationPipeBuilder.cs index b66134f49a6..27f33249552 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/ChildSpecificationPipeBuilder.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/ChildSpecificationPipeBuilder.cs @@ -1,36 +1,39 @@ namespace MassTransit.Configuration { - public class ChildSpecificationPipeBuilder : - ISpecificationPipeBuilder - where T : class, PipeContext + public partial class PipeConfigurator + where TContext : class, PipeContext { - readonly ISpecificationPipeBuilder _builder; - - public ChildSpecificationPipeBuilder(ISpecificationPipeBuilder builder, bool isImplemented, bool isDelegated) + public class ChildSpecificationPipeBuilder : + ISpecificationPipeBuilder { - _builder = builder; + readonly ISpecificationPipeBuilder _builder; - IsDelegated = isDelegated; - IsImplemented = isImplemented; - } + public ChildSpecificationPipeBuilder(ISpecificationPipeBuilder builder, bool isImplemented, bool isDelegated) + { + _builder = builder; - public void AddFilter(IFilter filter) - { - _builder.AddFilter(filter); - } + IsDelegated = isDelegated; + IsImplemented = isImplemented; + } - public bool IsDelegated { get; } + public void AddFilter(IFilter filter) + { + _builder.AddFilter(filter); + } - public bool IsImplemented { get; } + public bool IsDelegated { get; } - public ISpecificationPipeBuilder CreateDelegatedBuilder() - { - return new ChildSpecificationPipeBuilder(this, IsImplemented, true); - } + public bool IsImplemented { get; } - public ISpecificationPipeBuilder CreateImplementedBuilder() - { - return new ChildSpecificationPipeBuilder(this, true, IsDelegated); + public ISpecificationPipeBuilder CreateDelegatedBuilder() + { + return new ChildSpecificationPipeBuilder(this, IsImplemented, true); + } + + public ISpecificationPipeBuilder CreateImplementedBuilder() + { + return new ChildSpecificationPipeBuilder(this, true, IsDelegated); + } } } } diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Consume/ConsumePipeSpecificationObservable.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Consume/ConsumePipeSpecificationObservable.cs index 1f63d21204f..2f083ec6a04 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Consume/ConsumePipeSpecificationObservable.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Consume/ConsumePipeSpecificationObservable.cs @@ -12,5 +12,17 @@ public void MessageSpecificationCreated(IMessageConsumePipeSpecification s { ForEach(observer => observer.MessageSpecificationCreated(specification)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Filters/SplitFilterPipeSpecification.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Filters/SplitFilterPipeSpecification.cs index c6ae248c87c..ab782bd54f0 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Filters/SplitFilterPipeSpecification.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Filters/SplitFilterPipeSpecification.cs @@ -4,64 +4,66 @@ namespace MassTransit.Configuration using Middleware; - /// - /// Adds an arbitrary filter to the pipe - /// - /// - /// The filter type - public class SplitFilterPipeSpecification : - IPipeSpecification + public partial class PipeConfigurator where TContext : class, PipeContext - where TFilter : class, PipeContext { - readonly MergeFilterContextProvider _contextProvider; - readonly FilterContextProvider _inputContextProvider; - readonly IPipeSpecification _specification; - - public SplitFilterPipeSpecification(IPipeSpecification specification, MergeFilterContextProvider contextProvider, - FilterContextProvider inputContextProvider) - { - _specification = specification; - _contextProvider = contextProvider; - _inputContextProvider = inputContextProvider; - } - - public void Apply(IPipeBuilder builder) - { - var splitBuilder = new Builder(builder, _contextProvider, _inputContextProvider); - - _specification.Apply(splitBuilder); - } - - public IEnumerable Validate() - { - if (_specification == null) - yield return this.Failure("Specification", "must not be null"); - if (_contextProvider == null) - yield return this.Failure("ContextProvider", "must not be null"); - } - - - class Builder : - IPipeBuilder + /// + /// Adds an arbitrary filter to the pipe + /// + /// The filter type + public class SplitFilterPipeSpecification : + IPipeSpecification + where TFilter : class, PipeContext { - readonly IPipeBuilder _builder; readonly MergeFilterContextProvider _contextProvider; readonly FilterContextProvider _inputContextProvider; + readonly IPipeSpecification _specification; - public Builder(IPipeBuilder builder, MergeFilterContextProvider contextProvider, + public SplitFilterPipeSpecification(IPipeSpecification specification, MergeFilterContextProvider contextProvider, FilterContextProvider inputContextProvider) { - _builder = builder; + _specification = specification; _contextProvider = contextProvider; _inputContextProvider = inputContextProvider; } - public void AddFilter(IFilter filter) + public void Apply(IPipeBuilder builder) + { + var splitBuilder = new Builder(builder, _contextProvider, _inputContextProvider); + + _specification.Apply(splitBuilder); + } + + public IEnumerable Validate() { - var splitFilter = new SplitFilter(filter, _contextProvider, _inputContextProvider); + if (_specification == null) + yield return this.Failure("Specification", "must not be null"); + if (_contextProvider == null) + yield return this.Failure("ContextProvider", "must not be null"); + } + + + class Builder : + IPipeBuilder + { + readonly IPipeBuilder _builder; + readonly MergeFilterContextProvider _contextProvider; + readonly FilterContextProvider _inputContextProvider; + + public Builder(IPipeBuilder builder, MergeFilterContextProvider contextProvider, + FilterContextProvider inputContextProvider) + { + _builder = builder; + _contextProvider = contextProvider; + _inputContextProvider = inputContextProvider; + } + + public void AddFilter(IFilter filter) + { + var splitFilter = new SplitFilter(filter, _contextProvider, _inputContextProvider); - _builder.AddFilter(splitFilter); + _builder.AddFilter(splitFilter); + } } } } diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/PipeBuilder.cs b/src/MassTransit.Abstractions/Middleware/Configuration/PipeBuilder.cs index dbba7642131..7c82b83c981 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/PipeBuilder.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/PipeBuilder.cs @@ -1,41 +1,141 @@ namespace MassTransit.Configuration { using System.Collections.Generic; - using Middleware; + using System.Diagnostics; + using System.Threading.Tasks; - public class PipeBuilder : - IPipeBuilder + public partial class PipeConfigurator where TContext : class, PipeContext { - readonly List> _filters; + public class PipeBuilder : + IPipeBuilder + { + readonly List> _filters; + + public PipeBuilder(int capacity = 16) + { + _filters = new List>(capacity); + } + + public PipeBuilder(params IFilter[] filters) + { + _filters = new List>(filters); + } + + public void AddFilter(IFilter filter) + { + _filters.Add(filter); + } + + public void Method1() + { + } + + public void Method2() + { + } + + public IPipe Build() + { + if (_filters.Count == 0) + return Cache.EmptyPipe; + + IPipe current = new LastPipe(_filters[_filters.Count - 1]); + + for (var i = _filters.Count - 2; i >= 0; i--) + current = new FilterPipe(_filters[i], current); + + return current; + } + } + - public PipeBuilder(int capacity = 4) + internal static class Cache { - _filters = new List>(capacity); + internal static readonly IPipe EmptyPipe = new EmptyPipe(); + internal static readonly IPipe LastPipe = new Last(); } - public PipeBuilder(params IFilter[] filters) + + public class EmptyPipe : + IPipe { - _filters = new List>(filters); + [DebuggerNonUserCode] + Task IPipe.Send(TContext context) + { + return Task.CompletedTask; + } + + void IProbeSite.Probe(ProbeContext context) + { + } } - public void AddFilter(IFilter filter) + + public class FilterPipe : + IPipe { - _filters.Add(filter); + readonly IFilter _filter; + readonly IPipe _next; + + public FilterPipe(IFilter filter, IPipe next) + { + _filter = filter; + _next = next; + } + + public void Probe(ProbeContext context) + { + _filter.Probe(context); + _next.Probe(context); + } + + [DebuggerStepThrough] + public Task Send(TContext context) + { + return _filter.Send(context, _next); + } } - public IPipe Build() + + /// + /// The last pipe in a pipeline is always an end pipe that does nothing and returns synchronously + /// + public class LastPipe : + IPipe { - if (_filters.Count == 0) - return Pipe.Empty(); + readonly IFilter _filter; + + public LastPipe(IFilter filter) + { + _filter = filter; + } + + public void Probe(ProbeContext context) + { + _filter.Probe(context); + } + + [DebuggerStepThrough] + public Task Send(TContext context) + { + return _filter.Send(context, Cache.LastPipe); + } + } - IPipe current = new LastPipe(_filters[_filters.Count - 1]); - for (var i = _filters.Count - 2; i >= 0; i--) - current = new FilterPipe(_filters[i], current); + class Last : + IPipe + { + public void Probe(ProbeContext context) + { + } - return current; + public Task Send(TContext context) + { + return Task.CompletedTask; + } } } } diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/PipeConfigurator.cs b/src/MassTransit.Abstractions/Middleware/Configuration/PipeConfigurator.cs index f677974b76a..b13c9aff74f 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/PipeConfigurator.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/PipeConfigurator.cs @@ -2,10 +2,9 @@ namespace MassTransit.Configuration { using System; using System.Collections.Generic; - using System.Linq; - public class PipeConfigurator : + public partial class PipeConfigurator : IBuildPipeConfigurator where TContext : class, PipeContext { @@ -13,17 +12,22 @@ public class PipeConfigurator : public PipeConfigurator() { - _specifications = new List>(4); + _specifications = new List>(16); } public IEnumerable Validate() { - return _specifications.Count == 0 - ? Array.Empty() - : _specifications.SelectMany(x => x.Validate()); + if (_specifications.Count == 0) + yield break; + + for (var i = 0; i < _specifications.Count; i++) + { + foreach (var result in _specifications[i].Validate()) + yield return result; + } } - void IPipeConfigurator.AddPipeSpecification(IPipeSpecification specification) + public void AddPipeSpecification(IPipeSpecification specification) { if (specification == null) throw new ArgumentNullException(nameof(specification)); @@ -34,9 +38,9 @@ void IPipeConfigurator.AddPipeSpecification(IPipeSpecification Build() { if (_specifications.Count == 0) - return Pipe.Empty(); + return Cache.EmptyPipe; - var builder = new PipeBuilder(_specifications.Count); + var builder = new PipeBuilder(_specifications.Count); var count = _specifications.Count; for (var index = 0; index < count; index++) @@ -44,5 +48,13 @@ public IPipe Build() return builder.Build(); } + + public void Method1() + { + } + + public void Method2() + { + } } } diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Publish/MessagePublishPipeSpecification.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Publish/MessagePublishPipeSpecification.cs index 4bddd2755c0..ec2e6dd8e51 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Publish/MessagePublishPipeSpecification.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Publish/MessagePublishPipeSpecification.cs @@ -10,10 +10,10 @@ public class MessagePublishPipeSpecification : IMessagePublishPipeSpecification where TMessage : class { - readonly IList> _baseSpecifications; - readonly IList>> _implementedMessageTypeSpecifications; - readonly IList>> _parentMessageSpecifications; - readonly IList>> _specifications; + readonly List> _baseSpecifications; + readonly List>> _implementedMessageTypeSpecifications; + readonly List>> _parentMessageSpecifications; + readonly List>> _specifications; public MessagePublishPipeSpecification() { @@ -72,8 +72,8 @@ public void Apply(ISpecificationPipeBuilder> builder) { for (var index = 0; index < _baseSpecifications.Count; index++) { - var split = new SplitFilterPipeSpecification, PublishContext>(_baseSpecifications[index], MergeContext, - FilterContext); + var split = new PipeConfigurator>.SplitFilterPipeSpecification(_baseSpecifications[index], + MergeContext, FilterContext); split.Apply(builder); } @@ -82,7 +82,7 @@ public void Apply(ISpecificationPipeBuilder> builder) public IPipe> BuildMessagePipe() { - var pipeBuilder = new SpecificationPipeBuilder>(); + var pipeBuilder = new PipeConfigurator>.SpecificationPipeBuilder(); Apply(pipeBuilder); diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Publish/MessagePublishPipeSplitFilterSpecification.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Publish/MessagePublishPipeSplitFilterSpecification.cs index a52844c452b..0eb59116786 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Publish/MessagePublishPipeSplitFilterSpecification.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Publish/MessagePublishPipeSplitFilterSpecification.cs @@ -51,12 +51,12 @@ public void AddFilter(IFilter> filter) public ISpecificationPipeBuilder> CreateDelegatedBuilder() { - return new ChildSpecificationPipeBuilder>(this, IsImplemented, true); + return new PipeConfigurator>.ChildSpecificationPipeBuilder(this, IsImplemented, true); } public ISpecificationPipeBuilder> CreateImplementedBuilder() { - return new ChildSpecificationPipeBuilder>(this, true, IsDelegated); + return new PipeConfigurator>.ChildSpecificationPipeBuilder(this, true, IsDelegated); } PublishContext ContextProvider(PublishContext context, PublishContext splitContext) diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Publish/PublishPipeSpecification.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Publish/PublishPipeSpecification.cs index 55ab55bd50a..c6fb884227b 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Publish/PublishPipeSpecification.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Publish/PublishPipeSpecification.cs @@ -15,7 +15,7 @@ public class PublishPipeSpecification : readonly object _lock = new object(); readonly ConcurrentDictionary _messageSpecifications; readonly PublishPipeSpecificationObservable _observers; - readonly IList> _specifications; + readonly List> _specifications; public PublishPipeSpecification() { @@ -45,14 +45,15 @@ public void AddPipeSpecification(IPipeSpecification> specif void IPublishPipeConfigurator.AddPipeSpecification(IPipeSpecification specification) { - var splitSpecification = new SplitFilterPipeSpecification(specification, MergeContext, FilterContext); + var splitSpecification = new PipeConfigurator.SplitFilterPipeSpecification(specification, MergeContext, FilterContext); AddPipeSpecification(splitSpecification); } void IPublishPipeConfigurator.AddPipeSpecification(IPipeSpecification> specification) { - var splitSpecification = new SplitFilterPipeSpecification, SendContext>(specification, MergeContext, FilterContext); + var splitSpecification = + new PipeConfigurator>.SplitFilterPipeSpecification>(specification, MergeContext, FilterContext); AddPipeSpecification(splitSpecification); } diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Publish/PublishPipeSpecificationObservable.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Publish/PublishPipeSpecificationObservable.cs index 90a0ad65384..d125d387bfa 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Publish/PublishPipeSpecificationObservable.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Publish/PublishPipeSpecificationObservable.cs @@ -12,5 +12,17 @@ public void MessageSpecificationCreated(IMessagePublishPipeSpecification s { ForEach(observer => observer.MessageSpecificationCreated(specification)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Send/MessageSendPipeSpecification.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Send/MessageSendPipeSpecification.cs index ebe2ce4bd1f..785b38ccb24 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Send/MessageSendPipeSpecification.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Send/MessageSendPipeSpecification.cs @@ -10,10 +10,10 @@ public class MessageSendPipeSpecification : IMessageSendPipeSpecification where TMessage : class { - readonly IList> _baseSpecifications; - readonly IList>> _implementedMessageTypeSpecifications; - readonly IList>> _parentMessageSpecifications; - readonly IList>> _specifications; + readonly List> _baseSpecifications; + readonly List>> _implementedMessageTypeSpecifications; + readonly List>> _parentMessageSpecifications; + readonly List>> _specifications; public MessageSendPipeSpecification() { @@ -72,7 +72,8 @@ public void Apply(ISpecificationPipeBuilder> builder) { for (var index = 0; index < _baseSpecifications.Count; index++) { - var split = new SplitFilterPipeSpecification, SendContext>(_baseSpecifications[index], MergeContext, FilterContext); + var split = new PipeConfigurator>.SplitFilterPipeSpecification(_baseSpecifications[index], MergeContext, + FilterContext); split.Apply(builder); } @@ -81,7 +82,7 @@ public void Apply(ISpecificationPipeBuilder> builder) public IPipe> BuildMessagePipe() { - var pipeBuilder = new SpecificationPipeBuilder>(); + var pipeBuilder = new PipeConfigurator>.SpecificationPipeBuilder(); Apply(pipeBuilder); diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Send/MessageSendPipeSplitFilterSpecification.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Send/MessageSendPipeSplitFilterSpecification.cs index 6d3f0f01c68..4b05e46fee7 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Send/MessageSendPipeSplitFilterSpecification.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Send/MessageSendPipeSplitFilterSpecification.cs @@ -51,12 +51,12 @@ public void AddFilter(IFilter> filter) public ISpecificationPipeBuilder> CreateDelegatedBuilder() { - return new ChildSpecificationPipeBuilder>(this, IsImplemented, true); + return new PipeConfigurator>.ChildSpecificationPipeBuilder(this, IsImplemented, true); } public ISpecificationPipeBuilder> CreateImplementedBuilder() { - return new ChildSpecificationPipeBuilder>(this, true, IsDelegated); + return new PipeConfigurator>.ChildSpecificationPipeBuilder(this, true, IsDelegated); } SendContext ContextProvider(SendContext context, SendContext splitContext) diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Send/SendPipeSpecification.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Send/SendPipeSpecification.cs index c83d92fba07..093e35d7370 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Send/SendPipeSpecification.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Send/SendPipeSpecification.cs @@ -15,7 +15,7 @@ public class SendPipeSpecification : readonly object _lock = new object(); readonly ConcurrentDictionary _messageSpecifications; readonly SendPipeSpecificationObservable _observers; - readonly IList> _specifications; + readonly List> _specifications; public SendPipeSpecification() { diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/Send/SendPipeSpecificationObservable.cs b/src/MassTransit.Abstractions/Middleware/Configuration/Send/SendPipeSpecificationObservable.cs index fddfceb84e8..3cfbea7473b 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/Send/SendPipeSpecificationObservable.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/Send/SendPipeSpecificationObservable.cs @@ -12,5 +12,17 @@ public void MessageSpecificationCreated(IMessageSendPipeSpecification spec { ForEach(observer => observer.MessageSpecificationCreated(specification)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Middleware/Configuration/SpecificationPipeBuilder.cs b/src/MassTransit.Abstractions/Middleware/Configuration/SpecificationPipeBuilder.cs index fd2b13c54bc..3c862726360 100644 --- a/src/MassTransit.Abstractions/Middleware/Configuration/SpecificationPipeBuilder.cs +++ b/src/MassTransit.Abstractions/Middleware/Configuration/SpecificationPipeBuilder.cs @@ -1,62 +1,64 @@ namespace MassTransit.Configuration { using System.Collections.Generic; - using Middleware; - public class SpecificationPipeBuilder : - ISpecificationPipeBuilder + public partial class PipeConfigurator where TContext : class, PipeContext { - readonly List> _filters; - - public SpecificationPipeBuilder() + public class SpecificationPipeBuilder : + ISpecificationPipeBuilder { - _filters = new List>(); - } + readonly List> _filters; - public void AddFilter(IFilter filter) - { - _filters.Add(filter); - } + public SpecificationPipeBuilder() + { + _filters = new List>(16); + } - public IPipe Build() - { - if (_filters.Count == 0) - return Pipe.Empty(); + public void AddFilter(IFilter filter) + { + _filters.Add(filter); + } - IPipe current = new LastPipe(_filters[_filters.Count - 1]); + public bool IsDelegated => false; + public bool IsImplemented => false; - for (var i = _filters.Count - 2; i >= 0; i--) - current = new FilterPipe(_filters[i], current); + public ISpecificationPipeBuilder CreateDelegatedBuilder() + { + return new ChildSpecificationPipeBuilder(this, IsImplemented, true); + } - return current; - } + public ISpecificationPipeBuilder CreateImplementedBuilder() + { + return new ChildSpecificationPipeBuilder(this, true, IsDelegated); + } - public IPipe Build(IPipe lastPipe) - { - if (_filters.Count == 0) - return lastPipe; + public IPipe Build() + { + if (_filters.Count == 0) + return Cache.EmptyPipe; - IPipe current = lastPipe; + IPipe current = new LastPipe(_filters[_filters.Count - 1]); - for (var i = _filters.Count - 1; i >= 0; i--) - current = new FilterPipe(_filters[i], current); + for (var i = _filters.Count - 2; i >= 0; i--) + current = new FilterPipe(_filters[i], current); - return current; - } + return current; + } - public bool IsDelegated => false; - public bool IsImplemented => false; + public IPipe Build(IPipe lastPipe) + { + if (_filters.Count == 0) + return lastPipe; - public ISpecificationPipeBuilder CreateDelegatedBuilder() - { - return new ChildSpecificationPipeBuilder(this, IsImplemented, true); - } + IPipe current = lastPipe; - public ISpecificationPipeBuilder CreateImplementedBuilder() - { - return new ChildSpecificationPipeBuilder(this, true, IsDelegated); + for (var i = _filters.Count - 1; i >= 0; i--) + current = new FilterPipe(_filters[i], current); + + return current; + } } } } diff --git a/src/MassTransit.Abstractions/Middleware/Contracts/SetConcurrencyLimit.cs b/src/MassTransit.Abstractions/Middleware/Contracts/SetConcurrencyLimit.cs index 1b810c6c81c..d28c3442c8f 100644 --- a/src/MassTransit.Abstractions/Middleware/Contracts/SetConcurrencyLimit.cs +++ b/src/MassTransit.Abstractions/Middleware/Contracts/SetConcurrencyLimit.cs @@ -1,4 +1,3 @@ -#nullable enable namespace MassTransit.Contracts { using System; diff --git a/src/MassTransit.Abstractions/Middleware/Middleware/Agent.cs b/src/MassTransit.Abstractions/Middleware/Middleware/Agent.cs index c5730d3ed39..da5f3fefb1e 100644 --- a/src/MassTransit.Abstractions/Middleware/Middleware/Agent.cs +++ b/src/MassTransit.Abstractions/Middleware/Middleware/Agent.cs @@ -3,6 +3,7 @@ using System; using System.Threading; using System.Threading.Tasks; + using Internals; /// @@ -16,9 +17,6 @@ public class Agent : readonly Lazy _stopped; readonly Lazy _stopping; - bool _isStopped; - bool _isStopping; - TaskCompletionSource? _setCompleted; CancellationTokenSource? _setCompletedCancel; @@ -36,7 +34,7 @@ public Agent() _stopped = new Lazy(() => { var source = new CancellationTokenSource(); - if (_isStopped) + if (IsStopped) source.Cancel(); return source; @@ -44,7 +42,7 @@ public Agent() _stopping = new Lazy(() => { var source = new CancellationTokenSource(); - if (_isStopping) + if (IsStopping) source.Cancel(); return source; @@ -54,12 +52,12 @@ public Agent() /// /// True if the agent is in the process of stopping or is stopped /// - protected bool IsStopping => _isStopping; + protected bool IsStopping { get; private set; } /// /// True if the agent is stopped /// - protected bool IsStopped => _isStopped; + protected bool IsStopped { get; private set; } protected bool IsAlreadyReady => _ready.Task.IsCompleted; @@ -80,13 +78,16 @@ public Agent() /// public async Task Stop(StopContext context) { - _isStopping = true; + if (IsStopping) + return; + + IsStopping = true; if (_stopping.IsValueCreated) _stopping.Value.Cancel(); await StopAgent(context).ConfigureAwait(false); - _isStopped = true; + IsStopped = true; if (_stopped.IsValueCreated) _stopped.Value.Cancel(); } @@ -132,7 +133,14 @@ protected void SetReady(Task readyTask) { // if a previous readyTask is already completed, no sense in trying if (_setReady.Task.IsCompleted) + { + if (_setReady.Task.IsFaulted) + { + var _ = _setReady.Task.Exception; + } + return; + } _setReadyCancel?.Cancel(); @@ -141,7 +149,14 @@ protected void SetReady(Task readyTask) } if (_ready.Task.IsCompleted) + { + if (_ready.Task.IsFaulted) + { + var _ = _ready.Task.Exception; + } + return; + } var setReadyCancel = _setReadyCancel = new CancellationTokenSource(); @@ -150,13 +165,7 @@ void OnSetReady(Task task) if (setReadyCancel.IsCancellationRequested) return; - if (task.IsCanceled) - _ready.TrySetCanceled(); - else if (task.IsFaulted) - - _ready.TrySetException(task.Exception!); - else - _ready.TrySetResult(task.Result); + _ready.TrySetFromTask(task); } TaskCompletionSource setReady = _setReady = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -167,14 +176,7 @@ void OnCompleted(Task task) if (setReadyCancel.IsCancellationRequested) return; - if (task.IsCanceled) - setReady.TrySetCanceled(); - else if (task.IsFaulted) - - // ReSharper disable once AssignNullToNotNullAttribute - setReady.TrySetException(task.Exception!); - else - setReady.TrySetResult(true); + setReady.TrySetFromTask(task, true); } readyTask.ContinueWith(OnCompleted, TaskScheduler.Default); @@ -211,12 +213,7 @@ void OnSetCompleted(Task task) if (setCompletedCancel.IsCancellationRequested) return; - if (task.IsCanceled) - _completed.TrySetCanceled(); - else if (task.IsFaulted && task.Exception is { } aggregateException) - _completed.TrySetException(aggregateException); - else - _completed.TrySetResult(task.Result); + _completed.TrySetFromTask(task); } TaskCompletionSource setCompleted = _setCompleted = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); @@ -227,12 +224,7 @@ void OnCompleted(Task task) if (setCompletedCancel.IsCancellationRequested) return; - if (task.IsCanceled) - setCompleted.TrySetCanceled(); - else if (task.IsFaulted && task.Exception is { } aggregateException) - setCompleted.TrySetException(aggregateException); - else - setCompleted.TrySetResult(true); + setCompleted.TrySetFromTask(task, true); } completedTask.ContinueWith(OnCompleted, TaskScheduler.Default); @@ -245,12 +237,21 @@ void OnCompleted(Task task) /// protected void SetFaulted(Task task) { - if (task.IsCanceled) - _ready.TrySetCanceled(); - else if (task.IsFaulted && task.Exception != null) - _ready.TrySetException(task.Exception.InnerExceptions); - else - _ready.TrySetException(new InvalidOperationException("The context faulted but no exception was present.")); + switch (task) + { + case { IsCanceled: true }: + _ready.TrySetCanceled(); + break; + case { IsFaulted: true, Exception.InnerExceptions: not null }: + _ready.TrySetException(task.Exception.InnerExceptions); + break; + case { IsFaulted: true, Exception: not null }: + _ready.TrySetException(task.Exception); + break; + default: + _ready.TrySetException(new InvalidOperationException("The context faulted but no exception was present.")); + break; + } _completed.TrySetResult(true); } diff --git a/src/MassTransit.Abstractions/Middleware/Middleware/BasePipeContext.cs b/src/MassTransit.Abstractions/Middleware/Middleware/BasePipeContext.cs index 170a096cb65..961299df83a 100644 --- a/src/MassTransit.Abstractions/Middleware/Middleware/BasePipeContext.cs +++ b/src/MassTransit.Abstractions/Middleware/Middleware/BasePipeContext.cs @@ -1,7 +1,7 @@ namespace MassTransit.Middleware { using System; - using System.Reflection; + using System.Diagnostics.CodeAnalysis; using System.Threading; using Payloads; @@ -80,11 +80,6 @@ protected BasePipeContext(IPayloadCache payloadCache, CancellationToken cancella _payloadCache = payloadCache; } - /// - /// Returns the CancellationToken for the context (implicit interface) - /// - public virtual CancellationToken CancellationToken { get; } - protected IPayloadCache PayloadCache { get @@ -99,6 +94,11 @@ protected IPayloadCache PayloadCache } } + /// + /// Returns the CancellationToken for the context (implicit interface) + /// + public virtual CancellationToken CancellationToken { get; } + /// /// Returns true if the payload type is included with or supported by the context type /// @@ -106,7 +106,7 @@ protected IPayloadCache PayloadCache /// public virtual bool HasPayloadType(Type payloadType) { - return payloadType.GetTypeInfo().IsInstanceOfType(this) || PayloadCache.HasPayloadType(payloadType); + return payloadType.IsInstanceOfType(this) || PayloadCache.HasPayloadType(payloadType); } /// @@ -115,7 +115,7 @@ public virtual bool HasPayloadType(Type payloadType) /// /// /// - public virtual bool TryGetPayload(out T? payload) + public virtual bool TryGetPayload([NotNullWhen(true)] out T? payload) where T : class { if (this is T context) diff --git a/src/MassTransit.Abstractions/Middleware/Middleware/CopyContextPipe.cs b/src/MassTransit.Abstractions/Middleware/Middleware/CopyContextPipe.cs index f34513305a2..385f61cf3d8 100644 --- a/src/MassTransit.Abstractions/Middleware/Middleware/CopyContextPipe.cs +++ b/src/MassTransit.Abstractions/Middleware/Middleware/CopyContextPipe.cs @@ -32,7 +32,16 @@ public Task Send(SendContext context) context.TimeToLive = _context.ExpirationTime.Value.ToUniversalTime() - DateTime.UtcNow; foreach (KeyValuePair header in _context.Headers.GetAll()) - context.Headers.Set(header.Key, header.Value); + { + switch (header.Key) + { + case MessageHeaders.RedeliveryCount: + case MessageHeaders.SchedulingTokenId: + continue; + } + + context.Headers.Set(header.Key, header.Value, false); + } _callback?.Invoke(_context, context); diff --git a/src/MassTransit.Abstractions/Middleware/Middleware/ProxyPipeContext.cs b/src/MassTransit.Abstractions/Middleware/Middleware/ProxyPipeContext.cs index 7b82fd4f809..0621fe48fb5 100644 --- a/src/MassTransit.Abstractions/Middleware/Middleware/ProxyPipeContext.cs +++ b/src/MassTransit.Abstractions/Middleware/Middleware/ProxyPipeContext.cs @@ -1,7 +1,7 @@ namespace MassTransit.Middleware { using System; - using System.Reflection; + using System.Diagnostics.CodeAnalysis; using System.Threading; @@ -32,7 +32,7 @@ protected ProxyPipeContext(PipeContext parentContext) /// public virtual bool HasPayloadType(Type payloadType) { - return payloadType.GetTypeInfo().IsInstanceOfType(this) || _parentContext.HasPayloadType(payloadType); + return payloadType.IsInstanceOfType(this) || _parentContext.HasPayloadType(payloadType); } /// @@ -41,7 +41,7 @@ public virtual bool HasPayloadType(Type payloadType) /// /// /// - public virtual bool TryGetPayload(out T? payload) + public virtual bool TryGetPayload([NotNullWhen(true)] out T? payload) where T : class { if (this is T context) diff --git a/src/MassTransit.Abstractions/Middleware/Middleware/ScopePipeContext.cs b/src/MassTransit.Abstractions/Middleware/Middleware/ScopePipeContext.cs index 5549aa1e83b..e992deff50f 100644 --- a/src/MassTransit.Abstractions/Middleware/Middleware/ScopePipeContext.cs +++ b/src/MassTransit.Abstractions/Middleware/Middleware/ScopePipeContext.cs @@ -1,7 +1,7 @@ namespace MassTransit.Middleware { using System; - using System.Reflection; + using System.Diagnostics.CodeAnalysis; using System.Threading; using Payloads; @@ -51,10 +51,10 @@ IPayloadCache PayloadCache public virtual bool HasPayloadType(Type payloadType) { - return payloadType.GetTypeInfo().IsInstanceOfType(this) || PayloadCache.HasPayloadType(payloadType) || _context.HasPayloadType(payloadType); + return payloadType.IsInstanceOfType(this) || PayloadCache.HasPayloadType(payloadType) || _context.HasPayloadType(payloadType); } - public virtual bool TryGetPayload(out T? payload) + public virtual bool TryGetPayload([NotNullWhen(true)] out T? payload) where T : class { if (this is T context) diff --git a/src/MassTransit.Abstractions/Middleware/Middleware/Supervisor.cs b/src/MassTransit.Abstractions/Middleware/Middleware/Supervisor.cs index 217a6e31498..22ff1992578 100644 --- a/src/MassTransit.Abstractions/Middleware/Middleware/Supervisor.cs +++ b/src/MassTransit.Abstractions/Middleware/Middleware/Supervisor.cs @@ -17,8 +17,6 @@ public class Supervisor : { readonly Dictionary _agents; long _nextId; - int _peakActiveCount; - long _totalCount; /// /// Creates a Supervisor @@ -39,11 +37,11 @@ public void Add(IAgent agent) { _agents.Add(id, agent); - _totalCount++; + TotalCount++; var currentActiveCount = _agents.Count; - if (currentActiveCount > _peakActiveCount) - _peakActiveCount = currentActiveCount; + if (currentActiveCount > PeakActiveCount) + PeakActiveCount = currentActiveCount; SetReady(); } @@ -57,22 +55,22 @@ void RemoveAgent(Task task) } /// - public int PeakActiveCount => _peakActiveCount; + public int PeakActiveCount { get; private set; } /// - public long TotalCount => _totalCount; + public long TotalCount { get; private set; } /// public override void SetReady() { - if (!IsAlreadyReady) + if (IsAlreadyReady) + return; + + lock (_agents) { - lock (_agents) - { - SetReady(_agents.Count == 0 - ? Task.CompletedTask - : Task.WhenAll(_agents.Values.Select(x => x.Ready).ToArray())); - } + SetReady(_agents.Count == 0 + ? Task.CompletedTask + : Task.WhenAll(_agents.Values.Select(x => x.Ready).ToArray())); } } @@ -92,28 +90,31 @@ protected override Task StopAgent(StopContext context) protected virtual async Task StopSupervisor(StopSupervisorContext context) { - if (context.Agents.Length == 0) - SetCompleted(Task.CompletedTask); - - if (context.Agents.Length == 1) + switch (context.Agents.Length) { - SetCompleted(context.Agents[0].Completed); - - await context.Agents[0].Stop(context).OrCanceled(context.CancellationToken).ConfigureAwait(false); - } - else if (context.Agents.Length > 1) - { - var completedTasks = new Task[context.Agents.Length]; - for (var i = 0; i < context.Agents.Length; i++) - completedTasks[i] = context.Agents[i].Completed; + case 0: + SetCompleted(Task.CompletedTask); + break; + case 1: + SetCompleted(context.Agents[0].Completed); + + await context.Agents[0].Stop(context).OrCanceled(context.CancellationToken).ConfigureAwait(false); + break; + case > 1: + { + var completedTasks = new Task[context.Agents.Length]; + for (var i = 0; i < context.Agents.Length; i++) + completedTasks[i] = context.Agents[i].Completed; - SetCompleted(Task.WhenAll(completedTasks)); + SetCompleted(Task.WhenAll(completedTasks)); - var stopTasks = new Task[context.Agents.Length]; - for (var i = 0; i < context.Agents.Length; i++) - stopTasks[i] = context.Agents[i].Stop(context); + var stopTasks = new Task[context.Agents.Length]; + for (var i = 0; i < context.Agents.Length; i++) + stopTasks[i] = context.Agents[i].Stop(context); - await Task.WhenAll(stopTasks).OrCanceled(context.CancellationToken).ConfigureAwait(false); + await Task.WhenAll(stopTasks).OrCanceled(context.CancellationToken).ConfigureAwait(false); + break; + } } await Completed.OrCanceled(context.CancellationToken).ConfigureAwait(false); diff --git a/src/MassTransit.Abstractions/Middleware/OneTimeContext.cs b/src/MassTransit.Abstractions/Middleware/OneTimeContext.cs new file mode 100644 index 00000000000..2b645879f91 --- /dev/null +++ b/src/MassTransit.Abstractions/Middleware/OneTimeContext.cs @@ -0,0 +1,7 @@ +namespace MassTransit; + +public interface OneTimeContext + where TPayload : class +{ + void Evict(); +} diff --git a/src/MassTransit.Abstractions/Middleware/OneTimeContextPayload.cs b/src/MassTransit.Abstractions/Middleware/OneTimeContextPayload.cs new file mode 100644 index 00000000000..6d271539fdb --- /dev/null +++ b/src/MassTransit.Abstractions/Middleware/OneTimeContextPayload.cs @@ -0,0 +1,125 @@ +namespace MassTransit; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.ExceptionServices; +using System.Threading.Tasks; + + +class OneTimeContextPayload : + OneTimeContext + where TPayload : class +{ + Task? _createValue; + Queue? _pending; + TaskCompletionSource _value; + + public OneTimeContextPayload() + { + _value = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + public Task Value => _value.Task; + + public bool HasValue => _value.Task.Status == TaskStatus.RanToCompletion; + + public bool IsFaultedOrCanceled => _value.Task.IsFaulted || _value.Task.IsCanceled; + + public void Evict() + { + lock (this) + { + _value = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + _createValue = null; + } + } + + public Task RunOneTime(Func oneTimeSetupMethodFactory) + { + lock (this) + { + if (HasValue) + return _value.Task; + + var pendingValue = oneTimeSetupMethodFactory(); + + if (_createValue == null) + _createValue = RunOnce(pendingValue); + else + (_pending ??= new Queue(1)).Enqueue(pendingValue); + + return pendingValue.Value; + } + } + + async Task RunOnce(OneTimeSetupMethod oneTimeSetupMethod) + { + ExceptionDispatchInfo dispatchInfo; + + var setupPayload = oneTimeSetupMethod; + do + { + try + { + await setupPayload.SetupPayload().ConfigureAwait(false); + + SetResult(true); + + return true; + } + catch (Exception ex) + { + dispatchInfo = ExceptionDispatchInfo.Capture(ex.GetBaseException()); + } + } + while (TryTake(out setupPayload)); + + SetException(dispatchInfo); + + dispatchInfo.Throw(); + + throw dispatchInfo.SourceException; + } + + bool TryTake([NotNullWhen(true)] out OneTimeSetupMethod? pendingValue) + { + lock (this) + { + if (_pending is { Count: > 0 }) + { + pendingValue = _pending.Dequeue(); + return true; + } + } + + pendingValue = default; + return false; + } + + void SetResult(bool value) + { + lock (this) + { + _value.TrySetResult(value); + + while (_pending is { Count: > 0 }) + _pending.Dequeue().SetPayload(_value.Task); + + _pending = null; + } + } + + void SetException(ExceptionDispatchInfo dispatchInfo) + { + lock (this) + { + _value.TrySetException(dispatchInfo.SourceException); + + while (_pending is { Count: > 0 }) + _pending.Dequeue().SetPayload(_value.Task); + + _pending = null; + } + } +} diff --git a/src/MassTransit.Abstractions/Middleware/OneTimeSetupCallback.cs b/src/MassTransit.Abstractions/Middleware/OneTimeSetupCallback.cs new file mode 100644 index 00000000000..9f9dcfa03fd --- /dev/null +++ b/src/MassTransit.Abstractions/Middleware/OneTimeSetupCallback.cs @@ -0,0 +1,6 @@ +namespace MassTransit; + +using System.Threading.Tasks; + + +public delegate Task OneTimeSetupCallback(); diff --git a/src/MassTransit.Abstractions/Middleware/OneTimeSetupMethod.cs b/src/MassTransit.Abstractions/Middleware/OneTimeSetupMethod.cs new file mode 100644 index 00000000000..a32f8724ef5 --- /dev/null +++ b/src/MassTransit.Abstractions/Middleware/OneTimeSetupMethod.cs @@ -0,0 +1,69 @@ +namespace MassTransit; + +using System; +using System.Threading.Tasks; +using Internals; + + +class OneTimeSetupMethod +{ + readonly OneTimeSetupCallback _callback; + readonly TaskCompletionSource _value; + + public OneTimeSetupMethod(OneTimeSetupCallback callback) + { + _callback = callback; + _value = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + public Task Value => _value.Task; + + public Task SetupPayload() + { + try + { + var task = _callback(); + if (task.Status == TaskStatus.RanToCompletion) + { + _value.TrySetResult(true); + + return Task.CompletedTask; + } + + async Task SetupAsync() + { + try + { + await task.ConfigureAwait(false); + + _value.TrySetResult(true); + } + catch (Exception exception) + { + _value.TrySetException(exception); + + throw; + } + } + + return SetupAsync(); + } + catch (Exception exception) + { + _value.TrySetException(exception); + + throw; + } + } + + public void SetPayload(Task value) + { + _value.TrySetFromTask(value); + } +} + + +public interface OneTimeContext +{ + void Evict(); +} diff --git a/src/MassTransit.Abstractions/Middleware/Pipe.cs b/src/MassTransit.Abstractions/Middleware/Pipe.cs index 7822d179cdb..95a1c5f7e9b 100644 --- a/src/MassTransit.Abstractions/Middleware/Pipe.cs +++ b/src/MassTransit.Abstractions/Middleware/Pipe.cs @@ -3,7 +3,6 @@ namespace MassTransit using System; using System.Threading.Tasks; using Configuration; - using Middleware; public static class Pipe @@ -102,7 +101,7 @@ public static IPipe ExecuteAsync(Func action) public static IPipe Empty() where T : class, PipeContext { - return Cache.EmptyPipe; + return PipeConfigurator.Cache.EmptyPipe; } /// @@ -118,7 +117,7 @@ public static IPipe ToPipe(this IFilter filter) if (filter == null) throw new ArgumentNullException(nameof(filter)); - return new LastPipe(filter); + return new PipeConfigurator.LastPipe(filter); } @@ -151,8 +150,8 @@ class PushPipe : IPipe where T : class, PipeContext { - readonly IPipe _nextPipe; readonly Action _callback; + readonly IPipe _nextPipe; public PushPipe(IPipe nextPipe, Action callback) { @@ -195,12 +194,5 @@ public void Probe(ProbeContext context) context.CreateFilterScope("executeAsync"); } } - - - static class Cache - where TContext : class, PipeContext - { - internal static readonly IPipe EmptyPipe = new EmptyPipe(); - } } } diff --git a/src/MassTransit.Abstractions/Middleware/PipeExtensions.cs b/src/MassTransit.Abstractions/Middleware/PipeExtensions.cs index aea2eb25fa8..116f28cef41 100644 --- a/src/MassTransit.Abstractions/Middleware/PipeExtensions.cs +++ b/src/MassTransit.Abstractions/Middleware/PipeExtensions.cs @@ -1,9 +1,8 @@ -#nullable enable namespace MassTransit { using System; using System.Threading.Tasks; - using Middleware; + using Configuration; public static class PipeExtensions @@ -20,7 +19,7 @@ public static bool IsNotEmpty(this IPipe? pipe) return pipe switch { null => false, - EmptyPipe _ => false, + PipeConfigurator.EmptyPipe _ => false, _ => true }; } @@ -37,7 +36,7 @@ public static bool IsEmpty(this IPipe? pipe) return pipe switch { null => true, - EmptyPipe _ => true, + PipeConfigurator.EmptyPipe _ => false, _ => false }; } @@ -77,80 +76,20 @@ public static TPayload GetPayload(this PipeContext context, TPayload d /// The payload type, should be an interface /// The pipe context /// The setup method, called once regardless of the thread count - /// The factory method for the payload context, optional if an interface is specified /// - public static async Task OneTimeSetup(this PipeContext context, Func setupMethod, PayloadFactory payloadFactory) + public static async Task> OneTimeSetup(this PipeContext context, OneTimeSetupCallback setupMethod) where T : class { if (context == null) throw new ArgumentNullException(nameof(context)); if (setupMethod == null) throw new ArgumentNullException(nameof(setupMethod)); - if (payloadFactory == null) - throw new ArgumentNullException(nameof(payloadFactory)); - OneTime? newContext = null; - var existingContext = context.GetOrAddPayload>(() => - { - var payload = payloadFactory(); - - newContext = new OneTime(payload); - - return newContext; - }); - - if (newContext == existingContext) - { - try - { - await setupMethod(newContext.Payload).ConfigureAwait(false); - - newContext.SetReady(); - } - catch (Exception exception) - { - newContext.SetFaulted(exception); + OneTimeContextPayload oneTimeContext = context.GetOrAddPayload(() => new OneTimeContextPayload()); - throw; - } - } - else - await existingContext.Ready.ConfigureAwait(false); - } - - - interface OneTimeSetupContext - where TPayload : class - { - Task Ready { get; } - } + await oneTimeContext.RunOneTime(() => new OneTimeSetupMethod(setupMethod)).ConfigureAwait(false); - - class OneTime : - OneTimeSetupContext - where TPayload : class - { - readonly TaskCompletionSource _ready; - - public OneTime(TPayload payload) - { - Payload = payload; - _ready = new TaskCompletionSource(TaskCreationOptions.None | TaskCreationOptions.RunContinuationsAsynchronously); - } - - public TPayload Payload { get; } - - public Task Ready => _ready.Task; - - public void SetReady() - { - _ready.TrySetResult(Payload); - } - - public void SetFaulted(Exception exception) - { - _ready.TrySetException(exception); - } + return oneTimeContext; } } } diff --git a/src/MassTransit.Abstractions/NewId/NewId.cs b/src/MassTransit.Abstractions/NewId/NewId.cs index f95eb6c8ab1..897f658498a 100644 --- a/src/MassTransit.Abstractions/NewId/NewId.cs +++ b/src/MassTransit.Abstractions/NewId/NewId.cs @@ -13,6 +13,10 @@ namespace MassTransit #endif +// We need to target netstandard2.0, so keep using ref parameter. +// CS9191: The 'ref' modifier for argument 2 corresponding to 'in' parameter is equivalent to 'in'. Consider using 'in' instead. +#pragma warning disable CS9191 + /// /// A NewId is a type that fits into the same space as a Guid/Uuid/unique identifier, /// but is guaranteed to be both unique and ordered, assuming it is generated using @@ -90,6 +94,11 @@ public DateTime Timestamp { var ticks = (long)(((ulong)_a << 32) | (uint)_b); + if (ticks > DateTime.MaxValue.Ticks) + return DateTime.MaxValue; + if (ticks < DateTime.MinValue.Ticks) + return DateTime.MinValue; + return new DateTime(ticks, DateTimeKind.Utc); } } diff --git a/src/MassTransit.Abstractions/NewId/NewIdFormatters/DashedHexFormatter.cs b/src/MassTransit.Abstractions/NewId/NewIdFormatters/DashedHexFormatter.cs index 86d06c0a8a5..39fef81585d 100644 --- a/src/MassTransit.Abstractions/NewId/NewIdFormatters/DashedHexFormatter.cs +++ b/src/MassTransit.Abstractions/NewId/NewIdFormatters/DashedHexFormatter.cs @@ -9,6 +9,10 @@ #endif +// We need to target netstandard2.0, so keep using ref parameter. +// CS9191: The 'ref' modifier for argument 2 corresponding to 'in' parameter is equivalent to 'in'. Consider using 'in' instead. +#pragma warning disable CS9191 + public class DashedHexFormatter : INewIdFormatter { diff --git a/src/MassTransit.Abstractions/NewId/NewIdFormatters/IntrinsicsHelper.cs b/src/MassTransit.Abstractions/NewId/NewIdFormatters/IntrinsicsHelper.cs index c7810ae11bd..f25e75963ec 100644 --- a/src/MassTransit.Abstractions/NewId/NewIdFormatters/IntrinsicsHelper.cs +++ b/src/MassTransit.Abstractions/NewId/NewIdFormatters/IntrinsicsHelper.cs @@ -9,6 +9,10 @@ namespace MassTransit.NewIdFormatters using System.Runtime.Intrinsics.X86; +// We need to target netstandard2.0, so keep using ref parameter. +// CS9191: The 'ref' modifier for argument 2 corresponding to 'in' parameter is equivalent to 'in'. Consider using 'in' instead. +#pragma warning disable CS9191 + internal static class IntrinsicsHelper { [MethodImpl(MethodImplOptions.AggressiveInlining)] diff --git a/src/MassTransit.Abstractions/Observers/IConsumeMessageObserverConnector.cs b/src/MassTransit.Abstractions/Observers/IConsumeMessageObserverConnector.cs index 4b3cbb56c8e..32f24fb21b6 100644 --- a/src/MassTransit.Abstractions/Observers/IConsumeMessageObserverConnector.cs +++ b/src/MassTransit.Abstractions/Observers/IConsumeMessageObserverConnector.cs @@ -8,4 +8,14 @@ public interface IConsumeMessageObserverConnector ConnectHandle ConnectConsumeMessageObserver(IConsumeMessageObserver observer) where T : class; } + + + /// + /// Supports connection of a message observer to the pipeline + /// + public interface IConsumeMessageObserverConnector + where T : class + { + ConnectHandle ConnectConsumeMessageObserver(IConsumeMessageObserver observer); + } } diff --git a/src/MassTransit.Abstractions/Observers/Observables/ConsumeMessageObservable.cs b/src/MassTransit.Abstractions/Observers/Observables/ConsumeMessageObservable.cs new file mode 100644 index 00000000000..adcd3735992 --- /dev/null +++ b/src/MassTransit.Abstractions/Observers/Observables/ConsumeMessageObservable.cs @@ -0,0 +1,28 @@ +namespace MassTransit.Observables +{ + using System; + using System.Threading.Tasks; + using Util; + + + public class ConsumeMessageObservable : + Connectable>, + IConsumeMessageObserver + where T : class + { + public Task PreConsume(ConsumeContext context) + { + return ForEachAsync(x => x.PreConsume(context)); + } + + public Task PostConsume(ConsumeContext context) + { + return ForEachAsync(x => x.PostConsume(context)); + } + + public Task ConsumeFault(ConsumeContext context, Exception exception) + { + return ForEachAsync(x => x.ConsumeFault(context, exception)); + } + } +} diff --git a/src/MassTransit.Abstractions/Observers/Observables/ConsumeObserverAdapter.cs b/src/MassTransit.Abstractions/Observers/Observables/ConsumeObserverAdapter.cs deleted file mode 100644 index fa0f8937df0..00000000000 --- a/src/MassTransit.Abstractions/Observers/Observables/ConsumeObserverAdapter.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace MassTransit.Observables -{ - using System; - using System.Threading.Tasks; - - - public class ConsumeObserverAdapter : - IFilterObserver - { - readonly IConsumeObserver _observer; - - public ConsumeObserverAdapter(IConsumeObserver observer) - { - _observer = observer; - } - - Task IFilterObserver.PreSend(T context) - { - return ConsumeObserverConverterCache.PreConsume(typeof(T), _observer, context); - } - - Task IFilterObserver.PostSend(T context) - { - return ConsumeObserverConverterCache.PostConsume(typeof(T), _observer, context); - } - - Task IFilterObserver.SendFault(T context, Exception exception) - { - return ConsumeObserverConverterCache.ConsumeFault(typeof(T), _observer, context, exception); - } - } - - - public class ConsumeObserverAdapter : - IFilterObserver> - where T : class - { - readonly IConsumeMessageObserver _observer; - - public ConsumeObserverAdapter(IConsumeMessageObserver observer) - { - _observer = observer; - } - - Task IFilterObserver>.PreSend(ConsumeContext context) - { - return _observer.PreConsume(context); - } - - Task IFilterObserver>.PostSend(ConsumeContext context) - { - return _observer.PostConsume(context); - } - - Task IFilterObserver>.SendFault(ConsumeContext context, Exception exception) - { - return _observer.ConsumeFault(context, exception); - } - } -} diff --git a/src/MassTransit.Abstractions/Observers/Observables/ConsumeObserverConverterCache.cs b/src/MassTransit.Abstractions/Observers/Observables/ConsumeObserverConverterCache.cs deleted file mode 100644 index fe1b7372143..00000000000 --- a/src/MassTransit.Abstractions/Observers/Observables/ConsumeObserverConverterCache.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace MassTransit.Observables -{ - using System; - using System.Collections.Concurrent; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Internals; - - - /// - /// Caches the converters that allow a raw object to be published using the object's type through - /// the generic Send method. - /// - public class ConsumeObserverConverterCache - { - readonly ConcurrentDictionary> _types = new ConcurrentDictionary>(); - - IConsumeObserverConverter this[Type type] => _types.GetOrAdd(type, CreateTypeConverter).Value; - - public static Task PreConsume(Type messageType, IConsumeObserver observer, object context) - { - return Cached.Converters.Value[messageType].PreConsume(observer, context); - } - - public static Task PostConsume(Type messageType, IConsumeObserver observer, object context) - { - return Cached.Converters.Value[messageType].PostConsume(observer, context); - } - - public static Task ConsumeFault(Type messageType, IConsumeObserver observer, object context, Exception exception) - { - return Cached.Converters.Value[messageType].ConsumeFault(observer, context, exception); - } - - static Lazy CreateTypeConverter(Type type) - { - return new Lazy(() => CreateConverter(type)); - } - - static IConsumeObserverConverter CreateConverter(Type type) - { - if (type.ClosesType(typeof(ConsumeContext<>))) - { - var messageType = type.GetClosingArguments(typeof(ConsumeContext<>)).Single(); - - var converterType = typeof(ConsumeObserverConverter<>).MakeGenericType(messageType); - - return Activator.CreateInstance(converterType) as IConsumeObserverConverter - ?? throw new InvalidOperationException("Failed to create Consume Observer converter"); - } - - throw new ArgumentException($"The context was not a ConsumeContext: {TypeCache.GetShortName(type)}", nameof(type)); - } - - - static class Cached - { - internal static readonly Lazy Converters = - new Lazy(() => new ConsumeObserverConverterCache(), LazyThreadSafetyMode.PublicationOnly); - } - } -} diff --git a/src/MassTransit.Abstractions/Observers/Observables/FilterObservable.cs b/src/MassTransit.Abstractions/Observers/Observables/FilterObservable.cs index f35204fb844..fc96aef5ab4 100644 --- a/src/MassTransit.Abstractions/Observers/Observables/FilterObservable.cs +++ b/src/MassTransit.Abstractions/Observers/Observables/FilterObservable.cs @@ -26,6 +26,18 @@ public Task SendFault(T context, Exception exception) { return ForEachAsync(x => x.SendFault(context, exception)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } @@ -48,5 +60,17 @@ public Task SendFault(TContext context, Exception exception) { return ForEachAsync(x => x.SendFault(context, exception)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Observers/Observables/RetryFaultObserverCache.cs b/src/MassTransit.Abstractions/Observers/Observables/RetryFaultObserverCache.cs index ee32f6a5b9a..542ff094c35 100644 --- a/src/MassTransit.Abstractions/Observers/Observables/RetryFaultObserverCache.cs +++ b/src/MassTransit.Abstractions/Observers/Observables/RetryFaultObserverCache.cs @@ -2,7 +2,6 @@ { using System; using System.Collections.Concurrent; - using System.Threading; using System.Threading.Tasks; @@ -53,8 +52,7 @@ public Task RetryFault(IRetryObserver observer, RetryContext context) static class Cached { - internal static readonly Lazy Converters = - new Lazy(() => new RetryFaultObserverCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy Converters = new Lazy(() => new RetryFaultObserverCache()); } } } diff --git a/src/MassTransit.Abstractions/Saga/IQuerySagaRepository.cs b/src/MassTransit.Abstractions/Saga/IQuerySagaRepository.cs index 01c0b8c7667..64fb93e7382 100644 --- a/src/MassTransit.Abstractions/Saga/IQuerySagaRepository.cs +++ b/src/MassTransit.Abstractions/Saga/IQuerySagaRepository.cs @@ -5,7 +5,8 @@ namespace MassTransit using System.Threading.Tasks; - public interface IQuerySagaRepository + public interface IQuerySagaRepository : + IProbeSite where TSaga : class, ISaga { Task> Find(ISagaQuery query); diff --git a/src/MassTransit.Abstractions/SagaStateMachine/AsyncEventExceptionMessageFactory.cs b/src/MassTransit.Abstractions/SagaStateMachine/AsyncEventExceptionMessageFactory.cs index 6394b03f7bc..c40abc83ead 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/AsyncEventExceptionMessageFactory.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/AsyncEventExceptionMessageFactory.cs @@ -13,7 +13,7 @@ namespace MassTransit /// /// public delegate Task AsyncEventExceptionMessageFactory(BehaviorExceptionContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception; @@ -28,7 +28,7 @@ public delegate Task AsyncEventExceptionMessageFactory public delegate Task AsyncEventExceptionMessageFactory( BehaviorExceptionContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception where T : class; diff --git a/src/MassTransit.Abstractions/SagaStateMachine/AsyncEventMessageFactory.cs b/src/MassTransit.Abstractions/SagaStateMachine/AsyncEventMessageFactory.cs index 05d7a3d2441..65bf210e048 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/AsyncEventMessageFactory.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/AsyncEventMessageFactory.cs @@ -4,12 +4,12 @@ namespace MassTransit public delegate Task AsyncEventMessageFactory(BehaviorContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where T : class; public delegate Task AsyncEventMessageFactory(BehaviorContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where T : class; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/BehaviorContext.cs b/src/MassTransit.Abstractions/SagaStateMachine/BehaviorContext.cs index 174d08f6a36..42a022d428b 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/BehaviorContext.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/BehaviorContext.cs @@ -10,13 +10,13 @@ /// The state instance type public interface BehaviorContext : SagaConsumeContext - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { StateMachine StateMachine { get; } Event Event { get; } - [Obsolete("Deprecated, use Saga instead")] + [Obsolete("Use Saga instead. Visit https://masstransit.io/obsolete for details.")] TSaga Instance { get; } /// @@ -65,12 +65,12 @@ BehaviorContext CreateProxy(Event @event, T data) public interface BehaviorContext : SagaConsumeContext, BehaviorContext - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { new Event Event { get; } - [Obsolete("Deprecated, use Message instead")] + [Obsolete("Use Message instead. Visit https://masstransit.io/obsolete for details.")] TMessage Data { get; } new Task> Init(object values) diff --git a/src/MassTransit.Abstractions/SagaStateMachine/BehaviorExceptionContext.cs b/src/MassTransit.Abstractions/SagaStateMachine/BehaviorExceptionContext.cs index 59e569a770b..04a1caa5401 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/BehaviorExceptionContext.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/BehaviorExceptionContext.cs @@ -11,7 +11,7 @@ public interface BehaviorExceptionContext : BehaviorContext where TException : Exception - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { TException Exception { get; } @@ -37,7 +37,7 @@ public interface BehaviorExceptionContext : BehaviorContext, BehaviorExceptionContext where TException : Exception - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { /// diff --git a/src/MassTransit.Abstractions/SagaStateMachine/CompositeEventStatus.cs b/src/MassTransit.Abstractions/SagaStateMachine/CompositeEventStatus.cs index 4536fc08284..e5aacc828d9 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/CompositeEventStatus.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/CompositeEventStatus.cs @@ -58,5 +58,10 @@ public void Set(int flag) { _bits |= flag; } + + public bool IsSet(int flag) + { + return (_bits & flag) == flag; + } } } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/Configuration/CompositeEventOptions.cs b/src/MassTransit.Abstractions/SagaStateMachine/Configuration/CompositeEventOptions.cs index 01af850a669..3e37a72445e 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/Configuration/CompositeEventOptions.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/Configuration/CompositeEventOptions.cs @@ -16,6 +16,11 @@ public enum CompositeEventOptions /// /// Include the composite event in the final state /// - IncludeFinal = 2 + IncludeFinal = 2, + + /// + /// Specifies that the composite event should only be raised once and ignore any subsequent events + /// + RaiseOnce = 4, } } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/Configuration/IRequestConfigurator.cs b/src/MassTransit.Abstractions/SagaStateMachine/Configuration/IRequestConfigurator.cs index dd23fb1fc1f..4d83892a13c 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/Configuration/IRequestConfigurator.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/Configuration/IRequestConfigurator.cs @@ -1,6 +1,7 @@ namespace MassTransit { using System; + using Contracts; public interface IRequestConfigurator @@ -17,8 +18,70 @@ public interface IRequestConfigurator /// /// Set the time to live of the request message sent by the saga. If not specified, and the timeout - /// is > TimeSpan.Zero, the value is used. + /// is > TimeSpan.Zero, the value is used. /// TimeSpan? TimeToLive { set; } + + /// + /// By default, the RequestId is not cleared when the request is Faulted. Set to true to clear the requestId + /// + bool ClearRequestIdOnFaulted { set; } + } + + + public interface IRequestConfigurator : + IRequestConfigurator + where TInstance : class, SagaStateMachineInstance + where TRequest : class + where TResponse : class + { + /// + /// Configure the behavior of the Completed event, the same was Events are configured on + /// the state machine. + /// + Action> Completed { set; } + + /// + /// Configure the behavior of the Faulted event, the same was Events are configured on + /// the state machine. + /// + Action>> Faulted { set; } + + /// + /// Configure the behavior of the Faulted event, the same was Events are configured on + /// the state machine. + /// + Action>> TimeoutExpired { set; } + } + + + public interface IRequestConfigurator : + IRequestConfigurator + where TInstance : class, SagaStateMachineInstance + where TResponse : class + where TResponse2 : class + where TRequest : class + { + /// + /// Configure the behavior of the Completed event, the same was Events are configured on + /// the state machine. + /// + Action> Completed2 { set; } + } + + + public interface IRequestConfigurator : + IRequestConfigurator + where TInstance : class, SagaStateMachineInstance + where TResponse : class + where TResponse2 : class + where TResponse3 : class + where TRequest : class + { + /// + /// Configure the behavior of the Completed event, the same was Events are configured on + /// the state machine. + /// + Action> Completed3 { set; } } } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/EventExceptionMessageFactory.cs b/src/MassTransit.Abstractions/SagaStateMachine/EventExceptionMessageFactory.cs index 815514c48fd..9178b6951c1 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/EventExceptionMessageFactory.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/EventExceptionMessageFactory.cs @@ -12,7 +12,7 @@ namespace MassTransit /// /// public delegate TMessage EventExceptionMessageFactory(BehaviorExceptionContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception; @@ -26,7 +26,7 @@ public delegate TMessage EventExceptionMessageFactory /// public delegate T EventExceptionMessageFactory(BehaviorExceptionContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/EventMessageFactory.cs b/src/MassTransit.Abstractions/SagaStateMachine/EventMessageFactory.cs index d2d44423129..3867a92d95d 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/EventMessageFactory.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/EventMessageFactory.cs @@ -1,12 +1,12 @@ namespace MassTransit { public delegate T EventMessageFactory(BehaviorContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where T : class; public delegate T EventMessageFactory(BehaviorContext context) - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TMessage : class where T : class; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/IBehavior.cs b/src/MassTransit.Abstractions/SagaStateMachine/IBehavior.cs index f4eb0f06ced..794c13e2b93 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/IBehavior.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/IBehavior.cs @@ -10,7 +10,7 @@ namespace MassTransit /// The state type public interface IBehavior : IVisitable - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { /// /// Execute the activity with the given behavior context @@ -56,7 +56,7 @@ Task Faulted(BehaviorExceptionContext context /// The data type of the behavior public interface IBehavior : IVisitable - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { /// diff --git a/src/MassTransit.Abstractions/SagaStateMachine/IEventObserver.cs b/src/MassTransit.Abstractions/SagaStateMachine/IEventObserver.cs index 7062f52b8fe..480bb7708de 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/IEventObserver.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/IEventObserver.cs @@ -5,7 +5,7 @@ namespace MassTransit public interface IEventObserver - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { /// /// Called before the event context is delivered to the activities diff --git a/src/MassTransit.Abstractions/SagaStateMachine/IStateAccessor.cs b/src/MassTransit.Abstractions/SagaStateMachine/IStateAccessor.cs index 393fc4c84f3..5ba0e9c6aac 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/IStateAccessor.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/IStateAccessor.cs @@ -7,7 +7,7 @@ public interface IStateAccessor : IProbeSite - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { Task> Get(BehaviorContext context); diff --git a/src/MassTransit.Abstractions/SagaStateMachine/IStateMachineActivity.cs b/src/MassTransit.Abstractions/SagaStateMachine/IStateMachineActivity.cs index 97ac15fc39d..2092d313c67 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/IStateMachineActivity.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/IStateMachineActivity.cs @@ -16,7 +16,7 @@ public interface IStateMachineActivity : /// public interface IStateMachineActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { /// /// Execute the activity with the given behavior context @@ -66,7 +66,7 @@ Task Faulted(BehaviorExceptionContext conte /// public interface IStateMachineActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { /// diff --git a/src/MassTransit.Abstractions/SagaStateMachine/IStateObserver.cs b/src/MassTransit.Abstractions/SagaStateMachine/IStateObserver.cs index 1edb4e74954..ce72789d71c 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/IStateObserver.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/IStateObserver.cs @@ -4,7 +4,7 @@ namespace MassTransit public interface IStateObserver - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { /// /// Invoked prior to changing the state of the state machine diff --git a/src/MassTransit.Abstractions/SagaStateMachine/Request.cs b/src/MassTransit.Abstractions/SagaStateMachine/Request.cs index cbfa66f26ce..77cbda90bf0 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/Request.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/Request.cs @@ -11,7 +11,7 @@ /// The request type /// The response type /// - public interface Request + public interface Request where TSaga : class, SagaStateMachineInstance where TRequest : class where TResponse : class @@ -24,7 +24,7 @@ public interface Request /// /// The settings that are used for the request, including the timeout /// - RequestSettings Settings { get; } + RequestSettings Settings { get; } /// /// The event that is raised when the request completes and the response is received @@ -68,7 +68,7 @@ public interface Request Guid GenerateRequestId(TSaga instance); /// - /// Set the headers on the outgoing request + /// Set the headers on the outgoing request /// /// void SetSendContextHeaders(SendContext context); @@ -83,13 +83,18 @@ public interface Request /// The response type /// /// - public interface Request : + public interface Request : Request where TSaga : class, SagaStateMachineInstance where TRequest : class where TResponse : class where TResponse2 : class { + /// + /// The settings that are used for the request, including the timeout + /// + new RequestSettings Settings { get; } + /// /// The event that is raised when the request completes and the response is received /// @@ -106,7 +111,7 @@ public interface Request : /// /// /// - public interface Request : + public interface Request : Request where TSaga : class, SagaStateMachineInstance where TRequest : class @@ -114,6 +119,11 @@ public interface Request where TResponse2 : class where TResponse3 : class { + /// + /// The settings that are used for the request, including the timeout + /// + new RequestSettings Settings { get; } + /// /// The event that is raised when the request completes and the response is received /// diff --git a/src/MassTransit.Abstractions/SagaStateMachine/RequestSettings.cs b/src/MassTransit.Abstractions/SagaStateMachine/RequestSettings.cs index a3f2d6e1c3e..2cb8326d5cd 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/RequestSettings.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/RequestSettings.cs @@ -1,13 +1,17 @@ namespace MassTransit { using System; + using Contracts; /// /// The request settings include the address of the request handler, as well as the timeout to use /// for requests. /// - public interface RequestSettings + public interface RequestSettings + where TSaga : class, SagaStateMachineInstance + where TRequest : class + where TResponse : class { /// /// The endpoint address of the service that handles the request @@ -19,9 +23,71 @@ public interface RequestSettings /// TimeSpan Timeout { get; } + /// + /// If true, the requestId is cleared when Faulted is triggered + /// + bool ClearRequestIdOnFaulted { get; } + /// /// If specified, the TimeToLive is set on the outgoing request /// TimeSpan? TimeToLive { get; } + + /// + /// Configures the behavior of the Completed event, the same was Events are configured on + /// the state machine. + /// + Action> Completed { get; } + + /// + /// Configures the behavior of the Faulted event, the same was Events are configured on + /// the state machine. + /// + Action>> Faulted { get; } + + /// + /// Configures the behavior of the Timeout Expired event, the same was Events are configured on + /// the state machine. + /// + Action>> TimeoutExpired { get; } + } + + + /// + /// The request settings include the address of the request handler, as well as the timeout to use + /// for requests. + /// + public interface RequestSettings : + RequestSettings + where TSaga : class, SagaStateMachineInstance + where TRequest : class + where TResponse : class + where TResponse2 : class + { + /// + /// Configures the behavior of the Completed event, the same was Events are configured on + /// the state machine. + /// + Action> Completed2 { get; } + } + + + /// + /// The request settings include the address of the request handler, as well as the timeout to use + /// for requests. + /// + public interface RequestSettings : + RequestSettings + where TSaga : class, SagaStateMachineInstance + where TRequest : class + where TResponse : class + where TResponse2 : class + where TResponse3 : class + { + /// + /// Configures the behavior of the Completed event, the same was Events are configured on + /// the state machine. + /// + Action> Completed3 { get; } } } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/ScheduleDelayExceptionProvider.cs b/src/MassTransit.Abstractions/SagaStateMachine/ScheduleDelayExceptionProvider.cs index 0478cd79677..d9e74783b57 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/ScheduleDelayExceptionProvider.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/ScheduleDelayExceptionProvider.cs @@ -4,12 +4,12 @@ public delegate TimeSpan ScheduleDelayExceptionProvider(BehaviorExceptionContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception; public delegate TimeSpan ScheduleDelayExceptionProvider(BehaviorExceptionContext context) where TMessage : class - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/ScheduleDelayProvider.cs b/src/MassTransit.Abstractions/SagaStateMachine/ScheduleDelayProvider.cs index 4e2f6bfc09a..81b458d3646 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/ScheduleDelayProvider.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/ScheduleDelayProvider.cs @@ -4,10 +4,10 @@ public delegate TimeSpan ScheduleDelayProvider(BehaviorContext context) - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; public delegate TimeSpan ScheduleDelayProvider(BehaviorContext context) where TMessage : class - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/ScheduleTimeExceptionProvider.cs b/src/MassTransit.Abstractions/SagaStateMachine/ScheduleTimeExceptionProvider.cs index 1993332344c..dfea54caf9e 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/ScheduleTimeExceptionProvider.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/ScheduleTimeExceptionProvider.cs @@ -4,12 +4,12 @@ namespace MassTransit public delegate DateTime ScheduleTimeExceptionProvider(BehaviorExceptionContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception; public delegate DateTime ScheduleTimeExceptionProvider(BehaviorExceptionContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/ScheduleTimeProvider.cs b/src/MassTransit.Abstractions/SagaStateMachine/ScheduleTimeProvider.cs index 5a0bc5ae24b..a0fbf0d8a18 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/ScheduleTimeProvider.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/ScheduleTimeProvider.cs @@ -4,10 +4,10 @@ namespace MassTransit public delegate DateTime ScheduleTimeProvider(BehaviorContext context) - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; public delegate DateTime ScheduleTimeProvider(BehaviorContext context) where TMessage : class - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/SendContextCallback.cs b/src/MassTransit.Abstractions/SagaStateMachine/SendContextCallback.cs index ef0622b47ba..d1ef03ff75a 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/SendContextCallback.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/SendContextCallback.cs @@ -1,12 +1,12 @@ namespace MassTransit { public delegate void SendContextCallback(BehaviorContext context, SendContext sendContext) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where T : class; public delegate void SendContextCallback(BehaviorContext context, SendContext sendContext) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where T : class; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/SendExceptionContextCallback.cs b/src/MassTransit.Abstractions/SagaStateMachine/SendExceptionContextCallback.cs index 861ef246324..2de97d2ac3e 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/SendExceptionContextCallback.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/SendExceptionContextCallback.cs @@ -5,14 +5,14 @@ namespace MassTransit public delegate void SendExceptionContextCallback(BehaviorExceptionContext context, SendContext sendContext) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception where T : class; public delegate void SendExceptionContextCallback(BehaviorExceptionContext context, SendContext sendContext) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception where T : class; diff --git a/src/MassTransit.Abstractions/SagaStateMachine/ServiceAddressExceptionProvider.cs b/src/MassTransit.Abstractions/SagaStateMachine/ServiceAddressExceptionProvider.cs index 242f2e25369..c5e183df2d1 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/ServiceAddressExceptionProvider.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/ServiceAddressExceptionProvider.cs @@ -11,7 +11,7 @@ namespace MassTransit /// /// public delegate Uri ServiceAddressExceptionProvider(BehaviorExceptionContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception; @@ -24,7 +24,7 @@ public delegate Uri ServiceAddressExceptionProvider(Behavi /// /// public delegate Uri ServiceAddressExceptionProvider(BehaviorExceptionContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/ServiceAddressProvider.cs b/src/MassTransit.Abstractions/SagaStateMachine/ServiceAddressProvider.cs index 59c7e205822..5a05288bb60 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/ServiceAddressProvider.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/ServiceAddressProvider.cs @@ -10,7 +10,7 @@ namespace MassTransit /// /// public delegate Uri ServiceAddressProvider(BehaviorContext context) - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; /// @@ -21,6 +21,6 @@ public delegate Uri ServiceAddressProvider(BehaviorContext context /// /// public delegate Uri ServiceAddressProvider(BehaviorContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/State.cs b/src/MassTransit.Abstractions/SagaStateMachine/State.cs index cae39ce07ab..0a89d27162a 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/State.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/State.cs @@ -39,7 +39,7 @@ public interface State : /// The instance type to which the state applies public interface State : State - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { IEnumerable Events { get; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/StateAccessorExtensions.cs b/src/MassTransit.Abstractions/SagaStateMachine/StateAccessorExtensions.cs index ed82017ab22..c4b117449dd 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/StateAccessorExtensions.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/StateAccessorExtensions.cs @@ -6,13 +6,13 @@ public static class StateAccessorExtensions { public static Task> GetState(this IStateAccessor accessor, BehaviorContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return accessor.Get(context); } public static Task> GetState(this StateMachine accessor, BehaviorContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return accessor.Accessor.Get(context); } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/StateMachine.cs b/src/MassTransit.Abstractions/SagaStateMachine/StateMachine.cs index 024ba3fe556..90045d1a463 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/StateMachine.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/StateMachine.cs @@ -77,7 +77,7 @@ public interface StateMachine : /// public interface StateMachine : StateMachine - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { /// /// Exposes the current state on the given instance diff --git a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineAsyncCondition.cs b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineAsyncCondition.cs index 141aefcfebb..40dda66247c 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineAsyncCondition.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineAsyncCondition.cs @@ -11,7 +11,7 @@ namespace MassTransit /// /// public delegate Task StateMachineAsyncCondition(BehaviorContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class; @@ -22,5 +22,5 @@ public delegate Task StateMachineAsyncCondition(Behavi /// /// public delegate Task StateMachineAsyncCondition(BehaviorContext context) - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineAsyncExceptionCondition.cs b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineAsyncExceptionCondition.cs index eae2b379865..85bf3f24a4e 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineAsyncExceptionCondition.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineAsyncExceptionCondition.cs @@ -13,7 +13,7 @@ namespace MassTransit /// public delegate Task StateMachineAsyncExceptionCondition(BehaviorExceptionContext context) where TException : Exception - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; /// @@ -27,6 +27,6 @@ public delegate Task StateMachineAsyncExceptionCondition StateMachineAsyncExceptionCondition( BehaviorExceptionContext context) where TException : Exception - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineCondition.cs b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineCondition.cs index f4783c07857..bdd4b8a280a 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineCondition.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineCondition.cs @@ -7,7 +7,7 @@ namespace MassTransit /// /// public delegate bool StateMachineCondition(BehaviorContext context) - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; /// @@ -18,6 +18,6 @@ public delegate bool StateMachineCondition(BehaviorContext context /// /// public delegate bool StateMachineCondition(BehaviorContext context) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineExceptionCondition.cs b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineExceptionCondition.cs index deb475615e4..8450cefdf59 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineExceptionCondition.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineExceptionCondition.cs @@ -12,7 +12,7 @@ namespace MassTransit /// public delegate bool StateMachineExceptionCondition(BehaviorExceptionContext context) where TException : Exception - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; /// @@ -25,6 +25,6 @@ public delegate bool StateMachineExceptionCondition(Behavi /// public delegate bool StateMachineExceptionCondition(BehaviorExceptionContext context) where TException : Exception - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineVisitor.cs b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineVisitor.cs index c330530e814..19357ae3936 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/StateMachineVisitor.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/StateMachineVisitor.cs @@ -15,17 +15,17 @@ void Visit(Event @event, Action> next) void Visit(IStateMachineActivity activity); void Visit(IBehavior behavior) - where T : class, ISaga; + where T : class, SagaStateMachineInstance; void Visit(IBehavior behavior, Action> next) - where T : class, ISaga; + where T : class, SagaStateMachineInstance; void Visit(IBehavior behavior) - where T : class, ISaga + where T : class, SagaStateMachineInstance where TMessage : class; void Visit(IBehavior behavior, Action> next) - where T : class, ISaga + where T : class, SagaStateMachineInstance where TMessage : class; void Visit(IStateMachineActivity activity, Action next); diff --git a/src/MassTransit.Abstractions/SagaStateMachine/UnhandledEventCallback.cs b/src/MassTransit.Abstractions/SagaStateMachine/UnhandledEventCallback.cs index 4662a1e743d..7cdcb1be5f2 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/UnhandledEventCallback.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/UnhandledEventCallback.cs @@ -10,5 +10,5 @@ namespace MassTransit /// The event context /// public delegate Task UnhandledEventCallback(UnhandledEventContext context) - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; } diff --git a/src/MassTransit.Abstractions/SagaStateMachine/UnhandledEventContext.cs b/src/MassTransit.Abstractions/SagaStateMachine/UnhandledEventContext.cs index d4f5d10f5e5..f14f47398eb 100644 --- a/src/MassTransit.Abstractions/SagaStateMachine/UnhandledEventContext.cs +++ b/src/MassTransit.Abstractions/SagaStateMachine/UnhandledEventContext.cs @@ -9,7 +9,7 @@ namespace MassTransit /// public interface UnhandledEventContext : BehaviorContext - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { /// /// The current state of the state machine diff --git a/src/MassTransit.Abstractions/SchedulePublishExtensions.cs b/src/MassTransit.Abstractions/SchedulePublishExtensions.cs index 91b075e1459..42432d990c2 100644 --- a/src/MassTransit.Abstractions/SchedulePublishExtensions.cs +++ b/src/MassTransit.Abstractions/SchedulePublishExtensions.cs @@ -106,8 +106,7 @@ public static Task SchedulePublish(this ConsumeContext context } /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The consume context /// The time at which the message should be delivered to the queue @@ -305,8 +304,7 @@ public static Task SchedulePublish(this ConsumeContext context } /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The consume context /// The message object diff --git a/src/MassTransit.Abstractions/Scheduling/ConsumeContextSchedulerExtensions.cs b/src/MassTransit.Abstractions/Scheduling/ConsumeContextSchedulerExtensions.cs index 3afdeee596b..2404e22df5b 100644 --- a/src/MassTransit.Abstractions/Scheduling/ConsumeContextSchedulerExtensions.cs +++ b/src/MassTransit.Abstractions/Scheduling/ConsumeContextSchedulerExtensions.cs @@ -103,8 +103,7 @@ public static Task ScheduleSend(this ConsumeContext context, U } /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The consume context /// The message object @@ -299,8 +298,7 @@ public static Task ScheduleSend(this ConsumeContext context, U } /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The consume context /// The message object diff --git a/src/MassTransit.Abstractions/Scheduling/ConsumeContextSelfSchedulerExtensions.cs b/src/MassTransit.Abstractions/Scheduling/ConsumeContextSelfSchedulerExtensions.cs index 90bf42bdabe..19ad6a84f44 100644 --- a/src/MassTransit.Abstractions/Scheduling/ConsumeContextSelfSchedulerExtensions.cs +++ b/src/MassTransit.Abstractions/Scheduling/ConsumeContextSelfSchedulerExtensions.cs @@ -98,8 +98,7 @@ public static Task ScheduleSend(this ConsumeContext context, D } /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The consume context /// The message object @@ -284,8 +283,7 @@ public static Task ScheduleSend(this ConsumeContext context, T } /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The consume context /// The message object diff --git a/src/MassTransit.Abstractions/Scheduling/IMessageScheduler.cs b/src/MassTransit.Abstractions/Scheduling/IMessageScheduler.cs index 066f0e3a3e8..c51974c2292 100644 --- a/src/MassTransit.Abstractions/Scheduling/IMessageScheduler.cs +++ b/src/MassTransit.Abstractions/Scheduling/IMessageScheduler.cs @@ -76,8 +76,7 @@ Task ScheduleSend(Uri destinationAddress, DateTime scheduledTi CancellationToken cancellationToken = default); /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The destination address where the schedule message should be sent /// The time at which the message should be delivered to the queue @@ -151,7 +150,8 @@ Task> ScheduleSend(Uri destinationAddress, DateTime sched /// /// The destination address of the scheduled message /// The tokenId of the scheduled message - Task CancelScheduledSend(Uri destinationAddress, Guid tokenId); + /// + Task CancelScheduledSend(Uri destinationAddress, Guid tokenId, CancellationToken cancellationToken = default); /// /// Send a message @@ -210,8 +210,7 @@ Task> SchedulePublish(DateTime scheduledTime, T message, Task SchedulePublish(DateTime scheduledTime, object message, Type messageType, CancellationToken cancellationToken = default); /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The time at which the message should be delivered to the queue /// The message object @@ -278,7 +277,8 @@ Task> SchedulePublish(DateTime scheduledTime, object valu /// the destinationAddress. /// /// The tokenId of the scheduled message - Task CancelScheduledPublish(Guid tokenId) + /// + Task CancelScheduledPublish(Guid tokenId, CancellationToken cancellationToken = default) where T : class; /// @@ -287,6 +287,7 @@ Task CancelScheduledPublish(Guid tokenId) /// /// /// The tokenId of the scheduled message - Task CancelScheduledPublish(Type messageType, Guid tokenId); + /// + Task CancelScheduledPublish(Type messageType, Guid tokenId, CancellationToken cancellationToken = default); } } diff --git a/src/MassTransit.Abstractions/Scheduling/IRecurringMessageScheduler.cs b/src/MassTransit.Abstractions/Scheduling/IRecurringMessageScheduler.cs index c41dbd59e64..0dffa6ca5e4 100644 --- a/src/MassTransit.Abstractions/Scheduling/IRecurringMessageScheduler.cs +++ b/src/MassTransit.Abstractions/Scheduling/IRecurringMessageScheduler.cs @@ -77,8 +77,7 @@ Task ScheduleRecurringSend(Uri destinationAddress, Re CancellationToken cancellationToken = default); /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The destination address where the schedule message should be sent /// The schedule for the message to be delivered @@ -149,6 +148,129 @@ Task> ScheduleRecurringSend(Uri destinationAddre CancellationToken cancellationToken = default) where T : class; + /// + /// Send a message + /// + /// The message type + /// The schedule for the message to be delivered + /// The message + /// + /// The task which is completed once the Send is acknowledged by the broker + Task> ScheduleRecurringPublish(RecurringSchedule schedule, T message, CancellationToken cancellationToken = default) + where T : class; + + /// + /// Publish a message + /// + /// The message type + /// The schedule for the message to be delivered + /// The message + /// + /// + /// The task which is completed once the Publish is acknowledged by the broker + Task> ScheduleRecurringPublish(RecurringSchedule schedule, T message, IPipe> pipe, + CancellationToken cancellationToken = default) + where T : class; + + /// + /// Publish a message + /// + /// The message type + /// The schedule for the message to be delivered + /// The message + /// + /// + /// The task which is completed once the Publish is acknowledged by the broker + Task> ScheduleRecurringPublish(RecurringSchedule schedule, T message, IPipe pipe, + CancellationToken cancellationToken = default) + where T : class; + + /// + /// Publishes an object as a message, using the type of the message instance. + /// + /// The schedule for the message to be delivered + /// The message object + /// + /// The task which is completed once the Publish is acknowledged by the broker + Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, CancellationToken cancellationToken = default); + + /// + /// Publishes an object as a message, using the message type specified. If the object cannot be cast + /// to the specified message type, an exception will be thrown. + /// + /// The schedule for the message to be delivered + /// The message object + /// The type of the message (use message.GetType() if desired) + /// + /// The task which is completed once the Publish is acknowledged by the broker + Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, Type messageType, + CancellationToken cancellationToken = default); + + /// + /// Publishes an object as a message. + /// + /// The schedule for the message to be delivered + /// The message object + /// + /// + /// The task which is completed once the Publish is acknowledged by the broker + Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, IPipe pipe, + CancellationToken cancellationToken = default); + + /// + /// Publishes an object as a message, using the message type specified. If the object cannot be cast + /// to the specified message type, an exception will be thrown. + /// + /// The schedule for the message to be delivered + /// The message object + /// The type of the message (use message.GetType() if desired) + /// + /// + /// The task which is completed once the Publish is acknowledged by the broker + Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, Type messageType, IPipe pipe, + CancellationToken cancellationToken = default); + + /// + /// Publishes an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The schedule for the message to be delivered + /// The property values to initialize on the interface + /// + /// The task which is completed once the Publish is acknowledged by the broker + Task> ScheduleRecurringPublish(RecurringSchedule schedule, object values, + CancellationToken cancellationToken = default) + where T : class; + + /// + /// Publishes an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The schedule for the message to be delivered + /// The property values to initialize on the interface + /// + /// + /// The task which is completed once the Publish is acknowledged by the broker + Task> ScheduleRecurringPublish(RecurringSchedule schedule, object values, IPipe> pipe, + CancellationToken cancellationToken = default) + where T : class; + + /// + /// Publishes an interface message, initializing the properties of the interface using the anonymous + /// object specified + /// + /// The interface type to send + /// The schedule for the message to be delivered + /// The property values to initialize on the interface + /// + /// + /// The task which is completed once the Publish is acknowledged by the broker + Task> ScheduleRecurringPublish(RecurringSchedule schedule, object values, IPipe pipe, + CancellationToken cancellationToken = default) + where T : class; + /// /// Cancel a scheduled message by TokenId /// diff --git a/src/MassTransit.Abstractions/Scheduling/IScheduleMessageProvider.cs b/src/MassTransit.Abstractions/Scheduling/IScheduleMessageProvider.cs index 78e354256b3..f1875f6b35b 100644 --- a/src/MassTransit.Abstractions/Scheduling/IScheduleMessageProvider.cs +++ b/src/MassTransit.Abstractions/Scheduling/IScheduleMessageProvider.cs @@ -25,13 +25,15 @@ Task> ScheduleSend(Uri destinationAddress, DateTime sched /// Cancel a scheduled message by TokenId /// /// The tokenId of the scheduled message - Task CancelScheduledSend(Guid tokenId); + /// + Task CancelScheduledSend(Guid tokenId, CancellationToken cancellationToken); /// /// Cancel a scheduled message by TokenId /// /// The destination address of the scheduled message /// The tokenId of the scheduled message - Task CancelScheduledSend(Uri destinationAddress, Guid tokenId); + /// + Task CancelScheduledSend(Uri destinationAddress, Guid tokenId, CancellationToken cancellationToken); } } diff --git a/src/MassTransit.Abstractions/Scheduling/MessageSchedulerContext.cs b/src/MassTransit.Abstractions/Scheduling/MessageSchedulerContext.cs index f6d3402a63a..3f0a617ec3b 100644 --- a/src/MassTransit.Abstractions/Scheduling/MessageSchedulerContext.cs +++ b/src/MassTransit.Abstractions/Scheduling/MessageSchedulerContext.cs @@ -66,8 +66,7 @@ Task> ScheduleSend(DateTime scheduledTime, T message, IPi Task ScheduleSend(DateTime scheduledTime, object message, Type messageType, CancellationToken cancellationToken = default); /// - /// Sends an object as a message, using the message type specified. If the object cannot be cast - /// to the specified message type, an exception will be thrown. + /// Sends an object as a message. /// /// The time at which the message should be delivered to the queue /// The message object diff --git a/src/MassTransit.Abstractions/Serialization/IMessageDeserializer.cs b/src/MassTransit.Abstractions/Serialization/IMessageDeserializer.cs index 75be39a1393..9c33dd0c1f4 100644 --- a/src/MassTransit.Abstractions/Serialization/IMessageDeserializer.cs +++ b/src/MassTransit.Abstractions/Serialization/IMessageDeserializer.cs @@ -1,4 +1,3 @@ -#nullable enable namespace MassTransit { using System; diff --git a/src/MassTransit.Abstractions/Serialization/IMessageSerializer.cs b/src/MassTransit.Abstractions/Serialization/IMessageSerializer.cs index 03491b5f455..bdb20e4f0ef 100644 --- a/src/MassTransit.Abstractions/Serialization/IMessageSerializer.cs +++ b/src/MassTransit.Abstractions/Serialization/IMessageSerializer.cs @@ -1,4 +1,3 @@ -#nullable enable namespace MassTransit { using System.Net.Mime; diff --git a/src/MassTransit.Abstractions/Serialization/IObjectDeserializer.cs b/src/MassTransit.Abstractions/Serialization/IObjectDeserializer.cs index da58bd5f15b..269ed85024a 100644 --- a/src/MassTransit.Abstractions/Serialization/IObjectDeserializer.cs +++ b/src/MassTransit.Abstractions/Serialization/IObjectDeserializer.cs @@ -1,4 +1,3 @@ -#nullable enable namespace MassTransit { public interface IObjectDeserializer diff --git a/src/MassTransit.Abstractions/Serialization/ISerialization.cs b/src/MassTransit.Abstractions/Serialization/ISerialization.cs index 141872872d0..b173d0f3954 100644 --- a/src/MassTransit.Abstractions/Serialization/ISerialization.cs +++ b/src/MassTransit.Abstractions/Serialization/ISerialization.cs @@ -1,6 +1,6 @@ -#nullable enable namespace MassTransit { + using System.Diagnostics.CodeAnalysis; using System.Net.Mime; diff --git a/src/MassTransit.Abstractions/Serialization/Serialization/EmptyHeaders.cs b/src/MassTransit.Abstractions/Serialization/Serialization/EmptyHeaders.cs index 43f20b9f889..2186c047526 100644 --- a/src/MassTransit.Abstractions/Serialization/Serialization/EmptyHeaders.cs +++ b/src/MassTransit.Abstractions/Serialization/Serialization/EmptyHeaders.cs @@ -2,6 +2,7 @@ namespace MassTransit.Serialization { using System.Collections; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -19,7 +20,7 @@ public IEnumerable> GetAll() return Enumerable.Empty>(); } - public bool TryGetHeader(string key, out object? value) + public bool TryGetHeader(string key, [NotNullWhen(true)] out object? value) { value = default; return false; diff --git a/src/MassTransit.Abstractions/Serialization/SerializerContextExtensions.cs b/src/MassTransit.Abstractions/Serialization/SerializerContextExtensions.cs index 424d619999e..3cc49a27d76 100644 --- a/src/MassTransit.Abstractions/Serialization/SerializerContextExtensions.cs +++ b/src/MassTransit.Abstractions/Serialization/SerializerContextExtensions.cs @@ -1,8 +1,8 @@ -#nullable enable namespace MassTransit { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; using Serialization; using Transports; diff --git a/src/MassTransit.Abstractions/Topology/Configuration/ConsumeTopologyConfigurationObservable.cs b/src/MassTransit.Abstractions/Topology/Configuration/ConsumeTopologyConfigurationObservable.cs index 955b988393b..a398754ba50 100644 --- a/src/MassTransit.Abstractions/Topology/Configuration/ConsumeTopologyConfigurationObservable.cs +++ b/src/MassTransit.Abstractions/Topology/Configuration/ConsumeTopologyConfigurationObservable.cs @@ -12,5 +12,17 @@ public void MessageTopologyCreated(IMessageConsumeTopologyConfigurator con { ForEach(observer => observer.MessageTopologyCreated(configuration)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Topology/Configuration/IMessageConsumeTopologyConvention.cs b/src/MassTransit.Abstractions/Topology/Configuration/IMessageConsumeTopologyConvention.cs index 4afcc0c7b1d..758377f1537 100644 --- a/src/MassTransit.Abstractions/Topology/Configuration/IMessageConsumeTopologyConvention.cs +++ b/src/MassTransit.Abstractions/Topology/Configuration/IMessageConsumeTopologyConvention.cs @@ -1,16 +1,19 @@ namespace MassTransit.Configuration { + using System.Diagnostics.CodeAnalysis; + + public interface IMessageConsumeTopologyConvention : IMessageConsumeTopologyConvention where TMessage : class { - bool TryGetMessageConsumeTopology(out IMessageConsumeTopology messageConsumeTopology); + bool TryGetMessageConsumeTopology([NotNullWhen(true)] out IMessageConsumeTopology? messageConsumeTopology); } public interface IMessageConsumeTopologyConvention { - bool TryGetMessageConsumeTopologyConvention(out IMessageConsumeTopologyConvention convention) + bool TryGetMessageConsumeTopologyConvention([NotNullWhen(true)] out IMessageConsumeTopologyConvention? convention) where T : class; } } diff --git a/src/MassTransit.Abstractions/Topology/Configuration/MessageTopologyConfigurationObservable.cs b/src/MassTransit.Abstractions/Topology/Configuration/MessageTopologyConfigurationObservable.cs index 2fa5d6a277f..9ed0cc8c30f 100644 --- a/src/MassTransit.Abstractions/Topology/Configuration/MessageTopologyConfigurationObservable.cs +++ b/src/MassTransit.Abstractions/Topology/Configuration/MessageTopologyConfigurationObservable.cs @@ -12,5 +12,17 @@ public void MessageTopologyCreated(IMessageTopologyConfigurator configurat { ForEach(observer => observer.MessageTopologyCreated(configuration)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Topology/Configuration/PublishToSendTopologyConfigurationObserver.cs b/src/MassTransit.Abstractions/Topology/Configuration/PublishToSendTopologyConfigurationObserver.cs index 20016a7eddd..7869fcf8907 100644 --- a/src/MassTransit.Abstractions/Topology/Configuration/PublishToSendTopologyConfigurationObserver.cs +++ b/src/MassTransit.Abstractions/Topology/Configuration/PublishToSendTopologyConfigurationObserver.cs @@ -1,6 +1,7 @@ namespace MassTransit.Configuration { using System; + using System.Diagnostics.CodeAnalysis; using Middleware; @@ -43,7 +44,7 @@ public void Apply(ITopologyPipeBuilder> builder) public bool Exclude => false; - public bool TryGetPublishAddress(Uri baseAddress, out Uri? publishAddress) + public bool TryGetPublishAddress(Uri baseAddress, [NotNullWhen(true)] out Uri? publishAddress) { publishAddress = null; return false; diff --git a/src/MassTransit.Abstractions/Topology/Configuration/PublishTopologyConfigurationObservable.cs b/src/MassTransit.Abstractions/Topology/Configuration/PublishTopologyConfigurationObservable.cs index 1b722f1197f..e1dfb9b098e 100644 --- a/src/MassTransit.Abstractions/Topology/Configuration/PublishTopologyConfigurationObservable.cs +++ b/src/MassTransit.Abstractions/Topology/Configuration/PublishTopologyConfigurationObservable.cs @@ -12,5 +12,17 @@ public void MessageTopologyCreated(IMessagePublishTopologyConfigurator con { ForEach(observer => observer.MessageTopologyCreated(configurator)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Topology/Configuration/SendTopologyConfigurationObservable.cs b/src/MassTransit.Abstractions/Topology/Configuration/SendTopologyConfigurationObservable.cs index 9d173d8a9d6..124f8fb5d72 100644 --- a/src/MassTransit.Abstractions/Topology/Configuration/SendTopologyConfigurationObservable.cs +++ b/src/MassTransit.Abstractions/Topology/Configuration/SendTopologyConfigurationObservable.cs @@ -12,5 +12,17 @@ public void MessageTopologyCreated(IMessageSendTopologyConfigurator config { ForEach(observer => observer.MessageTopologyCreated(configuration)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit.Abstractions/Topology/IMessagePublishTopology.cs b/src/MassTransit.Abstractions/Topology/IMessagePublishTopology.cs index a6af33f72fc..beabbf7333b 100644 --- a/src/MassTransit.Abstractions/Topology/IMessagePublishTopology.cs +++ b/src/MassTransit.Abstractions/Topology/IMessagePublishTopology.cs @@ -1,6 +1,7 @@ namespace MassTransit { using System; + using System.Diagnostics.CodeAnalysis; using Configuration; diff --git a/src/MassTransit.Abstractions/Topology/MessageNameFormatterEntityNameFormatter.cs b/src/MassTransit.Abstractions/Topology/MessageNameFormatterEntityNameFormatter.cs index 4d1fdaf773e..f4dc2de8062 100644 --- a/src/MassTransit.Abstractions/Topology/MessageNameFormatterEntityNameFormatter.cs +++ b/src/MassTransit.Abstractions/Topology/MessageNameFormatterEntityNameFormatter.cs @@ -15,7 +15,7 @@ public MessageNameFormatterEntityNameFormatter(IMessageNameFormatter formatter) string IEntityNameFormatter.FormatEntityName() { - return _formatter.GetMessageName(typeof(T)).ToString(); + return _formatter.GetMessageName(typeof(T)); } } } diff --git a/src/MassTransit.Abstractions/Topology/Topology/MessagePublishTopology.cs b/src/MassTransit.Abstractions/Topology/Topology/MessagePublishTopology.cs index 959fea6ee74..6ec60a14e6e 100644 --- a/src/MassTransit.Abstractions/Topology/Topology/MessagePublishTopology.cs +++ b/src/MassTransit.Abstractions/Topology/Topology/MessagePublishTopology.cs @@ -2,7 +2,7 @@ namespace MassTransit.Topology { using System; using System.Collections.Generic; - using System.Linq; + using System.Diagnostics.CodeAnalysis; using Configuration; using Internals; @@ -11,20 +11,25 @@ public class MessagePublishTopology : IMessagePublishTopologyConfigurator where TMessage : class { - readonly IList> _conventions; - readonly IList> _delegateTopologies; - readonly IList> _topologies; + readonly List> _conventions; + readonly List> _delegateTopologies; + readonly IPublishTopology _publishTopology; + readonly List> _topologies; + bool? _exclude; public MessagePublishTopology(IPublishTopology publishTopology) { - _conventions = new List>(); - _topologies = new List>(); - _delegateTopologies = new List>(); - - Exclude = IsMessageTypeExcluded(publishTopology); + _publishTopology = publishTopology; + _conventions = new List>(8); + _topologies = new List>(8); + _delegateTopologies = new List>(8); } - public bool Exclude { get; set; } + public bool Exclude + { + get => _exclude ??= IsMessageTypeExcluded(); + set => _exclude = value; + } public void Add(IMessagePublishTopology publishTopology) { @@ -40,12 +45,12 @@ public void Apply(ITopologyPipeBuilder> builder) { ITopologyPipeBuilder> delegatedBuilder = builder.CreateDelegatedBuilder(); - foreach (IMessagePublishTopology topology in _delegateTopologies) - topology.Apply(delegatedBuilder); + for (var i = 0; i < _delegateTopologies.Count; i++) + _delegateTopologies[i].Apply(delegatedBuilder); - foreach (IMessagePublishTopologyConvention convention in _conventions) + for (var i = 0; i < _conventions.Count; i++) { - if (convention.TryGetMessagePublishTopology(out IMessagePublishTopology topology)) + if (_conventions[i].TryGetMessagePublishTopology(out IMessagePublishTopology topology)) topology.Apply(builder); } @@ -61,12 +66,24 @@ public virtual bool TryGetPublishAddress(Uri baseAddress, [NotNullWhen(true)] ou public bool TryAddConvention(IMessagePublishTopologyConvention convention) { - if (_conventions.Any(x => x.GetType() == convention.GetType())) - return false; + var conventionType = convention.GetType(); + + for (var i = 0; i < _conventions.Count; i++) + { + if (_conventions[i].GetType() == conventionType) + return false; + } + _conventions.Add(convention); return true; } + public bool TryAddConvention(IPublishTopologyConvention convention) + { + return convention.TryGetMessagePublishTopologyConvention(out IMessagePublishTopologyConvention messagePublishTopologyConvention) + && TryAddConvention(messagePublishTopologyConvention); + } + public void AddOrUpdateConvention(Func add, Func update) where TConvention : class, IMessagePublishTopologyConvention { @@ -74,8 +91,7 @@ public void AddOrUpdateConvention(Func add, Func(Func add, Func Validate() { - return Enumerable.Empty(); + yield break; } - static bool IsMessageTypeExcluded(IPublishTopology publishTopology) + bool IsMessageTypeExcluded() { - if (typeof(TMessage).GetCustomAttributes(typeof(ExcludeFromTopologyAttribute), false).Any()) + if (typeof(TMessage).GetCustomAttributes(typeof(ExcludeFromTopologyAttribute), false).Length > 0) return true; - if (typeof(TMessage).ClosesType(typeof(Fault<>), out Type[] types) && publishTopology.GetMessageTopology(types[0]).Exclude) + if (typeof(TMessage).ClosesType(typeof(Fault<>), out Type[] types) && _publishTopology.GetMessageTopology(types[0]).Exclude) return true; return false; diff --git a/src/MassTransit.Abstractions/Topology/Topology/MessageSendTopology.cs b/src/MassTransit.Abstractions/Topology/Topology/MessageSendTopology.cs index 6f7fbd8d79a..531ef98fb98 100644 --- a/src/MassTransit.Abstractions/Topology/Topology/MessageSendTopology.cs +++ b/src/MassTransit.Abstractions/Topology/Topology/MessageSendTopology.cs @@ -2,6 +2,7 @@ namespace MassTransit.Topology { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; using Configuration; @@ -10,15 +11,15 @@ public class MessageSendTopology : IMessageSendTopologyConfigurator where TMessage : class { - readonly IList> _conventions; - readonly IList> _delegateTopologies; - readonly IList> _topologies; + readonly List> _conventions; + readonly List> _delegateTopologies; + readonly List> _topologies; public MessageSendTopology() { - _conventions = new List>(); - _topologies = new List>(); - _delegateTopologies = new List>(); + _conventions = new List>(8); + _topologies = new List>(8); + _delegateTopologies = new List>(8); } public void Add(IMessageSendTopology sendTopology) @@ -35,29 +36,27 @@ public void Apply(ITopologyPipeBuilder> builder) { ITopologyPipeBuilder> delegatedBuilder = builder.CreateDelegatedBuilder(); - foreach (IMessageSendTopology topology in _delegateTopologies) - topology.Apply(delegatedBuilder); + for (var i = 0; i < _delegateTopologies.Count; i++) + _delegateTopologies[i].Apply(delegatedBuilder); - foreach (IMessageSendTopologyConvention convention in _conventions) + for (var i = 0; i < _conventions.Count; i++) { - if (convention.TryGetMessageSendTopology(out IMessageSendTopology topology)) + if (_conventions[i].TryGetMessageSendTopology(out IMessageSendTopology topology)) topology.Apply(builder); } - foreach (IMessageSendTopology topology in _topologies) - topology.Apply(builder); + for (var i = 0; i < _topologies.Count; i++) + _topologies[i].Apply(builder); } - public bool TryGetConvention(out TConvention? convention) + public bool TryGetConvention([NotNullWhen(true)] out TConvention? convention) where TConvention : class, IMessageSendTopologyConvention { for (var i = 0; i < _conventions.Count; i++) { convention = _conventions[i] as TConvention; if (convention != null) - { return true; - } } convention = default; @@ -66,23 +65,32 @@ public bool TryGetConvention(out TConvention? convention) public bool TryAddConvention(IMessageSendTopologyConvention convention) { - if (_conventions.Any(x => x.GetType() == convention.GetType())) - return false; + var conventionType = convention.GetType(); + + for (var i = 0; i < _conventions.Count; i++) + { + if (_conventions[i].GetType() == conventionType) + return false; + } _conventions.Add(convention); return true; } + public bool TryAddConvention(ISendTopologyConvention convention) + { + return convention.TryGetMessageSendTopologyConvention(out IMessageSendTopologyConvention messageSendTopologyConvention) + && TryAddConvention(messageSendTopologyConvention); + } + public void UpdateConvention(Func update) where TConvention : class, IMessageSendTopologyConvention { for (var i = 0; i < _conventions.Count; i++) { - var convention = _conventions[i] as TConvention; - if (convention != null) + if (_conventions[i] is TConvention convention) { - var updatedConvention = update(convention); - _conventions[i] = updatedConvention; + _conventions[i] = update(convention); return; } } @@ -93,11 +101,9 @@ public void AddOrUpdateConvention(Func add, Func : IMessageTopologyConfigurator where TMessage : class { - readonly Lazy _entityName; + string? _entityName; public MessageTopology(IMessageEntityNameFormatter entityNameFormatter) { EntityNameFormatter = entityNameFormatter; - - _entityName = new Lazy(() => EntityNameFormatter.FormatEntityName()); } public IMessageEntityNameFormatter EntityNameFormatter { get; private set; } - public string EntityName => _entityName.Value; + public string EntityName => _entityName ??= EntityNameFormatter.FormatEntityName(); public void SetEntityNameFormatter(IMessageEntityNameFormatter entityNameFormatter) { if (entityNameFormatter == null) throw new ArgumentNullException(nameof(entityNameFormatter)); - if (_entityName.IsValueCreated) + if (_entityName != null) { - if (_entityName.Value == entityNameFormatter.FormatEntityName()) + if (_entityName == entityNameFormatter.FormatEntityName()) return; throw new ConfigurationException( - $"The message type {TypeCache.ShortName} entity name was already evaluated: {_entityName.Value}"); + $"The message type {TypeCache.ShortName} entity name was already evaluated: {_entityName}"); } EntityNameFormatter = entityNameFormatter; diff --git a/src/MassTransit.Abstractions/Topology/Topology/PublishTopology.cs b/src/MassTransit.Abstractions/Topology/Topology/PublishTopology.cs index c2a9c4d65d0..5cf9df73e6d 100644 --- a/src/MassTransit.Abstractions/Topology/Topology/PublishTopology.cs +++ b/src/MassTransit.Abstractions/Topology/Topology/PublishTopology.cs @@ -12,21 +12,20 @@ public class PublishTopology : IPublishTopologyConfigurator, IPublishTopologyConfigurationObserver { - readonly IList _conventions; + readonly List _conventions; readonly object _lock = new object(); - readonly ConcurrentDictionary _messageTypeFactoryCache; - readonly ConcurrentDictionary _messageTypes; + readonly ConcurrentDictionary> _messageTypes; + readonly ConcurrentDictionary _messageTypeSelectorCache; readonly PublishTopologyConfigurationObservable _observers; public PublishTopology() { - _messageTypes = new ConcurrentDictionary(); + _messageTypes = new ConcurrentDictionary>(); + _messageTypeSelectorCache = new ConcurrentDictionary(); - _observers = new PublishTopologyConfigurationObservable(); - _messageTypeFactoryCache = new ConcurrentDictionary(); - - _conventions = new List(); + _conventions = new List(8); + _observers = new PublishTopologyConfigurationObservable(); _observers.Connect(this); } @@ -47,8 +46,7 @@ IMessagePublishTopologyConfigurator IPublishTopologyConfigurator.GetMessageTo public bool TryGetPublishAddress(Type messageType, Uri baseAddress, out Uri? publishAddress) { - return _messageTypes.GetOrAdd(messageType, CreateMessageType) - .TryGetPublishAddress(baseAddress, out publishAddress); + return GetMessageTopology(messageType).TryGetPublishAddress(baseAddress, out publishAddress); } public ConnectHandle ConnectPublishTopologyConfigurationObserver(IPublishTopologyConfigurationObserver observer) @@ -58,13 +56,23 @@ public ConnectHandle ConnectPublishTopologyConfigurationObserver(IPublishTopolog public bool TryAddConvention(IPublishTopologyConvention convention) { + var conventionType = convention.GetType(); + lock (_lock) { - if (_conventions.Any(x => x.GetType() == convention.GetType())) - return false; + for (var i = 0; i < _conventions.Count; i++) + { + if (_conventions[i].GetType() == conventionType) + return false; + } + _conventions.Add(convention); - return true; } + + foreach (Lazy messagePublishTopologyConfigurator in _messageTypes.Values) + messagePublishTopologyConfigurator.Value.TryAddConvention(convention); + + return true; } void IPublishTopologyConfigurator.AddMessagePublishTopology(IMessagePublishTopology topology) @@ -76,7 +84,7 @@ void IPublishTopologyConfigurator.AddMessagePublishTopology(IMessagePublishTo public virtual IEnumerable Validate() { - return _messageTypes.Values.SelectMany(x => x.Validate()); + return _messageTypes.Values.SelectMany(x => x.Value.Validate()); } IMessagePublishTopology IPublishTopology.GetMessageTopology(Type messageType) @@ -89,12 +97,11 @@ public IMessagePublishTopologyConfigurator GetMessageTopology(Type messageType) if (MessageTypeCache.IsValidMessageType(messageType) == false) throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason(messageType), nameof(messageType)); - var topology = _messageTypes.GetOrAdd(messageType, CreateMessageType); - - return topology; + return _messageTypeSelectorCache.GetOrAdd(messageType, _ => Activation.Activate(messageType, new MessageTypeSelectorFactory(), this)) + .GetMessageTopology(); } - protected virtual IMessagePublishTopologyConfigurator CreateMessageTopology(Type type) + protected virtual IMessagePublishTopologyConfigurator CreateMessageTopology() where T : class { var messageTopology = new MessagePublishTopology(this); @@ -114,9 +121,10 @@ protected IMessagePublishTopologyConfigurator GetMessageTopology() if (MessageTypeCache.IsValidMessageType == false) throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); - var topology = _messageTypes.GetOrAdd(typeof(T), CreateMessageTopology); + Lazy topology = + _messageTypes.GetOrAdd(typeof(T), _ => new Lazy(() => CreateMessageTopology())); - return (IMessagePublishTopologyConfigurator)topology; + return (IMessagePublishTopologyConfigurator)topology.Value; } protected void OnMessageTopologyCreated(IMessagePublishTopologyConfigurator messageTopology) @@ -127,8 +135,8 @@ protected void OnMessageTopologyCreated(IMessagePublishTopologyConfigurator(Action callback) { - foreach (T configurator in _messageTypes.Values) - callback(configurator); + foreach (Lazy configurator in _messageTypes.Values) + callback((T)configurator.Value); } void ApplyConventionsToMessageTopology(IMessagePublishTopologyConfigurator messageTopology) @@ -145,17 +153,6 @@ void ApplyConventionsToMessageTopology(IMessagePublishTopologyConfigurator } } - IMessagePublishTopologyConfigurator CreateMessageType(Type messageType) - { - return GetOrAddByMessageType(messageType).CreateMessageType(); - } - - IMessageTypeFactory GetOrAddByMessageType(Type type) - { - return _messageTypeFactoryCache.GetOrAdd(type, - _ => (IMessageTypeFactory)Activator.CreateInstance(typeof(MessageTypeFactory<>).MakeGenericType(type), this)!); - } - class ImplementedMessageTypeConnector : IImplementedMessageType @@ -175,26 +172,37 @@ public void ImplementsMessageType(bool direct) } - interface IMessageTypeFactory + readonly struct MessageTypeSelectorFactory : + IActivationType { - IMessagePublishTopologyConfigurator CreateMessageType(); + public IMessageTypeSelector ActivateType(PublishTopology consumeTopology) + where T : class + { + return new MessageTypeSelector(consumeTopology); + } } - class MessageTypeFactory : - IMessageTypeFactory + interface IMessageTypeSelector + { + IMessagePublishTopologyConfigurator GetMessageTopology(); + } + + + class MessageTypeSelector : + IMessageTypeSelector where T : class { - readonly IPublishTopologyConfigurator _configurator; + readonly PublishTopology _publishTopology; - public MessageTypeFactory(IPublishTopologyConfigurator configurator) + public MessageTypeSelector(PublishTopology publishTopology) { - _configurator = configurator; + _publishTopology = publishTopology; } - public IMessagePublishTopologyConfigurator CreateMessageType() + public IMessagePublishTopologyConfigurator GetMessageTopology() { - return _configurator.GetMessageTopology(); + return _publishTopology.GetMessageTopology(); } } } diff --git a/src/MassTransit.Abstractions/Topology/Topology/SendTopology.cs b/src/MassTransit.Abstractions/Topology/Topology/SendTopology.cs index eacbcb2491b..2a6bdfe4670 100644 --- a/src/MassTransit.Abstractions/Topology/Topology/SendTopology.cs +++ b/src/MassTransit.Abstractions/Topology/Topology/SendTopology.cs @@ -11,18 +11,18 @@ public class SendTopology : ISendTopologyConfigurator, ISendTopologyConfigurationObserver { - readonly IList _conventions; + readonly List _conventions; readonly object _lock = new object(); - readonly ConcurrentDictionary _messageTypes; + readonly ConcurrentDictionary> _messageTypes; readonly SendTopologyConfigurationObservable _observers; public SendTopology() { - _messageTypes = new ConcurrentDictionary(); + _messageTypes = new ConcurrentDictionary>(); _observers = new SendTopologyConfigurationObservable(); - _conventions = new List(); + _conventions = new List(8); DeadLetterQueueNameFormatter = DefaultDeadLetterQueueNameFormatter.Instance; ErrorQueueNameFormatter = DefaultErrorQueueNameFormatter.Instance; @@ -30,23 +30,24 @@ public SendTopology() _observers.Connect(this); } - public IDeadLetterQueueNameFormatter DeadLetterQueueNameFormatter { get; set; } - public IErrorQueueNameFormatter ErrorQueueNameFormatter { get; set; } - void ISendTopologyConfigurationObserver.MessageTopologyCreated(IMessageSendTopologyConfigurator messageTopology) { ApplyConventionsToMessageTopology(messageTopology); } + public IDeadLetterQueueNameFormatter DeadLetterQueueNameFormatter { get; set; } + public IErrorQueueNameFormatter ErrorQueueNameFormatter { get; set; } + public IMessageSendTopologyConfigurator GetMessageTopology() where T : class { if (MessageTypeCache.IsValidMessageType == false) throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); - var specification = _messageTypes.GetOrAdd(typeof(T), CreateMessageTopology); + Lazy? specification = _messageTypes.GetOrAdd(typeof(T), + type => new Lazy(() => CreateMessageTopology(type))); - return (IMessageSendTopologyConfigurator)specification; + return (IMessageSendTopologyConfigurator)specification.Value; } public ConnectHandle ConnectSendTopologyConfigurationObserver(ISendTopologyConfigurationObserver observer) @@ -56,13 +57,23 @@ public ConnectHandle ConnectSendTopologyConfigurationObserver(ISendTopologyConfi public bool TryAddConvention(ISendTopologyConvention convention) { + var conventionType = convention.GetType(); + lock (_lock) { - if (_conventions.Any(x => x.GetType() == convention.GetType())) - return false; + for (var i = 0; i < _conventions.Count; i++) + { + if (_conventions[i].GetType() == conventionType) + return false; + } + _conventions.Add(convention); - return true; } + + foreach (Lazy messageSendTopologyConfigurator in _messageTypes.Values) + messageSendTopologyConfigurator.Value.TryAddConvention(convention); + + return true; } void ISendTopologyConfigurator.AddMessageSendTopology(IMessageSendTopology topology) @@ -74,7 +85,7 @@ void ISendTopologyConfigurator.AddMessageSendTopology(IMessageSendTopology public virtual IEnumerable Validate() { - return _messageTypes.Values.SelectMany(x => x.Validate()); + return _messageTypes.Values.SelectMany(x => x.Value.Validate()); } protected virtual IMessageSendTopologyConfigurator CreateMessageTopology(Type type) diff --git a/src/MassTransit.Abstractions/Transports/DictionaryHeaderProvider.cs b/src/MassTransit.Abstractions/Transports/DictionaryHeaderProvider.cs index 6faf7be6784..8044583df82 100644 --- a/src/MassTransit.Abstractions/Transports/DictionaryHeaderProvider.cs +++ b/src/MassTransit.Abstractions/Transports/DictionaryHeaderProvider.cs @@ -1,6 +1,7 @@ namespace MassTransit.Transports { using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; /// diff --git a/src/MassTransit.Abstractions/Transports/IHeaderProvider.cs b/src/MassTransit.Abstractions/Transports/IHeaderProvider.cs index b2a83f8c36e..3852baab457 100644 --- a/src/MassTransit.Abstractions/Transports/IHeaderProvider.cs +++ b/src/MassTransit.Abstractions/Transports/IHeaderProvider.cs @@ -1,6 +1,7 @@ namespace MassTransit.Transports { using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; /// diff --git a/src/MassTransit.Abstractions/Transports/IMessageNameFormatter.cs b/src/MassTransit.Abstractions/Transports/IMessageNameFormatter.cs index 57785537791..f5ad6141065 100644 --- a/src/MassTransit.Abstractions/Transports/IMessageNameFormatter.cs +++ b/src/MassTransit.Abstractions/Transports/IMessageNameFormatter.cs @@ -9,6 +9,6 @@ namespace MassTransit.Transports /// public interface IMessageNameFormatter { - MessageName GetMessageName(Type type); + string GetMessageName(Type type); } } diff --git a/src/MassTransit.Abstractions/Transports/MessageName.cs b/src/MassTransit.Abstractions/Transports/MessageName.cs deleted file mode 100644 index 825086c972d..00000000000 --- a/src/MassTransit.Abstractions/Transports/MessageName.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace MassTransit.Transports -{ - using System; - using System.Runtime.Serialization; - - - /// - /// Class encapsulating naming strategies for exchanges corresponding - /// to message types. - /// - [Serializable] - public class MessageName : - ISerializable - { - public MessageName(string name) - { - Name = name; - } - - protected MessageName(SerializationInfo info, StreamingContext context) - { - Name = info.GetString("Name"); - } - - public string? Name { get; } - - public void GetObjectData(SerializationInfo info, StreamingContext context) - { - info.AddValue("Name", Name); - } - - public override string ToString() - { - return Name ?? ""; - } - } -} diff --git a/src/MassTransit.Abstractions/TypeCache.cs b/src/MassTransit.Abstractions/TypeCache.cs index bea3ff9a8f4..6cba31ff246 100644 --- a/src/MassTransit.Abstractions/TypeCache.cs +++ b/src/MassTransit.Abstractions/TypeCache.cs @@ -2,16 +2,15 @@ namespace MassTransit { using System; using System.Collections.Concurrent; - using System.Threading; using Internals; + using Metadata; public static class TypeCache { static CachedType GetOrAdd(Type type) { - return Cached.Instance.GetOrAdd(type, _ => Activator.CreateInstance(typeof(CachedType<>).MakeGenericType(type)) as CachedType - ?? throw new InvalidOperationException("Failed to create cached type")); + return Cached.Instance.GetOrAdd(type, _ => Activation.Activate(type, new Factory())); } internal static void GetOrAdd(Type type, ITypeCache typeCache) @@ -25,6 +24,17 @@ public static string GetShortName(Type type) } + readonly struct Factory : + IActivationType + { + public CachedType ActivateType() + where T : class + { + return new CachedType(); + } + } + + static class Cached { internal static readonly ConcurrentDictionary Instance = new ConcurrentDictionary(); @@ -53,6 +63,14 @@ public CachedType(ITypeCache typeCache) } public string ShortName => _shortName ??= TypeCache.ShortName; + + public void Method1() + { + } + + public void Method2() + { + } } } @@ -82,10 +100,18 @@ public class TypeCache : IReadWritePropertyCache ITypeCache.ReadWritePropertyCache => _writePropertyCache.Value; string ITypeCache.ShortName => _shortName; + public void Method1() + { + } + + public void Method2() + { + } + static class Cached { - internal static readonly Lazy> Metadata = new Lazy>(() => new TypeCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy> Metadata = new Lazy>(() => new TypeCache()); } } } diff --git a/src/MassTransit.Abstractions/Util/ActiveRequest.cs b/src/MassTransit.Abstractions/Util/ActiveRequest.cs index 7bb4ebf3f43..886a450c3ef 100644 --- a/src/MassTransit.Abstractions/Util/ActiveRequest.cs +++ b/src/MassTransit.Abstractions/Util/ActiveRequest.cs @@ -8,14 +8,25 @@ namespace MassTransit.Util public struct ActiveRequest : IDisposable { - public readonly int ResultLimit; readonly RequestRateAlgorithm _algorithm; + readonly CancellationTokenRegistration _registration; + readonly CancellationTokenSource _source; + readonly TimeSpan _timeout; bool _completed; - public ActiveRequest(RequestRateAlgorithm algorithm, int resultLimit) + public readonly CancellationToken CancellationToken; + public readonly int ResultLimit; + + public ActiveRequest(RequestRateAlgorithm algorithm, int resultLimit, CancellationToken cancellationToken, TimeSpan timeout) { - ResultLimit = resultLimit; _algorithm = algorithm; + _timeout = timeout; + _source = new CancellationTokenSource(); + _registration = cancellationToken.Register(Callback, this); + + CancellationToken = _source.Token; + ResultLimit = resultLimit; + _completed = false; } @@ -28,10 +39,18 @@ public Task Complete(int count, CancellationToken cancellationToken = default) public void Dispose() { + _registration.Dispose(); + _source.Dispose(); + if (_completed) return; _algorithm.CancelRequest(ResultLimit); } + + void Callback(object? obj) + { + _source.CancelAfter(_timeout); + } } } diff --git a/src/MassTransit.Abstractions/Util/Connectable.cs b/src/MassTransit.Abstractions/Util/Connectable.cs index 1c0d189cfdd..9b3208dfdce 100644 --- a/src/MassTransit.Abstractions/Util/Connectable.cs +++ b/src/MassTransit.Abstractions/Util/Connectable.cs @@ -2,7 +2,6 @@ namespace MassTransit.Util { using System; using System.Collections.Generic; - using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,19 +14,43 @@ public class Connectable where T : class { readonly Dictionary _connections; - T[] _connected; + T[]? _connected; long _nextId; public Connectable() { _connections = new Dictionary(); - _connected = Array.Empty(); + _connected = null; + } + + public T[] Connected + { + get + { + T[]? read = Volatile.Read(ref _connected); + if (read != null) + return read; + + lock (_connections) + { + read = Volatile.Read(ref _connected); + if (read != null) + return read; + + var connected = new T[_connections.Count]; + _connections.Values.CopyTo(connected, 0); + + Volatile.Write(ref _connected, connected); + + return connected; + } + } } /// /// The number of connections /// - public int Count => _connected.Length; + public int Count => Connected.Length; /// /// Connect a connectable type @@ -44,7 +67,7 @@ public ConnectHandle Connect(T connection) lock (_connections) { _connections.Add(id, connection); - _connected = _connections.Values.ToArray(); + _connected = null; } return new Handle(id, this); @@ -60,9 +83,7 @@ public Task ForEachAsync(Func callback) if (callback == null) throw new ArgumentNullException(nameof(callback)); - T[] connected; - lock (_connections) - connected = _connected; + T[] connected = Connected; if (connected.Length == 0) return Task.CompletedTask; @@ -89,9 +110,7 @@ public Task ForEachAsync(Func callback) public void ForEach(Action callback) { - T[] connected; - lock (_connections) - connected = _connected; + T[] connected = Connected; switch (connected.Length) { @@ -111,9 +130,7 @@ public void ForEach(Action callback) public bool All(Func callback) { - T[] connected; - lock (_connections) - connected = _connected; + T[] connected = Connected; if (connected.Length == 0) return true; @@ -135,10 +152,22 @@ void Disconnect(long id) lock (_connections) { _connections.Remove(id); - _connected = _connections.Values.ToArray(); + _connected = null; } } + public void Method1() + { + } + + public void Method2() + { + } + + public void Method3() + { + } + class Handle : ConnectHandle @@ -161,6 +190,14 @@ public void Dispose() { Disconnect(); } + + public void Method1() + { + } + + public void Method2() + { + } } } } diff --git a/src/MassTransit.Abstractions/Util/ExceptionUtil.cs b/src/MassTransit.Abstractions/Util/ExceptionUtil.cs index 94c4fe8aaac..c7c97936c2c 100644 --- a/src/MassTransit.Abstractions/Util/ExceptionUtil.cs +++ b/src/MassTransit.Abstractions/Util/ExceptionUtil.cs @@ -40,18 +40,25 @@ public static string GetStackTrace(Exception? exception) } public static IDictionary GetExceptionHeaderDictionary(Exception exception) + { + (Dictionary? dictionary, var message) = GetExceptionHeaderDetail(exception); + + return dictionary; + } + + public static (Dictionary, string) GetExceptionHeaderDetail(Exception exception) { exception = exception.GetBaseException() ?? exception; var exceptionMessage = GetMessage(exception); - return new Dictionary + return (new Dictionary { { MessageHeaders.Reason, "fault" }, { MessageHeaders.FaultExceptionType, TypeCache.GetShortName(exception.GetType()) }, { MessageHeaders.FaultMessage, exceptionMessage }, { MessageHeaders.FaultStackTrace, GetStackTrace(exception) } - }; + }, exceptionMessage); } } } diff --git a/src/MassTransit.Abstractions/Util/PendingTaskCollection.cs b/src/MassTransit.Abstractions/Util/PendingTaskCollection.cs index 087ae886a40..f48f857d1e4 100644 --- a/src/MassTransit.Abstractions/Util/PendingTaskCollection.cs +++ b/src/MassTransit.Abstractions/Util/PendingTaskCollection.cs @@ -9,7 +9,7 @@ namespace MassTransit.Util public class PendingTaskCollection { - readonly IDictionary _tasks; + readonly Dictionary _tasks; long _nextId; public PendingTaskCollection(int capacity) diff --git a/src/MassTransit.Abstractions/Util/RequestRateAlgorithm.cs b/src/MassTransit.Abstractions/Util/RequestRateAlgorithm.cs index 0b0d7cfced7..5f52ed97e27 100644 --- a/src/MassTransit.Abstractions/Util/RequestRateAlgorithm.cs +++ b/src/MassTransit.Abstractions/Util/RequestRateAlgorithm.cs @@ -31,6 +31,7 @@ public class RequestRateAlgorithm : readonly SemaphoreSlim? _rateLimitSemaphore; readonly Timer? _rateLimitTimer; readonly int _refreshThreshold; + readonly TimeSpan _requestCancellationTimeout; readonly int _requestLimit; readonly object _requestLock = new object(); readonly SemaphoreSlim _requestSemaphore; @@ -56,6 +57,7 @@ public RequestRateAlgorithm(RequestRateAlgorithmOptions options) _options = options; + _requestCancellationTimeout = _options.RequestCancellationTimeout ?? TimeSpan.FromSeconds(1); _disposeToken = new CancellationTokenSource(); _requestCount = 1; @@ -130,11 +132,11 @@ public void Dispose() /// /// /// - public async Task Run(RequestCallback requestCallback, CancellationToken cancellationToken = default) + public async Task Run(RequestCallback requestCallback, CancellationToken cancellationToken = default) { var requestCount = _requestCount; - var tasks = new List(requestCount); + var tasks = new List>(requestCount); try { @@ -147,16 +149,20 @@ public async Task Run(RequestCallback requestCallback, CancellationToken cancell throw; } - await Task.WhenAll(tasks).ConfigureAwait(false); + var counts = await Task.WhenAll(tasks).ConfigureAwait(false); + + return counts.Sum(); } - async Task RunRequest(RequestCallback requestCallback, CancellationToken cancellationToken = default) + async Task RunRequest(RequestCallback requestCallback, CancellationToken cancellationToken = default) { using var activeRequest = await BeginRequest(cancellationToken).ConfigureAwait(false); - var count = await requestCallback(activeRequest.ResultLimit, cancellationToken).ConfigureAwait(false); + var count = await requestCallback(activeRequest.ResultLimit, activeRequest.CancellationToken).ConfigureAwait(false); await activeRequest.Complete(count, CancellationToken.None).ConfigureAwait(false); + + return count; } /// @@ -166,11 +172,11 @@ async Task RunRequest(RequestCallback requestCallback, CancellationToken cancell /// /// /// - public async Task Run(RequestCallback requestCallback, ResultCallback resultCallback, CancellationToken cancellationToken = default) + public async Task Run(RequestCallback requestCallback, ResultCallback resultCallback, CancellationToken cancellationToken = default) { var requestCount = _requestCount; - var tasks = new List(requestCount); + var tasks = new List>(requestCount); try { @@ -183,27 +189,29 @@ public async Task Run(RequestCallback requestCallback, ResultCallback r throw; } - await Task.WhenAll(tasks).ConfigureAwait(false); + var counts = await Task.WhenAll(tasks).ConfigureAwait(false); + + return counts.Sum(); } - async Task RunRequest(RequestCallback requestCallback, ResultCallback resultCallback, CancellationToken cancellationToken = default) + async Task RunRequest(RequestCallback requestCallback, ResultCallback resultCallback, CancellationToken cancellationToken = default) { using var activeRequest = await BeginRequest(cancellationToken).ConfigureAwait(false); - IEnumerable results = await requestCallback(activeRequest.ResultLimit, cancellationToken).ConfigureAwait(false); + IEnumerable results = await requestCallback(activeRequest.ResultLimit, activeRequest.CancellationToken).ConfigureAwait(false); var count = 0; try { foreach (var result in results) { - await _resultSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + await _resultSemaphore.WaitAsync(activeRequest.CancellationToken).ConfigureAwait(false); async Task RunResultCallback() { try { - await resultCallback(result, cancellationToken).ConfigureAwait(false); + await resultCallback(result, activeRequest.CancellationToken).ConfigureAwait(false); } finally { @@ -223,6 +231,8 @@ async Task RunResultCallback() } await activeRequest.Complete(count, CancellationToken.None).ConfigureAwait(false); + + return count; } /// @@ -235,7 +245,7 @@ async Task RunResultCallback() /// /// /// - public async Task Run(RequestCallback requestCallback, ResultCallback resultCallback, GroupCallback groupCallback, + public async Task Run(RequestCallback requestCallback, ResultCallback resultCallback, GroupCallback groupCallback, OrderCallback orderCallback, CancellationToken cancellationToken = default) { var requestCount = _requestCount; @@ -257,7 +267,7 @@ public async Task Run(RequestCallback requestCallback, ResultCallbac List> resultSets = groupCallback(results.SelectMany(x => x)).ToList(); - var resultTasks = new List(ResultLimit); + var resultTasks = new List>(ResultLimit); try { @@ -270,24 +280,26 @@ public async Task Run(RequestCallback requestCallback, ResultCallbac throw; } - await Task.WhenAll(resultTasks).ConfigureAwait(false); + var counts = await Task.WhenAll(resultTasks).ConfigureAwait(false); + + return counts.Sum(); } async Task> RunRequest(RequestCallback requestCallback, CancellationToken cancellationToken = default) { using var activeRequest = await BeginRequest(cancellationToken).ConfigureAwait(false); - List results = (await requestCallback(activeRequest.ResultLimit, cancellationToken).ConfigureAwait(false)).ToList(); + List results = (await requestCallback(activeRequest.ResultLimit, activeRequest.CancellationToken).ConfigureAwait(false)).ToList(); await activeRequest.Complete(results.Count, CancellationToken.None).ConfigureAwait(false); return results; } - async Task RunResultSet(IGrouping results, ResultCallback resultCallback, OrderCallback orderCallback, + async Task RunResultSet(IGrouping results, ResultCallback resultCallback, OrderCallback orderCallback, CancellationToken cancellationToken = default) { - var tasks = new List(ResultLimit); + var count = 0; try { @@ -307,16 +319,17 @@ async Task RunResultCallback() } } - tasks.Add(RunResultCallback()); + Add(RunResultCallback()); + count++; } } catch (Exception) { - if (tasks.Count == 0) + if (count == 0) throw; } - await Task.WhenAll(tasks).ConfigureAwait(false); + return count; } public async Task BeginRequest(CancellationToken cancellationToken = default) @@ -355,7 +368,7 @@ public async Task BeginRequest(CancellationToken cancellationToke _pendingResultCount += resultLimit; } - return new ActiveRequest(this, resultLimit); + return new ActiveRequest(this, resultLimit, cancellationToken, _requestCancellationTimeout); } catch (OperationCanceledException) { diff --git a/src/MassTransit.Abstractions/Util/RequestRateAlgorithmOptions.cs b/src/MassTransit.Abstractions/Util/RequestRateAlgorithmOptions.cs index c6cf63fed5a..1fa435cbc2a 100644 --- a/src/MassTransit.Abstractions/Util/RequestRateAlgorithmOptions.cs +++ b/src/MassTransit.Abstractions/Util/RequestRateAlgorithmOptions.cs @@ -29,5 +29,10 @@ public class RequestRateAlgorithmOptions /// The interval at which the request rate limit is reset /// public TimeSpan? RequestRateInterval { get; set; } + + /// + /// If specified, provides additional time when a request is canceled to avoid interrupting in-progress requests + /// + public TimeSpan? RequestCancellationTimeout { get; set; } } } diff --git a/src/MassTransit.Analyzers/MassTransit.Analyzers.csproj b/src/MassTransit.Analyzers/MassTransit.Analyzers.csproj index 9032e816871..bc91d322f32 100644 --- a/src/MassTransit.Analyzers/MassTransit.Analyzers.csproj +++ b/src/MassTransit.Analyzers/MassTransit.Analyzers.csproj @@ -9,7 +9,7 @@ - Remco Blok + Remco Blok, $(Authors) MassTransit $(Description) true diff --git a/src/MassTransit.Analyzers/MessageContractAnalyzer.cs b/src/MassTransit.Analyzers/MessageContractAnalyzer.cs index acf83246e86..7d6183042c4 100644 --- a/src/MassTransit.Analyzers/MessageContractAnalyzer.cs +++ b/src/MassTransit.Analyzers/MessageContractAnalyzer.cs @@ -162,7 +162,7 @@ static bool TypesAreStructurallyCompatible(TypeConversionHelper typeConverterHel foreach (var inputProperty in inputProperties) { - var contractProperty = contractProperties.FirstOrDefault(m => m.Name == inputProperty.Name); + var contractProperty = contractProperties.FirstOrDefault(m => m.Name.Equals(inputProperty.Name, StringComparison.OrdinalIgnoreCase)); var propertyPath = Append(path, inputProperty.Name); @@ -210,8 +210,7 @@ static bool PropertyTypesAreStructurallyCompatible(TypeConversionHelper typeConv { if (contractPropertyType.TypeKind.IsClassOrInterface()) { - if (!TypesAreStructurallyCompatible(typeConverterHelper, contractPropertyType, - inputPropertyType, path, incompatibleProperties)) + if (!TypesAreStructurallyCompatible(typeConverterHelper, contractPropertyType, inputPropertyType, path, incompatibleProperties)) return false; } else @@ -309,8 +308,7 @@ static bool KeyValueTypesAreStructurallyCompatible(TypeConversionHelper typeConv if (contractValueType.TypeKind.IsClassOrInterface()) { - if (!TypesAreStructurallyCompatible(typeConverterHelper, contractValueType, - inputValueType, path, incompatibleProperties)) + if (!TypesAreStructurallyCompatible(typeConverterHelper, contractValueType, inputValueType, path, incompatibleProperties)) return false; } else @@ -357,7 +355,7 @@ static bool HasMissingProperties(ITypeSymbol inputType, ITypeSymbol contractType foreach (var contractProperty in contractProperties) { - var inputProperty = inputProperties.FirstOrDefault(m => m.Name == contractProperty.Name); + var inputProperty = inputProperties.FirstOrDefault(m => m.Name.Equals(contractProperty.Name, StringComparison.OrdinalIgnoreCase)); var propertyPath = Append(path, contractProperty.Name); diff --git a/src/MassTransit.Analyzers/MessageContractCodeFixProvider.cs b/src/MassTransit.Analyzers/MessageContractCodeFixProvider.cs index eb83b0c300e..52379e5363e 100644 --- a/src/MassTransit.Analyzers/MessageContractCodeFixProvider.cs +++ b/src/MassTransit.Analyzers/MessageContractCodeFixProvider.cs @@ -1,5 +1,6 @@ namespace MassTransit.Analyzers { + using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Composition; @@ -53,9 +54,9 @@ public sealed override async Task RegisterCodeFixesAsync(CodeFixContext context) } static async Task AddMissingProperties(Document document, - AnonymousObjectCreationExpressionSyntax anonymousObject, - string fullType, - CancellationToken cancellationToken) + AnonymousObjectCreationExpressionSyntax anonymousObject, + string fullType, + CancellationToken cancellationToken) { var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false); var semanticModel = await document.GetSemanticModelAsync(cancellationToken).ConfigureAwait(false); @@ -92,7 +93,7 @@ static async Task FindAnonymousTypesWithMessageContractsInTree(IDictionary p.Name == name); + var contractProperty = contractProperties.FirstOrDefault(m => m.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (contractProperty != null) { @@ -122,9 +123,9 @@ await FindAnonymousTypesWithMessageContractsInTree(dictionary, anonymousObjectPr .ConfigureAwait(false); } else if (initializer.Expression is InvocationExpressionSyntax invocationExpressionSyntax - && semanticModel.GetSymbolInfo(invocationExpressionSyntax).Symbol is IMethodSymbol method - && method.ReturnType.IsList(out var methodReturnTypeArgument) - && methodReturnTypeArgument.IsAnonymousType) + && semanticModel.GetSymbolInfo(invocationExpressionSyntax).Symbol is IMethodSymbol method + && method.ReturnType.IsList(out var methodReturnTypeArgument) + && methodReturnTypeArgument.IsAnonymousType) { if (contractProperty.Type.IsImmutableArray(out var contractElementType) || contractProperty.Type.IsList(out contractElementType) || @@ -198,7 +199,8 @@ static SyntaxNode AddMissingProperties(SyntaxNode root, AnonymousObjectCreationE var propertiesToAdd = new List(); foreach (var messageContractProperty in contractProperties) { - var initializer = anonymousObject.Initializers.FirstOrDefault(i => GetName(i) == messageContractProperty.Name); + var initializer = anonymousObject.Initializers + .FirstOrDefault(i => GetName(i).Equals(messageContractProperty.Name, StringComparison.OrdinalIgnoreCase)); if (initializer == null) { var path = Enumerable.Empty(); @@ -237,8 +239,8 @@ static AnonymousObjectMemberDeclaratorSyntax CreateProperty(IPropertySymbol cont ExpressionSyntax expression; if (contractProperty.Type.IsImmutableArray(out var contractElementType) || - contractProperty.Type.IsList(out contractElementType) || - contractProperty.Type.IsArray(out contractElementType)) + contractProperty.Type.IsList(out contractElementType) || + contractProperty.Type.IsArray(out contractElementType)) { if (path.Contains(contractElementType, SymbolEqualityComparer.Default)) expression = CreateEmptyArray(contractElementType); @@ -265,16 +267,16 @@ static AnonymousObjectMemberDeclaratorSyntax CreateProperty(IPropertySymbol cont static ExpressionSyntax CreateEmptyArray(ITypeSymbol type) { return SyntaxFactory.InvocationExpression( - SyntaxFactory.MemberAccessExpression( - SyntaxKind.SimpleMemberAccessExpression, - SyntaxFactory.IdentifierName("Array"), - SyntaxFactory.GenericName( - SyntaxFactory.Identifier("Empty")) - .WithTypeArgumentList( - SyntaxFactory.TypeArgumentList( - SyntaxFactory.SingletonSeparatedList( - SyntaxFactory.IdentifierName(type.Name)))))) - .NormalizeWhitespace(); + SyntaxFactory.MemberAccessExpression( + SyntaxKind.SimpleMemberAccessExpression, + SyntaxFactory.IdentifierName("Array"), + SyntaxFactory.GenericName( + SyntaxFactory.Identifier("Empty")) + .WithTypeArgumentList( + SyntaxFactory.TypeArgumentList( + SyntaxFactory.SingletonSeparatedList( + SyntaxFactory.IdentifierName(type.Name)))))) + .NormalizeWhitespace(); } static ImplicitArrayCreationExpressionSyntax CreateImplicitArray(ITypeSymbol type, IEnumerable path) diff --git a/src/MassTransit.Interop.NServiceBus/MassTransit.Interop.NServiceBus.csproj b/src/MassTransit.Interop.NServiceBus/MassTransit.Interop.NServiceBus.csproj index ea2e3d0111b..5f249615b9a 100644 --- a/src/MassTransit.Interop.NServiceBus/MassTransit.Interop.NServiceBus.csproj +++ b/src/MassTransit.Interop.NServiceBus/MassTransit.Interop.NServiceBus.csproj @@ -1,12 +1,12 @@  - netstandard2.0;net6.0 + netstandard2.0;net6.0;net8.0 MassTransit - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -16,10 +16,6 @@ MassTransit Interoperability support for NServiceBus; $(Description) - - - - diff --git a/src/MassTransit.MessagePack/Configuration/MessagePackConfigurationExtensions.cs b/src/MassTransit.MessagePack/Configuration/MessagePackConfigurationExtensions.cs new file mode 100644 index 00000000000..855a1bd18e7 --- /dev/null +++ b/src/MassTransit.MessagePack/Configuration/MessagePackConfigurationExtensions.cs @@ -0,0 +1,45 @@ +namespace MassTransit; + +using Serialization; + + +public static class MessagePackConfigurationExtensions +{ + /// + /// Use the MessagePack serializer as the default serializer for the receive endpoint + /// + /// + /// + public static void UseMessagePackSerializer(this IReceiveEndpointConfigurator configurator, bool isDefault = true) + { + var factory = new MessagePackSerializerFactory(); + + configurator.AddSerializer(factory, isDefault); + configurator.AddDeserializer(factory, isDefault); + } + + /// + /// Use the MessagePack serializer + /// + /// + /// + public static void UseMessagePackSerializer(this IBusFactoryConfigurator configurator, bool isDefault = true) + { + var factory = new MessagePackSerializerFactory(); + + configurator.AddSerializer(factory, isDefault); + configurator.AddDeserializer(factory, isDefault); + } + + /// + /// Use the MessagePack deserializer, optionally setting it as the default message deserializer if no content type is found. + /// + /// + /// + public static void UseMessagePackDeserializer(this IBusFactoryConfigurator configurator, bool isDefault = false) + { + var factory = new MessagePackSerializerFactory(); + + configurator.AddDeserializer(factory, isDefault); + } +} diff --git a/src/MassTransit.MessagePack/MassTransit.MessagePack.csproj b/src/MassTransit.MessagePack/MassTransit.MessagePack.csproj new file mode 100644 index 00000000000..99c10599054 --- /dev/null +++ b/src/MassTransit.MessagePack/MassTransit.MessagePack.csproj @@ -0,0 +1,30 @@ + + + + + netstandard2.0;net6.0;net8.0 + MassTransit + enable + + + + $(TargetFrameworks);net472 + + + + MassTransit.MessagePack + MassTransit.MessagePack + MassTransit;MessagePack;MsgPack + MassTransit MessagePack support; $(Description) + + + + + + + + + + + + diff --git a/src/MassTransit.PrometheusIntegration/MassTransit.PrometheusIntegration.csproj.DotSettings b/src/MassTransit.MessagePack/MassTransit.MessagePack.csproj.DotSettings similarity index 100% rename from src/MassTransit.PrometheusIntegration/MassTransit.PrometheusIntegration.csproj.DotSettings rename to src/MassTransit.MessagePack/MassTransit.MessagePack.csproj.DotSettings diff --git a/src/MassTransit.MessagePack/NullableAttributes.cs b/src/MassTransit.MessagePack/NullableAttributes.cs new file mode 100644 index 00000000000..78ebe713d4f --- /dev/null +++ b/src/MassTransit.MessagePack/NullableAttributes.cs @@ -0,0 +1,24 @@ +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 + namespace System.Diagnostics.CodeAnalysis + { + using System; + + + [AttributeUsage(AttributeTargets.Parameter)] + sealed class NotNullWhenAttribute : + Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } + } +#endif diff --git a/src/MassTransit.MessagePack/Serialization/InternalMessagePackResolver.cs b/src/MassTransit.MessagePack/Serialization/InternalMessagePackResolver.cs new file mode 100644 index 00000000000..e10d0a52e40 --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/InternalMessagePackResolver.cs @@ -0,0 +1,16 @@ +namespace MassTransit.Serialization; + +using MessagePack; +using MessagePack.Resolvers; + + +static class InternalMessagePackResolver +{ + static IFormatterResolver InternalResolverInstance { get; } = + CompositeResolver.Create(NativeDateTimeResolver.Instance, + ContractlessStandardResolverAllowPrivate.Instance, + MassTransitMessagePackFormatterResolver.Instance, + DynamicGenericResolver.Instance); + + public static MessagePackSerializerOptions Options { get; } = MessagePackSerializerOptions.Standard.WithResolver(InternalResolverInstance); +} diff --git a/src/MassTransit.MessagePack/Serialization/MassTransitMessagePackFormatterResolver.cs b/src/MassTransit.MessagePack/Serialization/MassTransitMessagePackFormatterResolver.cs new file mode 100644 index 00000000000..b9b237ede3a --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MassTransitMessagePackFormatterResolver.cs @@ -0,0 +1,182 @@ +#define USE_CONCRETE_MAPPERS +namespace MassTransit.Serialization; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Contracts.JobService; +using Courier.Contracts; +using Courier.Messages; +using Events; +using JobService.Messages; +using MessagePack; +using MessagePack.Formatters; +using MessagePackFormatters; +using Metadata; +using Scheduling; + + +class MassTransitMessagePackFormatterResolver : + IFormatterResolver +{ + public static MassTransitMessagePackFormatterResolver Instance { get; } = new(); + + readonly Dictionary _mappedNonGenericTypes; + readonly Dictionary _mappedGenericTypes; + + readonly ConcurrentDictionary _cachedFormatters; + + MassTransitMessagePackFormatterResolver() + { + _mappedGenericTypes = new Dictionary + { + // May only contain open generic types. + { typeof(MessageData<>), typeof(MessageDataFormatter<>) }, + }; + + _mappedNonGenericTypes = new Dictionary + { + // May only contain non-open generic types. + #if USE_CONCRETE_MAPPERS + { typeof(Fault), typeof(InterfaceConcreteMapFormatter) }, + { typeof(ReceiveFault), typeof(InterfaceConcreteMapFormatter) }, + { typeof(ExceptionInfo), typeof(InterfaceConcreteMapFormatter) }, + { typeof(HostInfo), typeof(InterfaceConcreteMapFormatter) }, + { typeof(ScheduleMessage), typeof(InterfaceConcreteMapFormatter) }, + { typeof(ScheduleRecurringMessage), typeof(InterfaceConcreteMapFormatter) }, + { typeof(CancelScheduledMessage), typeof(InterfaceConcreteMapFormatter) }, + { + typeof(CancelScheduledRecurringMessage), + typeof(InterfaceConcreteMapFormatter) + }, + { + typeof(PauseScheduledRecurringMessage), + typeof(InterfaceConcreteMapFormatter) + }, + { + typeof(ResumeScheduledRecurringMessage), + typeof(InterfaceConcreteMapFormatter) + }, + { typeof(RoutingSlip), typeof(InterfaceConcreteMapFormatter) }, + { typeof(Activity), typeof(InterfaceConcreteMapFormatter) }, + { typeof(ActivityLog), typeof(InterfaceConcreteMapFormatter) }, + { typeof(CompensateLog), typeof(InterfaceConcreteMapFormatter) }, + { typeof(ActivityException), typeof(InterfaceConcreteMapFormatter) }, + { typeof(Subscription), typeof(InterfaceConcreteMapFormatter) }, + { typeof(RoutingSlipCompleted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(RoutingSlipFaulted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(RoutingSlipActivityCompleted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(RoutingSlipActivityFaulted), typeof(InterfaceConcreteMapFormatter) }, + { + typeof(RoutingSlipActivityCompensated), + typeof(InterfaceConcreteMapFormatter) + }, + { + typeof(RoutingSlipActivityCompensationFailed), + typeof(InterfaceConcreteMapFormatter) + }, + { + typeof(RoutingSlipCompensationFailed), + typeof(InterfaceConcreteMapFormatter) + }, + { typeof(RoutingSlipTerminated), typeof(InterfaceConcreteMapFormatter) }, + { typeof(RoutingSlipRevised), typeof(InterfaceConcreteMapFormatter) }, + { typeof(RecurringJobSchedule), typeof(InterfaceConcreteMapFormatter) }, + { typeof(AllocateJobSlot), typeof(InterfaceConcreteMapFormatter) }, + { typeof(CancelJob), typeof(InterfaceConcreteMapFormatter) }, + { typeof(CancelJobAttempt), typeof(InterfaceConcreteMapFormatter) }, + { typeof(CompleteJob), typeof(InterfaceConcreteMapFormatter) }, + { typeof(FaultJob), typeof(InterfaceConcreteMapFormatter) }, + { typeof(FinalizeJob), typeof(InterfaceConcreteMapFormatter) }, + { typeof(FinalizeJobAttempt), typeof(InterfaceConcreteMapFormatter) }, + { typeof(GetJobAttemptStatus), typeof(InterfaceConcreteMapFormatter) }, + { typeof(GetJobState), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobAttemptCanceled), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobAttemptCompleted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobAttemptFaulted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobAttemptStarted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobCanceled), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobCompleted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobFaulted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobRetryDelayElapsed), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobSlotAllocated), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobSlotReleased), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobSlotUnavailable), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobSlotWaitElapsed), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobState), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobStarted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobStatusCheckRequested), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobSubmissionAccepted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(JobSubmitted), typeof(InterfaceConcreteMapFormatter) }, + { typeof(RetryJob), typeof(InterfaceConcreteMapFormatter) }, + { typeof(RunJob), typeof(InterfaceConcreteMapFormatter) }, + { typeof(SaveJobState), typeof(InterfaceConcreteMapFormatter) }, + { typeof(SetConcurrentJobLimit), typeof(InterfaceConcreteMapFormatter) }, + { typeof(SetJobProgress), typeof(InterfaceConcreteMapFormatter) }, + { typeof(StartJob), typeof(InterfaceConcreteMapFormatter) }, + { typeof(StartJobAttempt), typeof(InterfaceConcreteMapFormatter) } + #endif + }; + + _cachedFormatters = new ConcurrentDictionary(); + } + + public IMessagePackFormatter? GetFormatter() + { + var tType = typeof(T); + + if (_cachedFormatters.TryGetValue(tType, out var cachedFormatter)) + return (IMessagePackFormatter)cachedFormatter; + + if (TryGetMappedType(tType, out Type? formatterType)) + { + var createdFormatterInstance = Activator.CreateInstance(formatterType); + + if (createdFormatterInstance is null) + throw new InvalidOperationException($"Failed to create an instance of {formatterType}."); + + var formatter = (IMessagePackFormatter)createdFormatterInstance; + _cachedFormatters[tType] = formatter; + return (IMessagePackFormatter)formatter; + } + + if (!typeof(T).IsInterface) + { + // If we have no mapper for the type, and it's not an interface, we can't create a formatter. + return null; + } + + var createdConcreteFormatter = new InterfaceMessagePackFormatter(); + _cachedFormatters[tType] = createdConcreteFormatter; + + return createdConcreteFormatter; + } + + + bool TryGetMappedType(Type originType, [NotNullWhen(true)] out Type? mappedTargetType) + { + // If the type is not generic, or it is a generic type definition, we use the non-generic mapping. + return !originType.IsGenericType || originType.IsGenericTypeDefinition + ? TryGetNonGenericMappedType(originType, out mappedTargetType) + : TryGetOpenGenericMappedType(originType, out mappedTargetType); + } + + bool TryGetNonGenericMappedType(Type originType, out Type? mappedTargetType) + { + return _mappedNonGenericTypes.TryGetValue(originType, out mappedTargetType); + } + + bool TryGetOpenGenericMappedType(Type originType, out Type? mappedTargetType) + { + var genericTypeDefinition = originType.GetGenericTypeDefinition(); + if (!_mappedGenericTypes.TryGetValue(genericTypeDefinition, out Type? openGenericMappedType)) + { + mappedTargetType = null; + return false; + } + + mappedTargetType = openGenericMappedType.MakeGenericType(originType.GenericTypeArguments); + return true; + } +} diff --git a/src/MassTransit.MessagePack/Serialization/MessagePackEnvelope.cs b/src/MassTransit.MessagePack/Serialization/MessagePackEnvelope.cs new file mode 100644 index 00000000000..70b34101981 --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MessagePackEnvelope.cs @@ -0,0 +1,196 @@ +namespace MassTransit.Serialization; + +using System; +using System.Collections.Generic; +using MessagePack; +using Metadata; + + +public class MessagePackEnvelope : + MessageEnvelope +{ + public string? MessageId { get; set; } + public string? RequestId { get; set; } + public string? CorrelationId { get; set; } + public string? ConversationId { get; set; } + public string? InitiatorId { get; set; } + public string? SourceAddress { get; set; } + public string? DestinationAddress { get; set; } + public string? ResponseAddress { get; set; } + public string? FaultAddress { get; set; } + public string[]? MessageType { get; set; } + public bool IsMessageNativeMessagePackSerialized { get; set; } + public object? Message { get; set; } + public DateTime? ExpirationTime { get; set; } + public DateTime? SentTime { get; set; } + public Dictionary? Headers { get; set; } + public HostInfo? Host { get; set; } + + public MessagePackEnvelope(SendContext context, object message) + { + if (context.MessageId.HasValue) + MessageId = context.MessageId.Value.ToString(); + + if (context.RequestId.HasValue) + RequestId = context.RequestId.Value.ToString(); + + if (context.CorrelationId.HasValue) + CorrelationId = context.CorrelationId.Value.ToString(); + + if (context.ConversationId.HasValue) + ConversationId = context.ConversationId.Value.ToString(); + + if (context.InitiatorId.HasValue) + InitiatorId = context.InitiatorId.Value.ToString(); + + if (context.SourceAddress != null) + SourceAddress = context.SourceAddress.ToString(); + + if (context.DestinationAddress != null) + DestinationAddress = context.DestinationAddress.ToString(); + + if (context.ResponseAddress != null) + ResponseAddress = context.ResponseAddress.ToString(); + + if (context.FaultAddress != null) + FaultAddress = context.FaultAddress.ToString(); + + MessageType = context.SupportedMessageTypes; + + IsMessageNativeMessagePackSerialized = true; + Message = MessagePackSerializer.Serialize(message, InternalMessagePackResolver.Options); + + if (context.TimeToLive.HasValue) + ExpirationTime = DateTime.UtcNow + context.TimeToLive; + + SentTime = context.SentTime ?? DateTime.UtcNow; + + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair header in context.Headers.GetAll()) + Headers[header.Key] = header.Value; + + Host = HostMetadataCache.Host; + } + + public MessagePackEnvelope(MessageEnvelope envelope) + { + MessageId = envelope.MessageId; + RequestId = envelope.RequestId; + CorrelationId = envelope.CorrelationId; + ConversationId = envelope.ConversationId; + InitiatorId = envelope.InitiatorId; + SourceAddress = envelope.SourceAddress; + DestinationAddress = envelope.DestinationAddress; + ResponseAddress = envelope.ResponseAddress; + FaultAddress = envelope.FaultAddress; + + MessageType = envelope.MessageType; + IsMessageNativeMessagePackSerialized = true; + Message = MessagePackSerializer.Serialize(envelope.Message, InternalMessagePackResolver.Options); + + ExpirationTime = envelope.ExpirationTime; + + SentTime = envelope.SentTime ?? DateTime.UtcNow; + + Headers = envelope.Headers != null + ? new Dictionary(envelope.Headers, StringComparer.OrdinalIgnoreCase) + : new Dictionary(StringComparer.OrdinalIgnoreCase); + + Host = envelope.Host ?? HostMetadataCache.Host; + } + + public MessagePackEnvelope(MessageContext context, object message, string[] messageTypesNames) + { + if (context.MessageId.HasValue) + MessageId = context.MessageId.Value.ToString(); + + if (context.RequestId.HasValue) + RequestId = context.RequestId.Value.ToString(); + + if (context.CorrelationId.HasValue) + CorrelationId = context.CorrelationId.Value.ToString(); + + if (context.ConversationId.HasValue) + ConversationId = context.ConversationId.Value.ToString(); + + if (context.InitiatorId.HasValue) + InitiatorId = context.InitiatorId.Value.ToString(); + + if (context.SourceAddress != null) + SourceAddress = context.SourceAddress.ToString(); + + if (context.DestinationAddress != null) + DestinationAddress = context.DestinationAddress.ToString(); + + if (context.ResponseAddress != null) + ResponseAddress = context.ResponseAddress.ToString(); + + if (context.FaultAddress != null) + FaultAddress = context.FaultAddress.ToString(); + + MessageType = messageTypesNames; + + IsMessageNativeMessagePackSerialized = true; + Message = MessagePackSerializer.Serialize(message, InternalMessagePackResolver.Options); + + ExpirationTime = context.ExpirationTime; + + SentTime = context.SentTime ?? DateTime.UtcNow; + + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair header in context.Headers.GetAll()) + Headers[header.Key] = header.Value; + + Host = HostMetadataCache.Host; + } + + /// + /// Used for deserialization. + /// + MessagePackEnvelope() + { + } + + internal void Update(SendContext context) + where T : class + { + DestinationAddress = context.DestinationAddress?.ToString(); + + if (context.SourceAddress != null) + SourceAddress = context.SourceAddress.ToString(); + + if (context.ResponseAddress != null) + ResponseAddress = context.ResponseAddress.ToString(); + + if (context.FaultAddress != null) + FaultAddress = context.FaultAddress.ToString(); + + if (context.MessageId.HasValue) + MessageId = context.MessageId.ToString(); + + if (context.RequestId.HasValue) + RequestId = context.RequestId.ToString(); + + if (context.ConversationId.HasValue) + ConversationId = context.ConversationId.ToString(); + + if (context.CorrelationId.HasValue) + CorrelationId = context.CorrelationId.ToString(); + + if (context.InitiatorId.HasValue) + InitiatorId = context.InitiatorId.ToString(); + + if (context.TimeToLive.HasValue) + ExpirationTime = DateTime.UtcNow + (context.TimeToLive > TimeSpan.Zero ? context.TimeToLive : TimeSpan.FromSeconds(1)); + + Headers ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (KeyValuePair header in context.Headers.GetAll()) + Headers[header.Key] = header.Value; + + if (MessageType != null) + context.SupportedMessageTypes = MessageType; + } +} diff --git a/src/MassTransit.MessagePack/Serialization/MessagePackFormatters/InterfaceConcreteMapFormatter.cs b/src/MassTransit.MessagePack/Serialization/MessagePackFormatters/InterfaceConcreteMapFormatter.cs new file mode 100644 index 00000000000..11f51775877 --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MessagePackFormatters/InterfaceConcreteMapFormatter.cs @@ -0,0 +1,27 @@ +namespace MassTransit.Serialization.MessagePackFormatters; + +using MessagePack; +using MessagePack.Formatters; + + +public class InterfaceConcreteMapFormatter : + IMessagePackFormatter + where TImplementation : TInterface +{ + public virtual void Serialize(ref MessagePackWriter writer, TInterface value, MessagePackSerializerOptions options) + { + IMessagePackFormatter innerFormatter = options.Resolver.GetFormatterWithVerify(); + + if (value is TImplementation implementation) + innerFormatter.Serialize(ref writer, implementation, options); + else + innerFormatter.Serialize(ref writer, (TImplementation)value!, options); + } + + public virtual TInterface Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + IMessagePackFormatter innerFormatter = options.Resolver.GetFormatterWithVerify(); + + return innerFormatter.Deserialize(ref reader, options); + } +} diff --git a/src/MassTransit.MessagePack/Serialization/MessagePackFormatters/InterfaceMessagePackFormatter.cs b/src/MassTransit.MessagePack/Serialization/MessagePackFormatters/InterfaceMessagePackFormatter.cs new file mode 100644 index 00000000000..4be7dfb2538 --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MessagePackFormatters/InterfaceMessagePackFormatter.cs @@ -0,0 +1,116 @@ +namespace MassTransit.Serialization.MessagePackFormatters; + +using System; +using System.Linq.Expressions; +using System.Reflection; +using System.Runtime.CompilerServices; +using Internals; +using MessagePack; +using MessagePack.Formatters; +using Metadata; + + +delegate void SerializeDelegate(ref MessagePackWriter writer, TConcrete value, MessagePackSerializerOptions options); + + +delegate TConcrete DeserializeDelegate(ref MessagePackReader reader, MessagePackSerializerOptions options); + + +public class InterfaceMessagePackFormatter : + IMessagePackFormatter +{ + static readonly FormatterProxyInfo _formatterProxyInfo; + + static InterfaceMessagePackFormatter() + { + var proxyType = TypeMetadataCache.GetImplementationType(typeof(TInterface)); + _formatterProxyInfo = GetFormatterProxyInfoFromType(proxyType); + } + + public void Serialize(ref MessagePackWriter writer, TInterface value, MessagePackSerializerOptions options) + { + FormatterProxyInfo formatterProxyInfoToUse; + + var typeOfValue = value?.GetType(); + + // If the value is not null and not an interface, use the formatter for the concrete type. + if (typeOfValue != null && !typeOfValue.IsInterface) + formatterProxyInfoToUse = GetFormatterProxyInfoFromType(typeOfValue); + else + formatterProxyInfoToUse = _formatterProxyInfo; + + + // IMessagePackFormatter of unknown type + var formatter = formatterProxyInfoToUse.GetFormatterMethodInfo.Invoke(options.Resolver, BindingFlags.Default, null, null, null); + + // Call Serialize method of IMessagePackFormatter + var writerParameter = Expression.Parameter(typeof(MessagePackWriter).MakeByRefType(), "writer"); + var valueParameterForCall = Expression.Parameter(formatterProxyInfoToUse.TargetType, "value"); + var optionsParameter = Expression.Parameter(typeof(MessagePackSerializerOptions), "options"); + + var formatterInstance = Expression.Constant(formatter); + var call = Expression.Call(formatterInstance, formatterProxyInfoToUse.SerializeMethodInfo, writerParameter, valueParameterForCall, optionsParameter); + + var delegateType = typeof(SerializeDelegate<>).MakeGenericType(formatterProxyInfoToUse.TargetType); + + var proxyFuncDelegate = Expression + .Lambda(delegateType, call, writerParameter, valueParameterForCall, optionsParameter) + .CompileFast(); + + var proxyFunc = Unsafe.As>(proxyFuncDelegate); + + proxyFunc(ref writer, value, options); + } + + public TInterface Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + var formatter = _formatterProxyInfo.GetFormatterMethodInfo.Invoke(options.Resolver, BindingFlags.Default, null, null, null); + + var readerParameter = Expression.Parameter(typeof(MessagePackReader).MakeByRefType(), "reader"); + var optionsParameter = Expression.Parameter(typeof(MessagePackSerializerOptions), "options"); + + var formatterInstance = Expression.Constant(formatter); + var call = Expression.Call(formatterInstance, _formatterProxyInfo.DeserializeMethodInfo, readerParameter, optionsParameter); + DeserializeDelegate? proxyFunc = Expression + .Lambda>(call, readerParameter, optionsParameter) + .CompileFast(); + + return proxyFunc(ref reader, options); + } + + static FormatterProxyInfo GetFormatterProxyInfoFromType(Type targetType) + { + // The null-forgiving operator (!) is used because the methods are guaranteed to exist, + // during normal operation. In case it doesn't, we likely won't even get here. + + var getFormatterMethodInfo = typeof(IFormatterResolver) + .GetMethod(nameof(IFormatterResolver.GetFormatter))! + .MakeGenericMethod(targetType); + + var formatterType = typeof(IMessagePackFormatter<>) + .MakeGenericType(targetType); + + var serializeMethodInfo = formatterType + .GetMethod(nameof(IMessagePackFormatter.Serialize))!; + + var deserializeMethodInfo = formatterType + .GetMethod(nameof(IMessagePackFormatter.Deserialize))!; + + return new FormatterProxyInfo + { + TargetType = targetType, + GetFormatterMethodInfo = getFormatterMethodInfo, + SerializeMethodInfo = serializeMethodInfo, + DeserializeMethodInfo = deserializeMethodInfo + }; + } + + + struct FormatterProxyInfo + { + public Type TargetType { get; set; } + public MethodInfo GetFormatterMethodInfo { get; set; } + public MethodInfo SerializeMethodInfo { get; set; } + public MethodInfo DeserializeMethodInfo { get; set; } + } +} diff --git a/src/MassTransit.MessagePack/Serialization/MessagePackFormatters/MessageDataFormatter.cs b/src/MassTransit.MessagePack/Serialization/MessagePackFormatters/MessageDataFormatter.cs new file mode 100644 index 00000000000..5d577b21864 --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MessagePackFormatters/MessageDataFormatter.cs @@ -0,0 +1,38 @@ +namespace MassTransit.Serialization.MessagePackFormatters; + +using JsonConverters; +using MessageData.Values; +using MessagePack; +using MessagePack.Formatters; + + +public class MessageDataFormatter : + IMessagePackFormatter> +{ + public void Serialize(ref MessagePackWriter writer, MessageData value, MessagePackSerializerOptions options) + { + var reference = new SystemTextMessageDataReference { Reference = value.Address }; + + // Borrows System.Text.Json's SystemTextMessageDataReference type. + IMessagePackFormatter? innerFormatter = options.Resolver.GetFormatterWithVerify(); + + innerFormatter.Serialize(ref writer, reference, options); + } + + public MessageData Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) + { + IMessagePackFormatter? innerFormatter = options.Resolver.GetFormatterWithVerify(); + + var reference = innerFormatter.Deserialize(ref reader, options); + + if (reference?.Text != null) + return (MessageData)new StringInlineMessageData(reference.Text, reference.Reference); + if (reference?.Data != null) + return (MessageData)new BytesInlineMessageData(reference.Data, reference.Reference); + + if (reference?.Reference == null) + return EmptyMessageData.Instance; + + return new DeserializedMessageData(reference.Reference); + } +} diff --git a/src/MassTransit.MessagePack/Serialization/MessagePackMessageBody.cs b/src/MassTransit.MessagePack/Serialization/MessagePackMessageBody.cs new file mode 100644 index 00000000000..f38eb47eaa9 --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MessagePackMessageBody.cs @@ -0,0 +1,45 @@ +namespace MassTransit.Serialization; + +using System; +using System.IO; +using MessagePack; + + +public class MessagePackMessageBody : + MessageBody + where TMessage : class +{ + public long? Length => _lazyMessagePackSerializedObject.Value.Length; + + readonly Lazy _lazyMessagePackSerializedObject; + + public MessagePackMessageBody(SendContext context, MessagePackEnvelope? envelope = null) + { + _lazyMessagePackSerializedObject = new Lazy(() => + { + var envelopeToSerialize = envelope ?? new MessagePackEnvelope(context, context.Message); + + return MessagePackSerializer.Serialize(envelopeToSerialize, InternalMessagePackResolver.Options); + }); + } + + public MessagePackMessageBody(TMessage message) + { + _lazyMessagePackSerializedObject = new Lazy(() => MessagePackSerializer.Serialize(message, InternalMessagePackResolver.Options)); + } + + public Stream GetStream() + { + return new MemoryStream(_lazyMessagePackSerializedObject.Value, false); + } + + public byte[] GetBytes() + { + return _lazyMessagePackSerializedObject.Value; + } + + public string GetString() + { + return Convert.ToBase64String(_lazyMessagePackSerializedObject.Value); + } +} diff --git a/src/MassTransit.MessagePack/Serialization/MessagePackMessageBodySerializer.cs b/src/MassTransit.MessagePack/Serialization/MessagePackMessageBodySerializer.cs new file mode 100644 index 00000000000..9b4c1e3de0b --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MessagePackMessageBodySerializer.cs @@ -0,0 +1,65 @@ +namespace MassTransit.Serialization; + +using System; +using System.Collections.Generic; +using System.Net.Mime; +using Internals; +using MessagePack; + + +class MessagePackMessageBodySerializer : + IMessageSerializer +{ + public ContentType ContentType { get; } = MessagePackMessageSerializer.MessagePackContentType; + + readonly MessagePackEnvelope _envelope; + + public MessagePackMessageBodySerializer(MessageEnvelope envelope) + { + _envelope = new MessagePackEnvelope(envelope); + } + + public MessageBody GetMessageBody(SendContext context) + where T : class + { + _envelope.Update(context); + + if (_envelope.MessageType != null) + context.SupportedMessageTypes = _envelope.MessageType; + + return new MessagePackMessageBody(context, _envelope); + } + + public void OverrideMessage(T message) + where T : class + { + Dictionary currentMessage; + + if (_envelope.Message is not null) + { + currentMessage = MessagePackSerializer + .Deserialize>((byte[])_envelope.Message); + } + else + currentMessage = new Dictionary(0); + + currentMessage = new Dictionary(currentMessage, StringComparer.OrdinalIgnoreCase); + var messageToMerge = message + .Transform>(SystemTextJsonMessageSerializer.Options); + + if (messageToMerge is null) + { + // If we are unable to transform the message, + // we should not override the message. + return; + } + + foreach (KeyValuePair overlay in messageToMerge) + currentMessage[overlay.Key] = overlay.Value; + + + _envelope.IsMessageNativeMessagePackSerialized = false; + _envelope.Message = MessagePackSerializer + .Serialize(currentMessage, InternalMessagePackResolver.Options); + } +} diff --git a/src/MassTransit.MessagePack/Serialization/MessagePackMessageSerializer.cs b/src/MassTransit.MessagePack/Serialization/MessagePackMessageSerializer.cs new file mode 100644 index 00000000000..8e2c0ed6f91 --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MessagePackMessageSerializer.cs @@ -0,0 +1,120 @@ +namespace MassTransit.Serialization; + +using System; +using System.Collections.Generic; +using System.Net.Mime; +using Initializers; +using Initializers.TypeConverters; +using Internals; +using MessagePack; + + +public class MessagePackMessageSerializer : + IMessageSerializer, + IMessageDeserializer, + IObjectDeserializer +{ + const string ContentTypeHeaderValue = "application/vnd.masstransit+msgpack"; + const string ProviderKey = "MessagePack"; + + public static readonly ContentType MessagePackContentType = new(ContentTypeHeaderValue); + + public ContentType ContentType => MessagePackContentType; + + public ConsumeContext Deserialize(ReceiveContext receiveContext) + { + var serializerContext = Deserialize(receiveContext.Body, receiveContext.TransportHeaders, receiveContext.InputAddress); + return new BodyConsumeContext(receiveContext, serializerContext); + } + + public SerializerContext Deserialize(MessageBody body, Headers headers, Uri? destinationAddress = null) + { + var messageBuffer = body.GetBytes(); + var envelope = DeserializeMessageBuffer(messageBuffer); + + var messageContext = new EnvelopeMessageContext(envelope, this); + + var messageTypes = envelope.MessageType ?? []; + + return new MessagePackMessageSerializerContext(this, messageContext, messageTypes, envelope); + } + + public MessageBody GetMessageBody(string text) + { + return new Base64MessageBody(text); + } + + public MessageBody GetMessageBody(SendContext context) + where T : class + { + return new MessagePackMessageBody(context); + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateScope("messagepack"); + scope.Add("contentType", ContentType.MediaType); + scope.Add("provider", ProviderKey); + } + + public static byte[] EnsureObjectBufferFormatIsByteArray(object serializedObjectAsUnknownFormat) + { + return serializedObjectAsUnknownFormat switch + { + string base64EncodedMessagePackBody => Convert.FromBase64String(base64EncodedMessagePackBody), + byte[] messagePackBody => messagePackBody, + _ => MessagePackSerializer.Serialize(serializedObjectAsUnknownFormat, InternalMessagePackResolver.Options) + }; + } + + public T? DeserializeObject(object? value, T? defaultValue = default) + where T : class + { + if (value is Dictionary objectByStringPairs) + { + // If the object is a Dictionary, we deserialize internally using JSON. + // MessagePack is case-sensitive, and would not be able to deserialize without correct casing. + + return objectByStringPairs.Transform(SystemTextJsonMessageSerializer.Options); + } + + return InternalDeserializeObject(value, defaultValue); + } + + public T? DeserializeObject(object? value, T? defaultValue = null) + where T : struct + { + return InternalDeserializeObject(value, defaultValue); + } + + public MessageBody SerializeObject(object? value) + { + if (value is null) + return new EmptyMessageBody(); + + return new MessagePackMessageBody(value); + } + + static T InternalDeserializeObject(object? value, T defaultValue) + { + if (value is null || Equals(value, defaultValue)) + return defaultValue; + + if (value is T valueAsT) + return valueAsT; + + if (value is string text + && TypeConverterCache.TryGetTypeConverter(out ITypeConverter? typeConverter) + && typeConverter.TryConvert(text, out var result)) + return result; + + var messageSerializedBuffer = EnsureObjectBufferFormatIsByteArray(value); + + return DeserializeMessageBuffer(messageSerializedBuffer); + } + + static T DeserializeMessageBuffer(byte[] messageBuffer) + { + return MessagePackSerializer.Deserialize(messageBuffer, InternalMessagePackResolver.Options); + } +} diff --git a/src/MassTransit.MessagePack/Serialization/MessagePackMessageSerializerContext.cs b/src/MassTransit.MessagePack/Serialization/MessagePackMessageSerializerContext.cs new file mode 100644 index 00000000000..2433e7ad031 --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MessagePackMessageSerializerContext.cs @@ -0,0 +1,105 @@ +namespace MassTransit.Serialization; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Internals; +using MessagePack; + + +public class MessagePackMessageSerializerContext : + BaseSerializerContext +{ + readonly MessagePackEnvelope _envelope; + + public MessagePackMessageSerializerContext(MessagePackMessageSerializer serializer, MessageContext context, string[] supportedMessageTypes, + MessagePackEnvelope envelope) + : base(serializer, context, supportedMessageTypes) + { + _envelope = envelope; + + if (_envelope.Message is null) + throw new ArgumentException("Message cannot be null.", nameof(envelope)); + } + + public override bool TryGetMessage(out T? message) + where T : class + { + if (!TryGetMessage(typeof(T), out var outMessage)) + { + message = default; + return false; + } + + message = (T)outMessage!; + return true; + } + + public override bool TryGetMessage(Type messageType, [NotNullWhen(true)] out object? message) + { + try + { + if (!IsSupportedMessageType(messageType)) + { + message = null; + return false; + } + + var messagePackSerializedObjectBuffer = MessagePackMessageSerializer.EnsureObjectBufferFormatIsByteArray(_envelope.Message!); + + if (_envelope.IsMessageNativeMessagePackSerialized) + message = MessagePackSerializer.Deserialize(messageType, messagePackSerializedObjectBuffer, InternalMessagePackResolver.Options); + else + { + // If a message is serialized as dictionary of string-object pairs, we need to deserialize using a different approach. + + var messageAsDictionary = MessagePackSerializer + .Deserialize>(messagePackSerializedObjectBuffer, InternalMessagePackResolver.Options); + + message = messageAsDictionary.Transform(messageType, SystemTextJsonMessageSerializer.Options); + } + + return message != default; + } + catch + { + message = default; + return false; + } + } + + public override IMessageSerializer GetMessageSerializer() + { + if (_envelope is null) + throw new InvalidOperationException("Context has no envelope."); + + return new MessagePackMessageBodySerializer(_envelope); + } + + public override IMessageSerializer GetMessageSerializer(MessageEnvelope envelope, T message) + { + var messageEnvelopeSerializer = new MessagePackMessageBodySerializer(envelope); + + messageEnvelopeSerializer.OverrideMessage(message); + + return messageEnvelopeSerializer; + } + + public override IMessageSerializer GetMessageSerializer(object message, string[] messageTypes) + { + var messagePackEnvelope = new MessagePackEnvelope(this, message, messageTypes); + + return new MessagePackMessageBodySerializer(messagePackEnvelope); + } + + public override Dictionary ToDictionary(T? message) + where T : class + { + if (message is null) + return new Dictionary(0, StringComparer.OrdinalIgnoreCase); + + // We serialize internally using JSON. + return message.Transform>(SystemTextJsonMessageSerializer.Options) + ?? new Dictionary(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/src/MassTransit.MessagePack/Serialization/MessagePackSerializerFactory.cs b/src/MassTransit.MessagePack/Serialization/MessagePackSerializerFactory.cs new file mode 100644 index 00000000000..e609ef0cdce --- /dev/null +++ b/src/MassTransit.MessagePack/Serialization/MessagePackSerializerFactory.cs @@ -0,0 +1,28 @@ +namespace MassTransit.Serialization; + +using System; +using System.Net.Mime; + + +public class MessagePackSerializerFactory + : ISerializerFactory +{ + public ContentType ContentType => MessagePackMessageSerializer.MessagePackContentType; + + readonly Lazy _serializer; + + public MessagePackSerializerFactory() + { + _serializer = new Lazy(() => new MessagePackMessageSerializer()); + } + + public IMessageSerializer CreateSerializer() + { + return _serializer.Value; + } + + public IMessageDeserializer CreateDeserializer() + { + return _serializer.Value; + } +} diff --git a/src/MassTransit.Newtonsoft/Configuration/EncryptedSerializerConfigurationExtensions.cs b/src/MassTransit.Newtonsoft/Configuration/EncryptedSerializerConfigurationExtensions.cs new file mode 100644 index 00000000000..3ee272bd505 --- /dev/null +++ b/src/MassTransit.Newtonsoft/Configuration/EncryptedSerializerConfigurationExtensions.cs @@ -0,0 +1,114 @@ +namespace MassTransit +{ + using Serialization; + + + public static class EncryptedSerializerConfigurationExtensions + { + /// + /// Serialize messages using the BSON message serializer with AES Encryption + /// + /// + /// + /// + public static void UseEncryptedSerializerV2WithFallback(this IBusFactoryConfigurator configurator, ICryptoStreamProviderV2 primaryProvider, + ICryptoStreamProviderV2 secondaryProvider) + { + var factory = new EncryptedFallbackSerializerFactoryV2(primaryProvider, secondaryProvider); + + configurator.AddSerializer(factory); + configurator.AddDeserializer(factory); + } + + /// + /// Serialize messages using the BSON message serializer with AES Encryption + /// + /// + /// + /// + public static void UseEncryptedSerializerV2WithFallback(this IReceiveEndpointConfigurator configurator, ICryptoStreamProviderV2 primaryProvider, + ICryptoStreamProviderV2 secondaryProvider) + { + var factory = new EncryptedFallbackSerializerFactoryV2(primaryProvider, secondaryProvider); + + configurator.AddSerializer(factory); + configurator.AddDeserializer(factory); + } + + /// + /// Serialize messages using the BSON message serializer with AES Encryption + /// + /// + /// + /// Cryptographic key for both encryption of plaintext message and decryption of ciphertext message + /// + /// + /// Cryptographic key for decryption of ciphertext message if the primary key fails + /// + public static void UseEncryptedSerializerV2WithFallback(this IBusFactoryConfigurator configurator, byte[] primarySymmetricKey, + byte[] secondarySymmetricKey) + { + var primaryKeyProvider = new ConstantSecureKeyProvider(primarySymmetricKey); + var secondaryKeyProvider = new ConstantSecureKeyProvider(secondarySymmetricKey); + + configurator.UseEncryptedSerializerV2WithFallback(primaryKeyProvider, secondaryKeyProvider); + } + + /// + /// Serialize messages using the BSON message serializer with AES Encryption + /// + /// + /// + /// Cryptographic key for both encryption of plaintext message and decryption of ciphertext message + /// + /// + /// Cryptographic key for decryption of ciphertext message if the primary key fails + /// + public static void UseEncryptedSerializerV2WithFallback(this IReceiveEndpointConfigurator configurator, byte[] primarySymmetricKey, + byte[] secondarySymmetricKey) + { + var primaryKeyProvider = new ConstantSecureKeyProvider(primarySymmetricKey); + var secondaryKeyProvider = new ConstantSecureKeyProvider(secondarySymmetricKey); + + configurator.UseEncryptedSerializerV2WithFallback(primaryKeyProvider, secondaryKeyProvider); + } + + /// + /// Serialize messages using the BSON message serializer with AES Encryption + /// + /// + /// + /// The custom key provider to provide the symmetric key for encryption of plaintext message and decryption of ciphertext message + /// + /// + /// The custom key provider to provide the symmetric key for decryption of ciphertext message if the primary key fails + /// + public static void UseEncryptedSerializerV2WithFallback(this IBusFactoryConfigurator configurator, ISecureKeyProvider primaryKeyProvider, + ISecureKeyProvider secondaryKeyProvider) + { + var primaryProvider = new AesCryptoStreamProviderV2(primaryKeyProvider); + var secondaryProvider = new AesCryptoStreamProviderV2(secondaryKeyProvider); + + configurator.UseEncryptedSerializerV2WithFallback(primaryProvider, secondaryProvider); + } + + /// + /// Serialize messages using the BSON message serializer with AES Encryption + /// + /// + /// + /// The custom key provider to provide the symmetric key for encryption of plaintext message and decryption of ciphertext message + /// + /// + /// The custom key provider to provide the symmetric key for decryption of ciphertext message if the primary key fails + /// + public static void UseEncryptedSerializerV2WithFallback(this IReceiveEndpointConfigurator configurator, ISecureKeyProvider primaryKeyProvider, + ISecureKeyProvider secondaryKeyProvider) + { + var primaryProvider = new AesCryptoStreamProviderV2(primaryKeyProvider); + var secondaryProvider = new AesCryptoStreamProviderV2(secondaryKeyProvider); + + configurator.UseEncryptedSerializerV2WithFallback(primaryProvider, secondaryProvider); + } + } +} diff --git a/src/MassTransit.Newtonsoft/Configuration/NewtonsoftRawJsonConfigurationExtensions.cs b/src/MassTransit.Newtonsoft/Configuration/NewtonsoftRawJsonConfigurationExtensions.cs index 4ea3b770ac6..0ed3ae41435 100644 --- a/src/MassTransit.Newtonsoft/Configuration/NewtonsoftRawJsonConfigurationExtensions.cs +++ b/src/MassTransit.Newtonsoft/Configuration/NewtonsoftRawJsonConfigurationExtensions.cs @@ -19,6 +19,20 @@ public static void UseNewtonsoftRawJsonSerializer(this IBusFactoryConfigurator c configurator.AddDeserializer(factory); } + /// + /// Add support for RAW JSON message serialization and deserialization using Newtonsoft (does not change the default serializer) + /// + /// + /// Options for the raw serializer behavior + public static void AddNewtonsoftRawJsonSerializer(this IBusFactoryConfigurator configurator, RawSerializerOptions options = + RawSerializerOptions.AddTransportHeaders | RawSerializerOptions.CopyHeaders) + { + var factory = new NewtonsoftRawJsonSerializerFactory(options); + + configurator.AddSerializer(factory, false); + configurator.AddDeserializer(factory); + } + /// /// Add the Newtonsoft raw JSON deserializer to the bus /// diff --git a/src/MassTransit.Newtonsoft/MassTransit.Newtonsoft.csproj b/src/MassTransit.Newtonsoft/MassTransit.Newtonsoft.csproj index c1bd40b426a..c957b15b7fa 100644 --- a/src/MassTransit.Newtonsoft/MassTransit.Newtonsoft.csproj +++ b/src/MassTransit.Newtonsoft/MassTransit.Newtonsoft.csproj @@ -2,12 +2,12 @@ - netstandard2.0;net6.0 + netstandard2.0;net6.0;net8.0 MassTransit - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -22,7 +22,6 @@ - diff --git a/src/MassTransit.Newtonsoft/NullableAttributes.cs b/src/MassTransit.Newtonsoft/NullableAttributes.cs new file mode 100644 index 00000000000..3f38561b675 --- /dev/null +++ b/src/MassTransit.Newtonsoft/NullableAttributes.cs @@ -0,0 +1,24 @@ +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using System; + + + [AttributeUsage(AttributeTargets.Parameter)] + sealed class NotNullWhenAttribute : + Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +#endif diff --git a/src/MassTransit.Newtonsoft/Serialization/EncryptedFallbackMessageDeserializerV2.cs b/src/MassTransit.Newtonsoft/Serialization/EncryptedFallbackMessageDeserializerV2.cs new file mode 100644 index 00000000000..164a7330140 --- /dev/null +++ b/src/MassTransit.Newtonsoft/Serialization/EncryptedFallbackMessageDeserializerV2.cs @@ -0,0 +1,106 @@ +namespace MassTransit.Serialization +{ + using System; + using System.Linq; + using System.Net.Mime; + using System.Runtime.Serialization; + using Newtonsoft.Json; + + + public class EncryptedFallbackMessageDeserializerV2 : + IMessageDeserializer + { + static readonly Type[] ExceptionsToRetry = { typeof(ArgumentOutOfRangeException), typeof(JsonReaderException) }; + + readonly IMessageDeserializer _primaryMessageDeserializer; + readonly IMessageDeserializer _secondaryMessageDeserializer; + + public EncryptedFallbackMessageDeserializerV2(ICryptoStreamProviderV2 primaryCryptoStream, ICryptoStreamProviderV2 secondaryCryptoStream) + { + _primaryMessageDeserializer = new EncryptedMessageDeserializerV2(BsonMessageSerializer.Deserializer, primaryCryptoStream); + _secondaryMessageDeserializer = new EncryptedMessageDeserializerV2(BsonMessageSerializer.Deserializer, secondaryCryptoStream); + } + + public EncryptedFallbackMessageDeserializerV2(IMessageDeserializer primaryMessageDeserializer, IMessageDeserializer secondaryMessageDeserializer) + { + _primaryMessageDeserializer = primaryMessageDeserializer; + _secondaryMessageDeserializer = secondaryMessageDeserializer; + } + + public ContentType ContentType => _primaryMessageDeserializer.ContentType; + + public void Probe(ProbeContext context) + { + _primaryMessageDeserializer.Probe(context); + _secondaryMessageDeserializer.Probe(context); + } + + public ConsumeContext Deserialize(ReceiveContext receiveContext) + { + return Deserialize(receiveContext, false); + } + + public SerializerContext Deserialize(MessageBody body, Headers headers, Uri destinationAddress = null) + { + return Deserialize(body, headers, false, destinationAddress); + } + + public MessageBody GetMessageBody(string text) + { + return GetMessageBody(text, false); + } + + ConsumeContext Deserialize(ReceiveContext receiveContext, bool isRetry) + { + try + { + return _primaryMessageDeserializer.Deserialize(receiveContext); + } + catch (SerializationException e) when (ShouldRetry(isRetry, e)) + { + return Deserialize(receiveContext, true); + } + catch + { + return _secondaryMessageDeserializer.Deserialize(receiveContext); + } + } + + SerializerContext Deserialize(MessageBody body, Headers headers, bool isRetry, Uri destinationAddress = null) + { + try + { + return _primaryMessageDeserializer.Deserialize(body, headers, destinationAddress); + } + catch (SerializationException e) when (ShouldRetry(isRetry, e)) + { + return Deserialize(body, headers, true, destinationAddress); + } + catch + { + return _secondaryMessageDeserializer.Deserialize(body, headers, destinationAddress); + } + } + + MessageBody GetMessageBody(string text, bool isRetry) + { + try + { + return _primaryMessageDeserializer.GetMessageBody(text); + } + catch (SerializationException e) when (ShouldRetry(isRetry, e)) + { + return GetMessageBody(text, true); + } + catch + { + return _secondaryMessageDeserializer.GetMessageBody(text); + } + } + + static bool ShouldRetry(bool isRetry, SerializationException e) + { + return ExceptionsToRetry.Contains(e.InnerException?.GetType()) && !isRetry; + } + } +} diff --git a/src/MassTransit.Newtonsoft/Serialization/EncryptedFallbackSerializerFactoryV2.cs b/src/MassTransit.Newtonsoft/Serialization/EncryptedFallbackSerializerFactoryV2.cs new file mode 100644 index 00000000000..9c5659b2a07 --- /dev/null +++ b/src/MassTransit.Newtonsoft/Serialization/EncryptedFallbackSerializerFactoryV2.cs @@ -0,0 +1,30 @@ +namespace MassTransit.Serialization +{ + using System.Net.Mime; + + + public class EncryptedFallbackSerializerFactoryV2 : + ISerializerFactory + { + readonly ICryptoStreamProviderV2 _cryptoStream; + readonly ICryptoStreamProviderV2 _fallbackCryptoStream; + + public EncryptedFallbackSerializerFactoryV2(ICryptoStreamProviderV2 cryptoStream, ICryptoStreamProviderV2 fallbackCryptoStream) + { + _cryptoStream = cryptoStream; + _fallbackCryptoStream = fallbackCryptoStream; + } + + public ContentType ContentType => EncryptedMessageSerializerV2.EncryptedContentType; + + public IMessageSerializer CreateSerializer() + { + return new EncryptedMessageSerializerV2(_cryptoStream); + } + + public IMessageDeserializer CreateDeserializer() + { + return new EncryptedFallbackMessageDeserializerV2(_cryptoStream, _fallbackCryptoStream); + } + } +} diff --git a/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializer.cs b/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializer.cs index ad1e973551f..d5db8db998b 100644 --- a/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializer.cs +++ b/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializer.cs @@ -40,7 +40,11 @@ public SerializerContext Deserialize(MessageBody body, Headers headers, Uri? des { try { - using var stream = body.GetStream(); + var messageBody = body is StringMessageBody smb + ? new Base64MessageBody(smb.GetString()) + : body; + + using var stream = messageBody.GetStream(); using var cryptoStream = _provider.GetDecryptStream(stream, headers); using var jsonReader = new BsonDataReader(cryptoStream); diff --git a/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializerV2.cs b/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializerV2.cs index fa599830e92..7d9b20d5cc2 100644 --- a/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializerV2.cs +++ b/src/MassTransit.Newtonsoft/Serialization/EncryptedMessageDeserializerV2.cs @@ -40,7 +40,11 @@ public SerializerContext Deserialize(MessageBody body, Headers headers, Uri? des { try { - using var stream = body.GetStream(); + var messageBody = body is StringMessageBody smb + ? new Base64MessageBody(smb.GetString()) + : body; + + using var stream = messageBody.GetStream(); using var disposingCryptoStream = _cryptoStreamProvider.GetDecryptStream(stream, headers); using var jsonReader = new BsonDataReader(disposingCryptoStream); diff --git a/src/MassTransit.Newtonsoft/Serialization/JsonConverters/CaseInsensitiveDictionaryJsonConverter.cs b/src/MassTransit.Newtonsoft/Serialization/JsonConverters/CaseInsensitiveDictionaryJsonConverter.cs index ac9fc255b95..f4df60f739e 100644 --- a/src/MassTransit.Newtonsoft/Serialization/JsonConverters/CaseInsensitiveDictionaryJsonConverter.cs +++ b/src/MassTransit.Newtonsoft/Serialization/JsonConverters/CaseInsensitiveDictionaryJsonConverter.cs @@ -25,13 +25,12 @@ protected override IConverter ValueFactory(Type objectType) static bool CanConvert(Type objectType, out Type keyType, out Type valueType) { - var typeInfo = objectType.GetTypeInfo(); - if (typeInfo.IsGenericType) + if (objectType.IsGenericType) { - if (typeInfo.ClosesType(typeof(IDictionary<,>), out Type[] elementTypes) - || typeInfo.ClosesType(typeof(IReadOnlyDictionary<,>), out elementTypes) - || typeInfo.ClosesType(typeof(Dictionary<,>), out elementTypes) - || (typeInfo.ClosesType(typeof(IEnumerable<>), out Type[] enumerableType) + if (objectType.ClosesType(typeof(IDictionary<,>), out Type[] elementTypes) + || objectType.ClosesType(typeof(IReadOnlyDictionary<,>), out elementTypes) + || objectType.ClosesType(typeof(Dictionary<,>), out elementTypes) + || (objectType.ClosesType(typeof(IEnumerable<>), out Type[] enumerableType) && enumerableType[0].ClosesType(typeof(KeyValuePair<,>), out elementTypes))) { keyType = elementTypes[0]; @@ -40,7 +39,7 @@ static bool CanConvert(Type objectType, out Type keyType, out Type valueType) if (keyType != typeof(string)) return false; - if (typeInfo.IsFSharpType()) + if (objectType.IsFSharpType()) return false; return true; diff --git a/src/MassTransit.Newtonsoft/Serialization/JsonConverters/InterfaceProxyConverter.cs b/src/MassTransit.Newtonsoft/Serialization/JsonConverters/InterfaceProxyConverter.cs index 500a71dad70..21a7a7e244c 100644 --- a/src/MassTransit.Newtonsoft/Serialization/JsonConverters/InterfaceProxyConverter.cs +++ b/src/MassTransit.Newtonsoft/Serialization/JsonConverters/InterfaceProxyConverter.cs @@ -16,7 +16,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s protected override IConverter ValueFactory(Type objectType) { - if (objectType.GetTypeInfo().IsInterface && MessageTypeCache.IsValidMessageType(objectType)) + if (objectType.IsInterface && MessageTypeCache.IsValidMessageType(objectType)) return (IConverter)Activator.CreateInstance(typeof(CachedConverter<>).MakeGenericType(objectType)); return new Unsupported(); diff --git a/src/MassTransit.Newtonsoft/Serialization/JsonConverters/InternalTypeConverter.cs b/src/MassTransit.Newtonsoft/Serialization/JsonConverters/InternalTypeConverter.cs index 43dd510f638..b6c1a3ff66c 100644 --- a/src/MassTransit.Newtonsoft/Serialization/JsonConverters/InternalTypeConverter.cs +++ b/src/MassTransit.Newtonsoft/Serialization/JsonConverters/InternalTypeConverter.cs @@ -21,9 +21,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s protected override IConverter ValueFactory(Type objectType) { - var typeInfo = objectType.GetTypeInfo(); - - if (typeInfo.IsInterface) + if (objectType.IsInterface) { if (objectType == typeof(Fault)) return new CachedConverter(); @@ -78,11 +76,11 @@ protected override IConverter ValueFactory(Type objectType) if (objectType == typeof(RoutingSlipRevised)) return new CachedConverter(); - if (typeInfo.IsGenericType && !typeInfo.IsGenericTypeDefinition) + if (objectType.IsGenericType && !objectType.IsGenericTypeDefinition) { - if (typeInfo.GetGenericTypeDefinition() == typeof(Fault<>)) + if (objectType.GetGenericTypeDefinition() == typeof(Fault<>)) { - Type[] arguments = typeInfo.GetGenericArguments(); + Type[] arguments = objectType.GetGenericArguments(); if (arguments.Length == 1 && !arguments[0].IsGenericParameter) { return (IConverter)Activator.CreateInstance(typeof(CachedConverter<>).MakeGenericType( @@ -90,9 +88,9 @@ protected override IConverter ValueFactory(Type objectType) } } - if (typeInfo.GetGenericTypeDefinition() == typeof(Batch<>)) + if (objectType.GetGenericTypeDefinition() == typeof(Batch<>)) { - Type[] arguments = typeInfo.GetGenericArguments(); + Type[] arguments = objectType.GetGenericArguments(); if (arguments.Length == 1 && !arguments[0].IsGenericParameter) { return (IConverter)Activator.CreateInstance(typeof(CachedConverter<>).MakeGenericType( diff --git a/src/MassTransit.Newtonsoft/Serialization/JsonConverters/ListJsonConverter.cs b/src/MassTransit.Newtonsoft/Serialization/JsonConverters/ListJsonConverter.cs index a761b28b87e..3ab31e6d883 100644 --- a/src/MassTransit.Newtonsoft/Serialization/JsonConverters/ListJsonConverter.cs +++ b/src/MassTransit.Newtonsoft/Serialization/JsonConverters/ListJsonConverter.cs @@ -28,38 +28,37 @@ protected override IConverter ValueFactory(Type objectType) static bool CanConvert(Type objectType, out Type elementType) { - var typeInfo = objectType.GetTypeInfo(); - if (typeInfo.IsGenericType) + if (objectType.IsGenericType) { - if (typeInfo.ClosesType(typeof(IDictionary<,>)) - || typeInfo.ClosesType(typeof(IReadOnlyDictionary<,>)) - || typeInfo.ClosesType(typeof(Dictionary<,>)) - || typeInfo.ClosesType(typeof(IEnumerable<>), out Type[] enumerableType) && enumerableType[0].ClosesType(typeof(KeyValuePair<,>))) + if (objectType.ClosesType(typeof(IDictionary<,>)) + || objectType.ClosesType(typeof(IReadOnlyDictionary<,>)) + || objectType.ClosesType(typeof(Dictionary<,>)) + || objectType.ClosesType(typeof(IEnumerable<>), out Type[] enumerableType) && enumerableType[0].ClosesType(typeof(KeyValuePair<,>))) { elementType = default; return false; } - if (typeInfo.ClosesType(typeof(IList<>), out Type[] elementTypes) - || typeInfo.ClosesType(typeof(IReadOnlyList<>), out elementTypes) - || typeInfo.ClosesType(typeof(List<>), out elementTypes) - || typeInfo.ClosesType(typeof(IReadOnlyCollection<>), out elementTypes) - || typeInfo.ClosesType(typeof(IEnumerable<>), out elementTypes)) + if (objectType.ClosesType(typeof(IList<>), out Type[] elementTypes) + || objectType.ClosesType(typeof(IReadOnlyList<>), out elementTypes) + || objectType.ClosesType(typeof(List<>), out elementTypes) + || objectType.ClosesType(typeof(IReadOnlyCollection<>), out elementTypes) + || objectType.ClosesType(typeof(IEnumerable<>), out elementTypes)) { elementType = elementTypes[0]; if (elementType.IsAbstract) return false; - if (typeInfo.IsFSharpType()) + if (objectType.IsFSharpType()) return false; return true; } } - if (typeInfo.IsArray && typeInfo.HasElementType && typeInfo.GetArrayRank() == 1) + if (objectType.IsArray && objectType.HasElementType && objectType.GetArrayRank() == 1) { - elementType = typeInfo.GetElementType(); + elementType = objectType.GetElementType(); if (elementType == typeof(byte)) return false; diff --git a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftBsonMessageBody.cs b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftBsonMessageBody.cs index a7f2cd3469d..17345a81c16 100644 --- a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftBsonMessageBody.cs +++ b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftBsonMessageBody.cs @@ -35,7 +35,7 @@ public byte[] GetBytes() try { - var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message, MessageTypeCache.MessageTypeNames); + var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message); using var stream = new MemoryStream(); using var jsonWriter = new BsonDataWriter(stream); diff --git a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftEnvelopeSerializerContext.cs b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftEnvelopeSerializerContext.cs index 4de2621414f..916d3347bed 100644 --- a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftEnvelopeSerializerContext.cs +++ b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftEnvelopeSerializerContext.cs @@ -3,6 +3,7 @@ namespace MassTransit.Serialization { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using JsonConverters; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -51,7 +52,7 @@ public override bool TryGetMessage(out T? message) return false; } - public override bool TryGetMessage(Type messageType, out object? message) + public override bool TryGetMessage(Type messageType, [NotNullWhen(true)] out object? message) { if (_message != null && messageType.IsInstanceOfType(_message)) { diff --git a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftJsonMessageBody.cs b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftJsonMessageBody.cs index b8c289f483d..eae15888e05 100644 --- a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftJsonMessageBody.cs +++ b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftJsonMessageBody.cs @@ -37,7 +37,7 @@ public byte[] GetBytes() try { - var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message, MessageTypeCache.MessageTypeNames); + var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message); using var stream = new MemoryStream(); using var writer = new StreamWriter(stream, MessageDefaults.Encoding, 1024, true); diff --git a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonBodyMessageSerializer.cs b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonBodyMessageSerializer.cs index 2613cf6ad96..61e34dc4708 100644 --- a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonBodyMessageSerializer.cs +++ b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonBodyMessageSerializer.cs @@ -9,14 +9,14 @@ namespace MassTransit.Serialization /// Used to serialize an existing deserialized message when a message is forwarded, scheduled, etc. /// public class NewtonsoftRawJsonBodyMessageSerializer : + RawMessageSerializer, IMessageSerializer { readonly string[]? _messageTypes; readonly RawSerializerOptions _options; JToken _message; - public NewtonsoftRawJsonBodyMessageSerializer(JToken message, ContentType contentType, RawSerializerOptions options, - string[]? messageTypes = null) + public NewtonsoftRawJsonBodyMessageSerializer(JToken message, ContentType contentType, RawSerializerOptions options, string[]? messageTypes = null) { _message = message; _options = options; @@ -30,8 +30,11 @@ public NewtonsoftRawJsonBodyMessageSerializer(JToken message, ContentType conten public MessageBody GetMessageBody(SendContext context) where T : class { - if (_messageTypes != null && _options.HasFlag(RawSerializerOptions.AddTransportHeaders)) - context.Headers.Set(MessageHeaders.MessageType, string.Join(";", _messageTypes)); + if (_messageTypes != null) + context.SupportedMessageTypes = _messageTypes; + + if (_options.HasFlag(RawSerializerOptions.AddTransportHeaders)) + SetRawMessageHeaders(context); return new NewtonsoftRawJsonMessageBody(context, _message); } diff --git a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonMessageSerializer.cs b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonMessageSerializer.cs index e9f8120863e..70f47716ac9 100644 --- a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonMessageSerializer.cs +++ b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonMessageSerializer.cs @@ -24,7 +24,7 @@ public MessageBody GetMessageBody(SendContext context) where T : class { if (_options.HasFlag(RawSerializerOptions.AddTransportHeaders)) - SetRawMessageHeaders(context); + SetRawMessageHeaders(context); return new NewtonsoftRawJsonMessageBody(context); } diff --git a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonSerializerContext.cs b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonSerializerContext.cs index 94df89f7ec5..4176b6b01a6 100644 --- a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonSerializerContext.cs +++ b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftRawJsonSerializerContext.cs @@ -49,7 +49,7 @@ public override IMessageSerializer GetMessageSerializer() public override IMessageSerializer GetMessageSerializer(MessageEnvelope envelope, T message) { - var serializer = new NewtonsoftJsonBodyMessageSerializer(envelope, _contentType); + var serializer = new NewtonsoftRawJsonBodyMessageSerializer(envelope.Message as JToken, _contentType, _options, envelope.MessageType); serializer.Overlay(message); diff --git a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftXmlMessageBody.cs b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftXmlMessageBody.cs index f7fae214ba6..b7572eca30e 100644 --- a/src/MassTransit.Newtonsoft/Serialization/NewtonsoftXmlMessageBody.cs +++ b/src/MassTransit.Newtonsoft/Serialization/NewtonsoftXmlMessageBody.cs @@ -36,7 +36,7 @@ public byte[] GetBytes() try { - var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message, MessageTypeCache.MessageTypeNames); + var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message); using var stream = new MemoryStream(); diff --git a/src/MassTransit.Newtonsoft/Serialization/RawXmlMessageSerializer.cs b/src/MassTransit.Newtonsoft/Serialization/RawXmlMessageSerializer.cs index baf9488ed2e..8d40645102d 100644 --- a/src/MassTransit.Newtonsoft/Serialization/RawXmlMessageSerializer.cs +++ b/src/MassTransit.Newtonsoft/Serialization/RawXmlMessageSerializer.cs @@ -23,7 +23,7 @@ public MessageBody GetMessageBody(SendContext context) where T : class { if (_options.HasFlag(RawSerializerOptions.AddTransportHeaders)) - SetRawMessageHeaders(context); + SetRawMessageHeaders(context); return new NewtonsoftRawXmlMessageBody(context); } diff --git a/src/MassTransit.PrometheusIntegration/Configuration/PrometheusConfigurationExtensions.cs b/src/MassTransit.PrometheusIntegration/Configuration/PrometheusConfigurationExtensions.cs deleted file mode 100644 index 83104d86061..00000000000 --- a/src/MassTransit.PrometheusIntegration/Configuration/PrometheusConfigurationExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace MassTransit -{ - using System; - using System.Diagnostics; - using System.IO; - using PrometheusIntegration; - using PrometheusIntegration.Configuration; - - - public static class PrometheusConfigurationExtensions - { - /// - /// Configure the bus to capture metrics for publication using Prometheus. - /// - /// - /// - /// - /// The service name for metrics reporting, defaults to the current process main module filename - /// - public static void UsePrometheusMetrics(this IBusFactoryConfigurator configurator, - Action configureOptions = null, - string serviceName = default) - { - var options = PrometheusMetricsOptions.CreateDefault(); - - configureOptions?.Invoke(options); - - PrometheusMetrics.TryConfigure(GetServiceName(serviceName), options); - - configurator.ConnectConsumerConfigurationObserver(new PrometheusConsumerConfigurationObserver()); - configurator.ConnectHandlerConfigurationObserver(new PrometheusHandlerConfigurationObserver()); - configurator.ConnectSagaConfigurationObserver(new PrometheusSagaConfigurationObserver()); - configurator.ConnectActivityConfigurationObserver(new PrometheusActivityConfigurationObserver()); - configurator.ConnectEndpointConfigurationObserver(new PrometheusReceiveEndpointConfiguratorObserver()); - configurator.ConnectBusObserver(new PrometheusBusObserver()); - } - - static string GetServiceName(string serviceName) - { - return string.IsNullOrWhiteSpace(serviceName) - ? Path.GetFileNameWithoutExtension(Process.GetCurrentProcess().MainModule.FileName) - : serviceName; - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/MassTransit.PrometheusIntegration.csproj b/src/MassTransit.PrometheusIntegration/MassTransit.PrometheusIntegration.csproj deleted file mode 100644 index 5e7418fcdaa..00000000000 --- a/src/MassTransit.PrometheusIntegration/MassTransit.PrometheusIntegration.csproj +++ /dev/null @@ -1,26 +0,0 @@ - - - - - netstandard2.0;net6.0 - MassTransit - - - - $(TargetFrameworks);net462 - - - - MassTransit.Prometheus - MassTransit.Prometheus - MassTransit;Prometheus;Grafana;Metrics - MassTransit Prometheus support; $(Description) - - - - - - - - - diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusActivityConfigurationObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusActivityConfigurationObserver.cs deleted file mode 100644 index 82f270f951d..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusActivityConfigurationObserver.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - using System; - - - public class PrometheusActivityConfigurationObserver : - IActivityConfigurationObserver - { - readonly IActivityObserver _observer; - - public PrometheusActivityConfigurationObserver() - { - _observer = new PrometheusActivityObserver(); - } - - public void ActivityConfigured(IExecuteActivityConfigurator configurator, Uri compensateAddress) - where TActivity : class, IExecuteActivity - where TArguments : class - { - var specification = new PrometheusExecuteActivitySpecification(); - - configurator.AddPipeSpecification(specification); - - configurator.ConnectActivityObserver(_observer); - } - - public void ExecuteActivityConfigured(IExecuteActivityConfigurator configurator) - where TActivity : class, IExecuteActivity - where TArguments : class - { - var specification = new PrometheusExecuteActivitySpecification(); - - configurator.AddPipeSpecification(specification); - - configurator.ConnectActivityObserver(_observer); - } - - public void CompensateActivityConfigured(ICompensateActivityConfigurator configurator) - where TActivity : class, ICompensateActivity - where TLog : class - { - var specification = new PrometheusCompensateActivitySpecification(); - - configurator.AddPipeSpecification(specification); - - configurator.ConnectActivityObserver(_observer); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusCompensateActivitySpecification.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusCompensateActivitySpecification.cs deleted file mode 100644 index 32c44643077..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusCompensateActivitySpecification.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class PrometheusCompensateActivitySpecification : - IPipeSpecification> - where TActivity : class, ICompensateActivity - where TLog : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new PrometheusCompensateActivityFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusConsumerConfigurationObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusConsumerConfigurationObserver.cs deleted file mode 100644 index 0d62f5d2d4d..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusConsumerConfigurationObserver.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - using System; - using Internals; - - - public class PrometheusConsumerConfigurationObserver : - IConsumerConfigurationObserver - { - public void ConsumerConfigured(IConsumerConfigurator configurator) - where TConsumer : class - { - } - - public void ConsumerMessageConfigured(IConsumerMessageConfigurator configurator) - where TConsumer : class - where TMessage : class - { - if (typeof(TMessage).ClosesType(typeof(Batch<>), out Type[] types)) - { - typeof(PrometheusConsumerConfigurationObserver) - .GetMethod(nameof(BatchConsumerConfigured)) - .MakeGenericMethod(typeof(TConsumer), types[0]) - .Invoke(this, new object[] {configurator}); - } - else - { - var specification = new PrometheusConsumerSpecification(); - - configurator.AddPipeSpecification(specification); - } - } - - public void BatchConsumerConfigured(IConsumerMessageConfigurator> configurator) - where TConsumer : class, IConsumer> - where TMessage : class - { - var specification = new PrometheusConsumerSpecification>(); - - configurator.AddPipeSpecification(specification); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusConsumerSpecification.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusConsumerSpecification.cs deleted file mode 100644 index dbe142e11be..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusConsumerSpecification.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class PrometheusConsumerSpecification : - IPipeSpecification> - where TConsumer : class - where TMessage : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new PrometheusConsumerFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } - - - public class PrometheusReceiveSpecification : - IPipeSpecification - { - public void Apply(IPipeBuilder builder) - { - builder.AddFilter(new PrometheusReceiveFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusExecuteActivitySpecification.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusExecuteActivitySpecification.cs deleted file mode 100644 index 6156d164a7c..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusExecuteActivitySpecification.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class PrometheusExecuteActivitySpecification : - IPipeSpecification> - where TActivity : class, IExecuteActivity - where TArguments : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new PrometheusExecuteActivityFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusHandlerConfigurationObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusHandlerConfigurationObserver.cs deleted file mode 100644 index 1be73ef9f98..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusHandlerConfigurationObserver.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - public class PrometheusHandlerConfigurationObserver : - IHandlerConfigurationObserver - { - void IHandlerConfigurationObserver.HandlerConfigured(IHandlerConfigurator configurator) - { - var specification = new PrometheusHandlerSpecification(); - - configurator.AddPipeSpecification(specification); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusHandlerSpecification.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusHandlerSpecification.cs deleted file mode 100644 index 982d953ee4f..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusHandlerSpecification.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class PrometheusHandlerSpecification : - IPipeSpecification> - where TMessage : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new PrometheusHandlerFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusReceiveEndpointConfiguratorObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusReceiveEndpointConfiguratorObserver.cs deleted file mode 100644 index 3f36879b700..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusReceiveEndpointConfiguratorObserver.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - public class PrometheusReceiveEndpointConfiguratorObserver : - IEndpointConfigurationObserver - { - public void EndpointConfigured(T configurator) - where T : IReceiveEndpointConfigurator - { - var specification = new PrometheusReceiveSpecification(); - - configurator.ConfigureReceive(r => r.AddPipeSpecification(specification)); - - configurator.ConnectReceiveEndpointObserver(new PrometheusReceiveEndpointObserver()); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusSagaConfigurationObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusSagaConfigurationObserver.cs deleted file mode 100644 index d0b4bb0dbb1..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusSagaConfigurationObserver.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - public class PrometheusSagaConfigurationObserver : - ISagaConfigurationObserver - { - public void SagaConfigured(ISagaConfigurator configurator) - where T : class, ISaga - { - } - - public void StateMachineSagaConfigured(ISagaConfigurator configurator, SagaStateMachine stateMachine) - where TInstance : class, ISaga, SagaStateMachineInstance - { - } - - public void SagaMessageConfigured(ISagaMessageConfigurator configurator) - where T : class, ISaga - where TMessage : class - { - var specification = new PrometheusSagaSpecification(); - - configurator.AddPipeSpecification(specification); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusSagaSpecification.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusSagaSpecification.cs deleted file mode 100644 index d990ed941e6..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Configuration/PrometheusSagaSpecification.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class PrometheusSagaSpecification : - IPipeSpecification> - where TSaga : class, ISaga - where TMessage : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new PrometheusSagaFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusCompensateActivityFilter.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusCompensateActivityFilter.cs deleted file mode 100644 index 42606f98d06..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusCompensateActivityFilter.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Middleware -{ - using System.Threading.Tasks; - - - public class PrometheusCompensateActivityFilter : - IFilter> - where TActivity : class, ICompensateActivity - where TLog : class - { - public async Task Send(CompensateActivityContext context, IPipe> next) - { - using var inProgress = PrometheusMetrics.TrackCompensateActivityInProgress(context); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("prometheus"); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusConsumerFilter.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusConsumerFilter.cs deleted file mode 100644 index 965a88e7c16..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusConsumerFilter.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Middleware -{ - using System.Threading.Tasks; - - - public class PrometheusConsumerFilter : - IFilter> - where TConsumer : class - where TMessage : class - { - public async Task Send(ConsumerConsumeContext context, IPipe> next) - { - using var inProgress = PrometheusMetrics.TrackConsumerInProgress(); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("prometheus"); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusExecuteActivityFilter.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusExecuteActivityFilter.cs deleted file mode 100644 index 4a65acb88f7..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusExecuteActivityFilter.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Middleware -{ - using System.Threading.Tasks; - - - public class PrometheusExecuteActivityFilter : - IFilter> - where TActivity : class, IExecuteActivity - where TArguments : class - { - public async Task Send(ExecuteActivityContext context, IPipe> next) - { - using var inProgress = PrometheusMetrics.TrackExecuteActivityInProgress(context); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("prometheus"); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusHandlerFilter.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusHandlerFilter.cs deleted file mode 100644 index 3afa5adb13b..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusHandlerFilter.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Middleware -{ - using System.Threading.Tasks; - - - public class PrometheusHandlerFilter : - IFilter> - where TMessage : class - { - public async Task Send(ConsumeContext context, IPipe> next) - { - using var inProgress = PrometheusMetrics.TrackHandlerInProgress(); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("prometheus"); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusReceiveFilter.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusReceiveFilter.cs deleted file mode 100644 index 6358dfdb3c8..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusReceiveFilter.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Middleware -{ - using System.Threading.Tasks; - - - public class PrometheusReceiveFilter : - IFilter - { - public async Task Send(ReceiveContext context, IPipe next) - { - using var inProgress = PrometheusMetrics.TrackReceiveInProgress(context); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("prometheus"); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusSagaFilter.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusSagaFilter.cs deleted file mode 100644 index db2dc6a03a7..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/Middleware/PrometheusSagaFilter.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Middleware -{ - using System.Threading.Tasks; - - - public class PrometheusSagaFilter : - IFilter> - where TSaga : class, ISaga - where TMessage : class - { - public async Task Send(SagaConsumeContext context, IPipe> next) - { - using var inProgress = PrometheusMetrics.TrackSagaInProgress(); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("prometheus"); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusActivityObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusActivityObserver.cs deleted file mode 100644 index e1588781b92..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusActivityObserver.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace MassTransit.PrometheusIntegration -{ - using System; - using System.Threading.Tasks; - - - public class PrometheusActivityObserver : - IActivityObserver - { - public Task PreExecute(ExecuteActivityContext context) - where TActivity : class, IExecuteActivity - where TArguments : class - { - return Task.CompletedTask; - } - - public Task PostExecute(ExecuteActivityContext context) - where TActivity : class, IExecuteActivity - where TArguments : class - { - PrometheusMetrics.MeasureExecute(context); - - return Task.CompletedTask; - } - - public Task ExecuteFault(ExecuteActivityContext context, Exception exception) - where TActivity : class, IExecuteActivity - where TArguments : class - { - PrometheusMetrics.MeasureExecute(context, exception); - - return Task.CompletedTask; - } - - public Task PreCompensate(CompensateActivityContext context) - where TActivity : class, ICompensateActivity - where TLog : class - { - return Task.CompletedTask; - } - - public Task PostCompensate(CompensateActivityContext context) - where TActivity : class, ICompensateActivity - where TLog : class - { - PrometheusMetrics.MeasureCompensate(context); - - return Task.CompletedTask; - } - - public Task CompensateFail(CompensateActivityContext context, Exception exception) - where TActivity : class, ICompensateActivity - where TLog : class - { - PrometheusMetrics.MeasureCompensate(context, exception); - - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusBusObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusBusObserver.cs deleted file mode 100644 index 75977000086..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusBusObserver.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace MassTransit.PrometheusIntegration -{ - using System; - using System.Threading.Tasks; - - - public class PrometheusBusObserver : - IBusObserver - { - public void PostCreate(IBus bus) - { - bus.ConnectPublishObserver(new PrometheusPublishObserver()); - bus.ConnectSendObserver(new PrometheusSendObserver()); - bus.ConnectReceiveObserver(new PrometheusReceiveObserver()); - } - - public void CreateFaulted(Exception exception) - { - } - - public Task PreStart(IBus bus) - { - return Task.CompletedTask; - } - - public Task PostStart(IBus bus, Task busReady) - { - PrometheusMetrics.BusStarted(); - - return Task.CompletedTask; - } - - public Task StartFaulted(IBus bus, Exception exception) - { - return Task.CompletedTask; - } - - public Task PreStop(IBus bus) - { - return Task.CompletedTask; - } - - public Task PostStop(IBus bus) - { - PrometheusMetrics.BusStopped(); - - return Task.CompletedTask; - } - - public Task StopFaulted(IBus bus, Exception exception) - { - PrometheusMetrics.BusStopped(); - - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusPublishObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusPublishObserver.cs deleted file mode 100644 index 111268c18f5..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusPublishObserver.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace MassTransit.PrometheusIntegration -{ - using System; - using System.Threading.Tasks; - - - public class PrometheusPublishObserver : - IPublishObserver - { - public Task PrePublish(PublishContext context) - where T : class - { - return Task.CompletedTask; - } - - public Task PostPublish(PublishContext context) - where T : class - { - PrometheusMetrics.MeasurePublish(); - - return Task.CompletedTask; - } - - public Task PublishFault(PublishContext context, Exception exception) - where T : class - { - PrometheusMetrics.MeasurePublish(exception); - - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusReceiveEndpointObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusReceiveEndpointObserver.cs deleted file mode 100644 index 9bace7a404e..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusReceiveEndpointObserver.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace MassTransit.PrometheusIntegration -{ - using System.Threading.Tasks; - - - public class PrometheusReceiveEndpointObserver : - IReceiveEndpointObserver - { - public Task Ready(ReceiveEndpointReady ready) - { - PrometheusMetrics.EndpointReady(ready); - - return Task.CompletedTask; - } - - public Task Stopping(ReceiveEndpointStopping stopping) - { - return Task.CompletedTask; - } - - public Task Completed(ReceiveEndpointCompleted completed) - { - PrometheusMetrics.EndpointCompleted(completed); - - return Task.CompletedTask; - } - - public Task Faulted(ReceiveEndpointFaulted faulted) - { - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusReceiveObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusReceiveObserver.cs deleted file mode 100644 index afb177f10ed..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusReceiveObserver.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace MassTransit.PrometheusIntegration -{ - using System; - using System.Threading.Tasks; - - - public class PrometheusReceiveObserver : - IReceiveObserver - { - public Task PreReceive(ReceiveContext context) - { - return Task.CompletedTask; - } - - public Task PostReceive(ReceiveContext context) - { - PrometheusMetrics.MeasureReceived(context); - - return Task.CompletedTask; - } - - public Task PostConsume(ConsumeContext context, TimeSpan duration, string consumerType) - where T : class - { - PrometheusMetrics.MeasureConsume(context, duration, consumerType); - - return Task.CompletedTask; - } - - public Task ConsumeFault(ConsumeContext context, TimeSpan duration, string consumerType, Exception exception) - where T : class - { - PrometheusMetrics.MeasureConsume(context, duration, consumerType, exception); - - return Task.CompletedTask; - } - - public Task ReceiveFault(ReceiveContext context, Exception exception) - { - PrometheusMetrics.MeasureReceived(context, exception); - - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusSendObserver.cs b/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusSendObserver.cs deleted file mode 100644 index cf1b988cdf1..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusIntegration/PrometheusSendObserver.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace MassTransit.PrometheusIntegration -{ - using System; - using System.Threading.Tasks; - - - public class PrometheusSendObserver : - ISendObserver - { - public Task PreSend(SendContext context) - where T : class - { - return Task.CompletedTask; - } - - public Task PostSend(SendContext context) - where T : class - { - PrometheusMetrics.MeasureSend(); - - return Task.CompletedTask; - } - - public Task SendFault(SendContext context, Exception exception) - where T : class - { - PrometheusMetrics.MeasureSend(exception); - - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusMetrics.cs b/src/MassTransit.PrometheusIntegration/PrometheusMetrics.cs deleted file mode 100644 index 7950b9c5f27..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusMetrics.cs +++ /dev/null @@ -1,498 +0,0 @@ -namespace MassTransit -{ - using System; - using System.Collections.Concurrent; - using System.Linq; - using System.Reflection; - using System.Text; - using Prometheus; - - - public static class PrometheusMetrics - { - static readonly ConcurrentDictionary _labelCache = new ConcurrentDictionary(); - - static bool _isConfigured; - static string _serviceLabel; - static Gauge _busInstances; - static Gauge _endpointInstances; - static Counter _receiveTotal; - static Counter _receiveFaultTotal; - static Gauge _receiveInProgress; - static Histogram _receiveDuration; - static Counter _consumeTotal; - static Counter _consumeFaultTotal; - static Counter _consumeRetryTotal; - static Counter _publishTotal; - static Counter _publishFaultTotal; - static Counter _sendTotal; - static Counter _sendFaultTotal; - static Counter _executeTotal; - static Counter _executeFaultTotal; - static Counter _compensateTotal; - static Counter _compensateFailureTotal; - static Gauge _consumerInProgress; - static Gauge _handlerInProgress; - static Gauge _sagaInProgress; - static Gauge _executeInProgress; - static Gauge _compensateInProgress; - static Histogram _consumeDuration; - static Histogram _deliveryDuration; - static Histogram _executeDuration; - static Histogram _compensateDuration; - - static readonly char[] _delimiters = {'<', '>'}; - - public static void BusStarted() - { - _busInstances.WithLabels(_serviceLabel).Inc(); - } - - public static void BusStopped() - { - _busInstances.WithLabels(_serviceLabel).Dec(); - } - - public static void EndpointReady(ReceiveEndpointReady ready) - { - var endpointLabel = GetEndpointLabel(ready.InputAddress); - - _endpointInstances.WithLabels(_serviceLabel, endpointLabel).Inc(); - } - - public static void EndpointCompleted(ReceiveEndpointCompleted completed) - { - var endpointLabel = GetEndpointLabel(completed.InputAddress); - - _endpointInstances.WithLabels(_serviceLabel, endpointLabel).Dec(); - } - - public static void MeasureReceived(ReceiveContext context, Exception exception = default) - { - var endpointLabel = GetEndpointLabel(context.InputAddress); - - _receiveTotal.Labels(_serviceLabel, endpointLabel).Inc(); - _receiveDuration.Labels(_serviceLabel, endpointLabel).Observe(context.ElapsedTime.TotalSeconds); - - if (exception != null) - { - var exceptionType = exception.GetType().Name; - _receiveFaultTotal.Labels(_serviceLabel, endpointLabel, exceptionType).Inc(); - } - } - - public static void MeasureConsume(ConsumeContext context, TimeSpan duration, string consumerType, Exception exception = default) - where T : class - { - var messageType = GetMessageTypeLabel(); - var cleanConsumerType = GetConsumerTypeLabel(consumerType, TypeCache.ShortName, messageType); - - _consumeTotal.Labels(_serviceLabel, messageType, cleanConsumerType).Inc(); - _consumeDuration.Labels(_serviceLabel, messageType, cleanConsumerType).Observe(duration.TotalSeconds); - - if (exception != null) - { - var exceptionType = exception.GetType().Name; - _consumeFaultTotal.Labels(_serviceLabel, messageType, cleanConsumerType, exceptionType).Inc(); - } - - var retryAttempt = context.GetRetryAttempt(); - if (retryAttempt > 0) - _consumeRetryTotal.Inc(retryAttempt); - - if (!context.SentTime.HasValue) - return; - - var deliveryDuration = DateTime.UtcNow - context.SentTime.Value; - if (deliveryDuration < TimeSpan.Zero) - deliveryDuration = TimeSpan.Zero; - - _deliveryDuration.Labels(_serviceLabel, messageType, cleanConsumerType).Observe(deliveryDuration.TotalSeconds); - } - - public static void MeasureExecute(ExecuteActivityContext context, Exception exception = default) - where TActivity : class, IExecuteActivity - where TArguments : class - { - var argumentType = GetArgumentTypeLabel(); - - _executeTotal.Labels(_serviceLabel, context.ActivityName, argumentType).Inc(); - _executeDuration.Labels(_serviceLabel, context.ActivityName, argumentType).Observe(context.Elapsed.TotalSeconds); - - if (exception != null) - { - var exceptionType = exception.GetType().Name; - _executeFaultTotal.Labels(_serviceLabel, context.ActivityName, argumentType, exceptionType).Inc(); - } - } - - public static void MeasureCompensate(CompensateActivityContext context, Exception exception = default) - where TActivity : class, ICompensateActivity - where TLog : class - { - var logType = GetLogTypeLabel(); - - _compensateTotal.Labels(_serviceLabel, context.ActivityName, logType).Inc(); - _compensateDuration.Labels(_serviceLabel, context.ActivityName, logType).Observe(context.Elapsed.TotalSeconds); - - if (exception != null) - { - var exceptionType = exception.GetType().Name; - _compensateFailureTotal.Labels(_serviceLabel, context.ActivityName, logType, exceptionType).Inc(); - } - } - - public static void MeasurePublish(Exception exception = default) - where T : class - { - var messageType = GetMessageTypeLabel(); - - _publishTotal.Labels(_serviceLabel, messageType).Inc(); - - if (exception != null) - { - var exceptionType = exception.GetType().Name; - _publishFaultTotal.Labels(_serviceLabel, messageType, exceptionType).Inc(); - } - } - - public static void MeasureSend(Exception exception = default) - where T : class - { - var messageType = GetMessageTypeLabel(); - - _sendTotal.Labels(_serviceLabel, messageType).Inc(); - - if (exception != null) - { - var exceptionType = exception.GetType().Name; - _sendFaultTotal.Labels(_serviceLabel, messageType, exceptionType).Inc(); - } - } - - public static IDisposable TrackReceiveInProgress(ReceiveContext context) - { - var endpointLabel = GetEndpointLabel(context.InputAddress); - - return _receiveInProgress.Labels(_serviceLabel, endpointLabel).TrackInProgress(); - } - - public static IDisposable TrackConsumerInProgress() - where TConsumer : class - where TMessage : class - { - var messageType = GetMessageTypeLabel(); - var cleanConsumerType = GetConsumerTypeLabel(TypeCache.ShortName, TypeCache.ShortName, messageType); - - return _consumerInProgress.Labels(_serviceLabel, messageType, cleanConsumerType).TrackInProgress(); - } - - public static IDisposable TrackSagaInProgress() - where TSaga : class, ISaga - where TMessage : class - { - var messageType = GetMessageTypeLabel(); - var cleanConsumerType = GetConsumerTypeLabel(TypeCache.ShortName, TypeCache.ShortName, messageType); - - return _sagaInProgress.Labels(_serviceLabel, messageType, cleanConsumerType).TrackInProgress(); - } - - public static IDisposable TrackExecuteActivityInProgress(ExecuteActivityContext context) - where TActivity : class, IExecuteActivity - where TArguments : class - { - var argumentType = GetArgumentTypeLabel(); - - return _executeInProgress.Labels(_serviceLabel, context.ActivityName, argumentType).TrackInProgress(); - } - - public static IDisposable TrackCompensateActivityInProgress(CompensateActivityContext context) - where TActivity : class, ICompensateActivity - where TLog : class - { - var argumentType = GetArgumentTypeLabel(); - - return _compensateInProgress.Labels(_serviceLabel, context.ActivityName, argumentType).TrackInProgress(); - } - - public static IDisposable TrackHandlerInProgress() - where TMessage : class - { - var messageType = GetMessageTypeLabel(); - - return _handlerInProgress.Labels(_serviceLabel, messageType).TrackInProgress(); - } - - public static void TryConfigure(string serviceName, PrometheusMetricsOptions options) - { - if (_isConfigured) - return; - - _serviceLabel = serviceName; - - string[] serviceLabels = {options.ServiceNameLabel}; - - string[] endpointLabels = {options.ServiceNameLabel, options.EndpointLabel}; - string[] endpointFaultLabels = {options.ServiceNameLabel, options.EndpointLabel, options.ExceptionTypeLabel}; - - string[] messageLabels = {options.ServiceNameLabel, options.MessageTypeLabel}; - string[] messageFaultLabels = {options.ServiceNameLabel, options.MessageTypeLabel, options.ExceptionTypeLabel}; - - string[] executeLabels = {options.ServiceNameLabel, options.ActivityNameLabel, options.ArgumentTypeLabel}; - string[] executeFaultLabels = {options.ServiceNameLabel, options.ActivityNameLabel, options.ArgumentTypeLabel, options.ExceptionTypeLabel}; - - string[] compensateLabels = {options.ServiceNameLabel, options.ActivityNameLabel, options.LogTypeLabel}; - string[] compensateFailureLabels = {options.ServiceNameLabel, options.ActivityNameLabel, options.LogTypeLabel, options.ExceptionTypeLabel}; - - string[] consumerLabels = {options.ServiceNameLabel, options.MessageTypeLabel, options.ConsumerTypeLabel}; - string[] consumerFaultLabels = {options.ServiceNameLabel, options.MessageTypeLabel, options.ConsumerTypeLabel, options.ExceptionTypeLabel}; - - // Counters - - _receiveTotal = Metrics.CreateCounter( - options.ReceiveTotal, - "Total number of messages received", - new CounterConfiguration {LabelNames = endpointLabels}); - - _receiveFaultTotal = Metrics.CreateCounter( - options.ReceiveFaultTotal, - "Total number of messages receive faults", - new CounterConfiguration {LabelNames = endpointFaultLabels}); - - _consumeTotal = Metrics.CreateCounter( - options.ConsumeTotal, - "Total number of messages consumed", - new CounterConfiguration {LabelNames = consumerLabels}); - - _consumeFaultTotal = Metrics.CreateCounter( - options.ConsumeFaultTotal, - "Total number of message consume faults", - new CounterConfiguration {LabelNames = consumerFaultLabels}); - - _consumeRetryTotal = Metrics.CreateCounter( - options.ConsumeRetryTotal, - "Total number of message consume faults", - new CounterConfiguration {LabelNames = consumerFaultLabels}); - - _publishTotal = Metrics.CreateCounter( - options.PublishTotal, - "Total number of messages published", - new CounterConfiguration {LabelNames = messageLabels}); - - _publishFaultTotal = Metrics.CreateCounter( - options.PublishFaultTotal, - "Total number of message publish faults", - new CounterConfiguration {LabelNames = messageFaultLabels}); - - _sendTotal = Metrics.CreateCounter( - options.SendTotal, - "Total number of messages sent", - new CounterConfiguration {LabelNames = messageLabels}); - - _sendFaultTotal = Metrics.CreateCounter( - options.SendFaultTotal, - "Total number of message send faults", - new CounterConfiguration {LabelNames = messageFaultLabels}); - - _executeTotal = Metrics.CreateCounter( - options.ActivityExecuteTotal, - "Total number of activities executed", - new CounterConfiguration {LabelNames = executeLabels}); - - _executeFaultTotal = Metrics.CreateCounter( - options.ActivityExecuteFaultTotal, - "Total number of activity execution faults", - new CounterConfiguration {LabelNames = executeFaultLabels}); - - _compensateTotal = Metrics.CreateCounter( - options.ActivityCompensateTotal, - "Total number of activities compensated", - new CounterConfiguration {LabelNames = compensateLabels}); - - _compensateFailureTotal = Metrics.CreateCounter( - options.ActivityCompensateFailureTotal, - "Total number of activity compensation failures", - new CounterConfiguration {LabelNames = compensateFailureLabels}); - - // Gauges - - _busInstances = Metrics.CreateGauge( - options.BusInstances, - "Number of bus instances", - new GaugeConfiguration {LabelNames = serviceLabels}); - - _endpointInstances = Metrics.CreateGauge( - options.EndpointInstances, - "Number of receive endpoint instances", - new GaugeConfiguration {LabelNames = endpointLabels}); - - _receiveInProgress = Metrics.CreateGauge( - options.ReceiveInProgress, - "Number of messages being received", - new GaugeConfiguration {LabelNames = endpointLabels}); - - _handlerInProgress = Metrics.CreateGauge( - options.HandlerInProgress, - "Number of handlers in progress", - new GaugeConfiguration {LabelNames = messageLabels}); - - _consumerInProgress = Metrics.CreateGauge( - options.ConsumerInProgress, - "Number of consumers in progress", - new GaugeConfiguration {LabelNames = consumerLabels}); - - _sagaInProgress = Metrics.CreateGauge( - options.SagaInProgress, - "Number of sagas in progress", - new GaugeConfiguration {LabelNames = consumerLabels}); - - _executeInProgress = Metrics.CreateGauge( - options.ExecuteInProgress, - "Number of activity executions in progress", - new GaugeConfiguration {LabelNames = executeLabels}); - - _compensateInProgress = Metrics.CreateGauge( - options.CompensateInProgress, - "Number of activity compensations in progress", - new GaugeConfiguration {LabelNames = compensateLabels}); - - // Histograms - - _receiveDuration = Metrics.CreateHistogram( - options.ReceiveDuration, - "Elapsed time spent receiving a message, in seconds", - new HistogramConfiguration - { - LabelNames = endpointLabels, - Buckets = options.HistogramBuckets - }); - - _consumeDuration = Metrics.CreateHistogram( - options.ConsumeDuration, - "Elapsed time spent consuming a message, in seconds", - new HistogramConfiguration - { - LabelNames = consumerLabels, - Buckets = options.HistogramBuckets - }); - - _deliveryDuration = Metrics.CreateHistogram( - options.DeliveryDuration, - "Elapsed time between when the message was sent and when it was consumed, in seconds.", - new HistogramConfiguration - { - LabelNames = consumerLabels, - Buckets = options.HistogramBuckets - }); - - _executeDuration = Metrics.CreateHistogram( - options.ActivityExecuteDuration, - "Elapsed time spent executing an activity, in seconds", - new HistogramConfiguration - { - LabelNames = executeLabels, - Buckets = options.HistogramBuckets - }); - - _compensateDuration = Metrics.CreateHistogram( - options.ActivityCompensateDuration, - "Elapsed time spent compensating an activity, in seconds", - new HistogramConfiguration - { - LabelNames = compensateLabels, - Buckets = options.HistogramBuckets - }); - - _isConfigured = true; - } - - static string GetConsumerTypeLabel(string consumerType, string messageType, string messageLabel) - { - return _labelCache.GetOrAdd(consumerType, type => - { - if (type.StartsWith("MassTransit.MessageHandler<")) - return "Handler"; - - var genericMessageType = "<" + messageType + ">"; - if (type.IndexOf(genericMessageType, StringComparison.Ordinal) >= 0) - type = type.Replace(genericMessageType, "_" + messageLabel); - - return CleanupLabel(type); - }); - } - - static string CleanupLabel(string label) - { - string SimpleClean(string text) - { - return text.Split('.', '+').Last(); - } - - var indexOf = label.IndexOfAny(_delimiters); - if (indexOf >= 0) - { - if (label[indexOf] == '<') - return SimpleClean(label.Substring(0, indexOf)) + "_" + CleanupLabel(label.Substring(indexOf + 1)); - - if (label[indexOf] == '>') - return SimpleClean(label.Substring(0, indexOf)) + CleanupLabel(label.Substring(indexOf + 1)); - - return SimpleClean(label); - } - - return SimpleClean(label); - } - - static string GetArgumentTypeLabel() - { - return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TArguments)) - .Replace("Arguments", "")); - } - - static string GetLogTypeLabel() - { - return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TLog)).Replace("Log", "")); - } - - static string GetEndpointLabel(Uri inputAddress) - { - return inputAddress?.AbsolutePath.Split('/').LastOrDefault()?.Replace(".", "_").Replace("/", "_"); - } - - static string GetMessageTypeLabel() - { - return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TMessage))); - } - - static string FormatTypeName(StringBuilder sb, Type type) - { - if (type.IsGenericParameter) - return ""; - - if (type.GetTypeInfo().IsGenericType) - { - var name = type.GetGenericTypeDefinition().Name; - - //remove `1 - var index = name.IndexOf('`'); - if (index > 0) - name = name.Remove(index); - - sb.Append(name); - sb.Append('_'); - Type[] arguments = type.GetTypeInfo().GenericTypeArguments; - for (var i = 0; i < arguments.Length; i++) - { - if (i > 0) - sb.Append('_'); - - FormatTypeName(sb, arguments[i]); - } - } - else - sb.Append(type.Name); - - return sb.ToString(); - } - } -} diff --git a/src/MassTransit.PrometheusIntegration/PrometheusMetricsOptions.cs b/src/MassTransit.PrometheusIntegration/PrometheusMetricsOptions.cs deleted file mode 100644 index 0f2d7835530..00000000000 --- a/src/MassTransit.PrometheusIntegration/PrometheusMetricsOptions.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace MassTransit -{ - public class PrometheusMetricsOptions - { - public string EndpointLabel { get; set; } - public string ConsumerTypeLabel { get; set; } - public string ExceptionTypeLabel { get; set; } - public string MessageTypeLabel { get; set; } - public string ActivityNameLabel { get; set; } - public string ArgumentTypeLabel { get; set; } - public string LogTypeLabel { get; set; } - public string ServiceNameLabel { get; set; } - - public double[] HistogramBuckets { get; set; } - - public string ReceiveTotal { get; set; } - public string ReceiveFaultTotal { get; set; } - public string ReceiveDuration { get; set; } - public string ReceiveInProgress { get; set; } - - public string ConsumeTotal { get; set; } - public string ConsumeFaultTotal { get; set; } - public string ConsumeRetryTotal { get; set; } - public string PublishTotal { get; set; } - public string PublishFaultTotal { get; set; } - public string SendTotal { get; set; } - public string SendFaultTotal { get; set; } - public string ActivityExecuteTotal { get; set; } - public string ActivityExecuteFaultTotal { get; set; } - public string ActivityExecuteDuration { get; set; } - public string ActivityCompensateTotal { get; set; } - public string ActivityCompensateFailureTotal { get; set; } - public string ActivityCompensateDuration { get; set; } - - public string BusInstances { get; set; } - public string EndpointInstances { get; set; } - public string ConsumerInProgress { get; set; } - public string HandlerInProgress { get; set; } - public string SagaInProgress { get; set; } - public string ExecuteInProgress { get; set; } - public string CompensateInProgress { get; set; } - - public string ConsumeDuration { get; set; } - public string DeliveryDuration { get; set; } - - public static PrometheusMetricsOptions CreateDefault() - { - return new PrometheusMetricsOptions - { - EndpointLabel = "endpoint_address", - ConsumerTypeLabel = "consumer_type", - ExceptionTypeLabel = "exception_type", - MessageTypeLabel = "message_type", - ActivityNameLabel = "activity_name", - ArgumentTypeLabel = "argument_type", - LogTypeLabel = "log_type", - ServiceNameLabel = "service_name", - HistogramBuckets = new[] {0, .005, .01, .025, .05, .075, .1, .25, .5, .75, 1, 2.5, 5, 7.5, 10, 30, 60, 120, 180, 240, 300}, - ReceiveTotal = "mt_receive_total", - ReceiveFaultTotal = "mt_receive_fault_total", - ReceiveDuration = "mt_receive_duration_seconds", - ReceiveInProgress = "mt_receive_in_progress", - ConsumeTotal = "mt_consume_total", - ConsumeFaultTotal = "mt_consume_fault_total", - ConsumeRetryTotal = "mt_consume_retry_total", - ConsumeDuration = "mt_consume_duration_seconds", - DeliveryDuration = "mt_delivery_duration_seconds", - PublishTotal = "mt_publish_total", - PublishFaultTotal = "mt_publish_fault_total", - SendTotal = "mt_send_total", - SendFaultTotal = "mt_send_fault_total", - ActivityExecuteTotal = "mt_activity_execute_total", - ActivityExecuteFaultTotal = "mt_activity_execute_fault_total", - ActivityExecuteDuration = "mt_activity_execute_duration", - ActivityCompensateTotal = "mt_activity_compensate_total", - ActivityCompensateFailureTotal = "mt_activity_compensate_failure_total", - ActivityCompensateDuration = "mt_activity_compensate_duration", - BusInstances = "mt_bus", - EndpointInstances = "mt_endpoint", - ConsumerInProgress = "mt_consumer_in_progress", - HandlerInProgress = "mt_handler_in_progress", - SagaInProgress = "mt_saga_in_progress", - ExecuteInProgress = "mt_activity_execute_in_progress", - CompensateInProgress = "mt_activity_compensate_in_progress" - }; - } - } -} diff --git a/src/MassTransit.SignalR/MassTransit.SignalR.csproj b/src/MassTransit.SignalR/MassTransit.SignalR.csproj index 73649feae32..3fc9fae4e23 100644 --- a/src/MassTransit.SignalR/MassTransit.SignalR.csproj +++ b/src/MassTransit.SignalR/MassTransit.SignalR.csproj @@ -2,11 +2,11 @@ - net6.0 + net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -14,18 +14,14 @@ MassTransit SignalR Backplane support; $(Description) - + - + - - - - diff --git a/src/MassTransit.StateMachineVisualizer/Abstractions/StateMachineGenerator.cs b/src/MassTransit.StateMachineVisualizer/Abstractions/StateMachineGenerator.cs new file mode 100644 index 00000000000..82fdc38a6c1 --- /dev/null +++ b/src/MassTransit.StateMachineVisualizer/Abstractions/StateMachineGenerator.cs @@ -0,0 +1,34 @@ +namespace MassTransit.Visualizer.Abstractions +{ + using System.Collections.Generic; + using System.Linq; + using QuikGraph; + using SagaStateMachine; + + + public abstract class StateMachineGenerator + { + protected readonly AdjacencyGraph> Graph; + + public StateMachineGenerator(StateMachineGraph data) + { + Graph = CreateAdjacencyGraph(data); + } + + static AdjacencyGraph> CreateAdjacencyGraph(StateMachineGraph data) + { + var graph = new AdjacencyGraph>(); + + List compositeTargets = data.Edges.Where(x => x.From.IsComposite).Select(x => x.To).ToList(); + + List targets = data.Edges.Select(x => x.To).ToList(); + List vertices = data.Vertices.Where(v => targets.Contains(v) || v.Title == "Initial").ToList(); + List edges = data.Edges.Where(x => vertices.Contains(x.From) && !compositeTargets.Contains(x.From)).ToList(); + + graph.AddVertexRange(vertices); + graph.AddEdgeRange(edges.Select(x => new Edge(x.From, x.To))); + + return graph; + } + } +} diff --git a/src/MassTransit.StateMachineVisualizer/MassTransit.StateMachineVisualizer.csproj b/src/MassTransit.StateMachineVisualizer/MassTransit.StateMachineVisualizer.csproj index 264fe7a5470..d1c15ae1b73 100644 --- a/src/MassTransit.StateMachineVisualizer/MassTransit.StateMachineVisualizer.csproj +++ b/src/MassTransit.StateMachineVisualizer/MassTransit.StateMachineVisualizer.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -19,7 +19,6 @@ - diff --git a/src/MassTransit.StateMachineVisualizer/StateMachineGraphGenerator.cs b/src/MassTransit.StateMachineVisualizer/StateMachineGraphGenerator.cs deleted file mode 100644 index a4474b078bf..00000000000 --- a/src/MassTransit.StateMachineVisualizer/StateMachineGraphGenerator.cs +++ /dev/null @@ -1,83 +0,0 @@ -namespace MassTransit.Visualizer -{ - using System; - using System.Collections.Generic; - using System.Linq; - using Internals; - using QuikGraph; - using QuikGraph.Graphviz; - using QuikGraph.Graphviz.Dot; - using SagaStateMachine; - - - public class StateMachineGraphvizGenerator - { - readonly AdjacencyGraph> _graph; - - public StateMachineGraphvizGenerator(StateMachineGraph data) - { - _graph = CreateAdjacencyGraph(data); - } - - public string CreateDotFile() - { - var algorithm = new GraphvizAlgorithm>(_graph); - algorithm.FormatVertex += VertexStyler; - return algorithm.Generate(); - } - - static void VertexStyler(object sender, FormatVertexEventArgs args) - { - args.VertexFormat.Label = args.Vertex.Title; - - if (args.Vertex.VertexType == typeof(Event)) - { - args.VertexFormat.FontColor = GraphvizColor.Black; - args.VertexFormat.Shape = args.Vertex.IsComposite ? GraphvizVertexShape.InvHouse : GraphvizVertexShape.Rectangle; - - if (args.Vertex.TargetType != typeof(Event) && args.Vertex.TargetType != typeof(Exception)) - { - if (args.Vertex.TargetType.ClosesType(typeof(Fault<>), out Type[] arguments)) - { - args.VertexFormat.Label += "<" + arguments[0].Name + ">"; - } - else - args.VertexFormat.Label += "<" + args.Vertex.TargetType.Name + ">"; - } - } - else - { - switch (args.Vertex.Title) - { - case "Initial": - args.VertexFormat.FillColor = GraphvizColor.White; - break; - case "Final": - args.VertexFormat.FillColor = GraphvizColor.White; - break; - default: - args.VertexFormat.FillColor = GraphvizColor.White; - args.VertexFormat.FontColor = GraphvizColor.Black; - break; - } - - args.VertexFormat.Shape = GraphvizVertexShape.Ellipse; - } - } - - static AdjacencyGraph> CreateAdjacencyGraph(StateMachineGraph data) - { - var graph = new AdjacencyGraph>(); - - List compositeTargets = data.Edges.Where(x => x.From.IsComposite).Select(x => x.To).ToList(); - - List targets = data.Edges.Select(x => x.To).ToList(); - List vertices = data.Vertices.Where(v => targets.Contains(v) || v.Title == "Initial").ToList(); - List edges = data.Edges.Where(x => vertices.Contains(x.From) && !compositeTargets.Contains(x.From)).ToList(); - - graph.AddVertexRange(vertices); - graph.AddEdgeRange(edges.Select(x => new Edge(x.From, x.To))); - return graph; - } - } -} diff --git a/src/MassTransit.StateMachineVisualizer/StateMachineGraphvizGenerator.cs b/src/MassTransit.StateMachineVisualizer/StateMachineGraphvizGenerator.cs new file mode 100644 index 00000000000..a9390d146e4 --- /dev/null +++ b/src/MassTransit.StateMachineVisualizer/StateMachineGraphvizGenerator.cs @@ -0,0 +1,63 @@ +namespace MassTransit.Visualizer +{ + using System; + using Abstractions; + using Internals; + using QuikGraph; + using QuikGraph.Graphviz; + using QuikGraph.Graphviz.Dot; + using SagaStateMachine; + + + public class StateMachineGraphvizGenerator : StateMachineGenerator + { + public StateMachineGraphvizGenerator(StateMachineGraph data) + : base(data) + { + } + + public string CreateDotFile() + { + var algorithm = new GraphvizAlgorithm>(Graph); + algorithm.FormatVertex += VertexStyler; + return algorithm.Generate(); + } + + static void VertexStyler(object sender, FormatVertexEventArgs args) + { + args.VertexFormat.Label = args.Vertex.Title; + + if (args.Vertex.VertexType == typeof(Event)) + { + args.VertexFormat.FontColor = GraphvizColor.Black; + args.VertexFormat.Shape = args.Vertex.IsComposite ? GraphvizVertexShape.InvHouse : GraphvizVertexShape.Rectangle; + + if (args.Vertex.TargetType != typeof(Event) && args.Vertex.TargetType != typeof(Exception)) + { + if (args.Vertex.TargetType.ClosesType(typeof(Fault<>), out Type[] arguments)) + args.VertexFormat.Label += "<" + arguments[0].Name + ">"; + else + args.VertexFormat.Label += "<" + args.Vertex.TargetType.Name + ">"; + } + } + else + { + switch (args.Vertex.Title) + { + case "Initial": + args.VertexFormat.FillColor = GraphvizColor.White; + break; + case "Final": + args.VertexFormat.FillColor = GraphvizColor.White; + break; + default: + args.VertexFormat.FillColor = GraphvizColor.White; + args.VertexFormat.FontColor = GraphvizColor.Black; + break; + } + + args.VertexFormat.Shape = GraphvizVertexShape.Ellipse; + } + } + } +} diff --git a/src/MassTransit.StateMachineVisualizer/StateMachineMermaidGenerator.cs b/src/MassTransit.StateMachineVisualizer/StateMachineMermaidGenerator.cs new file mode 100644 index 00000000000..a6400ee6473 --- /dev/null +++ b/src/MassTransit.StateMachineVisualizer/StateMachineMermaidGenerator.cs @@ -0,0 +1,72 @@ +namespace MassTransit.Visualizer +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Abstractions; + using Internals; + using QuikGraph; + using SagaStateMachine; + + + public class StateMachineMermaidGenerator : StateMachineGenerator + { + const string OpenBracket = "«"; + const string CloseBracket = "»"; + + public StateMachineMermaidGenerator(StateMachineGraph data) + : base(data) + { + } + + public string CreateMermaidFile() + { + StringBuilder output = new(); + List vertices = Graph.Vertices.ToList(); + + output.Append("flowchart TB;"); + + foreach (Edge edge in Graph.Edges) + { + var source = FormatVertex(edge.Source, vertices); + var target = FormatVertex(edge.Target, vertices); + var line = $"{Environment.NewLine} {source} --> {target};"; + + output.Append(line); + } + + return output.ToString(); + } + + static string GetVertexLabel(Vertex vertex, bool includeOptionalType) + { + if (includeOptionalType && vertex.TargetType != typeof(Event) && vertex.TargetType != typeof(Exception)) + { + if (vertex.TargetType.ClosesType(typeof(Fault<>), out Type[] arguments)) + return $"{vertex.Title}{OpenBracket}{arguments[0].Name}{CloseBracket}"; + + return $"{vertex.Title}{OpenBracket}{vertex.TargetType.Name}{CloseBracket}"; + } + + return vertex.Title; + } + + static string FormatVertex(Vertex vertex, List vertices) + { + var index = vertices.IndexOf(vertex); + + if (vertex.VertexType == typeof(Event)) + { + var vertexLabel = GetVertexLabel(vertex, true); + + if (vertex.IsComposite) + return $"{index}[\\\"{vertexLabel}\"/]"; + + return $"{index}[\"{vertexLabel}\"]"; + } + + return $"{index}([\"{GetVertexLabel(vertex, false)}\"])"; + } + } +} diff --git a/src/MassTransit.TestFramework/ForkJoint/Tests/BurgerFuture_Specs.cs b/src/MassTransit.TestFramework/ForkJoint/Tests/BurgerFuture_Specs.cs index eca9f50e4fd..397ca2bed68 100644 --- a/src/MassTransit.TestFramework/ForkJoint/Tests/BurgerFuture_Specs.cs +++ b/src/MassTransit.TestFramework/ForkJoint/Tests/BurgerFuture_Specs.cs @@ -36,10 +36,13 @@ public async Task Should_complete() } }); - Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); - Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); - Assert.That(response.Message.Burger.Cheese, Is.True); - Assert.That(response.Message.Burger.Weight, Is.EqualTo(1.0m)); + Assert.Multiple(() => + { + Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); + Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); + Assert.That(response.Message.Burger.Cheese, Is.True); + Assert.That(response.Message.Burger.Weight, Is.EqualTo(1.0m)); + }); } [Test] @@ -71,9 +74,12 @@ await client.GetResponse(new } catch (RequestFaultException exception) { - Assert.That(exception.Fault.Host, Is.Not.Null); - Assert.That(exception.Fault.Exceptions, Is.Not.Null.Or.Empty); - Assert.That(exception.Message, Contains.Substring("lettuce")); + Assert.Multiple(() => + { + Assert.That(exception.Fault.Host, Is.Not.Null); + Assert.That(exception.Fault.Exceptions, Is.Not.Null.Or.Empty); + Assert.That(exception.Message, Contains.Substring("lettuce")); + }); } } diff --git a/src/MassTransit.TestFramework/ForkJoint/Tests/CalculateFuture_Specs.cs b/src/MassTransit.TestFramework/ForkJoint/Tests/CalculateFuture_Specs.cs index 390c35af13f..e928c599c64 100644 --- a/src/MassTransit.TestFramework/ForkJoint/Tests/CalculateFuture_Specs.cs +++ b/src/MassTransit.TestFramework/ForkJoint/Tests/CalculateFuture_Specs.cs @@ -33,12 +33,15 @@ public async Task Should_complete() Number = 5 }, timeout: RequestTimeout.After(s: 5)); - Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); - Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); - Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); - Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); + Assert.Multiple(() => + { + Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); + Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); + Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); + Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); - Assert.That(response.Message.Description, Contains.Substring("Fries")); + Assert.That(response.Message.Description, Contains.Substring("Fries")); + }); Assert.That(response.Message.Description, Contains.Substring("Delicious")); } diff --git a/src/MassTransit.TestFramework/ForkJoint/Tests/ComboFuture_Specs.cs b/src/MassTransit.TestFramework/ForkJoint/Tests/ComboFuture_Specs.cs index 6d5a233a709..e215e3c151c 100644 --- a/src/MassTransit.TestFramework/ForkJoint/Tests/ComboFuture_Specs.cs +++ b/src/MassTransit.TestFramework/ForkJoint/Tests/ComboFuture_Specs.cs @@ -33,12 +33,15 @@ public async Task Should_complete() Number = 5 }, timeout: RequestTimeout.After(s: 5)); - Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); - Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); - Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); - Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); + Assert.Multiple(() => + { + Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); + Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); + Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); + Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); - Assert.That(response.Message.Description, Contains.Substring("Fries")); + Assert.That(response.Message.Description, Contains.Substring("Fries")); + }); Assert.That(response.Message.Description, Contains.Substring("Shake")); } diff --git a/src/MassTransit.TestFramework/ForkJoint/Tests/FryFuture_Specs.cs b/src/MassTransit.TestFramework/ForkJoint/Tests/FryFuture_Specs.cs index 89f75623e20..77e01f9e41c 100644 --- a/src/MassTransit.TestFramework/ForkJoint/Tests/FryFuture_Specs.cs +++ b/src/MassTransit.TestFramework/ForkJoint/Tests/FryFuture_Specs.cs @@ -13,6 +13,11 @@ namespace MassTransit.TestFramework.ForkJoint.Tests public class FryFuture_Specs : FutureTestFixture { + public FryFuture_Specs(IFutureTestFixtureConfigurator testFixtureConfigurator) + : base(testFixtureConfigurator) + { + } + [Test] public async Task Should_complete() { @@ -32,11 +37,14 @@ public async Task Should_complete() Size = Size.Medium }); - Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); - Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); - Assert.That(response.Message.Size, Is.EqualTo(Size.Medium)); - Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); - Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); + Assert.Multiple(() => + { + Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); + Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); + Assert.That(response.Message.Size, Is.EqualTo(Size.Medium)); + Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); + Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); + }); } protected override void ConfigureServices(IServiceCollection collection) @@ -49,10 +57,5 @@ protected override void ConfigureMassTransit(IBusRegistrationConfigurator config configurator.AddConsumer(); configurator.AddFuture(); } - - public FryFuture_Specs(IFutureTestFixtureConfigurator testFixtureConfigurator) - : base(testFixtureConfigurator) - { - } } } diff --git a/src/MassTransit.TestFramework/ForkJoint/Tests/FryShakeFuture_Specs.cs b/src/MassTransit.TestFramework/ForkJoint/Tests/FryShakeFuture_Specs.cs index cce6fbc10d2..523ba3dfbc0 100644 --- a/src/MassTransit.TestFramework/ForkJoint/Tests/FryShakeFuture_Specs.cs +++ b/src/MassTransit.TestFramework/ForkJoint/Tests/FryShakeFuture_Specs.cs @@ -34,12 +34,15 @@ public async Task Should_complete() Size = Size.Medium }); - Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); - Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); - Assert.That(response.Message.Size, Is.EqualTo(Size.Medium)); - Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); - Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); - Assert.That(response.Message.Description, Contains.Substring("FryShake(2)")); + Assert.Multiple(() => + { + Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); + Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); + Assert.That(response.Message.Size, Is.EqualTo(Size.Medium)); + Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); + Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); + Assert.That(response.Message.Description, Contains.Substring("FryShake(2)")); + }); } [Test] @@ -66,8 +69,11 @@ await client.GetResponse(new } catch (RequestFaultException exception) { - Assert.That(exception.Fault.Host, Is.Not.Null); - Assert.That(exception.Message, Contains.Substring("Strawberry is not available")); + Assert.Multiple(() => + { + Assert.That(exception.Fault.Host, Is.Not.Null); + Assert.That(exception.Message, Contains.Substring("Strawberry is not available")); + }); } } diff --git a/src/MassTransit.TestFramework/ForkJoint/Tests/OrderFuture_Specs.cs b/src/MassTransit.TestFramework/ForkJoint/Tests/OrderFuture_Specs.cs index f1bb97e1c69..513eb877360 100644 --- a/src/MassTransit.TestFramework/ForkJoint/Tests/OrderFuture_Specs.cs +++ b/src/MassTransit.TestFramework/ForkJoint/Tests/OrderFuture_Specs.cs @@ -52,10 +52,13 @@ public async Task Should_complete() FryShakes = default(FryShake[]) }, timeout: RequestTimeout.After(s: 5)); - Assert.That(response.Is(out Response completed), "Order did not complete"); + Assert.Multiple(() => + { + Assert.That(response.Is(out Response completed), "Order did not complete"); - Assert.That(completed.Message.OrderId, Is.EqualTo(orderId)); - Assert.That(completed.Message.LinesCompleted.Count, Is.EqualTo(2)); + Assert.That(completed.Message.OrderId, Is.EqualTo(orderId)); + Assert.That(completed.Message.LinesCompleted, Has.Count.EqualTo(2)); + }); } [Test] diff --git a/src/MassTransit.TestFramework/ForkJoint/Tests/ShakeFuture_Specs.cs b/src/MassTransit.TestFramework/ForkJoint/Tests/ShakeFuture_Specs.cs index 8f6fcfa4163..03078e69cf3 100644 --- a/src/MassTransit.TestFramework/ForkJoint/Tests/ShakeFuture_Specs.cs +++ b/src/MassTransit.TestFramework/ForkJoint/Tests/ShakeFuture_Specs.cs @@ -34,11 +34,14 @@ public async Task Should_complete() Size = Size.Medium }); - Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); - Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); - Assert.That(response.Message.Size, Is.EqualTo(Size.Medium)); - Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); - Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); + Assert.Multiple(() => + { + Assert.That(response.Message.OrderId, Is.EqualTo(orderId)); + Assert.That(response.Message.OrderLineId, Is.EqualTo(orderLineId)); + Assert.That(response.Message.Size, Is.EqualTo(Size.Medium)); + Assert.That(response.Message.Created, Is.GreaterThanOrEqualTo(startedAt)); + Assert.That(response.Message.Completed, Is.GreaterThanOrEqualTo(response.Message.Created)); + }); } [Test] @@ -65,8 +68,11 @@ await client.GetResponse(new } catch (RequestFaultException exception) { - Assert.That(exception.Fault.Host, Is.Not.Null); - Assert.That(exception.Message, Contains.Substring("Strawberry is not available")); + Assert.Multiple(() => + { + Assert.That(exception.Fault.Host, Is.Not.Null); + Assert.That(exception.Message, Contains.Substring("Strawberry is not available")); + }); } } diff --git a/src/MassTransit.TestFramework/Futures/BatchCompleted.cs b/src/MassTransit.TestFramework/Futures/BatchCompleted.cs new file mode 100644 index 00000000000..7f41a5b2129 --- /dev/null +++ b/src/MassTransit.TestFramework/Futures/BatchCompleted.cs @@ -0,0 +1,11 @@ +namespace MassTransit.TestFramework.Futures; + +using System; +using System.Collections.Generic; + + +public interface BatchCompleted +{ + public Guid CorrelationId { get; } + public IReadOnlyList ProcessedJobsNumbers { get; } +} diff --git a/src/MassTransit.TestFramework/Futures/BatchFaulted.cs b/src/MassTransit.TestFramework/Futures/BatchFaulted.cs new file mode 100644 index 00000000000..7e7e2bbad8d --- /dev/null +++ b/src/MassTransit.TestFramework/Futures/BatchFaulted.cs @@ -0,0 +1,11 @@ +namespace MassTransit.TestFramework.Futures; + +using System; +using System.Collections.Generic; + + +public interface BatchFaulted +{ + public Guid CorrelationId { get; } + public IReadOnlyList ProcessedJobsNumbers { get; } +} diff --git a/src/MassTransit.TestFramework/Futures/BatchFuture.cs b/src/MassTransit.TestFramework/Futures/BatchFuture.cs new file mode 100644 index 00000000000..4e074bd0428 --- /dev/null +++ b/src/MassTransit.TestFramework/Futures/BatchFuture.cs @@ -0,0 +1,46 @@ +namespace MassTransit.TestFramework.Futures; + +using System.Collections.Generic; +using System.Linq; + + +public class BatchFuture : + Future +{ + public BatchFuture() + { + ConfigureCommand(x => x.CorrelateById(context => context.Message.CorrelationId)); + + SendRequests(x => x.JobNumbers, + x => + { + x.UsingRequestInitializer(context => new + { + CorrelationId = InVar.Id, + JobNumber = context.Message + }); + x.TrackPendingRequest(message => message.CorrelationId); + }) + .OnResponseReceived(x => + { + x.CompletePendingRequest(y => y.CorrelationId); + }); + + WhenAllCompleted(r => r.SetCompletedUsingInitializer(MapResponse)); + WhenAllCompletedOrFaulted(r => r.SetFaultedUsingInitializer(MapResponse)); + } + + object MapResponse(BehaviorContext context) + { + var command = context.GetCommand(); + List processedJobNumbers = context + .SelectResults() + .Select(r => r.JobNumber).ToList(); + + return new + { + command.CorrelationId, + ProcessedJobsNumbers = processedJobNumbers + }; + } +} diff --git a/src/MassTransit.TestFramework/Futures/BatchRequest.cs b/src/MassTransit.TestFramework/Futures/BatchRequest.cs new file mode 100644 index 00000000000..1b3be1a7c24 --- /dev/null +++ b/src/MassTransit.TestFramework/Futures/BatchRequest.cs @@ -0,0 +1,12 @@ +namespace MassTransit.TestFramework.Futures; + +using System; +using System.Collections.Generic; + + +public interface BatchRequest +{ + public DateTime? BatchExpiry { get; } + public Guid CorrelationId { get; } + public IReadOnlyList JobNumbers { get; } +} diff --git a/src/MassTransit.TestFramework/Futures/ProcessBatchItem.cs b/src/MassTransit.TestFramework/Futures/ProcessBatchItem.cs new file mode 100644 index 00000000000..2e0d09c206d --- /dev/null +++ b/src/MassTransit.TestFramework/Futures/ProcessBatchItem.cs @@ -0,0 +1,10 @@ +namespace MassTransit.TestFramework.Futures; + +using System; + + +public interface ProcessBatchItem +{ + public Guid CorrelationId { get; } + public string JobNumber { get; } +} diff --git a/src/MassTransit.TestFramework/Futures/ProcessBatchItemCompleted.cs b/src/MassTransit.TestFramework/Futures/ProcessBatchItemCompleted.cs new file mode 100644 index 00000000000..111d2774987 --- /dev/null +++ b/src/MassTransit.TestFramework/Futures/ProcessBatchItemCompleted.cs @@ -0,0 +1,10 @@ +namespace MassTransit.TestFramework.Futures; + +using System; + + +public interface ProcessBatchItemCompleted +{ + public Guid CorrelationId { get; } + public string JobNumber { get; } +} diff --git a/src/MassTransit.TestFramework/Futures/ProcessBatchItemConsumer.cs b/src/MassTransit.TestFramework/Futures/ProcessBatchItemConsumer.cs new file mode 100644 index 00000000000..9df157480fb --- /dev/null +++ b/src/MassTransit.TestFramework/Futures/ProcessBatchItemConsumer.cs @@ -0,0 +1,29 @@ +namespace MassTransit.TestFramework.Futures; + +using System; +using System.Threading.Tasks; + + +public class ProcessBatchItemConsumer : + IConsumer +{ + public Task Consume(ConsumeContext context) + { + async Task WaitAndRespond(int milliSecond) + { + await Task.Delay(milliSecond); + await context.RespondAsync(new + { + context.Message.CorrelationId, + context.Message.JobNumber + }); + } + + return context.Message.JobNumber switch + { + "Delay" => WaitAndRespond(2000), + "Error" => throw new InvalidOperationException(), + _ => WaitAndRespond(0) + }; + } +} diff --git a/src/MassTransit.TestFramework/Futures/Tests/BatchFuture_Specs.cs b/src/MassTransit.TestFramework/Futures/Tests/BatchFuture_Specs.cs new file mode 100644 index 00000000000..8c1f6d374bb --- /dev/null +++ b/src/MassTransit.TestFramework/Futures/Tests/BatchFuture_Specs.cs @@ -0,0 +1,20 @@ +namespace MassTransit.TestFramework.Futures.Tests; + +using NUnit.Framework; + + +[TestFixture] +public class BatchFuture_Specs : + FutureTestFixture +{ + public BatchFuture_Specs(IFutureTestFixtureConfigurator testFixtureConfigurator) + : base(testFixtureConfigurator) + { + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + configurator.AddFuture(); + } +} diff --git a/src/MassTransit.TestFramework/InMemoryContainerTestFixture.cs b/src/MassTransit.TestFramework/InMemoryContainerTestFixture.cs index 2f5783b28ca..850a23c1d48 100644 --- a/src/MassTransit.TestFramework/InMemoryContainerTestFixture.cs +++ b/src/MassTransit.TestFramework/InMemoryContainerTestFixture.cs @@ -68,6 +68,7 @@ public async Task ScopeTearDown() [OneTimeSetUp] public async Task ContainerFixtureOneTimeSetup() { + #pragma warning disable CS0618 var collection = new ServiceCollection() .AddSingleton(provider => new TestOutputLoggerFactory(true)) .AddSingleton(typeof(ILogger<>), typeof(Logger<>)) @@ -77,6 +78,7 @@ public async Task ContainerFixtureOneTimeSetup() ConfigureMassTransit(cfg); }); + #pragma warning restore CS0618 collection = ConfigureServices(collection); @@ -162,10 +164,16 @@ protected IClientFactory GetClientFactory() return ServiceProvider.GetRequiredService(); } - protected ISagaRepository GetSagaRepository() + protected ILoadSagaRepository GetLoadSagaRepository() where T : class, ISaga { - return ServiceProvider.GetRequiredService>(); + return ServiceProvider.GetRequiredService>(); + } + + protected IQuerySagaRepository GetQuerySagaRepository() + where T : class, ISaga + { + return ServiceProvider.GetRequiredService>(); } protected async Task>> ConnectPublishHandler() diff --git a/src/MassTransit.TestFramework/IntentionalTestException.cs b/src/MassTransit.TestFramework/IntentionalTestException.cs index 2f96145153f..fa5ed197f14 100644 --- a/src/MassTransit.TestFramework/IntentionalTestException.cs +++ b/src/MassTransit.TestFramework/IntentionalTestException.cs @@ -26,6 +26,9 @@ public IntentionalTestException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected IntentionalTestException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit.TestFramework/Logging/TestOutputListenerObserver.cs b/src/MassTransit.TestFramework/Logging/TestOutputListenerObserver.cs index 02c9715de2a..8047a5be882 100644 --- a/src/MassTransit.TestFramework/Logging/TestOutputListenerObserver.cs +++ b/src/MassTransit.TestFramework/Logging/TestOutputListenerObserver.cs @@ -24,14 +24,14 @@ public void OnNext(KeyValuePair value) { var activity = Activity.Current; - TestContext.WriteLine($"{DateTime.Now:HH:mm:ss.fff}-A Start {activity.OperationName} {activity.Id} {activity.ParentId}"); + TestContext.Out.WriteLine($"{DateTime.Now:HH:mm:ss.fff}-A Start {activity.OperationName} {activity.Id} {activity.ParentId}"); } if (value.Key.EndsWith(".Stop", StringComparison.OrdinalIgnoreCase)) { var activity = Activity.Current; - TestContext.WriteLine( + TestContext.Out.WriteLine( $"{DateTime.Now:HH:mm:ss.fff}-A Stop {activity.OperationName} {activity.Id} {activity.ParentId ?? "--"} {activity.Duration.ToFriendlyString()}"); } } diff --git a/src/MassTransit.TestFramework/MassTransit.TestFramework.csproj b/src/MassTransit.TestFramework/MassTransit.TestFramework.csproj index b00b0fedd57..70db6945ee0 100644 --- a/src/MassTransit.TestFramework/MassTransit.TestFramework.csproj +++ b/src/MassTransit.TestFramework/MassTransit.TestFramework.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -15,10 +15,12 @@ - - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/MassTransit.TestFramework/TestConsumeContext.cs b/src/MassTransit.TestFramework/TestConsumeContext.cs index d3c05eaed4d..06711ee6a50 100644 --- a/src/MassTransit.TestFramework/TestConsumeContext.cs +++ b/src/MassTransit.TestFramework/TestConsumeContext.cs @@ -20,7 +20,7 @@ public static class TestConsumeContext static InMemoryReceiveEndpointContext Build() { - var topologyConfiguration = new InMemoryTopologyConfiguration(InMemoryBus.MessageTopology); + var topologyConfiguration = new InMemoryTopologyConfiguration(InMemoryBus.CreateMessageTopology()); IInMemoryBusConfiguration busConfiguration = new InMemoryBusConfiguration(topologyConfiguration, null); var receiveEndpointConfiguration = busConfiguration.HostConfiguration.CreateReceiveEndpointConfiguration("input-queue"); @@ -161,7 +161,7 @@ Task IPublishEndpoint.Publish(object values, IPipe publishPip public bool HasMessageType(Type messageType) { - return messageType.GetTypeInfo().IsAssignableFrom(typeof(TMessage)); + return messageType.IsAssignableFrom(typeof(TMessage)); } public bool TryGetMessage(out ConsumeContext consumeContext) diff --git a/src/MassTransit.TestFramework/TestStateMachineExtensions.cs b/src/MassTransit.TestFramework/TestStateMachineExtensions.cs index 1571c128464..6d023739f5a 100644 --- a/src/MassTransit.TestFramework/TestStateMachineExtensions.cs +++ b/src/MassTransit.TestFramework/TestStateMachineExtensions.cs @@ -14,7 +14,7 @@ public static class TestStateMachineExtensions public static Task RaiseEvent(this T machine, TInstance instance, Event @event, TData data, CancellationToken cancellationToken = default) where T : class, StateMachine, StateMachine - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TData : class { if (@event == null) @@ -22,7 +22,7 @@ public static Task RaiseEvent(this T machine, TInstance ins var consumeContext = new TestConsumeContext(data, cancellationToken); var sagaConsumeContext = new InMemorySagaConsumeContext(consumeContext, new SagaInstance(instance)); - var behaviorContext = new BehaviorContextProxy(machine, sagaConsumeContext, consumeContext, @event); + var behaviorContext = new MassTransitStateMachine.BehaviorContextProxy(machine, sagaConsumeContext, consumeContext, @event); return machine.RaiseEvent(behaviorContext); } @@ -30,7 +30,7 @@ public static Task RaiseEvent(this T machine, TInstance ins public static Task RaiseEvent(this T machine, TInstance instance, Func> eventSelector, TData data, CancellationToken cancellationToken = default) where T : class, StateMachine, StateMachine - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TData : class { Event @event = eventSelector(machine); @@ -39,21 +39,21 @@ public static Task RaiseEvent(this T machine, TInstance ins var consumeContext = new TestConsumeContext(data, cancellationToken); var sagaConsumeContext = new InMemorySagaConsumeContext(consumeContext, new SagaInstance(instance)); - var behaviorContext = new BehaviorContextProxy(machine, sagaConsumeContext, consumeContext, @event); + var behaviorContext = new MassTransitStateMachine.BehaviorContextProxy(machine, sagaConsumeContext, consumeContext, @event); return machine.RaiseEvent(behaviorContext); } public static Task RaiseEvent(this T machine, TInstance instance, Event @event, CancellationToken cancellationToken = default) where T : class, StateMachine, StateMachine - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { if (@event == null) throw new ArgumentNullException(nameof(@event)); var consumeContext = new TestConsumeContext(new Data(), cancellationToken); var sagaConsumeContext = new InMemorySagaConsumeContext(consumeContext, new SagaInstance(instance)); - var behaviorContext = new BehaviorContextProxy(machine, sagaConsumeContext, @event); + var behaviorContext = new MassTransitStateMachine.BehaviorContextProxy(machine, sagaConsumeContext, @event); return machine.RaiseEvent(behaviorContext); } @@ -61,7 +61,7 @@ public static Task RaiseEvent(this T machine, TInstance instance, public static Task RaiseEvent(this T machine, TInstance instance, Func eventSelector, CancellationToken cancellationToken = default) where T : class, StateMachine, StateMachine - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { var @event = eventSelector(machine); if (@event == null) @@ -69,43 +69,43 @@ public static Task RaiseEvent(this T machine, TInstance instance, var consumeContext = new TestConsumeContext(new Data(), cancellationToken); var sagaConsumeContext = new InMemorySagaConsumeContext(consumeContext, new SagaInstance(instance)); - var behaviorContext = new BehaviorContextProxy(machine, sagaConsumeContext, @event); + var behaviorContext = new MassTransitStateMachine.BehaviorContextProxy(machine, sagaConsumeContext, @event); return machine.RaiseEvent(behaviorContext); } public static async Task> NextEvents(this StateMachine machine, TInstance instance) - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { var consumeContext = new TestConsumeContext(new Data()); var sagaConsumeContext = new InMemorySagaConsumeContext(consumeContext, new SagaInstance(instance)); - var behaviorContext = new BehaviorContextProxy(machine, sagaConsumeContext, machine.Initial.Enter); + var behaviorContext = new MassTransitStateMachine.BehaviorContextProxy(machine, sagaConsumeContext, machine.Initial.Enter); return machine.NextEvents(await machine.Accessor.Get(behaviorContext).ConfigureAwait(false)); } public static Task> GetState(this StateMachine machine, TInstance instance) - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { var consumeContext = new TestConsumeContext(new Data()); var sagaConsumeContext = new InMemorySagaConsumeContext(consumeContext, new SagaInstance(instance)); - var behaviorContext = new BehaviorContextProxy(machine, sagaConsumeContext, machine.Initial.Enter); + var behaviorContext = new MassTransitStateMachine.BehaviorContextProxy(machine, sagaConsumeContext, machine.Initial.Enter); return machine.Accessor.Get(behaviorContext); } - public static Task TransitionToState(this StateMachine machine, TSaga instance, State state) - where TSaga : class, ISaga + public static Task TransitionToState(this StateMachine machine, TInstance instance, State state) + where TInstance : class, SagaStateMachineInstance { - IStateAccessor accessor = machine.Accessor; - State toState = machine.GetState(state.Name); + IStateAccessor accessor = machine.Accessor; + State toState = machine.GetState(state.Name); - IStateMachineActivity activity = new TransitionActivity(toState, accessor); - IBehavior behavior = new LastBehavior(activity); + IStateMachineActivity activity = new TransitionActivity(toState, accessor); + IBehavior behavior = new LastBehavior(activity); var consumeContext = new TestConsumeContext(new Data()); - var sagaConsumeContext = new InMemorySagaConsumeContext(consumeContext, new SagaInstance(instance)); - var behaviorContext = new BehaviorContextProxy(machine, sagaConsumeContext, machine.Initial.Enter); + var sagaConsumeContext = new InMemorySagaConsumeContext(consumeContext, new SagaInstance(instance)); + var behaviorContext = new MassTransitStateMachine.BehaviorContextProxy(machine, sagaConsumeContext, machine.Initial.Enter); return behavior.Execute(behaviorContext); } diff --git a/src/MassTransit/Caching/Internals/Index.cs b/src/MassTransit/Caching/Internals/Index.cs index 60366273940..eccdb5c8b55 100644 --- a/src/MassTransit/Caching/Internals/Index.cs +++ b/src/MassTransit/Caching/Internals/Index.cs @@ -32,9 +32,7 @@ public Index(INodeTracker nodeTracker, KeyProvider keyProv public void Clear() { lock (_lock) - { _index.Clear(); - } } public async Task Add(INode node) @@ -65,9 +63,7 @@ public bool TryGetExistingNode(TValue value, out INode node) var key = _keyProvider(value); lock (_lock) - { return TryGetExistingNode(key, out node); - } } public void ValueAdded(INode node, TValue value) @@ -75,9 +71,7 @@ public void ValueAdded(INode node, TValue value) var key = _keyProvider(value); lock (_lock) - { _index[key] = new WeakReference>(node, false); - } } public void ValueRemoved(INode node, TValue value) @@ -85,9 +79,7 @@ public void ValueRemoved(INode node, TValue value) var key = _keyProvider(value); lock (_lock) - { _index.Remove(key); - } } public void CacheCleared() diff --git a/src/MassTransit/Caching/Internals/NodeTracker.cs b/src/MassTransit/Caching/Internals/NodeTracker.cs index fb945169f27..09571c9028a 100644 --- a/src/MassTransit/Caching/Internals/NodeTracker.cs +++ b/src/MassTransit/Caching/Internals/NodeTracker.cs @@ -135,9 +135,7 @@ public void Clear() public void Rebucket(IBucketNode node) { lock (_lock) - { node.AssignToBucket(_currentBucket); - } } public ConnectHandle Connect(ICacheValueObserver observer) @@ -225,9 +223,7 @@ void OpenBucket(int index) if (_currentBucket != null) { lock (_currentBucket) - { _currentBucket.Stop(now); - } } CurrentBucketIndex = index; @@ -279,8 +275,8 @@ void Cleanup(DateTime now) var expiration = now - _maxAge; var aged = now - _minAge; while (AreLowOnBuckets - || bucket.HasExpired(expiration) - || itemsAboveCapacity > 0 && bucket.IsOldEnough(aged)) + || bucket.HasExpired(expiration) + || (itemsAboveCapacity > 0 && bucket.IsOldEnough(aged))) { IBucketNode node = bucket.Head; @@ -298,9 +294,9 @@ void Cleanup(DateTime now) --itemsAboveCapacity; // so if we don't await this, can't be too bad can it? -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + #pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed EvictNode(node); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + #pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed } else { diff --git a/src/MassTransit/ClientFactoryExtensions.cs b/src/MassTransit/ClientFactoryExtensions.cs index bd10ce2696e..2454573af2e 100644 --- a/src/MassTransit/ClientFactoryExtensions.cs +++ b/src/MassTransit/ClientFactoryExtensions.cs @@ -1,7 +1,6 @@ namespace MassTransit { using System; - using System.Threading.Tasks; using Clients; @@ -79,19 +78,6 @@ public static IClientFactory CreateClientFactory(this IBus bus, RequestTimeout t return new ClientFactory(new BusClientFactoryContext(bus, timeout)); } - /// - /// Connects a client factory to a host receive endpoint, using the bus as the send endpoint provider - /// - /// - /// - /// - public static IClientFactory CreateClientFactory(this ReceiveEndpointReady receiveEndpoint, RequestTimeout timeout = default) - { - var context = new ReceiveEndpointClientFactoryContext(receiveEndpoint, timeout); - - return new ClientFactory(context); - } - /// /// Connects a client factory to a host receive endpoint, using the bus as the send endpoint provider /// @@ -100,11 +86,9 @@ public static IClientFactory CreateClientFactory(this ReceiveEndpointReady recei /// /// /// - public static async Task CreateClientFactory(this HostReceiveEndpointHandle receiveEndpointHandle, RequestTimeout timeout = default) + public static IClientFactory CreateClientFactory(this HostReceiveEndpointHandle receiveEndpointHandle, RequestTimeout timeout = default) { - var ready = await receiveEndpointHandle.Ready.ConfigureAwait(false); - - var context = new HostReceiveEndpointClientFactoryContext(receiveEndpointHandle, ready, timeout); + var context = new HostReceiveEndpointClientFactoryContext(receiveEndpointHandle, timeout); return new ClientFactory(context); } @@ -115,7 +99,7 @@ public static async Task CreateClientFactory(this HostReceiveEnd /// The host to connect the new receive endpoint /// The default request timeout /// - public static Task CreateClientFactory(this IReceiveConnector connector, RequestTimeout timeout = default) + public static IClientFactory CreateClientFactory(this IReceiveConnector connector, RequestTimeout timeout = default) { var receiveEndpointHandle = connector.ConnectResponseEndpoint(); @@ -128,7 +112,7 @@ public static Task CreateClientFactory(this IReceiveConnector co /// The host to connect the new receive endpoint /// The default request timeout /// - public static Task ConnectClientFactory(this IReceiveConnector connector, RequestTimeout timeout = default) + public static IClientFactory ConnectClientFactory(this IReceiveConnector connector, RequestTimeout timeout = default) { var endpointDefinition = new TemporaryEndpointDefinition(); diff --git a/src/MassTransit/Clients/ClientFactory.cs b/src/MassTransit/Clients/ClientFactory.cs index 5f2cb251567..1b65665cb38 100644 --- a/src/MassTransit/Clients/ClientFactory.cs +++ b/src/MassTransit/Clients/ClientFactory.cs @@ -9,22 +9,20 @@ public class ClientFactory : IClientFactory, IAsyncDisposable { - readonly ClientFactoryContext _context; - public ClientFactory(ClientFactoryContext context) { - _context = context; + Context = context; } public ValueTask DisposeAsync() { - if (_context is IAsyncDisposable asyncDisposable) + if (Context is IAsyncDisposable asyncDisposable) return asyncDisposable.DisposeAsync(); return default; } - public ClientFactoryContext Context => _context; + public ClientFactoryContext Context { get; } public RequestHandle CreateRequest(T message, CancellationToken cancellationToken, RequestTimeout timeout) where T : class @@ -98,7 +96,7 @@ public IRequestClient CreateRequestClient(RequestTimeout timeout) if (EndpointConvention.TryGetDestinationAddress(out var destinationAddress)) return CreateRequestClient(destinationAddress, timeout); - return new RequestClient(_context, _context.GetRequestEndpoint(), timeout.Or(_context.DefaultTimeout)); + return new RequestClient(Context, Context.GetRequestEndpoint(), timeout.Or(Context.DefaultTimeout)); } public IRequestClient CreateRequestClient(ConsumeContext consumeContext, RequestTimeout timeout) @@ -107,21 +105,21 @@ public IRequestClient CreateRequestClient(ConsumeContext consumeContext, R if (EndpointConvention.TryGetDestinationAddress(out var destinationAddress)) return CreateRequestClient(consumeContext, destinationAddress, timeout); - return new RequestClient(_context, _context.GetRequestEndpoint(consumeContext), timeout.Or(_context.DefaultTimeout)); + return new RequestClient(Context, Context.GetRequestEndpoint(consumeContext), timeout.Or(Context.DefaultTimeout)); } public IRequestClient CreateRequestClient(Uri destinationAddress, RequestTimeout timeout) where T : class { - IRequestSendEndpoint requestSendEndpoint = _context.GetRequestEndpoint(destinationAddress); + IRequestSendEndpoint requestSendEndpoint = Context.GetRequestEndpoint(destinationAddress); - return new RequestClient(_context, requestSendEndpoint, timeout.Or(_context.DefaultTimeout)); + return new RequestClient(Context, requestSendEndpoint, timeout.Or(Context.DefaultTimeout)); } public IRequestClient CreateRequestClient(ConsumeContext consumeContext, Uri destinationAddress, RequestTimeout timeout) where T : class { - return new RequestClient(_context, _context.GetRequestEndpoint(destinationAddress, consumeContext), timeout.Or(_context.DefaultTimeout)); + return new RequestClient(Context, Context.GetRequestEndpoint(destinationAddress, consumeContext), timeout.Or(Context.DefaultTimeout)); } } } diff --git a/src/MassTransit/Clients/ClientRequestHandle.cs b/src/MassTransit/Clients/ClientRequestHandle.cs index 4cda6ffa88d..7e6b5b8c5ab 100644 --- a/src/MassTransit/Clients/ClientRequestHandle.cs +++ b/src/MassTransit/Clients/ClientRequestHandle.cs @@ -5,6 +5,7 @@ using System.Threading; using System.Threading.Tasks; using Configuration; + using Internals; using Util; @@ -16,7 +17,7 @@ public class ClientRequestHandle : public delegate Task SendRequestCallback(Guid requestId, IPipe> pipe, CancellationToken cancellationToken); - readonly IList _accept; + readonly List _accept; readonly CancellationToken _cancellationToken; readonly CancellationTokenSource _cancellationTokenSource; readonly ClientFactoryContext _context; @@ -201,8 +202,7 @@ Task MessageHandler(ConsumeContext> context) return FaultHandler(context); } - var connectHandle = _context.ConnectRequestHandler(RequestId, MessageHandler, - new PipeConfigurator>>()); + var connectHandle = _context.ConnectRequestHandler(RequestId, MessageHandler, new PipeConfigurator>>()); var handle = new FaultHandlerConnectHandle(connectHandle); @@ -237,6 +237,7 @@ void HandleFail() var wasSet = _sendContext.TrySetException(exception); _message.TrySetException(exception); + _message.Task.IgnoreUnobservedExceptions(); foreach (var handle in _responseHandlers.Values) { diff --git a/src/MassTransit/Clients/HostReceiveEndpointClientFactoryContext.cs b/src/MassTransit/Clients/HostReceiveEndpointClientFactoryContext.cs index 22eba1d9579..fca1530a89a 100644 --- a/src/MassTransit/Clients/HostReceiveEndpointClientFactoryContext.cs +++ b/src/MassTransit/Clients/HostReceiveEndpointClientFactoryContext.cs @@ -8,18 +8,17 @@ public class HostReceiveEndpointClientFactoryContext : ReceiveEndpointClientFactoryContext, IAsyncDisposable { - readonly HostReceiveEndpointHandle _receiveEndpointHandle; + readonly HostReceiveEndpointHandle _handle; - public HostReceiveEndpointClientFactoryContext(HostReceiveEndpointHandle receiveEndpointHandle, ReceiveEndpointReady receiveEndpointReady, - RequestTimeout defaultTimeout = default) - : base(receiveEndpointReady, defaultTimeout) + public HostReceiveEndpointClientFactoryContext(HostReceiveEndpointHandle handle, RequestTimeout defaultTimeout = default) + : base(handle, defaultTimeout) { - _receiveEndpointHandle = receiveEndpointHandle; + _handle = handle; } public async ValueTask DisposeAsync() { - await _receiveEndpointHandle.StopAsync().ConfigureAwait(false); + await _handle.StopAsync().ConfigureAwait(false); } } } diff --git a/src/MassTransit/Clients/MessageResponse.cs b/src/MassTransit/Clients/MessageResponse.cs index a4ce1f1d97f..d36812aa831 100644 --- a/src/MassTransit/Clients/MessageResponse.cs +++ b/src/MassTransit/Clients/MessageResponse.cs @@ -1,6 +1,7 @@ namespace MassTransit.Clients { using System; + using System.Collections.Generic; /// @@ -11,11 +12,12 @@ public class MessageResponse : Response where TResult : class { - readonly MessageContext _context; + readonly ConsumeContext _context; public MessageResponse(ConsumeContext context) { _context = context; + Message = context.Message; } @@ -35,5 +37,11 @@ public MessageResponse(ConsumeContext context) public TResult Message { get; } object Response.Message => Message; + + public T DeserializeObject(Dictionary dictionary) + where T : class + { + return _context.SerializerContext.DeserializeObject(dictionary); + } } } diff --git a/src/MassTransit/Clients/ReceiveEndpointClientFactoryContext.cs b/src/MassTransit/Clients/ReceiveEndpointClientFactoryContext.cs index 5e3b819201a..1d3aaf93cfc 100644 --- a/src/MassTransit/Clients/ReceiveEndpointClientFactoryContext.cs +++ b/src/MassTransit/Clients/ReceiveEndpointClientFactoryContext.cs @@ -7,13 +7,16 @@ namespace MassTransit.Clients public class ReceiveEndpointClientFactoryContext : ClientFactoryContext { + readonly HostReceiveEndpointHandle _handle; readonly IReceiveEndpoint _receiveEndpoint; - public ReceiveEndpointClientFactoryContext(ReceiveEndpointReady receiveEndpointReady, RequestTimeout defaultTimeout = default) + public ReceiveEndpointClientFactoryContext(HostReceiveEndpointHandle handle, RequestTimeout defaultTimeout = default) { - _receiveEndpoint = receiveEndpointReady.ReceiveEndpoint; + _handle = handle; + _receiveEndpoint = handle.ReceiveEndpoint; + + ResponseAddress = _receiveEndpoint.InputAddress; - ResponseAddress = receiveEndpointReady.InputAddress; DefaultTimeout = defaultTimeout.Or(RequestTimeout.Default); } @@ -40,13 +43,13 @@ public ConnectHandle ConnectRequestPipe(Guid requestId, IPipe GetRequestEndpoint(ConsumeContext? consumeContext = default) where T : class { - return new PublishRequestSendEndpoint(_receiveEndpoint, consumeContext); + return new ReceiveEndpointPublishRequestSendEndpoint(_handle, consumeContext); } public IRequestSendEndpoint GetRequestEndpoint(Uri destinationAddress, ConsumeContext? consumeContext = default) where T : class { - return new SendRequestSendEndpoint(_receiveEndpoint, destinationAddress, consumeContext); + return new ReceiveEndpointSendRequestSendEndpoint(_handle, destinationAddress, consumeContext); } public RequestTimeout DefaultTimeout { get; } diff --git a/src/MassTransit/Clients/ReceiveEndpointPublishRequestSendEndpoint.cs b/src/MassTransit/Clients/ReceiveEndpointPublishRequestSendEndpoint.cs new file mode 100644 index 00000000000..9a2fc30174b --- /dev/null +++ b/src/MassTransit/Clients/ReceiveEndpointPublishRequestSendEndpoint.cs @@ -0,0 +1,27 @@ +namespace MassTransit.Clients; + +using System.Threading.Tasks; +using Middleware; + + +public class ReceiveEndpointPublishRequestSendEndpoint : + RequestSendEndpoint + where TRequest : class +{ + readonly HostReceiveEndpointHandle _handle; + + public ReceiveEndpointPublishRequestSendEndpoint(HostReceiveEndpointHandle handle, ConsumeContext consumeContext) + : base(consumeContext) + { + _handle = handle; + } + + protected override async Task GetSendEndpoint() + { + var ready = await _handle.Ready.ConfigureAwait(false); + + var endpoint = await ready.ReceiveEndpoint.GetPublishSendEndpoint().ConfigureAwait(false); + + return endpoint.SkipOutbox(); + } +} diff --git a/src/MassTransit/Clients/ReceiveEndpointSendRequestSendEndpoint.cs b/src/MassTransit/Clients/ReceiveEndpointSendRequestSendEndpoint.cs new file mode 100644 index 00000000000..33dde8c1cd7 --- /dev/null +++ b/src/MassTransit/Clients/ReceiveEndpointSendRequestSendEndpoint.cs @@ -0,0 +1,30 @@ +namespace MassTransit.Clients; + +using System; +using System.Threading.Tasks; +using Middleware; + + +public class ReceiveEndpointSendRequestSendEndpoint : + RequestSendEndpoint + where TRequest : class +{ + readonly Uri _destinationAddress; + readonly HostReceiveEndpointHandle _handle; + + public ReceiveEndpointSendRequestSendEndpoint(HostReceiveEndpointHandle handle, Uri destinationAddress, ConsumeContext consumeContext) + : base(consumeContext) + { + _handle = handle; + _destinationAddress = destinationAddress; + } + + protected override async Task GetSendEndpoint() + { + var ready = await _handle.Ready.ConfigureAwait(false); + + var endpoint = await ready.ReceiveEndpoint.GetSendEndpoint(_destinationAddress).ConfigureAwait(false); + + return endpoint.SkipOutbox(); + } +} diff --git a/src/MassTransit/Clients/ResponseHandlerConfigurator.cs b/src/MassTransit/Clients/ResponseHandlerConfigurator.cs index 4375040f862..414037e5062 100644 --- a/src/MassTransit/Clients/ResponseHandlerConfigurator.cs +++ b/src/MassTransit/Clients/ResponseHandlerConfigurator.cs @@ -42,7 +42,7 @@ public ConnectHandle ConnectHandlerConfigurationObserver(IHandlerConfigurationOb public HandlerConnectHandle Connect(IRequestPipeConnector connector, Guid requestId) { - MessageHandler messageHandler = _handler != null ? (MessageHandler)AsyncMessageHandler : MessageHandler; + MessageHandler messageHandler = _handler != null ? AsyncMessageHandler : MessageHandler; var connectHandle = connector.ConnectRequestHandler(requestId, messageHandler, _pipeConfigurator); diff --git a/src/MassTransit/Clients/ResponseHandlerConnectHandle.cs b/src/MassTransit/Clients/ResponseHandlerConnectHandle.cs index 152f8817ec8..c097b33e156 100644 --- a/src/MassTransit/Clients/ResponseHandlerConnectHandle.cs +++ b/src/MassTransit/Clients/ResponseHandlerConnectHandle.cs @@ -3,6 +3,7 @@ using System; using System.Threading; using System.Threading.Tasks; + using Internals; /// @@ -39,11 +40,13 @@ public void Disconnect() public void TrySetException(Exception exception) { _completed.TrySetException(exception); + _completed.Task.IgnoreUnobservedExceptions(); } public void TrySetCanceled(CancellationToken cancellationToken) { _completed.TrySetCanceled(cancellationToken); + _completed.Task.IgnoreUnobservedExceptions(); } public Task> Task { get; } diff --git a/src/MassTransit/Configuration/ConcurrencyLimitConfigurationExtensions.cs b/src/MassTransit/Configuration/ConcurrencyLimitConfigurationExtensions.cs index 958f0bf4ddc..cf3146bbc0a 100644 --- a/src/MassTransit/Configuration/ConcurrencyLimitConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/ConcurrencyLimitConfigurationExtensions.cs @@ -58,7 +58,7 @@ public static void UseConcurrencyLimit(this IConsumePipeConfigurator configurato managementEndpointConfigurator.Instance(observer.Limiter, x => { x.UseConcurrentMessageLimit(1); - x.Message(m => m.UseRetry(r => r.None())); + x.Message(m => m.UseMessageRetry(r => r.None())); }); } } diff --git a/src/MassTransit/Configuration/ConcurrentMessageLimitExtensions.cs b/src/MassTransit/Configuration/ConcurrentMessageLimitExtensions.cs index 16efe8bb2a2..b157805e9a6 100644 --- a/src/MassTransit/Configuration/ConcurrentMessageLimitExtensions.cs +++ b/src/MassTransit/Configuration/ConcurrentMessageLimitExtensions.cs @@ -46,7 +46,7 @@ public static void UseConcurrentMessageLimit(this IConsumerConfigurat managementEndpointConfigurator.Instance(observer.Limiter, x => { x.UseConcurrentMessageLimit(1); - x.Message(m => m.UseRetry(r => r.None())); + x.Message(m => m.UseMessageRetry(r => r.None())); }); } @@ -85,7 +85,7 @@ public static void UseConcurrentMessageLimit(this ISagaConfigurator { x.UseConcurrentMessageLimit(1); - x.Message(m => m.UseRetry(r => r.None())); + x.Message(m => m.UseMessageRetry(r => r.None())); }); } @@ -124,7 +124,7 @@ public static void UseConcurrentMessageLimit(this IHandlerConfigurator managementEndpointConfigurator.Instance(observer.Limiter, x => { x.UseConcurrentMessageLimit(1); - x.Message(m => m.UseRetry(r => r.None())); + x.Message(m => m.UseMessageRetry(r => r.None())); }); } @@ -169,7 +169,7 @@ public static void UseConcurrentMessageLimit(this IPipeConfigurator { x.UseConcurrentMessageLimit(1); - x.Message(m => m.UseRetry(r => r.None())); + x.Message(m => m.UseMessageRetry(r => r.None())); }); } } diff --git a/src/MassTransit/Configuration/Configuration/BaseHostConfiguration.cs b/src/MassTransit/Configuration/Configuration/BaseHostConfiguration.cs index 42c87552e3b..87341cb62f4 100644 --- a/src/MassTransit/Configuration/Configuration/BaseHostConfiguration.cs +++ b/src/MassTransit/Configuration/Configuration/BaseHostConfiguration.cs @@ -87,6 +87,7 @@ public virtual IEnumerable Validate() public abstract IRetryPolicy ReceiveTransportRetryPolicy { get; } public virtual IRetryPolicy SendTransportRetryPolicy => ReceiveTransportRetryPolicy; + public TimeSpan? ConsumerStopTimeout { get; set; } public abstract IReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(string queueName, Action? configure); diff --git a/src/MassTransit/Configuration/Configuration/BindPipeSpecification.cs b/src/MassTransit/Configuration/Configuration/BindPipeSpecification.cs index 28bf12a044d..03f5f6e0312 100644 --- a/src/MassTransit/Configuration/Configuration/BindPipeSpecification.cs +++ b/src/MassTransit/Configuration/Configuration/BindPipeSpecification.cs @@ -65,9 +65,8 @@ BindContext ContextProvider(BindContext input, TLe return context as BindContext ?? new BindContextProxy(context, input.Right); } - _configurator.AddPipeSpecification(new SplitFilterPipeSpecification, TLeft>(specification, - ContextProvider, - context => context.Left)); + _configurator.AddPipeSpecification(new PipeConfigurator>.SplitFilterPipeSpecification(specification, + ContextProvider, context => context.Left)); } } } diff --git a/src/MassTransit/Configuration/Configuration/ConcurrencyLimit/ConcurrencyLimitConfigurationObserver.cs b/src/MassTransit/Configuration/Configuration/ConcurrencyLimit/ConcurrencyLimitConfigurationObserver.cs index ef5d38c0922..21fb07ca594 100644 --- a/src/MassTransit/Configuration/Configuration/ConcurrencyLimit/ConcurrencyLimitConfigurationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/ConcurrencyLimit/ConcurrencyLimitConfigurationObserver.cs @@ -27,5 +27,17 @@ public void MessageConfigured(IConsumePipeConfigurator configurator) configurator.AddPipeSpecification(specification); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Configuration/Configuration/ConsumeMessageFilterConfigurator.cs b/src/MassTransit/Configuration/Configuration/ConsumeMessageFilterConfigurator.cs index c7fa057cca0..69f9827214d 100644 --- a/src/MassTransit/Configuration/Configuration/ConsumeMessageFilterConfigurator.cs +++ b/src/MassTransit/Configuration/Configuration/ConsumeMessageFilterConfigurator.cs @@ -2,6 +2,7 @@ namespace MassTransit.Configuration { using System; using System.Linq; + using Internals; public class ConsumeMessageFilterConfigurator : @@ -19,6 +20,11 @@ public void Include(params Type[] messageTypes) Filter.Includes += message => Match(message, messageTypes); } + public void Include(Func filter) + { + Filter.Includes += context => context.GetType().ClosesType(typeof(ConsumeContext<>), out Type[] types) && filter(types[0]); + } + public void Include() where T : class { @@ -36,6 +42,11 @@ public void Exclude(params Type[] messageTypes) Filter.Excludes += message => Match(message, messageTypes); } + public void Exclude(Func filter) + { + Filter.Excludes += context => context.GetType().ClosesType(typeof(ConsumeContext<>), out Type[] types) && filter(types[0]); + } + public void Exclude() where T : class { diff --git a/src/MassTransit/Configuration/Configuration/ConsumePipeSpecification.cs b/src/MassTransit/Configuration/Configuration/ConsumePipeSpecification.cs index 007b1dd0fbc..74e2f44e496 100644 --- a/src/MassTransit/Configuration/Configuration/ConsumePipeSpecification.cs +++ b/src/MassTransit/Configuration/Configuration/ConsumePipeSpecification.cs @@ -183,7 +183,7 @@ public ConnectHandle ConnectConsumePipeSpecificationObserver(IConsumePipeSpecifi public IConsumePipe BuildConsumePipe() { - var filter = new DynamicFilter(new ConsumeContextConverterFactory(), GetRequestId); + var filter = new ConsumeContextMessageTypeFilter(); IBuildPipeConfigurator configurator = new PipeConfigurator(); @@ -208,11 +208,6 @@ public IConsumePipeSpecification CreateConsumePipeSpecification() return specification; } - static Guid GetRequestId(ConsumeContext context) - { - return context.RequestId ?? Guid.Empty; - } - IMessageConsumePipeSpecification CreateMessageSpecification(Type type) where T : class { diff --git a/src/MassTransit/Configuration/Configuration/DispatchPipeSpecification.cs b/src/MassTransit/Configuration/Configuration/DispatchPipeSpecification.cs index 833f0e2f899..8bc70968f65 100644 --- a/src/MassTransit/Configuration/Configuration/DispatchPipeSpecification.cs +++ b/src/MassTransit/Configuration/Configuration/DispatchPipeSpecification.cs @@ -12,7 +12,7 @@ public class DispatchPipeSpecification : where TInput : class, PipeContext { readonly IPipeContextConverterFactory _pipeContextConverterFactory; - readonly IList _specifications; + readonly List _specifications; public DispatchPipeSpecification(IPipeContextConverterFactory pipeContextConverterFactory) { diff --git a/src/MassTransit/Configuration/Configuration/IHostConfiguration.cs b/src/MassTransit/Configuration/Configuration/IHostConfiguration.cs index 1b7f58c3a54..e247aeb8484 100644 --- a/src/MassTransit/Configuration/Configuration/IHostConfiguration.cs +++ b/src/MassTransit/Configuration/Configuration/IHostConfiguration.cs @@ -37,8 +37,11 @@ public interface IHostConfiguration : IBusTopology Topology { get; } IRetryPolicy ReceiveTransportRetryPolicy { get; } + IRetryPolicy SendTransportRetryPolicy { get; } + TimeSpan? ConsumerStopTimeout { get; set; } + /// /// Create a receive endpoint configuration /// diff --git a/src/MassTransit/Configuration/Configuration/IReceiveEndpointConfiguration.cs b/src/MassTransit/Configuration/Configuration/IReceiveEndpointConfiguration.cs index b1f66aed136..a12fb0fbd6d 100644 --- a/src/MassTransit/Configuration/Configuration/IReceiveEndpointConfiguration.cs +++ b/src/MassTransit/Configuration/Configuration/IReceiveEndpointConfiguration.cs @@ -8,6 +8,7 @@ public interface IReceiveEndpointConfiguration : IEndpointConfiguration, + IReceiveEndpointObserverConnector, IReceiveEndpointDependentConnector { IConsumePipe ConsumePipe { get; } diff --git a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryCompensateContextOutboxSpecification.cs b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryCompensateContextOutboxSpecification.cs index 8a6061374a0..d10fc85b0e3 100644 --- a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryCompensateContextOutboxSpecification.cs +++ b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryCompensateContextOutboxSpecification.cs @@ -1,5 +1,6 @@ namespace MassTransit.Configuration { + using System; using System.Collections.Generic; using Middleware; using Middleware.InMemoryOutbox; @@ -10,12 +11,25 @@ public class InMemoryCompensateContextOutboxSpecification : IOutboxConfigurator where TArguments : class { + readonly ISetScopedConsumeContext _setter; + + public InMemoryCompensateContextOutboxSpecification(IRegistrationContext context) + : this(context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context))) + { + } + + public InMemoryCompensateContextOutboxSpecification(ISetScopedConsumeContext setter) + { + _setter = setter; + } + public bool ConcurrentMessageDelivery { get; set; } public void Apply(IPipeBuilder> builder) { builder.AddFilter( - new InMemoryOutboxFilter, InMemoryOutboxCompensateContext>(Factory, ConcurrentMessageDelivery)); + new InMemoryOutboxFilter, InMemoryOutboxCompensateContext>(_setter, Factory, + ConcurrentMessageDelivery)); } public IEnumerable Validate() diff --git a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryExecuteContextOutboxSpecification.cs b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryExecuteContextOutboxSpecification.cs index 6b5f823bb18..a5900caba6a 100644 --- a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryExecuteContextOutboxSpecification.cs +++ b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryExecuteContextOutboxSpecification.cs @@ -1,5 +1,6 @@ namespace MassTransit.Configuration { + using System; using System.Collections.Generic; using Middleware; using Middleware.InMemoryOutbox; @@ -10,12 +11,24 @@ public class InMemoryExecuteContextOutboxSpecification : IOutboxConfigurator where TArguments : class { + readonly ISetScopedConsumeContext _setter; + + public InMemoryExecuteContextOutboxSpecification(IRegistrationContext context) + : this(context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context))) + { + } + + public InMemoryExecuteContextOutboxSpecification(ISetScopedConsumeContext setter) + { + _setter = setter; + } + public bool ConcurrentMessageDelivery { get; set; } public void Apply(IPipeBuilder> builder) { builder.AddFilter( - new InMemoryOutboxFilter, InMemoryOutboxExecuteContext>(Factory, ConcurrentMessageDelivery)); + new InMemoryOutboxFilter, InMemoryOutboxExecuteContext>(_setter, Factory, ConcurrentMessageDelivery)); } public IEnumerable Validate() diff --git a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxConfigurationObserver.cs b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxConfigurationObserver.cs index 2b7d66b1d5f..6508db7ed4a 100644 --- a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxConfigurationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxConfigurationObserver.cs @@ -8,10 +8,18 @@ public class InMemoryOutboxConfigurationObserver : IMessageConfigurationObserver { readonly Action _configure; + readonly ISetScopedConsumeContext _setter; - public InMemoryOutboxConfigurationObserver(IConsumePipeConfigurator configurator, Action configure) + public InMemoryOutboxConfigurationObserver(IRegistrationContext context, IConsumePipeConfigurator configurator, Action configure) + : this(context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context)), configurator, configure) + { + } + + public InMemoryOutboxConfigurationObserver(ISetScopedConsumeContext setter, IConsumePipeConfigurator configurator, + Action configure) : base(configurator) { + _setter = setter; _configure = configure; Connect(this); @@ -20,7 +28,7 @@ public InMemoryOutboxConfigurationObserver(IConsumePipeConfigurator configurator public void MessageConfigured(IConsumePipeConfigurator configurator) where TMessage : class { - var specification = new InMemoryOutboxSpecification(); + var specification = new InMemoryOutboxSpecification(_setter); _configure?.Invoke(specification); @@ -29,7 +37,7 @@ public void MessageConfigured(IConsumePipeConfigurator configurator) public override void BatchConsumerConfigured(IConsumerMessageConfigurator> configurator) { - var specification = new InMemoryOutboxSpecification>(); + var specification = new InMemoryOutboxSpecification>(_setter); _configure?.Invoke(specification); @@ -38,7 +46,7 @@ public override void BatchConsumerConfigured(IConsumerMessa public override void ActivityConfigured(IExecuteActivityConfigurator configurator, Uri compensateAddress) { - var specification = new InMemoryExecuteContextOutboxSpecification(); + var specification = new InMemoryExecuteContextOutboxSpecification(_setter); _configure?.Invoke(specification); @@ -47,7 +55,7 @@ public override void ActivityConfigured(IExecuteActivityC public override void ExecuteActivityConfigured(IExecuteActivityConfigurator configurator) { - var specification = new InMemoryExecuteContextOutboxSpecification(); + var specification = new InMemoryExecuteContextOutboxSpecification(_setter); _configure?.Invoke(specification); @@ -56,11 +64,23 @@ public override void ExecuteActivityConfigured(IExecuteAc public override void CompensateActivityConfigured(ICompensateActivityConfigurator configurator) { - var specification = new InMemoryCompensateContextOutboxSpecification(); + var specification = new InMemoryCompensateContextOutboxSpecification(_setter); _configure?.Invoke(specification); configurator.Log(x => x.AddPipeSpecification(specification)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxConsumerConfigurationObserver.cs b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxConsumerConfigurationObserver.cs index 18520ff4f14..e7d5f8169d9 100644 --- a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxConsumerConfigurationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxConsumerConfigurationObserver.cs @@ -9,9 +9,18 @@ public class InMemoryOutboxConsumerConfigurationObserver : { readonly IConsumerConfigurator _configurator; readonly Action _configure; + readonly ISetScopedConsumeContext _setter; - public InMemoryOutboxConsumerConfigurationObserver(IConsumerConfigurator configurator, Action configure) + public InMemoryOutboxConsumerConfigurationObserver(IRegistrationContext context, IConsumerConfigurator configurator, + Action configure) + : this(context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context)), configurator, configure) { + } + + public InMemoryOutboxConsumerConfigurationObserver(ISetScopedConsumeContext setter, IConsumerConfigurator configurator, + Action configure) + { + _setter = setter; _configurator = configurator; _configure = configure; } @@ -22,7 +31,7 @@ void IConsumerConfigurationObserver.ConsumerConfigured(IConsumerConfigurator< void IConsumerConfigurationObserver.ConsumerMessageConfigured(IConsumerMessageConfigurator configurator) { - var specification = new InMemoryOutboxSpecification(); + var specification = new InMemoryOutboxSpecification(_setter); _configure?.Invoke(specification); diff --git a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxHandlerConfigurationObserver.cs b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxHandlerConfigurationObserver.cs index d928d101bbd..278715cda57 100644 --- a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxHandlerConfigurationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxHandlerConfigurationObserver.cs @@ -11,15 +11,22 @@ public class InMemoryOutboxHandlerConfigurationObserver : IHandlerConfigurationObserver { readonly Action _configure; + readonly ISetScopedConsumeContext _setter; - public InMemoryOutboxHandlerConfigurationObserver(Action configure) + public InMemoryOutboxHandlerConfigurationObserver(IRegistrationContext context, Action configure) + : this(context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context)), configure) { + } + + public InMemoryOutboxHandlerConfigurationObserver(ISetScopedConsumeContext setter, Action configure) + { + _setter = setter; _configure = configure; } void IHandlerConfigurationObserver.HandlerConfigured(IHandlerConfigurator configurator) { - var specification = new InMemoryOutboxSpecification(); + var specification = new InMemoryOutboxSpecification(_setter); _configure?.Invoke(specification); diff --git a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxSagaConfigurationObserver.cs b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxSagaConfigurationObserver.cs index fdd2ab0d0eb..c2d6d5725b3 100644 --- a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxSagaConfigurationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxSagaConfigurationObserver.cs @@ -9,9 +9,18 @@ public class InMemoryOutboxSagaConfigurationObserver : { readonly ISagaConfigurator _configurator; readonly Action _configure; + readonly ISetScopedConsumeContext _setter; - public InMemoryOutboxSagaConfigurationObserver(ISagaConfigurator configurator, Action configure) + public InMemoryOutboxSagaConfigurationObserver(IRegistrationContext context, ISagaConfigurator configurator, + Action configure) + : this(context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context)), configurator, configure) { + } + + public InMemoryOutboxSagaConfigurationObserver(ISetScopedConsumeContext setter, ISagaConfigurator configurator, + Action configure) + { + _setter = setter; _configurator = configurator; _configure = configure; } @@ -27,7 +36,7 @@ public void StateMachineSagaConfigured(ISagaConfigurator c void ISagaConfigurationObserver.SagaMessageConfigured(ISagaMessageConfigurator configurator) { - var specification = new InMemoryOutboxSpecification(); + var specification = new InMemoryOutboxSpecification(_setter); _configure?.Invoke(specification); diff --git a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxSpecification.cs b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxSpecification.cs index 04478424229..ed095e4d8c6 100644 --- a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxSpecification.cs +++ b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/InMemoryOutboxSpecification.cs @@ -1,5 +1,6 @@ namespace MassTransit.Configuration { + using System; using System.Collections.Generic; using Middleware; using Middleware.InMemoryOutbox; @@ -10,11 +11,23 @@ public class InMemoryOutboxSpecification : IOutboxConfigurator where T : class { + readonly ISetScopedConsumeContext _setter; + + public InMemoryOutboxSpecification(IRegistrationContext context) + : this(context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context))) + { + } + + public InMemoryOutboxSpecification(ISetScopedConsumeContext setter) + { + _setter = setter; + } + public bool ConcurrentMessageDelivery { get; set; } public void Apply(IPipeBuilder> builder) { - builder.AddFilter(new InMemoryOutboxFilter, InMemoryOutboxConsumeContext>(Factory, ConcurrentMessageDelivery)); + builder.AddFilter(new InMemoryOutboxFilter, InMemoryOutboxConsumeContext>(_setter, Factory, ConcurrentMessageDelivery)); } public IEnumerable Validate() diff --git a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/OutboxConsumePipeSpecificationObserver.cs b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/OutboxConsumePipeSpecificationObserver.cs index de72c3e43dc..756ea8e40a2 100644 --- a/src/MassTransit/Configuration/Configuration/InMemoryOutbox/OutboxConsumePipeSpecificationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/InMemoryOutbox/OutboxConsumePipeSpecificationObserver.cs @@ -1,6 +1,7 @@ namespace MassTransit.Configuration { using System; + using Courier.Contracts; using DependencyInjection; using JobService; using Metadata; @@ -11,21 +12,51 @@ namespace MassTransit.Configuration public class OutboxConsumePipeSpecificationObserver : IConsumerConfigurationObserver, ISagaConfigurationObserver, + IActivityConfigurationObserver, IOutboxOptionsConfigurator where TContext : class { readonly IReceiveEndpointConfigurator _configurator; - readonly IServiceProvider _provider; + readonly IServiceProvider _serviceProvider; + readonly ISetScopedConsumeContext _setter; - public OutboxConsumePipeSpecificationObserver(IReceiveEndpointConfigurator configurator, IServiceProvider provider) + public OutboxConsumePipeSpecificationObserver(IReceiveEndpointConfigurator configurator, IRegistrationContext context) + : this(configurator, context, context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context))) + { + } + + public OutboxConsumePipeSpecificationObserver(IReceiveEndpointConfigurator configurator, IServiceProvider serviceProvider, + ISetScopedConsumeContext setter) { _configurator = configurator; - _provider = provider; + _serviceProvider = serviceProvider; + _setter = setter; MessageDeliveryLimit = 1; MessageDeliveryTimeout = TimeSpan.FromSeconds(30); } + public void ActivityConfigured(IExecuteActivityConfigurator configurator, Uri compensateAddress) + where TActivity : class, IExecuteActivity + where TArguments : class + { + configurator.RoutingSlip(e => AddScopedFilter(e)); + } + + public void ExecuteActivityConfigured(IExecuteActivityConfigurator configurator) + where TActivity : class, IExecuteActivity + where TArguments : class + { + configurator.RoutingSlip(e => AddScopedFilter(e)); + } + + public void CompensateActivityConfigured(ICompensateActivityConfigurator configurator) + where TActivity : class, ICompensateActivity + where TLog : class + { + configurator.RoutingSlip(e => AddScopedFilter(e)); + } + public void ConsumerConfigured(IConsumerConfigurator configurator) where TConsumer : class { @@ -68,7 +99,7 @@ void AddScopedFilter(IPipeConfigurator> me where T : class where TMessage : class { - var scopeProvider = new ConsumeScopeProvider(_provider); + var scopeProvider = new ConsumeScopeProvider(_serviceProvider, _setter); var options = new OutboxConsumeOptions { diff --git a/src/MassTransit/Configuration/Configuration/MessageConsumePipeSpecification.cs b/src/MassTransit/Configuration/Configuration/MessageConsumePipeSpecification.cs index 1ad1b751776..75165cc9bb2 100644 --- a/src/MassTransit/Configuration/Configuration/MessageConsumePipeSpecification.cs +++ b/src/MassTransit/Configuration/Configuration/MessageConsumePipeSpecification.cs @@ -11,9 +11,9 @@ public class MessageConsumePipeSpecification : IMessageConsumePipeSpecification where TMessage : class { - readonly IList> _baseSpecifications; - readonly IList>> _parentMessageSpecifications; - readonly IList>> _specifications; + readonly List> _baseSpecifications; + readonly List>> _parentMessageSpecifications; + readonly List>> _specifications; public MessageConsumePipeSpecification() { @@ -63,8 +63,9 @@ public void Apply(ISpecificationPipeBuilder> builder) { for (var index = 0; index < _baseSpecifications.Count; index++) { - var split = new SplitFilterPipeSpecification, ConsumeContext>(_baseSpecifications[index], MergeContext, - FilterContext); + var split = new PipeConfigurator>.SplitFilterPipeSpecification(_baseSpecifications[index], + MergeContext, FilterContext); + split.Apply(builder); } } @@ -72,7 +73,7 @@ public void Apply(ISpecificationPipeBuilder> builder) public IPipe> BuildMessagePipe(IPipe> pipe) { - var pipeBuilder = new SpecificationPipeBuilder>(); + var pipeBuilder = new PipeConfigurator>.SpecificationPipeBuilder(); Apply(pipeBuilder); diff --git a/src/MassTransit/Configuration/Configuration/MessageTypeFilterConfigurator.cs b/src/MassTransit/Configuration/Configuration/MessageTypeFilterConfigurator.cs new file mode 100644 index 00000000000..257b6a1167c --- /dev/null +++ b/src/MassTransit/Configuration/Configuration/MessageTypeFilterConfigurator.cs @@ -0,0 +1,60 @@ +namespace MassTransit.Configuration +{ + using System; + using System.Linq; + + + public class MessageTypeFilterConfigurator : + IMessageTypeFilterConfigurator + { + public MessageTypeFilterConfigurator() + { + Filter = new CompositeFilter(); + } + + public CompositeFilter Filter { get; } + + public void Include(params Type[] messageTypes) + { + Filter.Includes += type => Match(type, messageTypes); + } + + public void Include(Func filter) + { + Filter.Includes += type => filter(type); + } + + public void Include() + where T : class + { + Filter.Includes += type => Match(type); + } + + public void Exclude(params Type[] messageTypes) + { + Filter.Excludes += type => Match(type, messageTypes); + } + + public void Exclude(Func filter) + { + Filter.Excludes += type => filter(type); + } + + public void Exclude() + where T : class + { + Filter.Excludes += type => Match(type); + } + + static bool Match(Type type, params Type[] messageTypes) + { + return messageTypes.Any(x => x == type); + } + + static bool Match(Type type) + where T : class + { + return type == typeof(T); + } + } +} diff --git a/src/MassTransit/Configuration/Configuration/Partition/PartitionMessageConfigurationObserver.cs b/src/MassTransit/Configuration/Configuration/Partition/PartitionMessageConfigurationObserver.cs index dfd06037a70..c4466cdc702 100644 --- a/src/MassTransit/Configuration/Configuration/Partition/PartitionMessageConfigurationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/Partition/PartitionMessageConfigurationObserver.cs @@ -28,5 +28,17 @@ public override void BatchConsumerConfigured(IConsumerMessa configurator.Message(m => m.AddPipeSpecification(specification)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Configuration/Configuration/ReceiveEndpointConfiguration.cs b/src/MassTransit/Configuration/Configuration/ReceiveEndpointConfiguration.cs index e029eb25fc9..37238237129 100644 --- a/src/MassTransit/Configuration/Configuration/ReceiveEndpointConfiguration.cs +++ b/src/MassTransit/Configuration/Configuration/ReceiveEndpointConfiguration.cs @@ -15,8 +15,8 @@ public abstract class ReceiveEndpointConfiguration : readonly Lazy _consumePipe; readonly HashSet _dependencies; readonly HashSet _dependents; - readonly IList _lateConfigurationKeys; - readonly IList _specifications; + readonly List _lateConfigurationKeys; + readonly List _specifications; IReceiveEndpoint _receiveEndpoint; protected ReceiveEndpointConfiguration(IHostConfiguration hostConfiguration, IEndpointConfiguration endpointConfiguration) @@ -53,11 +53,9 @@ public ConnectHandle ConnectReceiveEndpointObserver(IReceiveEndpointObserver obs return EndpointObservers.Connect(observer); } - public void AddDependent(IReceiveEndpointObserverConnector dependency) + public void AddDependent(IReceiveEndpointDependent dependent) { - var dependant = new ReceiveEndpointDependent(dependency); - - _dependents.Add(dependant); + _dependents.Add(dependent); } public override IEnumerable Validate() @@ -107,11 +105,13 @@ public void ConfigureMessageTopology(bool enabled = true) Topology.Consume.GetMessageTopology().ConfigureConsumeTopology = enabled; } - public void AddDependency(IReceiveEndpointDependentConnector connector) + public void ConfigureMessageTopology(Type messageType, bool enabled = true) { - var dependency = new ReceiveEndpointDependency(connector); - connector.AddDependent(this); + Topology.Consume.GetMessageTopology(messageType).ConfigureConsumeTopology = enabled; + } + public void AddDependency(IReceiveEndpointDependency dependency) + { _dependencies.Add(dependency); } @@ -141,89 +141,5 @@ protected virtual bool IsAlreadyConfigured() { return false; } - - - class ReceiveEndpointDependency : - IReceiveEndpointDependency, - IReceiveEndpointObserver - { - readonly ConnectHandle _handle; - readonly TaskCompletionSource _ready; - - public ReceiveEndpointDependency(IReceiveEndpointObserverConnector connector) - { - _ready = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - _handle = connector.ConnectReceiveEndpointObserver(this); - } - - public Task Ready => _ready.Task; - - Task IReceiveEndpointObserver.Ready(ReceiveEndpointReady ready) - { - _handle.Disconnect(); - - _ready.TrySetResult(ready); - - return Task.CompletedTask; - } - - Task IReceiveEndpointObserver.Stopping(ReceiveEndpointStopping stopping) - { - return Task.CompletedTask; - } - - Task IReceiveEndpointObserver.Completed(ReceiveEndpointCompleted completed) - { - return Task.CompletedTask; - } - - Task IReceiveEndpointObserver.Faulted(ReceiveEndpointFaulted faulted) - { - return Task.CompletedTask; - } - } - - - class ReceiveEndpointDependent : - IReceiveEndpointDependent, - IReceiveEndpointObserver - { - readonly TaskCompletionSource _completed; - readonly ConnectHandle _handle; - - public ReceiveEndpointDependent(IReceiveEndpointObserverConnector connector) - { - _completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - - _handle = connector.ConnectReceiveEndpointObserver(this); - } - - public Task Completed => _completed.Task; - - Task IReceiveEndpointObserver.Ready(ReceiveEndpointReady ready) - { - return Task.CompletedTask; - } - - Task IReceiveEndpointObserver.Stopping(ReceiveEndpointStopping stopping) - { - return Task.CompletedTask; - } - - Task IReceiveEndpointObserver.Completed(ReceiveEndpointCompleted completed) - { - _handle.Disconnect(); - - _completed.TrySetResult(completed); - - return Task.CompletedTask; - } - - Task IReceiveEndpointObserver.Faulted(ReceiveEndpointFaulted faulted) - { - return Task.CompletedTask; - } - } } } diff --git a/src/MassTransit/Configuration/Configuration/ReceiverConfiguration.cs b/src/MassTransit/Configuration/Configuration/ReceiverConfiguration.cs index 47169464849..9cc9a969fef 100644 --- a/src/MassTransit/Configuration/Configuration/ReceiverConfiguration.cs +++ b/src/MassTransit/Configuration/Configuration/ReceiverConfiguration.cs @@ -3,6 +3,7 @@ namespace MassTransit.Configuration using System; using System.Collections.Generic; using System.Linq; + using Transports; public class ReceiverConfiguration : @@ -10,7 +11,7 @@ public class ReceiverConfiguration : IReceiveEndpointConfigurator { readonly IReceiveEndpointConfiguration _configuration; - protected readonly IList Specifications; + protected readonly List Specifications; protected ReceiverConfiguration(IReceiveEndpointConfiguration endpointConfiguration) : base(endpointConfiguration) @@ -35,7 +36,7 @@ public bool PublishFaults set { } } - public void AddDependency(IReceiveEndpointDependentConnector dependent) + public void AddDependency(IReceiveEndpointDependency dependent) { } @@ -44,7 +45,7 @@ ConnectHandle IReceiveEndpointObserverConnector.ConnectReceiveEndpointObserver(I return _configuration.ConnectReceiveEndpointObserver(observer); } - public void AddDependent(IReceiveEndpointObserverConnector dependency) + public void AddDependent(IReceiveEndpointDependent dependent) { } @@ -53,6 +54,10 @@ public void ConfigureMessageTopology(bool enabled = true) { } + public void ConfigureMessageTopology(Type messageType, bool enabled = true) + { + } + public void AddEndpointSpecification(IReceiveEndpointSpecification specification) { Specifications.Add(specification); diff --git a/src/MassTransit/Configuration/Configuration/Redelivery/DelayedRedeliveryConfigurationObserver.cs b/src/MassTransit/Configuration/Configuration/Redelivery/DelayedRedeliveryConfigurationObserver.cs index 7e881fb66bc..6b607bd3ced 100644 --- a/src/MassTransit/Configuration/Configuration/Redelivery/DelayedRedeliveryConfigurationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/Redelivery/DelayedRedeliveryConfigurationObserver.cs @@ -19,5 +19,17 @@ protected override IRedeliveryPipeSpecification AddRedeliveryPipeSpecification configurator) public void AddPipeSpecification(IPipeSpecification specification) { - _configurator.AddPipeSpecification(new SplitFilterPipeSpecification(specification, - InputContext, Context)); + _configurator.AddPipeSpecification(new PipeConfigurator.SplitFilterPipeSpecification(specification, InputContext, Context)); } static TRescue Context(TRescue context) diff --git a/src/MassTransit/Configuration/Configuration/Retry/ConsumeContextRetryPipeSpecification.cs b/src/MassTransit/Configuration/Configuration/Retry/ConsumeContextRetryPipeSpecification.cs index 7ecb6d544d2..48e94fd587e 100644 --- a/src/MassTransit/Configuration/Configuration/Retry/ConsumeContextRetryPipeSpecification.cs +++ b/src/MassTransit/Configuration/Configuration/Retry/ConsumeContextRetryPipeSpecification.cs @@ -74,6 +74,9 @@ public ConsumeContextRetryPipeSpecification(Func builder) { + if (_policyFactory == null) + throw new ConfigurationException("The retry policy was not configured"); + var retryPolicy = _policyFactory(Filter); var contextRetryPolicy = new ConsumeContextRetryPolicy(retryPolicy, _cancellationToken, _contextFactory); diff --git a/src/MassTransit/Configuration/Configuration/Retry/MessageRetryConfigurationObserver.cs b/src/MassTransit/Configuration/Configuration/Retry/MessageRetryConfigurationObserver.cs index 6b9c6a66692..d4824ac52e4 100644 --- a/src/MassTransit/Configuration/Configuration/Retry/MessageRetryConfigurationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/Retry/MessageRetryConfigurationObserver.cs @@ -78,5 +78,17 @@ static RetryConsumeContext Factory(ConsumeContext { return new RetryConsumeContext(context, retryPolicy, retryContext); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Configuration/Configuration/SendMessageFilterConfigurator.cs b/src/MassTransit/Configuration/Configuration/SendMessageFilterConfigurator.cs index 95dfa21813c..340bc2287b2 100644 --- a/src/MassTransit/Configuration/Configuration/SendMessageFilterConfigurator.cs +++ b/src/MassTransit/Configuration/Configuration/SendMessageFilterConfigurator.cs @@ -2,6 +2,7 @@ namespace MassTransit.Configuration { using System; using System.Linq; + using Internals; public class SendMessageFilterConfigurator : @@ -14,12 +15,17 @@ public SendMessageFilterConfigurator() public CompositeFilter Filter { get; } - void IMessageFilterConfigurator.Include(params Type[] messageTypes) + void IMessageTypeFilterConfigurator.Include(params Type[] messageTypes) { Filter.Includes += message => Match(message, messageTypes); } - void IMessageFilterConfigurator.Include() + void IMessageTypeFilterConfigurator.Include(Func filter) + { + Filter.Includes += context => context.GetType().ClosesType(typeof(SendContext<>), out Type[] types) && filter(types[0]); + } + + void IMessageTypeFilterConfigurator.Include() { Filter.Includes += message => Match(message); } @@ -29,12 +35,17 @@ void IMessageFilterConfigurator.Include(Func filter) Filter.Includes += message => Match(message, filter); } - void IMessageFilterConfigurator.Exclude(params Type[] messageTypes) + void IMessageTypeFilterConfigurator.Exclude(params Type[] messageTypes) { Filter.Excludes += message => Match(message, messageTypes); } - void IMessageFilterConfigurator.Exclude() + void IMessageTypeFilterConfigurator.Exclude(Func filter) + { + Filter.Excludes += context => context.GetType().ClosesType(typeof(SendContext<>), out Type[] types) && filter(types[0]); + } + + void IMessageTypeFilterConfigurator.Exclude() { Filter.Excludes += message => Match(message); } diff --git a/src/MassTransit/Configuration/Configuration/Timeout/TimeoutConfigurationObserver.cs b/src/MassTransit/Configuration/Configuration/Timeout/TimeoutConfigurationObserver.cs index dcb5e8992ec..648047e363a 100644 --- a/src/MassTransit/Configuration/Configuration/Timeout/TimeoutConfigurationObserver.cs +++ b/src/MassTransit/Configuration/Configuration/Timeout/TimeoutConfigurationObserver.cs @@ -62,5 +62,17 @@ public override void CompensateActivityConfigured(ICompensateAc configurator.Log(x => x.AddPipeSpecification(specification)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Configuration/DefaultEndpointNameFormatter.cs b/src/MassTransit/Configuration/DefaultEndpointNameFormatter.cs index 47f73e8f752..63b1e372f16 100644 --- a/src/MassTransit/Configuration/DefaultEndpointNameFormatter.cs +++ b/src/MassTransit/Configuration/DefaultEndpointNameFormatter.cs @@ -21,9 +21,6 @@ public class DefaultEndpointNameFormatter : static readonly char[] _removeChars = { '.', '+' }; static readonly Regex _nonAlpha = new Regex("[^a-zA-Z0-9]", RegexOptions.Compiled | RegexOptions.Singleline); - readonly bool _includeNamespace; - readonly string _prefix; - readonly string _joinSeparator; /// /// Default endpoint name formatter. @@ -31,7 +28,7 @@ public class DefaultEndpointNameFormatter : /// If true, the namespace is included in the name public DefaultEndpointNameFormatter(bool includeNamespace) { - _includeNamespace = includeNamespace; + IncludeNamespace = includeNamespace; } /// @@ -41,8 +38,18 @@ public DefaultEndpointNameFormatter(bool includeNamespace) /// If true, the namespace is included in the name public DefaultEndpointNameFormatter(string prefix, bool includeNamespace) { - _prefix = prefix; - _includeNamespace = includeNamespace; + Prefix = prefix; + IncludeNamespace = includeNamespace; + } + + /// + /// Default endpoint name formatter with prefix. + /// + /// Prefix to start the name, should match the casing of the formatter (such as Dev or PreProd) + public DefaultEndpointNameFormatter(string prefix) + { + Prefix = prefix; + IncludeNamespace = false; } /// @@ -53,21 +60,82 @@ public DefaultEndpointNameFormatter(string prefix, bool includeNamespace) /// If true, the namespace is included in the name public DefaultEndpointNameFormatter(string joinSeparator, string prefix, bool includeNamespace) { - _prefix = prefix; - _includeNamespace = includeNamespace; - _joinSeparator = joinSeparator; + Prefix = prefix; + IncludeNamespace = includeNamespace; + JoinSeparator = joinSeparator; } protected DefaultEndpointNameFormatter() { - _includeNamespace = false; + IncludeNamespace = false; } + /// + /// Gets a value indicating whether the namespace is included in the name. + /// + protected bool IncludeNamespace { get; } + + /// + /// Gets the Prefix to start the name. + /// + protected string Prefix { get; } + + /// + /// Gets the join separator between the words + /// + protected string JoinSeparator { get; } + public static IEndpointNameFormatter Instance { get; } = new DefaultEndpointNameFormatter(); public string Separator { get; protected set; } = ""; - public string TemporaryEndpoint(string tag) + public virtual string TemporaryEndpoint(string tag) + { + return GetTemporaryQueueName(tag); + } + + public virtual string Consumer() + where T : class, IConsumer + { + return GetConsumerName(typeof(T)); + } + + public virtual string Message() + where T : class + { + return GetMessageName(typeof(T)); + } + + public virtual string Saga() + where T : class, ISaga + { + return GetSagaName(typeof(T)); + } + + public virtual string ExecuteActivity() + where T : class, IExecuteActivity + where TArguments : class + { + var activityName = GetActivityName(typeof(T), typeof(TArguments)); + + return $"{activityName}_execute"; + } + + public virtual string CompensateActivity() + where T : class, ICompensateActivity + where TLog : class + { + var activityName = GetActivityName(typeof(T), typeof(TLog)); + + return $"{activityName}_compensate"; + } + + public virtual string SanitizeName(string name) + { + return name; + } + + public static string GetTemporaryQueueName(string tag) { if (string.IsNullOrWhiteSpace(tag)) tag = "endpoint"; @@ -120,103 +188,106 @@ public string TemporaryEndpoint(string tag) return sb.ToString(); } - public string Consumer() - where T : class, IConsumer - { - return GetConsumerName(); - } - - public string Message() - where T : class - { - return GetMessageName(typeof(T)); - } - - public string Saga() - where T : class, ISaga - { - return GetSagaName(); - } - - public string ExecuteActivity() - where T : class, IExecuteActivity - where TArguments : class - { - var activityName = GetActivityName(); - - return $"{activityName}_execute"; - } - - public string CompensateActivity() - where T : class, ICompensateActivity - where TLog : class - { - var activityName = GetActivityName(); - - return $"{activityName}_compensate"; - } - - public virtual string SanitizeName(string name) - { - return name; - } - - string GetConsumerName() + /// + /// Gets the endpoint name for a consumer of the given type. + /// + /// The type of the consumer implementing + /// The fully formatted name as it will be provided via + protected virtual string GetConsumerName(Type type) { - if (typeof(T).IsGenericType && typeof(T).Name.Contains('`')) - return SanitizeName(FormatName(typeof(T).GetGenericArguments().Last())); + if (type.IsGenericType && type.Name.Contains('`')) + return SanitizeName(FormatName(type.GetGenericArguments().Last())); const string consumer = "Consumer"; - var consumerName = FormatName(typeof(T)); + var consumerName = FormatName(type); if (consumerName.EndsWith(consumer, StringComparison.InvariantCultureIgnoreCase)) + { consumerName = consumerName.Substring(0, consumerName.Length - consumer.Length); + if (string.IsNullOrWhiteSpace(consumerName)) + throw new ConfigurationException($"A consumer may not be named \"{consumer}\". Add a meaningful prefix when using ConfigureEndpoints."); + } + return SanitizeName(consumerName); } - string GetMessageName(Type type) + /// + /// Gets the endpoint name for a message of the given type. + /// + /// The type of the message + /// The fully formatted name as it will be provided via + protected virtual string GetMessageName(Type type) { if (type.IsGenericType && type.Name.Contains('`')) return SanitizeName(FormatName(type.GetGenericArguments().Last())); - var messageName = type.Name; + var messageName = FormatName(type); return SanitizeName(messageName); } - string GetSagaName() + /// + /// Gets the endpoint name for a saga of the given type. + /// + /// The type of the saga implementing + /// The fully formatted name as it will be provided via + protected virtual string GetSagaName(Type type) { const string saga = "Saga"; - var sagaName = FormatName(typeof(T)); + var sagaName = FormatName(type); if (sagaName.EndsWith(saga, StringComparison.InvariantCultureIgnoreCase)) + { sagaName = sagaName.Substring(0, sagaName.Length - saga.Length); + if (string.IsNullOrWhiteSpace(sagaName)) + throw new ConfigurationException($"A saga may not be named \"{saga}\". Add a meaningful prefix when using ConfigureEndpoints."); + } return SanitizeName(sagaName); } - string GetActivityName() + /// + /// Gets the name for an activity of the given type. + /// + /// + /// The activity name is used both for execution and compensation endpoint names. + /// + /// The type of the activity implementing + /// + /// For execution endpoints this is the activity arguments, for compensation this is the log type. + /// + /// The formatted activity name further used in and . + protected virtual string GetActivityName(Type activityType, Type argumentType) { const string activity = "Activity"; - var activityName = FormatName(typeof(T)); + var activityName = FormatName(activityType); if (activityName.EndsWith(activity, StringComparison.InvariantCultureIgnoreCase)) + { activityName = activityName.Substring(0, activityName.Length - activity.Length); + if (string.IsNullOrWhiteSpace(activityName)) + throw new ConfigurationException($"An activity may not be named \"{activity}\". Add a meaningful prefix when using ConfigureEndpoints."); + } return SanitizeName(activityName); } - string FormatName(Type type) + /// + /// Does a basic formatting of the type respecting settings like . + /// + /// The type to format. + /// A formatted type name, not yet sanitized via . + protected virtual string FormatName(Type type) { - var name = _includeNamespace - ? string.Join(_joinSeparator ?? "", TypeCache.GetShortName(type).Split(_removeChars)) + var name = IncludeNamespace + ? string.Join(JoinSeparator ?? "", TypeCache.GetShortName(type).Split(_removeChars)) : type.Name; - return string.IsNullOrWhiteSpace(_prefix) ? name : _prefix + name; + return string.IsNullOrWhiteSpace(Prefix) ? name : Prefix + name; } } } diff --git a/src/MassTransit/Configuration/DependencyInjection/ConfigureEndpointsProviderCallback.cs b/src/MassTransit/Configuration/DependencyInjection/ConfigureEndpointsProviderCallback.cs index ad3e56687a1..251f1feda80 100644 --- a/src/MassTransit/Configuration/DependencyInjection/ConfigureEndpointsProviderCallback.cs +++ b/src/MassTransit/Configuration/DependencyInjection/ConfigureEndpointsProviderCallback.cs @@ -1,7 +1,4 @@ namespace MassTransit { - using System; - - - public delegate void ConfigureEndpointsProviderCallback(IServiceProvider context, string queueName, IReceiveEndpointConfigurator configurator); + public delegate void ConfigureEndpointsProviderCallback(IRegistrationContext context, string queueName, IReceiveEndpointConfigurator configurator); } diff --git a/src/MassTransit/Configuration/DependencyInjection/DelayedMessageSchedulerRegistrationExtensions.cs b/src/MassTransit/Configuration/DependencyInjection/DelayedMessageSchedulerRegistrationExtensions.cs index 176838c6cb8..f8a43d55224 100644 --- a/src/MassTransit/Configuration/DependencyInjection/DelayedMessageSchedulerRegistrationExtensions.cs +++ b/src/MassTransit/Configuration/DependencyInjection/DelayedMessageSchedulerRegistrationExtensions.cs @@ -1,5 +1,6 @@ namespace MassTransit { + using DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -10,7 +11,7 @@ public static class DelayedMessageSchedulerRegistrationExtensions /// Add a to the container that uses transport message delay to schedule messages /// /// - public static void AddDelayedMessageScheduler(this IRegistrationConfigurator configurator) + public static void AddDelayedMessageScheduler(this IBusRegistrationConfigurator configurator) { configurator.TryAddScoped(provider => { @@ -19,5 +20,20 @@ public static void AddDelayedMessageScheduler(this IRegistrationConfigurator con return sendEndpointProvider.CreateDelayedMessageScheduler(bus.Topology); }); } + + /// + /// Add a to the container that uses transport message delay to schedule messages + /// + /// + public static void AddDelayedMessageScheduler(this IBusRegistrationConfigurator configurator) + where TBus : class, IBus + { + configurator.TryAddScoped(provider => + { + var bus = provider.GetRequiredService(); + var sendEndpointProvider = provider.GetRequiredService>().Value; + return Bind.Create(sendEndpointProvider.CreateDelayedMessageScheduler(bus.Topology)); + }); + } } } diff --git a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionExtensions.cs b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionExtensions.cs index a1f9397cf40..b33ceb2b88a 100644 --- a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionExtensions.cs +++ b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionExtensions.cs @@ -11,24 +11,54 @@ namespace MassTransit public static class DependencyInjectionExtensions { + /// + /// Creates a single scope for the receive endpoint that is used by all consumers, sagas, messages, etc. + /// + /// + /// + public static void UseServiceScope(this IConsumePipeConfigurator configurator, IRegistrationContext context) + { + var scopeProvider = new ConsumeScopeProvider(context); + var specification = new FilterPipeSpecification(new ScopeConsumeFilter(scopeProvider)); + + configurator.AddPrePipeSpecification(specification); + } + /// /// Creates a single scope for the receive endpoint that is used by all consumers, sagas, messages, etc. /// /// /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseServiceScope(this IConsumePipeConfigurator configurator, IServiceProvider serviceProvider) { - var scopeProvider = new ConsumeScopeProvider(serviceProvider); + var scopeProvider = new ConsumeScopeProvider(serviceProvider, LegacySetScopedConsumeContext.Instance); var specification = new FilterPipeSpecification(new ScopeConsumeFilter(scopeProvider)); configurator.AddPrePipeSpecification(specification); } + /// + /// Creates a scope for each message type, compatible with UseMessageRetry and UseInMemoryOutbox + /// + /// + /// + public static void UseMessageScope(this IConsumePipeConfigurator configurator, IRegistrationContext context) + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + var observer = new MessageScopeConfigurationObserver(configurator, context); + } + /// /// Creates a scope for each message type, compatible with UseMessageRetry and UseInMemoryOutbox /// /// /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseMessageScope(this IConsumePipeConfigurator configurator, IServiceProvider serviceProvider) { if (configurator == null) @@ -48,6 +78,8 @@ public static void RegisterInMemorySagaRepository(this IServiceCollection col where T : class, ISaga { collection.TryAddSingleton(new IndexedSagaDictionary()); + collection.RegisterLoadSagaRepository>(); + collection.RegisterQuerySagaRepository>(); collection.RegisterSagaRepository, InMemorySagaConsumeContextFactory, InMemorySagaRepositoryContextFactory>(); } @@ -83,7 +115,7 @@ public static IRequestClient CreateRequestClient(this IServiceProvider pro /// client that is not explicitly registered using AddRequestClient. /// /// - [Obsolete("This method is no longer required, the Generic Request Client is automatically registered")] + [Obsolete("Remove, the generic request client is automatically registered. Visit https://masstransit.io/obsolete for details.")] public static IServiceCollection AddGenericRequestClient(this IServiceCollection collection) { return collection; diff --git a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionFilterExtensions.cs b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionFilterExtensions.cs index e09253a70fb..3b4ba8dd832 100644 --- a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionFilterExtensions.cs +++ b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionFilterExtensions.cs @@ -11,15 +11,59 @@ public static class DependencyInjectionFilterExtensions /// /// /// Filter type - /// Configuration service provider - public static void UseConsumeFilter(this IConsumePipeConfigurator configurator, Type filterType, IServiceProvider provider) + /// Configuration registration context + public static void UseConsumeFilter(this IConsumePipeConfigurator configurator, Type filterType, IRegistrationContext context) + { + UseConsumeFilter(configurator, filterType, context, null); + } + + /// + /// Use scoped filter for + /// + /// + /// Filter type + /// Configuration registration context + /// Message type to which apply the filter + public static void UseConsumeFilter(this IConsumePipeConfigurator configurator, Type filterType, IRegistrationContext context, + Action configureMessageTypeFilter) + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + if (!filterType.IsGenericType || !filterType.IsGenericTypeDefinition) + throw new ConfigurationException("The scoped filter must be a generic type definition"); + + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + configureMessageTypeFilter?.Invoke(messageTypeFilterConfigurator); + + var observer = new ScopedConsumePipeSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); + + configurator.ConnectConsumerConfigurationObserver(observer); + configurator.ConnectSagaConfigurationObserver(observer); + } + + /// + /// Use scoped filter for + /// + /// + /// Configuration registration context + public static void UseConsumeFilter(this IConsumePipeConfigurator configurator, IRegistrationContext context) + where TFilter : class { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - if (provider == null) - throw new ArgumentNullException(nameof(provider)); + if (context == null) + throw new ArgumentNullException(nameof(context)); - var observer = new ScopedConsumePipeSpecificationObserver(filterType, provider); + var filterType = typeof(TFilter); + + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + messageTypeFilterConfigurator.Include(type => + typeof(IFilter<>).MakeGenericType(typeof(ConsumeContext<>).MakeGenericType(type)).IsAssignableFrom(filterType)); + + var observer = new ScopedConsumePipeSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); configurator.ConnectConsumerConfigurationObserver(observer); configurator.ConnectSagaConfigurationObserver(observer); @@ -30,32 +74,116 @@ public static void UseConsumeFilter(this IConsumePipeConfigurator configurator, /// /// /// Filter type - /// Configuration service provider - public static void UseSendFilter(this ISendPipelineConfigurator configurator, Type filterType, IServiceProvider provider) + /// Configuration registration context + public static void UseSendFilter(this ISendPipelineConfigurator configurator, Type filterType, IRegistrationContext context) + { + UseSendFilter(configurator, filterType, context, null); + } + + /// + /// Use scoped filter for + /// + /// + /// Filter type + /// Configuration registration context + /// Message type to which apply the filter + public static void UseSendFilter(this ISendPipelineConfigurator configurator, Type filterType, IRegistrationContext context, + Action configureMessageTypeFilter) { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - if (provider == null) - throw new ArgumentNullException(nameof(provider)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + if (!filterType.IsGenericType || !filterType.IsGenericTypeDefinition) + throw new ConfigurationException("The scoped filter must be a generic type definition"); - var observer = new ScopedFilterSpecificationObserver(filterType, provider); + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + configureMessageTypeFilter?.Invoke(messageTypeFilterConfigurator); + + var observer = new ScopedFilterSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); configurator.ConfigureSend(cfg => cfg.ConnectSendPipeSpecificationObserver(observer)); } + /// + /// Use scoped filter for + /// + /// + /// Configuration registration context + public static void UseSendFilter(this ISendPipelineConfigurator configurator, IRegistrationContext context) + where TFilter : class + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + var filterType = typeof(TFilter); + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + + messageTypeFilterConfigurator.Include(type => + typeof(IFilter<>).MakeGenericType(typeof(SendContext<>).MakeGenericType(type)).IsAssignableFrom(filterType)); + + var observer = new ScopedFilterSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); + configurator.ConfigureSend(cfg => cfg.ConnectSendPipeSpecificationObserver(observer)); + } + + /// + /// Use scoped filter for + /// + /// + /// Filter type + /// Configuration registration context + public static void UsePublishFilter(this IPublishPipelineConfigurator configurator, Type filterType, IRegistrationContext context) + { + UsePublishFilter(configurator, filterType, context, null); + } + /// /// Use scoped filter for /// /// /// Filter type - /// Configuration service provider - public static void UsePublishFilter(this IPublishPipelineConfigurator configurator, Type filterType, IServiceProvider provider) + /// Configuration registration context + /// Message type to which apply the filter + public static void UsePublishFilter(this IPublishPipelineConfigurator configurator, Type filterType, IRegistrationContext context, + Action configureMessageTypeFilter) { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - if (provider == null) - throw new ArgumentNullException(nameof(provider)); + if (context == null) + throw new ArgumentNullException(nameof(context)); - var observer = new ScopedFilterSpecificationObserver(filterType, provider); + if (!filterType.IsGenericType || !filterType.IsGenericTypeDefinition) + throw new ConfigurationException("The scoped filter must be a generic type definition"); + + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + configureMessageTypeFilter?.Invoke(messageTypeFilterConfigurator); + + var observer = new ScopedFilterSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); + configurator.ConfigurePublish(cfg => cfg.ConnectPublishPipeSpecificationObserver(observer)); + } + + /// + /// Use scoped filter for + /// + /// + /// Configuration registration context + public static void UsePublishFilter(this IPublishPipelineConfigurator configurator, IRegistrationContext context) + where TFilter : class + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + var filterType = typeof(TFilter); + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + + messageTypeFilterConfigurator.Include(type => + typeof(IFilter<>).MakeGenericType(typeof(PublishContext<>).MakeGenericType(type)).IsAssignableFrom(filterType)); + + var observer = new ScopedFilterSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); configurator.ConfigurePublish(cfg => cfg.ConnectPublishPipeSpecificationObserver(observer)); } @@ -64,15 +192,57 @@ public static void UsePublishFilter(this IPublishPipelineConfigurator configurat /// /// /// Filter type - /// Configuration service provider - public static void UseExecuteActivityFilter(this IConsumePipeConfigurator configurator, Type filterType, IServiceProvider provider) + /// Configuration registration context + public static void UseExecuteActivityFilter(this IConsumePipeConfigurator configurator, Type filterType, IRegistrationContext context) + { + UseExecuteActivityFilter(configurator, filterType, context, null); + } + + /// + /// Use scoped filter for + /// + /// + /// Filter type + /// Configuration registration context + /// Message type to which apply the filter + public static void UseExecuteActivityFilter(this IConsumePipeConfigurator configurator, Type filterType, IRegistrationContext context, + Action configureMessageTypeFilter) + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + if (!filterType.IsGenericType || !filterType.IsGenericTypeDefinition) + throw new ConfigurationException("The scoped filter must be a generic type definition"); + + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + configureMessageTypeFilter?.Invoke(messageTypeFilterConfigurator); + + var observer = new ScopedExecuteActivityPipeSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); + configurator.ConnectActivityConfigurationObserver(observer); + } + + /// + /// Use scoped filter for + /// + /// + /// Configuration registration context + public static void UseExecuteActivityFilter(this IConsumePipeConfigurator configurator, IRegistrationContext context) + where TFilter : class { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - if (provider == null) - throw new ArgumentNullException(nameof(provider)); + if (context == null) + throw new ArgumentNullException(nameof(context)); - var observer = new ScopedExecuteActivityPipeSpecificationObserver(filterType, provider); + var filterType = typeof(TFilter); + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + + messageTypeFilterConfigurator.Include(type => + typeof(IFilter<>).MakeGenericType(typeof(ExecuteContext<>).MakeGenericType(type)).IsAssignableFrom(filterType)); + + var observer = new ScopedExecuteActivityPipeSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); configurator.ConnectActivityConfigurationObserver(observer); } @@ -81,15 +251,57 @@ public static void UseExecuteActivityFilter(this IConsumePipeConfigurator config /// /// /// Filter type - /// Configuration service provider - public static void UseCompensateActivityFilter(this IConsumePipeConfigurator configurator, Type filterType, IServiceProvider provider) + /// Configuration registration context + public static void UseCompensateActivityFilter(this IConsumePipeConfigurator configurator, Type filterType, IRegistrationContext context) + { + UseCompensateActivityFilter(configurator, filterType, context, null); + } + + /// + /// Use scoped filter for + /// + /// + /// Filter type + /// Configuration registration context + /// Message type to which apply the filter + public static void UseCompensateActivityFilter(this IConsumePipeConfigurator configurator, Type filterType, IRegistrationContext context, + Action configureMessageTypeFilter) { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - if (provider == null) - throw new ArgumentNullException(nameof(provider)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + if (!filterType.IsGenericType || !filterType.IsGenericTypeDefinition) + throw new ConfigurationException("The scoped filter must be a generic type definition"); + + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + configureMessageTypeFilter?.Invoke(messageTypeFilterConfigurator); + + var observer = new ScopedCompensateActivityPipeSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); + configurator.ConnectActivityConfigurationObserver(observer); + } + + /// + /// Use scoped filter for + /// + /// + /// Configuration registration context + public static void UseCompensateActivityFilter(this IConsumePipeConfigurator configurator, IRegistrationContext context) + where TFilter : class + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + var filterType = typeof(TFilter); + var messageTypeFilterConfigurator = new MessageTypeFilterConfigurator(); + + messageTypeFilterConfigurator.Include(type => + typeof(IFilter<>).MakeGenericType(typeof(CompensateContext<>).MakeGenericType(type)).IsAssignableFrom(filterType)); - var observer = new ScopedCompensateActivityPipeSpecificationObserver(filterType, provider); + var observer = new ScopedCompensateActivityPipeSpecificationObserver(filterType, context, messageTypeFilterConfigurator.Filter); configurator.ConnectActivityConfigurationObserver(observer); } } diff --git a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionReceiveEndpointExtensions.cs b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionReceiveEndpointExtensions.cs index 339388c36c7..e2c364d9f74 100644 --- a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionReceiveEndpointExtensions.cs +++ b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionReceiveEndpointExtensions.cs @@ -8,6 +8,25 @@ namespace MassTransit public static class DependencyInjectionReceiveEndpointExtensions { + /// + /// Registers a consumer given the lifetime scope specified + /// + /// The consumer type + /// The service bus configurator + /// The LifetimeScope of the provider + /// + /// + public static void Consumer(this IReceiveEndpointConfigurator configurator, IRegistrationContext context, + Action> configure = null) + where T : class, IConsumer + { + IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(context); + + var consumerFactory = new ScopeConsumerFactory(scopeProvider); + + configurator.Consumer(consumerFactory, configure); + } + /// /// Registers a consumer given the lifetime scope specified /// @@ -16,17 +35,39 @@ public static class DependencyInjectionReceiveEndpointExtensions /// The LifetimeScope of the provider /// /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void Consumer(this IReceiveEndpointConfigurator configurator, IServiceProvider provider, Action> configure = null) where T : class, IConsumer { - IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(provider); + IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(provider, LegacySetScopedConsumeContext.Instance); var consumerFactory = new ScopeConsumerFactory(scopeProvider); configurator.Consumer(consumerFactory, configure); } + /// + /// Connect a consumer with a consumer factory method + /// + /// + /// + /// + /// + /// + /// + public static void Consumer(this IBatchConfigurator configurator, IRegistrationContext context, + Action>> configure = null) + where TConsumer : class, IConsumer> + where TMessage : class + { + IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(context); + + IConsumerFactory consumerFactory = new ScopeConsumerFactory(scopeProvider); + + configurator.Consumer(consumerFactory, configure); + } + /// /// Connect a consumer with a consumer factory method /// @@ -36,18 +77,41 @@ public static void Consumer(this IReceiveEndpointConfigurator configurator, I /// /// /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void Consumer(this IBatchConfigurator configurator, IServiceProvider provider, Action>> configure = null) where TConsumer : class, IConsumer> where TMessage : class { - IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(provider); + IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(provider, LegacySetScopedConsumeContext.Instance); IConsumerFactory consumerFactory = new ScopeConsumerFactory(scopeProvider); configurator.Consumer(consumerFactory, configure); } + /// + /// Connect a consumer to the bus/mediator + /// + /// + /// + /// + /// + /// + public static ConnectHandle ConnectConsumer(this IConsumePipeConnector connector, IRegistrationContext context, + params IPipeSpecification>[] pipeSpecifications) + where TConsumer : class, IConsumer + { + if (context == null) + throw new ArgumentNullException(nameof(context)); + + IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(context); + + IConsumerFactory consumerFactory = new ScopeConsumerFactory(scopeProvider); + + return connector.ConnectConsumer(consumerFactory, pipeSpecifications); + } + /// /// Connect a consumer to the bus/mediator /// @@ -56,6 +120,7 @@ public static void Consumer(this IBatchConfigurator /// /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static ConnectHandle ConnectConsumer(this IConsumePipeConnector connector, IServiceProvider provider, params IPipeSpecification>[] pipeSpecifications) where TConsumer : class, IConsumer @@ -63,13 +128,30 @@ public static ConnectHandle ConnectConsumer(this IConsumePipeConnecto if (provider == null) throw new ArgumentNullException(nameof(provider)); - IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(provider); + IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(provider, LegacySetScopedConsumeContext.Instance); IConsumerFactory consumerFactory = new ScopeConsumerFactory(scopeProvider); return connector.ConnectConsumer(consumerFactory, pipeSpecifications); } + /// + /// Registers a saga using the container that has the repository resolved from the container + /// + /// + /// + /// + /// + /// + public static void Saga(this IReceiveEndpointConfigurator configurator, IRegistrationContext context, + Action> configure = null) + where T : class, ISaga + { + ISagaRepository repository = new DependencyInjectionSagaRepository(context); + + configurator.Saga(repository, configure); + } + /// /// Registers a saga using the container that has the repository resolved from the container /// @@ -78,14 +160,33 @@ public static ConnectHandle ConnectConsumer(this IConsumePipeConnecto /// /// /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void Saga(this IReceiveEndpointConfigurator configurator, IServiceProvider provider, Action> configure = null) where T : class, ISaga { - var repository = provider.GetRequiredService>(); + ISagaRepository repository = new DependencyInjectionSagaRepository(provider, LegacySetScopedConsumeContext.Instance); configurator.Saga(repository, configure); } + /// + /// Subscribe a state machine saga to the endpoint + /// + /// The state machine instance type + /// + /// + /// The Container reference to resolve the repository + /// Optionally configure the saga + /// + public static void StateMachineSaga(this IReceiveEndpointConfigurator configurator, SagaStateMachine stateMachine, + IRegistrationContext context, Action> configure = null) + where TInstance : class, SagaStateMachineInstance + { + ISagaRepository repository = new DependencyInjectionSagaRepository(context); + + configurator.StateMachineSaga(stateMachine, repository, configure); + } + /// /// Subscribe a state machine saga to the endpoint /// @@ -95,11 +196,30 @@ public static void Saga(this IReceiveEndpointConfigurator configurator, IServ /// The Container reference to resolve the repository /// Optionally configure the saga /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void StateMachineSaga(this IReceiveEndpointConfigurator configurator, SagaStateMachine stateMachine, IServiceProvider serviceProvider, Action> configure = null) where TInstance : class, SagaStateMachineInstance { - var repository = serviceProvider.GetRequiredService>(); + ISagaRepository repository = new DependencyInjectionSagaRepository(serviceProvider, LegacySetScopedConsumeContext.Instance); + + configurator.StateMachineSaga(stateMachine, repository, configure); + } + + /// + /// Subscribe a state machine saga to the endpoint + /// + /// The state machine instance type + /// + /// The Container reference to resolve the repository + /// Optionally configure the saga + /// + public static void StateMachineSaga(this IReceiveEndpointConfigurator configurator, IRegistrationContext context, + Action> configure = null) + where TInstance : class, SagaStateMachineInstance + { + var stateMachine = context.GetRequiredService>(); + ISagaRepository repository = new DependencyInjectionSagaRepository(context); configurator.StateMachineSaga(stateMachine, repository, configure); } @@ -112,47 +232,87 @@ public static void StateMachineSaga(this IReceiveEndpointConfigurator /// The Container reference to resolve the repository /// Optionally configure the saga /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void StateMachineSaga(this IReceiveEndpointConfigurator configurator, IServiceProvider provider, Action> configure = null) where TInstance : class, SagaStateMachineInstance { var stateMachine = provider.GetRequiredService>(); - var repository = provider.GetRequiredService>(); + ISagaRepository repository = new DependencyInjectionSagaRepository(provider, LegacySetScopedConsumeContext.Instance); configurator.StateMachineSaga(stateMachine, repository, configure); } + public static void ExecuteActivityHost(this IReceiveEndpointConfigurator configurator, Uri compensateAddress, + IRegistrationContext context, Action> configure = null) + where TActivity : class, IExecuteActivity + where TArguments : class + { + var executeActivityScopeProvider = new ExecuteActivityScopeProvider(context); + + var factory = new ScopeExecuteActivityFactory(executeActivityScopeProvider); + + configurator.ExecuteActivityHost(compensateAddress, factory, configure); + } + + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void ExecuteActivityHost(this IReceiveEndpointConfigurator configurator, Uri compensateAddress, IServiceProvider provider, Action> configure = null) where TActivity : class, IExecuteActivity where TArguments : class { - var executeActivityScopeProvider = new ExecuteActivityScopeProvider(provider); + var executeActivityScopeProvider = new ExecuteActivityScopeProvider(provider, LegacySetScopedConsumeContext.Instance); var factory = new ScopeExecuteActivityFactory(executeActivityScopeProvider); configurator.ExecuteActivityHost(compensateAddress, factory, configure); } + public static void ExecuteActivityHost(this IReceiveEndpointConfigurator configurator, IRegistrationContext context, + Action> configure = null) + where TActivity : class, IExecuteActivity + where TArguments : class + { + var executeActivityScopeProvider = new ExecuteActivityScopeProvider(context); + + var factory = new ScopeExecuteActivityFactory(executeActivityScopeProvider); + + configurator.ExecuteActivityHost(factory, configure); + } + + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void ExecuteActivityHost(this IReceiveEndpointConfigurator configurator, IServiceProvider provider, Action> configure = null) where TActivity : class, IExecuteActivity where TArguments : class { - var executeActivityScopeProvider = new ExecuteActivityScopeProvider(provider); + var executeActivityScopeProvider = new ExecuteActivityScopeProvider(provider, LegacySetScopedConsumeContext.Instance); var factory = new ScopeExecuteActivityFactory(executeActivityScopeProvider); configurator.ExecuteActivityHost(factory, configure); } + public static void CompensateActivityHost(this IReceiveEndpointConfigurator configurator, IRegistrationContext context, + Action> configure = null) + where TActivity : class, ICompensateActivity + where TLog : class + { + var compensateActivityScopeProvider = new CompensateActivityScopeProvider(context); + + var factory = new ScopeCompensateActivityFactory(compensateActivityScopeProvider); + + configurator.CompensateActivityHost(factory, configure); + } + + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void CompensateActivityHost(this IReceiveEndpointConfigurator configurator, IServiceProvider provider, Action> configure = null) where TActivity : class, ICompensateActivity where TLog : class { - var compensateActivityScopeProvider = new CompensateActivityScopeProvider(provider); + var compensateActivityScopeProvider = new CompensateActivityScopeProvider(provider, LegacySetScopedConsumeContext.Instance); var factory = new ScopeCompensateActivityFactory(compensateActivityScopeProvider); diff --git a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionRegistrationExtensions.cs b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionRegistrationExtensions.cs index bccab575fde..b093181d076 100644 --- a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionRegistrationExtensions.cs +++ b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionRegistrationExtensions.cs @@ -34,6 +34,7 @@ public static IServiceCollection AddMassTransit(this IServiceCollection collecti } AddHostedService(collection); + AddInstrumentation(collection); var configurator = new ServiceCollectionBusConfigurator(collection); @@ -50,20 +51,34 @@ public static IServiceCollection AddMassTransit(this IServiceCollection collecti /// /// /// - public static IServiceCollection AddMediator(this IServiceCollection collection, Action configure = null) + /// + public static IServiceCollection AddMediator(this IServiceCollection collection, Uri baseAddress, Action configure = null) { if (collection.Any(d => d.ServiceType == typeof(IMediator))) throw new ConfigurationException("AddMediator() was already called and may only be called once per container."); - var configurator = new ServiceCollectionMediatorConfigurator(collection); + var configurator = new ServiceCollectionMediatorConfigurator(collection, baseAddress); configure?.Invoke(configurator); + AddInstrumentation(collection); + configurator.Complete(); return collection; } + /// + /// Adds the MassTransit Mediator to the , and allows consumers, sagas, and activities (which are not supported + /// by the Mediator) to be configured. + /// + /// + /// + public static IServiceCollection AddMediator(this IServiceCollection collection, Action configure = null) + { + return AddMediator(collection, null, configure); + } + /// /// Configure a MassTransit bus instance, using the specified bus type, which must inherit directly from . /// A type that implements is required, specified by the parameter. @@ -85,6 +100,7 @@ public static IServiceCollection AddMassTransit(this IServic } AddHostedService(collection); + AddInstrumentation(collection); var configurator = new ServiceCollectionBusConfigurator(collection); @@ -109,6 +125,7 @@ public static IServiceCollection AddMassTransit(this IServiceCollection co throw new ArgumentNullException(nameof(configure)); AddHostedService(collection); + AddInstrumentation(collection); var doIt = new Callback(collection, configure); @@ -155,6 +172,12 @@ public static void ReplaceScoped(this IServiceCollect services.Replace(new ServiceDescriptor(typeof(TService), typeof(TImplementation), ServiceLifetime.Scoped)); } + static void AddInstrumentation(IServiceCollection collection) + { + collection.AddOptions(); + collection.AddSingleton, ConfigureDefaultInstrumentationOptions>(); + } + static void AddHostedService(IServiceCollection collection) { collection.AddOptions(); @@ -162,6 +185,7 @@ static void AddHostedService(IServiceCollection collection) collection.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureBusHealthCheckServiceOptions>()); collection.AddOptions(); + collection.TryAddSingleton, ValidateMassTransitHostOptions>(); collection.TryAddEnumerable(ServiceDescriptor.Singleton()); } @@ -175,13 +199,15 @@ internal static void RemoveMassTransit(this IServiceCollection collection) collection.RemoveAll(); - collection.RemoveAll(); + collection.RemoveAll(); + collection.RemoveAll>(); + collection.RemoveAll>(); collection.RemoveAll>(); collection.RemoveAll(); collection.RemoveAll(); collection.RemoveAll(); - collection.RemoveAll(); collection.RemoveAll(typeof(IRequestClient<>)); + collection.RemoveAll(); collection.RemoveAll>(); collection.RemoveAll(); diff --git a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionTestingExtensions.cs b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionTestingExtensions.cs index a8bcac503bd..b322c53e283 100644 --- a/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionTestingExtensions.cs +++ b/src/MassTransit/Configuration/DependencyInjection/DependencyInjectionTestingExtensions.cs @@ -7,6 +7,7 @@ namespace MassTransit using System.IO; using System.Linq; using Configuration; + using DependencyInjection; using DependencyInjection.Registration; using DependencyInjection.Testing; using Internals; @@ -61,6 +62,8 @@ public static IServiceCollection AddMassTransitTestHarness(this IServiceCollecti options.WaitUntilStarted = true; }); + services.TryAddSingleton, ValidateMassTransitHostOptions>(); + // If the bus was already configured, well, let's use it and any existing registrations if (services.Any(d => d.ServiceType == typeof(IBus))) { @@ -196,6 +199,7 @@ static void RegisterSagaTestHarnesses(IServiceCollection services) /// /// Add the In-Memory test harness to the container, and configure it using the callback specified. /// + [Obsolete("Use AddMassTransitTestHarness instead. Visit https://masstransit.io/obsolete for details.")] public static IServiceCollection AddMassTransitInMemoryTestHarness(this IServiceCollection services, Action? configure = null) { @@ -293,16 +297,16 @@ public static void AddSagaStateMachineContainerTestHarness(thi services.TryAddSingleton>(); services.TryAddSingleton>(provider => provider.GetRequiredService>()); - #pragma warning disable CS0618 + #pragma warning disable CS0618 services.TryAddSingleton>(provider => - #pragma warning restore CS0618 + #pragma warning restore CS0618 provider.GetRequiredService>()); } /// /// Add a consumer test harness for the specified consumer to the container /// - [Obsolete("Consider migrating to AddMassTransitTestHarness, which does not require this extra configuration")] + [Obsolete("Use AddMassTransitTestHarness instead. Visit https://masstransit.io/obsolete for details.")] public static void AddConsumerTestHarness(this IBusRegistrationConfigurator configurator) where T : class, IConsumer { @@ -315,7 +319,7 @@ public static void AddConsumerTestHarness(this IBusRegistrationConfigurator c /// Add a saga test harness for the specified saga to the container. The saga must be added separately, including /// a valid saga repository. /// - [Obsolete("Consider migrating to AddMassTransitTestHarness, which does not require this extra configuration")] + [Obsolete("Use AddMassTransitTestHarness instead. Visit https://masstransit.io/obsolete for details.")] public static void AddSagaTestHarness(this IBusRegistrationConfigurator configurator) where T : class, ISaga { @@ -328,7 +332,7 @@ public static void AddSagaTestHarness(this IBusRegistrationConfigurator confi /// Add a saga test harness for the specified saga to the container. The saga must be added separately, including /// a valid saga repository. /// - [Obsolete("Consider migrating to AddMassTransitTestHarness, which does not require this extra configuration")] + [Obsolete("Use AddMassTransitTestHarness instead. Visit https://masstransit.io/obsolete for details.")] public static void AddSagaStateMachineTestHarness(this IBusRegistrationConfigurator configurator) where TSaga : class, SagaStateMachineInstance where TStateMachine : SagaStateMachine @@ -340,9 +344,9 @@ public static void AddSagaStateMachineTestHarness(this IBu configurator.AddSingleton>(); configurator.AddSingleton>(provider => provider.GetRequiredService>()); - #pragma warning disable CS0618 + #pragma warning disable CS0618 configurator.AddSingleton>(provider => - #pragma warning restore CS0618 + #pragma warning restore CS0618 provider.GetRequiredService>()); } diff --git a/src/MassTransit/Configuration/DependencyInjection/HandlerRegistrationConfiguratorExtensions.cs b/src/MassTransit/Configuration/DependencyInjection/HandlerRegistrationConfiguratorExtensions.cs index e2c42c053d0..967d252fb63 100644 --- a/src/MassTransit/Configuration/DependencyInjection/HandlerRegistrationConfiguratorExtensions.cs +++ b/src/MassTransit/Configuration/DependencyInjection/HandlerRegistrationConfiguratorExtensions.cs @@ -8,6 +8,22 @@ namespace MassTransit public static class HandlerRegistrationConfiguratorExtensions { + /// + /// Adds an empty message handler, which consumes the messages and does nothing else. Useful with the test harness to ensure + /// that produced messages are consumed, which can then be asserted in unit tests. + /// + /// + public static IConsumerRegistrationConfigurator AddHandler(this IRegistrationConfigurator configurator) + where T : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + configurator.TryAddSingleton(new MessageHandlerMethod((ConsumeContext context) => Task.CompletedTask)); + + return configurator.AddConsumer, MessageHandlerConsumerDefinition, T>>(); + } + /// /// Adds a method handler, using the first parameter to determine the message type /// @@ -29,17 +45,15 @@ public static IConsumerRegistrationConfigurator AddHandler(this IRegistration /// /// /// An asynchronous method to handle the message - public static IConsumerRegistrationConfigurator AddHandler(this IRegistrationConfigurator configurator, - Func, Task> handler) + public static IConsumerRegistrationConfigurator AddHandler(this IRegistrationConfigurator configurator, Func handler) where T : class - where TResponse : class { if (!MessageTypeCache.IsValidMessageType) throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); - configurator.TryAddSingleton(new RequestHandlerMethod(handler)); + configurator.TryAddSingleton(new MessageHandlerMethod(handler)); - return configurator.AddConsumer, MessageHandlerConsumerDefinition, T>>(); + return configurator.AddConsumer, MessageHandlerConsumerDefinition, T>>(); } /// @@ -47,15 +61,17 @@ public static IConsumerRegistrationConfigurator AddHandler(this IR /// /// /// An asynchronous method to handle the message - public static IConsumerRegistrationConfigurator AddHandler(this IRegistrationConfigurator configurator, Func handler) + public static IConsumerRegistrationConfigurator AddHandler(this IRegistrationConfigurator configurator, + Func, Task> handler) where T : class + where TResponse : class { if (!MessageTypeCache.IsValidMessageType) throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); - configurator.TryAddSingleton(new MessageHandlerMethod(handler)); + configurator.TryAddSingleton(new RequestHandlerMethod(handler)); - return configurator.AddConsumer, MessageHandlerConsumerDefinition, T>>(); + return configurator.AddConsumer, MessageHandlerConsumerDefinition, T>>(); } /// diff --git a/src/MassTransit/Configuration/DependencyInjection/IBusRegistrationConfigurator.cs b/src/MassTransit/Configuration/DependencyInjection/IBusRegistrationConfigurator.cs index 224d3b60d57..5d831ede4a1 100644 --- a/src/MassTransit/Configuration/DependencyInjection/IBusRegistrationConfigurator.cs +++ b/src/MassTransit/Configuration/DependencyInjection/IBusRegistrationConfigurator.cs @@ -16,10 +16,11 @@ public interface IBusRegistrationConfigurator : /// This method is being deprecated. Use the transport-specific UsingRabbitMq, UsingActiveMq, etc. methods instead. /// /// + [Obsolete("Use 'Using[TransportName]' instead. Visit https://masstransit.io/obsolete for details.")] void AddBus(Func busFactory); /// - /// Sets the bus factory. This is used by the transport extension methods (such as UsingRabbitMq, Using ActiveMq, etc.) to + /// Sets the bus factory. This is used by the transport extension methods (such as UsingRabbitMq, UsingActiveMq, etc.) to /// specify the bus factory. The extension method approach is preferred (since v7) over the AddBus method. /// /// @@ -32,6 +33,26 @@ void SetBusFactory(T busFactory) /// /// void AddRider(Action configure); + + /// + /// Adds a method that is called for each receive endpoint when it is configured, allowing additional + /// configuration to be specified. Multiple callbacks may be configured. + /// + /// Callback invoked for each receive endpoint + void AddConfigureEndpointsCallback(ConfigureEndpointsCallback callback); + + /// + /// Adds a method that is called for each receive endpoint when it is configured, allowing additional + /// configuration to be specified. Multiple callbacks may be configured. + /// + /// Callback invoked for each receive endpoint + void AddConfigureEndpointsCallback(ConfigureEndpointsProviderCallback callback); + + /// + /// Override the default request client factory to enable advanced scenarios. This is typically not called by end-users. + /// + /// + void SetRequestClientFactory(Func clientFactory); } diff --git a/src/MassTransit/Configuration/DependencyInjection/IHealthCheckOptionsConfigurator.cs b/src/MassTransit/Configuration/DependencyInjection/IHealthCheckOptionsConfigurator.cs index 9c2c9c46bd9..2adfb294bb8 100644 --- a/src/MassTransit/Configuration/DependencyInjection/IHealthCheckOptionsConfigurator.cs +++ b/src/MassTransit/Configuration/DependencyInjection/IHealthCheckOptionsConfigurator.cs @@ -1,5 +1,6 @@ namespace MassTransit { + using System; using System.Collections.Generic; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -15,8 +16,15 @@ public interface IHealthCheckOptionsConfigurator /// The that should be reported when the health check fails. /// If null then the default status of will be reported. /// + [Obsolete("Use MinimalFailureStatus instead.", true)] public HealthStatus? FailureStatus { set; } + /// + /// The minimal that should be reported when the health check fails. + /// If null then all statuses from to will be reported depending on app health. + /// + public HealthStatus? MinimalFailureStatus { set; } + /// /// A list of tags that can be used to filter sets of health checks /// diff --git a/src/MassTransit/Configuration/DependencyInjection/IJobSagaRegistrationConfigurator.cs b/src/MassTransit/Configuration/DependencyInjection/IJobSagaRegistrationConfigurator.cs new file mode 100644 index 00000000000..9050dcd949a --- /dev/null +++ b/src/MassTransit/Configuration/DependencyInjection/IJobSagaRegistrationConfigurator.cs @@ -0,0 +1,44 @@ +namespace MassTransit +{ + using System; + using Configuration; + + + public interface IJobSagaRegistrationConfigurator + { + /// + /// Configure all three saga endpoints (using the same configuration) + /// + /// + /// + IJobSagaRegistrationConfigurator Endpoints(Action configure); + + /// + /// Configure the JobAttemptSaga endpoint + /// + /// + /// + IJobSagaRegistrationConfigurator JobAttemptEndpoint(Action configure); + + /// + /// Configure the JobSaga endpoint + /// + /// + /// + IJobSagaRegistrationConfigurator JobEndpoint(Action configure); + + /// + /// Configure the JobTypeSaga endpoint + /// + /// + /// + IJobSagaRegistrationConfigurator JobTypeEndpoint(Action configure); + + /// + /// Internally used by the saga repositories to register as the saga repository for the job sagas + /// + /// + /// + IJobSagaRegistrationConfigurator UseRepositoryRegistrationProvider(ISagaRepositoryRegistrationProvider registrationProvider); + } +} diff --git a/src/MassTransit/Configuration/DependencyInjection/IJobServiceRegistrationConfigurator.cs b/src/MassTransit/Configuration/DependencyInjection/IJobServiceRegistrationConfigurator.cs new file mode 100644 index 00000000000..4c711ab074b --- /dev/null +++ b/src/MassTransit/Configuration/DependencyInjection/IJobServiceRegistrationConfigurator.cs @@ -0,0 +1,21 @@ +namespace MassTransit +{ + using System; + + + public interface IJobServiceRegistrationConfigurator + { + /// + /// Configure the job service options + /// + /// + /// + IJobServiceRegistrationConfigurator Options(Action configure); + + /// + /// Configure the instance endpoint settings + /// + /// + void Endpoint(Action configure); + } +} diff --git a/src/MassTransit/Configuration/DependencyInjection/IRegistrationConfigurator.cs b/src/MassTransit/Configuration/DependencyInjection/IRegistrationConfigurator.cs index 065d54eb15c..1e2213ebf5e 100644 --- a/src/MassTransit/Configuration/DependencyInjection/IRegistrationConfigurator.cs +++ b/src/MassTransit/Configuration/DependencyInjection/IRegistrationConfigurator.cs @@ -13,7 +13,7 @@ public interface IRegistrationConfigurator : /// /// /// The consumer type - IConsumerRegistrationConfigurator AddConsumer(Action> configure = null) + IConsumerRegistrationConfigurator AddConsumer(Action> configure = null) where T : class, IConsumer; /// @@ -22,7 +22,8 @@ IConsumerRegistrationConfigurator AddConsumer(ActionThe consumer definition type /// /// The consumer type - IConsumerRegistrationConfigurator AddConsumer(Type consumerDefinitionType, Action> configure = null) + IConsumerRegistrationConfigurator AddConsumer(Type consumerDefinitionType, + Action> configure = null) where T : class, IConsumer; /// @@ -31,7 +32,7 @@ IConsumerRegistrationConfigurator AddConsumer(Type consumerDefinitionType, /// /// /// The saga type - ISagaRegistrationConfigurator AddSaga(Action> configure = null) + ISagaRegistrationConfigurator AddSaga(Action> configure = null) where T : class, ISaga; /// @@ -41,7 +42,7 @@ ISagaRegistrationConfigurator AddSaga(Action> configu /// The saga definition type /// /// The saga type - ISagaRegistrationConfigurator AddSaga(Type sagaDefinitionType, Action> configure = null) + ISagaRegistrationConfigurator AddSaga(Type sagaDefinitionType, Action> configure = null) where T : class, ISaga; /// @@ -51,7 +52,7 @@ ISagaRegistrationConfigurator AddSaga(Type sagaDefinitionType, Action /// /// - ISagaRegistrationConfigurator AddSagaStateMachine(Action> configure = null) + ISagaRegistrationConfigurator AddSagaStateMachine(Action> configure = null) where TStateMachine : class, SagaStateMachine where T : class, SagaStateMachineInstance; @@ -63,7 +64,8 @@ ISagaRegistrationConfigurator AddSagaStateMachine(Action /// /// - ISagaRegistrationConfigurator AddSagaStateMachine(Type sagaDefinitionType, Action> configure = null) + ISagaRegistrationConfigurator AddSagaStateMachine(Type sagaDefinitionType, + Action> configure = null) where TStateMachine : class, SagaStateMachine where T : class, SagaStateMachineInstance; @@ -74,7 +76,7 @@ ISagaRegistrationConfigurator AddSagaStateMachine(Type saga /// The activity type /// The argument type IExecuteActivityRegistrationConfigurator AddExecuteActivity( - Action> configure = null) + Action> configure = null) where TActivity : class, IExecuteActivity where TArguments : class; @@ -86,7 +88,7 @@ IExecuteActivityRegistrationConfigurator AddExecuteActivi /// The activity type /// The argument type IExecuteActivityRegistrationConfigurator AddExecuteActivity(Type executeActivityDefinitionType, - Action> configure = null) + Action> configure = null) where TActivity : class, IExecuteActivity where TArguments : class; @@ -99,8 +101,8 @@ IExecuteActivityRegistrationConfigurator AddExecuteActivi /// The argument type /// The log type IActivityRegistrationConfigurator AddActivity( - Action> configureExecute = null, - Action> configureCompensate = null) + Action> configureExecute = null, + Action> configureCompensate = null) where TActivity : class, IActivity where TLog : class where TArguments : class; @@ -115,8 +117,8 @@ IActivityRegistrationConfigurator AddActivityThe argument type /// The log type IActivityRegistrationConfigurator AddActivity(Type activityDefinitionType, - Action> configureExecute = null, - Action> configureCompensate = null) + Action> configureExecute = null, + Action> configureCompensate = null) where TActivity : class, IActivity where TLog : class where TArguments : class; @@ -129,7 +131,7 @@ IActivityRegistrationConfigurator AddActivityThe endpoint definition to add void AddEndpoint(Type endpointDefinition); - void AddEndpoint(IEndpointSettings> settings = null) + void AddEndpoint(IRegistration registration, IEndpointSettings> settings = null) where TDefinition : class, IEndpointDefinition where T : class; @@ -171,6 +173,22 @@ void AddRequestClient(Uri destinationAddress, RequestTimeout timeout = defaul /// The request timeout void AddRequestClient(Type requestType, Uri destinationAddress, RequestTimeout timeout = default); + /// + /// Sets the default request timeout for this bus instance, used by the client factory to create request clients + /// + /// + void SetDefaultRequestTimeout(RequestTimeout timeout); + + /// + /// Sets the default request timeout for this bus instance, used by the client factory to create request clients + /// + /// days + /// hours + /// minutes + /// seconds + /// milliseconds + void SetDefaultRequestTimeout(int? d = null, int? h = null, int? m = null, int? s = null, int? ms = null); + /// /// Set the default endpoint name formatter used for endpoint names /// @@ -200,19 +218,5 @@ ISagaRegistrationConfigurator AddSagaRepository() /// IFutureRegistrationConfigurator AddFuture(Type futureDefinitionType = null) where TFuture : class, SagaStateMachine; - - /// - /// Adds a method that is called for each receive endpoint when it is configured, allowing additional - /// configuration to be specified. - /// - /// Callback invoked for each receive endpoint - void AddConfigureEndpointsCallback(ConfigureEndpointsCallback callback); - - /// - /// Adds a method that is called for each receive endpoint when it is configured, allowing additional - /// configuration to be specified. - /// - /// Callback invoked for each receive endpoint - void AddConfigureEndpointsCallback(ConfigureEndpointsProviderCallback callback); } } diff --git a/src/MassTransit/Configuration/DependencyInjection/ISetScopedConsumeContext.cs b/src/MassTransit/Configuration/DependencyInjection/ISetScopedConsumeContext.cs new file mode 100644 index 00000000000..2a9fc00fc3f --- /dev/null +++ b/src/MassTransit/Configuration/DependencyInjection/ISetScopedConsumeContext.cs @@ -0,0 +1,11 @@ +namespace MassTransit +{ + using System; + using Microsoft.Extensions.DependencyInjection; + + + public interface ISetScopedConsumeContext + { + IDisposable PushContext(IServiceScope serviceProvider, ConsumeContext context); + } +} diff --git a/src/MassTransit/Configuration/DependencyInjection/JobSagaBusConfigurationExtensions.cs b/src/MassTransit/Configuration/DependencyInjection/JobSagaBusConfigurationExtensions.cs new file mode 100644 index 00000000000..dd053d2026a --- /dev/null +++ b/src/MassTransit/Configuration/DependencyInjection/JobSagaBusConfigurationExtensions.cs @@ -0,0 +1,70 @@ +namespace MassTransit; + +using Contracts.JobService; + + +public static class JobSagaBusConfigurationExtensions +{ + /// + /// Add partition key formatters to support partitioned transports + /// + /// + public static void UseJobSagaPartitionKeyFormatters(this IBusFactoryConfigurator configurator) + { + //JobTypeSaga + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobTypeId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobTypeId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobTypeId.ToString("N")); + + // JobSaga + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter>(x => x.Message.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter>(x => x.Message.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + + // JobAttemptSaga + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter>(x => x.Message.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.AttemptId.ToString("N")); + + // Consumers + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + configurator.SendTopology.UsePartitionKeyFormatter(x => x.Message.JobId.ToString("N")); + } + + /// + /// Configure the job saga receive endpoints to use the SQL transport partitioned receive mode + /// + /// + /// + public static IJobSagaRegistrationConfigurator SetPartitionedReceiveMode(this IJobSagaRegistrationConfigurator configurator) + { + return configurator.Endpoints(e => + { + e.AddConfigureEndpointCallback(cfg => + { + if (cfg is ISqlReceiveEndpointConfigurator sql) + sql.SetReceiveMode(SqlReceiveMode.Partitioned); + }); + }); + } +} diff --git a/src/MassTransit/Configuration/DependencyInjection/RegistrationConfiguratorExtensions.cs b/src/MassTransit/Configuration/DependencyInjection/RegistrationConfiguratorExtensions.cs index f7accc1b078..21d024c5043 100644 --- a/src/MassTransit/Configuration/DependencyInjection/RegistrationConfiguratorExtensions.cs +++ b/src/MassTransit/Configuration/DependencyInjection/RegistrationConfiguratorExtensions.cs @@ -2,10 +2,188 @@ namespace MassTransit { using System; using Internals; + using Metadata; public static class RegistrationConfiguratorExtensions { + /// + /// Adds the consumer, allowing configuration when it is configured on an endpoint + /// + /// + /// + /// The consumer type + public static IConsumerRegistrationConfigurator AddConsumer(this IRegistrationConfigurator configurator, + Action> configure = null) + where T : class, IConsumer + { + return configure != null ? configurator.AddConsumer((_, cfg) => configure.Invoke(cfg)) : configurator.AddConsumer(); + } + + /// + /// Adds the consumer, allowing configuration when it is configured on an endpoint + /// + /// + /// The consumer definition type + /// + /// The consumer type + public static IConsumerRegistrationConfigurator AddConsumer(this IRegistrationConfigurator configurator, + Type consumerDefinitionType, Action> configure = null) + where T : class, IConsumer + { + return configure != null + ? configurator.AddConsumer(consumerDefinitionType, (_, cfg) => configure.Invoke(cfg)) + : configurator.AddConsumer(consumerDefinitionType); + } + + /// + /// Adds the saga, allowing configuration when it is configured on the endpoint. This should not + /// be used for state machine (Automatonymous) sagas. + /// + /// + /// + /// The saga type + public static ISagaRegistrationConfigurator AddSaga(this IRegistrationConfigurator configurator, + Action> configure = null) + where T : class, ISaga + { + return configure != null ? configurator.AddSaga((_, cfg) => configure.Invoke(cfg)) : configurator.AddSaga(); + } + + /// + /// Adds the saga, allowing configuration when it is configured on the endpoint. This should not + /// be used for state machine (Automatonymous) sagas. + /// + /// + /// The saga definition type + /// + /// The saga type + public static ISagaRegistrationConfigurator AddSaga(this IRegistrationConfigurator configurator, Type sagaDefinitionType, + Action> configure = null) + where T : class, ISaga + { + return configure != null + ? configurator.AddSaga(sagaDefinitionType, (_, cfg) => configure.Invoke(cfg)) + : configurator.AddSaga(sagaDefinitionType); + } + + /// + /// Adds a SagaStateMachine to the registry, using the factory method, and updates the registrar prior to registering so that the default + /// saga registrar isn't notified. + /// + /// + /// + /// + /// + public static ISagaRegistrationConfigurator AddSagaStateMachine(this IRegistrationConfigurator configurator, + Action> configure = null) + where TStateMachine : class, SagaStateMachine + where T : class, SagaStateMachineInstance + { + return configure != null + ? configurator.AddSagaStateMachine((_, cfg) => configure.Invoke(cfg)) + : configurator.AddSagaStateMachine(); + } + + /// + /// Adds a SagaStateMachine to the registry, using the factory method, and updates the registrar prior to registering so that the default + /// saga registrar isn't notified. + /// + /// + /// + /// + /// + /// + public static ISagaRegistrationConfigurator AddSagaStateMachine(this IRegistrationConfigurator configurator, + Type sagaDefinitionType, Action> configure = null) + where TStateMachine : class, SagaStateMachine + where T : class, SagaStateMachineInstance + { + return configure != null + ? configurator.AddSagaStateMachine(sagaDefinitionType, (_, cfg) => configure.Invoke(cfg)) + : configurator.AddSagaStateMachine(sagaDefinitionType); + } + + /// + /// Adds an execute activity (Courier), allowing configuration when it is configured on the endpoint. + /// + /// + /// + /// The activity type + /// The argument type + public static IExecuteActivityRegistrationConfigurator AddExecuteActivity( + this IRegistrationConfigurator configurator, + Action> configure = null) + where TActivity : class, IExecuteActivity + where TArguments : class + { + return configure != null + ? configurator.AddExecuteActivity((_, cfg) => configure.Invoke(cfg)) + : configurator.AddExecuteActivity(); + } + + /// + /// Adds an execute activity (Courier), allowing configuration when it is configured on the endpoint. + /// + /// + /// + /// + /// The activity type + /// The argument type + public static IExecuteActivityRegistrationConfigurator AddExecuteActivity( + this IRegistrationConfigurator configurator, Type executeActivityDefinitionType, + Action> configure = null) + where TActivity : class, IExecuteActivity + where TArguments : class + { + return configure != null + ? configurator.AddExecuteActivity(executeActivityDefinitionType, (_, cfg) => configure.Invoke(cfg)) + : configurator.AddExecuteActivity(executeActivityDefinitionType); + } + + /// + /// Adds an activity (Courier), allowing configuration when it is configured on the endpoint. + /// + /// + /// The execute configuration callback + /// The compensate configuration callback + /// The activity type + /// The argument type + /// The log type + public static IActivityRegistrationConfigurator AddActivity( + this IRegistrationConfigurator configurator, + Action> configureExecute = null, + Action> configureCompensate = null) + where TActivity : class, IActivity + where TLog : class + where TArguments : class + { + return configurator.AddActivity((_, cfg) => configureExecute?.Invoke(cfg), + (_, cfg) => configureCompensate?.Invoke(cfg)); + } + + /// + /// Adds an activity (Courier), allowing configuration when it is configured on the endpoint. + /// + /// + /// + /// The execute configuration callback + /// The compensate configuration callback + /// The activity type + /// The argument type + /// The log type + public static IActivityRegistrationConfigurator AddActivity( + this IRegistrationConfigurator configurator, Type activityDefinitionType, + Action> configureExecute = null, + Action> configureCompensate = null) + where TActivity : class, IActivity + where TLog : class + where TArguments : class + { + return configurator.AddActivity(activityDefinitionType, (_, cfg) => configureExecute?.Invoke(cfg), + (_, cfg) => configureCompensate?.Invoke(cfg)); + } + /// /// Adds the consumer, along with an optional consumer definition /// @@ -15,7 +193,7 @@ public static class RegistrationConfiguratorExtensions public static IConsumerRegistrationConfigurator AddConsumer(this IRegistrationConfigurator configurator, Type consumerType, Type consumerDefinitionType = null) { - if (MessageTypeCache.HasSagaInterfaces(consumerType)) + if (RegistrationMetadata.IsSaga(consumerType)) throw new ArgumentException($"{TypeCache.GetShortName(consumerType)} is a saga, and cannot be registered as a consumer", nameof(consumerType)); var register = (IRegisterConsumer)Activator.CreateInstance(typeof(RegisterConsumer<>).MakeGenericType(consumerType)); diff --git a/src/MassTransit/Configuration/DependencyInjection/TestHarnessOptions.cs b/src/MassTransit/Configuration/DependencyInjection/TestHarnessOptions.cs index 81e8b65d6b4..916a837a25d 100644 --- a/src/MassTransit/Configuration/DependencyInjection/TestHarnessOptions.cs +++ b/src/MassTransit/Configuration/DependencyInjection/TestHarnessOptions.cs @@ -7,6 +7,6 @@ namespace MassTransit public class TestHarnessOptions { public TimeSpan TestTimeout { get; set; } = Debugger.IsAttached ? TimeSpan.FromMinutes(50) : TimeSpan.FromSeconds(30); - public TimeSpan TestInactivityTimeout { get; set; } = TimeSpan.FromSeconds(1.2); + public TimeSpan TestInactivityTimeout { get; set; } = Debugger.IsAttached ? TimeSpan.FromMinutes(30) : TimeSpan.FromSeconds(1.2); } } diff --git a/src/MassTransit/Configuration/HostedServiceConfigurationExtensions.cs b/src/MassTransit/Configuration/HostedServiceConfigurationExtensions.cs index 329efee0834..2f6199c55bd 100644 --- a/src/MassTransit/Configuration/HostedServiceConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/HostedServiceConfigurationExtensions.cs @@ -13,7 +13,7 @@ public static class HostedServiceConfigurationExtensions /// Adds the MassTransit , which includes a bus and endpoint health check. /// /// - [Obsolete("Deprecated, hosted service is automatically added to the container")] + [Obsolete("Remove, the hosted service is automatically registered. Visit https://masstransit.io/obsolete for details.")] public static IServiceCollection AddMassTransitHostedService(this IServiceCollection services) { services.AddOptions(); @@ -26,7 +26,7 @@ public static IServiceCollection AddMassTransitHostedService(this IServiceCollec /// /// /// Await until bus fully started. (It will block application until bus becomes ready) - [Obsolete("Deprecated, hosted service is automatically added to the container. Configure MassTransitHostOptions to modify the default options.")] + [Obsolete("Remove, the hosted service is automatically registered. Visit https://masstransit.io/obsolete for details.")] public static IServiceCollection AddMassTransitHostedService(this IServiceCollection services, bool waitUntilStarted) { services.AddOptions() @@ -53,7 +53,7 @@ public static IServiceCollection AddMassTransitHostedService(this IServiceCollec /// In other words, bus shutdown will complete gracefully (subject to the specified timeout) even if instructed by ASP.NET Core /// to no longer be graceful. /// - [Obsolete("Deprecated, hosted service is automatically added to the container. Configure MassTransitHostOptions to modify the default options.")] + [Obsolete("Remove, the hosted service is automatically registered. Visit https://masstransit.io/obsolete for details.")] public static IServiceCollection AddMassTransitHostedService(this IServiceCollection services, bool waitUntilStarted, TimeSpan? startTimeout, TimeSpan? stopTimeout = null) { diff --git a/src/MassTransit/Configuration/IJobSagaOptionsConfigurator.cs b/src/MassTransit/Configuration/IJobSagaOptionsConfigurator.cs new file mode 100644 index 00000000000..d9030e51c1a --- /dev/null +++ b/src/MassTransit/Configuration/IJobSagaOptionsConfigurator.cs @@ -0,0 +1,49 @@ +namespace MassTransit +{ + using System; + + + public interface IJobSagaOptionsConfigurator + { + /// + /// The time to wait before attempting to allocate a job slot when no slots are available + /// + TimeSpan SlotWaitTime { set; } + + /// + /// Time to wait before checking the status of a job to ensure it is still running (not dead) + /// + TimeSpan StatusCheckInterval { set; } + + /// + /// Timeout on request to allocate a job slot + /// + TimeSpan SlotRequestTimeout { set; } + + /// + /// Timeout to wait for a job to start + /// + TimeSpan StartJobTimeout { set; } + + /// + /// The number of times to retry a suspect job before it is faulted. Defaults to zero. + /// + int SuspectJobRetryCount { set; } + + /// + /// The delay before retrying a suspect job + /// + TimeSpan SuspectJobRetryDelay { set; } + + /// + /// If specified, overrides the default saga partition count to reduce conflicts when using optimistic concurrency. + /// If using a saga repository with pessimistic concurrency, this is not recommended. + /// + int? SagaPartitionCount { set; } + + /// + /// If true, completed jobs are finalized, removing them from the saga repository + /// + bool FinalizeCompleted { set; } + } +} diff --git a/src/MassTransit/Configuration/IJobServiceConfigurator.cs b/src/MassTransit/Configuration/IJobServiceConfigurator.cs index 894a798d0be..af62683acc5 100644 --- a/src/MassTransit/Configuration/IJobServiceConfigurator.cs +++ b/src/MassTransit/Configuration/IJobServiceConfigurator.cs @@ -48,16 +48,6 @@ public interface IJobServiceConfigurator /// TimeSpan StatusCheckInterval { set; } - /// - /// Timeout on request to allocate a job slot - /// - TimeSpan SlotRequestTimeout { set; } - - /// - /// Timeout to wait for a job to start - /// - TimeSpan StartJobTimeout { set; } - /// /// The number of times to retry a suspect job before it is faulted. Defaults to zero. /// diff --git a/src/MassTransit/Configuration/InMemoryOutboxConfigurationExtensions.cs b/src/MassTransit/Configuration/InMemoryOutboxConfigurationExtensions.cs index bd606ef04fd..f8623044da6 100644 --- a/src/MassTransit/Configuration/InMemoryOutboxConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/InMemoryOutboxConfigurationExtensions.cs @@ -2,6 +2,7 @@ { using System; using Configuration; + using DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Middleware; @@ -16,14 +17,37 @@ public static class InMemoryOutboxConfigurationExtensions /// nearly complete with only the ack remaining. If an exception is thrown, the messages are not sent/published. /// /// The pipe configurator + /// /// Configure the outbox + public static void UseInMemoryOutbox(this IPipeConfigurator> configurator, IRegistrationContext context, + Action configure = default) + where T : class + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + + var specification = new InMemoryOutboxSpecification(context); + + configure?.Invoke(specification); + + configurator.AddPipeSpecification(specification); + } + + /// + /// Includes an outbox in the consume filter path, which delays outgoing messages until the return path + /// of the pipeline returns to the outbox filter. At this point, the message execution pipeline should be + /// nearly complete with only the ack remaining. If an exception is thrown, the messages are not sent/published. + /// + /// The pipe configurator + /// Configure the outbox + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseInMemoryOutbox(this IPipeConfigurator> configurator, Action configure = default) where T : class { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - var specification = new InMemoryOutboxSpecification(); + var specification = new InMemoryOutboxSpecification(LegacySetScopedConsumeContext.Instance); configure?.Invoke(specification); @@ -36,13 +60,50 @@ public static void UseInMemoryOutbox(this IPipeConfigurator /// nearly complete with only the ack remaining. If an exception is thrown, the messages are not sent/published. /// /// The pipe configurator + /// /// Configure the outbox + public static void UseInMemoryOutbox(this IConsumePipeConfigurator configurator, IRegistrationContext context, + Action configure = default) + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + + var observer = new InMemoryOutboxConfigurationObserver(context, configurator, configure); + } + + /// + /// Includes an outbox in the consume filter path, which delays outgoing messages until the return path + /// of the pipeline returns to the outbox filter. At this point, the message execution pipeline should be + /// nearly complete with only the ack remaining. If an exception is thrown, the messages are not sent/published. + /// + /// The pipe configurator + /// Configure the outbox + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseInMemoryOutbox(this IConsumePipeConfigurator configurator, Action configure = default) { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - var observer = new InMemoryOutboxConfigurationObserver(configurator, configure); + var observer = new InMemoryOutboxConfigurationObserver(LegacySetScopedConsumeContext.Instance, configurator, configure); + } + + /// + /// Includes an outbox in the consume filter path, which delays outgoing messages until the return path + /// of the pipeline returns to the outbox filter. At this point, the message execution pipeline should be + /// nearly complete with only the ack remaining. If an exception is thrown, the messages are not sent/published. + /// + /// + /// + /// Configure the outbox + public static void UseInMemoryOutbox(this IConsumerConfigurator configurator, IRegistrationContext context, + Action configure = default) + where TConsumer : class + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + + var observer = new InMemoryOutboxConsumerConfigurationObserver(context, configurator, configure); + configurator.ConnectConsumerConfigurationObserver(observer); } /// @@ -52,13 +113,14 @@ public static void UseInMemoryOutbox(this IConsumePipeConfigurator configurator, /// /// /// Configure the outbox + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseInMemoryOutbox(this IConsumerConfigurator configurator, Action configure = default) where TConsumer : class { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - var observer = new InMemoryOutboxConsumerConfigurationObserver(configurator, configure); + var observer = new InMemoryOutboxConsumerConfigurationObserver(LegacySetScopedConsumeContext.Instance, configurator, configure); configurator.ConnectConsumerConfigurationObserver(observer); } @@ -68,14 +130,34 @@ public static void UseInMemoryOutbox(this IConsumerConfigurator /// + /// /// Configure the outbox + public static void UseInMemoryOutbox(this ISagaConfigurator configurator, IRegistrationContext context, + Action configure = default) + where TSaga : class, ISaga + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + + var observer = new InMemoryOutboxSagaConfigurationObserver(context, configurator, configure); + configurator.ConnectSagaConfigurationObserver(observer); + } + + /// + /// Includes an outbox in the consume filter path, which delays outgoing messages until the return path + /// of the pipeline returns to the outbox filter. At this point, the message execution pipeline should be + /// nearly complete with only the ack remaining. If an exception is thrown, the messages are not sent/published. + /// + /// + /// Configure the outbox + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseInMemoryOutbox(this ISagaConfigurator configurator, Action configure = default) where TSaga : class, ISaga { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - var observer = new InMemoryOutboxSagaConfigurationObserver(configurator, configure); + var observer = new InMemoryOutboxSagaConfigurationObserver(LegacySetScopedConsumeContext.Instance, configurator, configure); configurator.ConnectSagaConfigurationObserver(observer); } @@ -85,14 +167,34 @@ public static void UseInMemoryOutbox(this ISagaConfigurator config /// nearly complete with only the ack remaining. If an exception is thrown, the messages are not sent/published. /// /// + /// /// Configure the outbox + public static void UseInMemoryOutbox(this IHandlerConfigurator configurator, IRegistrationContext context, + Action configure = default) + where TMessage : class + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + + var observer = new InMemoryOutboxHandlerConfigurationObserver(context, configure); + configurator.ConnectHandlerConfigurationObserver(observer); + } + + /// + /// Includes an outbox in the consume filter path, which delays outgoing messages until the return path + /// of the pipeline returns to the outbox filter. At this point, the message execution pipeline should be + /// nearly complete with only the ack remaining. If an exception is thrown, the messages are not sent/published. + /// + /// + /// Configure the outbox + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseInMemoryOutbox(this IHandlerConfigurator configurator, Action configure = default) where TMessage : class { if (configurator == null) throw new ArgumentNullException(nameof(configurator)); - var observer = new InMemoryOutboxHandlerConfigurationObserver(configure); + var observer = new InMemoryOutboxHandlerConfigurationObserver(LegacySetScopedConsumeContext.Instance, configure); configurator.ConnectHandlerConfigurationObserver(observer); } @@ -110,12 +212,32 @@ public static IServiceCollection AddInMemoryInboxOutbox(this IServiceCollection return collection; } + /// + /// Includes a combination inbox/outbox in the consume pipeline, which stores outgoing messages in memory until + /// the message consumer completes. + /// + /// + /// Configuration service provider + public static void UseInMemoryInboxOutbox(this IReceiveEndpointConfigurator configurator, IRegistrationContext context) + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + var observer = new OutboxConsumePipeSpecificationObserver(configurator, context); + + configurator.ConnectConsumerConfigurationObserver(observer); + configurator.ConnectSagaConfigurationObserver(observer); + } + /// /// Includes a combination inbox/outbox in the consume pipeline, which stores outgoing messages in memory until /// the message consumer completes. /// /// /// Configuration service provider + [Obsolete("Obsolete, use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseInMemoryInboxOutbox(this IReceiveEndpointConfigurator configurator, IServiceProvider provider) { if (configurator == null) @@ -123,7 +245,8 @@ public static void UseInMemoryInboxOutbox(this IReceiveEndpointConfigurator conf if (provider == null) throw new ArgumentNullException(nameof(provider)); - var observer = new OutboxConsumePipeSpecificationObserver(configurator, provider); + var observer = new OutboxConsumePipeSpecificationObserver(configurator, provider, + LegacySetScopedConsumeContext.Instance); configurator.ConnectConsumerConfigurationObserver(observer); configurator.ConnectSagaConfigurationObserver(observer); diff --git a/src/MassTransit/Configuration/InstrumentationConfigurationExtensions.cs b/src/MassTransit/Configuration/InstrumentationConfigurationExtensions.cs index 34255a0a5f0..6c188965799 100644 --- a/src/MassTransit/Configuration/InstrumentationConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/InstrumentationConfigurationExtensions.cs @@ -1,9 +1,8 @@ namespace MassTransit { using System; - using Metadata; + using Logging; using Monitoring; - using Monitoring.Configuration; public static class InstrumentationConfigurationExtensions @@ -20,25 +19,16 @@ public static class InstrumentationConfigurationExtensions public static void UseInstrumentation(this IBusFactoryConfigurator configurator, Action configureOptions = null, string serviceName = default) { - var options = InstrumentationOptions.CreateDefault(); + var options = new InstrumentationOptions(); + var configureDefault = new ConfigureDefaultInstrumentationOptions(); + configureDefault.Configure(options); configureOptions?.Invoke(options); - Instrumentation.TryConfigure(GetServiceName(serviceName), options); + if (!string.IsNullOrWhiteSpace(serviceName)) + options.ServiceName = serviceName; - configurator.ConnectConsumerConfigurationObserver(new InstrumentConsumerConfigurationObserver()); - configurator.ConnectHandlerConfigurationObserver(new InstrumentHandlerConfigurationObserver()); - configurator.ConnectSagaConfigurationObserver(new InstrumentSagaConfigurationObserver()); - configurator.ConnectActivityConfigurationObserver(new InstrumentActivityConfigurationObserver()); - configurator.ConnectEndpointConfigurationObserver(new InstrumentReceiveEndpointConfiguratorObserver()); - configurator.ConnectBusObserver(new InstrumentBusObserver()); - } - - static string GetServiceName(string serviceName) - { - return string.IsNullOrWhiteSpace(serviceName) - ? HostMetadataCache.Host.ProcessName - : serviceName; + LogContextInstrumentationExtensions.TryConfigure(options); } } } diff --git a/src/MassTransit/Configuration/JobConsumerOptions.cs b/src/MassTransit/Configuration/JobConsumerOptions.cs new file mode 100644 index 00000000000..172521637f4 --- /dev/null +++ b/src/MassTransit/Configuration/JobConsumerOptions.cs @@ -0,0 +1,41 @@ +namespace MassTransit +{ + using System; + using System.Collections.Generic; + using Configuration; + + + public class JobConsumerOptions : + IOptions, + ISpecification + { + public JobConsumerOptions() + { + HeartbeatInterval = TimeSpan.FromMinutes(1); + } + + public TimeSpan HeartbeatInterval { get; set; } + + IEnumerable ISpecification.Validate() + { + if (HeartbeatInterval <= TimeSpan.Zero) + yield return this.Failure("JobConsumerOptions", "HeartbeatInterval", "Must be > 0"); + } + + public JobConsumerOptions SetHeartbeatInterval(int? d = null, int? h = null, int? m = null, int? s = null, int? ms = null) + { + var value = new TimeSpan(d ?? 0, h ?? 0, m ?? 0, s ?? 0, ms ?? 0); + + HeartbeatInterval = value; + + return this; + } + + public JobConsumerOptions SetHeartbeatInterval(TimeSpan interval) + { + HeartbeatInterval = interval; + + return this; + } + } +} diff --git a/src/MassTransit/Configuration/JobOptions.cs b/src/MassTransit/Configuration/JobOptions.cs index 91d91a3ebc9..1fce94e629c 100644 --- a/src/MassTransit/Configuration/JobOptions.cs +++ b/src/MassTransit/Configuration/JobOptions.cs @@ -1,100 +1,250 @@ -namespace MassTransit +#nullable enable +namespace MassTransit; + +using System; +using System.Collections.Generic; +using Configuration; +using Observables; +using Serialization; + + +/// +/// JobOptions contains the options used to configure the job consumer and related components +/// +/// The Job Type +public class JobOptions : + IOptions, + ISpecification + where TJob : class { - using System; - using System.Collections.Generic; - using Configuration; - using Observables; + readonly JobPropertyCollection _instanceProperties; + readonly JobPropertyCollection _jobTypeProperties; + public JobOptions() + { + ConcurrentJobLimit = 1; + JobTimeout = TimeSpan.FromMinutes(5); + JobCancellationTimeout = TimeSpan.FromSeconds(30); + + RetryPolicy = Retry.None; + + ProgressBuffer = new ProgressBufferSettings(); + + _jobTypeProperties = new JobPropertyCollection(); + _instanceProperties = new JobPropertyCollection(); + } /// - /// JobOptions contains the options used to configure the job consumer and related components + /// Set the allowed time for a job to complete (per attempt). If the job timeout expires and the job has not yet completed, it will be canceled. /// - /// The Job Type - public class JobOptions : - IOptions, - ISpecification - where TJob : class + public TimeSpan JobTimeout { get; set; } + + /// + /// Set the allowed time for a job to stop execution after the cancellation. If the job cancellation timeout expires and the job has not yet completed, it will be + /// fully canceled. + /// + public TimeSpan JobCancellationTimeout { get; set; } + + /// + /// Set the concurrent job limit. The limit is applied to each instance if the job consumer is scaled out. + /// Do not use ConcurrentMessageLimit with job consumers."/> + /// + public int ConcurrentJobLimit { get; set; } + + public IRetryPolicy RetryPolicy { get; private set; } + + /// + /// Override the default job name (optional, automatically generated from the job type otherwise) that is displayed in the . + /// + public string? JobTypeName { get; set; } + + /// + /// Configure the job progress buffer settings, if using job progress (optional) + /// + public ProgressBufferSettings ProgressBuffer { get; } + + /// + /// Properties that are specific to the job type, which can be used by the job distribution strategy + /// + public JobPropertyCollection JobTypeProperties => _jobTypeProperties; + + /// + /// Properties that are specific to this job consumer bus instance, such as region, data center, tenant, etc. also used by the job distribution strategy + /// + public JobPropertyCollection InstanceProperties => _instanceProperties; + + /// + /// Optional, if specified, configures a global concurrent job limit across all job consumer instances + /// + public int? GlobalConcurrentJobLimit { get; set; } + + IEnumerable ISpecification.Validate() { - public JobOptions() - { - ConcurrentJobLimit = 1; - JobTimeout = TimeSpan.FromMinutes(5); + if (ConcurrentJobLimit <= 0) + yield return this.Failure("JobOptions", "ConcurrentJobLimit", "Must be > 0"); + if (JobTimeout <= TimeSpan.Zero) + yield return this.Failure("JobOptions", "JobTimeout", "Must be > TimeSpan.Zero"); + if (JobCancellationTimeout <= TimeSpan.Zero) + yield return this.Failure("JobOptions", "JobCancellationTimeout", "Must be > TimeSpan.Zero"); + } - RetryPolicy = Retry.None; - } + /// + /// Set job type properties that can be used by a custom job distribution strategy + /// + /// + /// + public JobOptions SetJobTypeProperties(Action? callback) + { + callback?.Invoke(_jobTypeProperties); + + return this; + } - /// - /// The maximum allowed time for the job to execute, per attempt - /// - public TimeSpan JobTimeout { get; set; } + /// + /// Set instance properties that can be used by a custom job distribution strategy + /// + /// + /// + public JobOptions SetInstanceProperties(Action? callback) + { + callback?.Invoke(_instanceProperties); - /// - /// Limits the concurrent number of job executing - /// - public int ConcurrentJobLimit { get; set; } + return this; + } + + /// + /// Set the allowed time for a job to complete (per attempt). If the job timeout expires and the job has not yet completed, it will be canceled. + /// + /// + /// + public JobOptions SetJobTimeout(TimeSpan timeout) + { + JobTimeout = timeout; - public IRetryPolicy RetryPolicy { get; private set; } + return this; + } - public IEnumerable Validate() + /// + /// Set the allowed time for a job to stop execution after the cancellation. If the job cancellation timeout expires and the job has not yet completed, it will be + /// fully canceled. + /// + /// + /// + public JobOptions SetJobCancellationTimeout(TimeSpan timeout) + { + JobCancellationTimeout = timeout; + + return this; + } + + /// + /// Set the concurrent job limit. The limit is applied to each instance if the job consumer is scaled out. + /// Do not use ConcurrentMessageLimit with job consumers."/> + /// + /// + /// + public JobOptions SetConcurrentJobLimit(int limit) + { + ConcurrentJobLimit = limit; + + return this; + } + + /// + /// Set the global concurrent job limit across all job consumer instances + /// Do not use ConcurrentMessageLimit with job consumers."/> + /// + /// + /// + public JobOptions SetGlobalConcurrentJobLimit(int? limit) + { + GlobalConcurrentJobLimit = limit; + + return this; + } + + /// + /// Override the default job name (optional, automatically generated from the job type otherwise) that is displayed in the . + /// + /// + /// + public JobOptions SetJobTypeName(string name) + { + JobTypeName = name; + + return this; + } + + /// + /// Set the job retry policy, used to handle faulted jobs. Retry middleware on the job consumer endpoint is not used. + /// + /// + /// + public JobOptions SetRetry(Action? configure) + { + var specification = new RetrySpecification(); + + configure?.Invoke(specification); + + specification.Validate().ThrowIfContainsFailure($"The retry policy was not properly configured: JobOptions<{TypeCache.ShortName}"); + + RetryPolicy = specification.Build(); + + return this; + } + + /// + /// Set the job progress buffer settings, either value can be set and will update the settings + /// + /// The number of updates to buffer before sending the most recent update to the job saga + /// The time since the first update after the last update sent to the job saga before an update must be sent + /// + public JobOptions SetProgressBuffer(int? updateLimit = default, TimeSpan? timeLimit = default) + { + if (updateLimit.HasValue) + ProgressBuffer.UpdateLimit = updateLimit.Value; + if (timeLimit.HasValue) + ProgressBuffer.TimeLimit = timeLimit.Value; + + return this; + } + + + class RetrySpecification : + ExceptionSpecification, + IRetryConfigurator, + ISpecification + { + readonly RetryObservable _observers; + RetryPolicyFactory? _policyFactory; + + public RetrySpecification() { - if (ConcurrentJobLimit <= 0) - yield return this.Failure("JobOptions", "ConcurrentJobLimit", "Must be > 0"); - if (JobTimeout <= TimeSpan.Zero) - yield return this.Failure("JobOptions", "JobTimeout", "Must be > TimeSpan.Zero"); + _observers = new RetryObservable(); } - public JobOptions SetJobTimeout(TimeSpan timeout) + public void SetRetryPolicy(RetryPolicyFactory factory) { - JobTimeout = timeout; - - return this; + _policyFactory = factory; } - public JobOptions SetConcurrentJobLimit(int limit) + ConnectHandle IRetryObserverConnector.ConnectRetryObserver(IRetryObserver observer) { - ConcurrentJobLimit = limit; - - return this; + return _observers.Connect(observer); } - public JobOptions SetRetry(Action configure) + public IEnumerable Validate() { - var specification = new RetrySpecification(); - - configure?.Invoke(specification); - - RetryPolicy = specification.Build(); - - return this; + if (_policyFactory == null) + yield return this.Failure("RetryPolicy", "must not be null"); } - - class RetrySpecification : - ExceptionSpecification, - IRetryConfigurator + public IRetryPolicy Build() { - readonly RetryObservable _observers; - RetryPolicyFactory _policyFactory; - - public RetrySpecification() - { - _observers = new RetryObservable(); - } - - public void SetRetryPolicy(RetryPolicyFactory factory) - { - _policyFactory = factory; - } - - ConnectHandle IRetryObserverConnector.ConnectRetryObserver(IRetryObserver observer) - { - return _observers.Connect(observer); - } - - public IRetryPolicy Build() - { - return _policyFactory(Filter); - } + if (_policyFactory == null) + throw new ConfigurationException($"The retry policy was not properly configured: JobOptions<{TypeCache.ShortName}"); + + return _policyFactory(Filter); } } } diff --git a/src/MassTransit/Configuration/JobSagaOptions.cs b/src/MassTransit/Configuration/JobSagaOptions.cs new file mode 100644 index 00000000000..ef391796ec1 --- /dev/null +++ b/src/MassTransit/Configuration/JobSagaOptions.cs @@ -0,0 +1,89 @@ +namespace MassTransit +{ + using System; + using System.Collections.Generic; + using Configuration; + + + public class JobSagaOptions : + JobSagaSettingsConfigurator, + ISpecification + { + Uri _jobAttemptSagaEndpointAddress; + Uri _jobSagaEndpointAddress; + Uri _jobTypeSagaEndpointAddress; + + public JobSagaOptions() + { + StatusCheckInterval = TimeSpan.FromMinutes(1); + SlotWaitTime = TimeSpan.FromSeconds(30); + HeartbeatTimeout = TimeSpan.FromMinutes(5); + + SuspectJobRetryCount = 3; + ConcurrentMessageLimit = 16; + FinalizeCompleted = true; + } + + /// + /// The number of concurrent messages + /// + public int? ConcurrentMessageLimit { get; set; } + + IEnumerable ISpecification.Validate() + { + if (SlotWaitTime < TimeSpan.FromSeconds(1)) + yield return this.Failure(nameof(SlotWaitTime), "must be >= 1 second"); + if (StatusCheckInterval < TimeSpan.FromSeconds(30)) + yield return this.Failure(nameof(StatusCheckInterval), "must be >= 30 seconds"); + } + + Uri JobSagaSettingsConfigurator.JobSagaEndpointAddress + { + set => _jobSagaEndpointAddress = value; + } + + Uri JobSagaSettingsConfigurator.JobTypeSagaEndpointAddress + { + set => _jobTypeSagaEndpointAddress = value; + } + + Uri JobSagaSettingsConfigurator.JobAttemptSagaEndpointAddress + { + set => _jobAttemptSagaEndpointAddress = value; + } + + Uri JobSagaSettings.JobAttemptSagaEndpointAddress => _jobAttemptSagaEndpointAddress; + Uri JobSagaSettings.JobTypeSagaEndpointAddress => _jobTypeSagaEndpointAddress; + Uri JobSagaSettings.JobSagaEndpointAddress => _jobSagaEndpointAddress; + + /// + /// The time to wait for a job slot when one is unavailable + /// + public TimeSpan SlotWaitTime { get; set; } + + /// + /// The time after which the status of a job should be checked + /// + public TimeSpan StatusCheckInterval { get; set; } + + /// + /// The time after which an instance will automatically be purged from the instance list + /// + public TimeSpan HeartbeatTimeout { get; set; } + + /// + /// The number of times to retry a suspect job before it is faulted. Defaults to zero. + /// + public int SuspectJobRetryCount { get; set; } + + /// + /// The delay before retrying a suspect job + /// + public TimeSpan? SuspectJobRetryDelay { get; set; } + + /// + /// If true, completed jobs will be finalized, removing the saga from the repository + /// + public bool FinalizeCompleted { get; set; } + } +} diff --git a/src/MassTransit/Configuration/JobServiceConfigurationExtensions.cs b/src/MassTransit/Configuration/JobServiceConfigurationExtensions.cs index 0d466d9dcfe..1ca6f1c7d95 100644 --- a/src/MassTransit/Configuration/JobServiceConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/JobServiceConfigurationExtensions.cs @@ -6,6 +6,29 @@ namespace MassTransit public static class JobServiceConfigurationExtensions { + /// + /// Configures support for job consumers on the service instance, which supports executing long-running jobs without blocking the consumer pipeline. + /// Job consumers use multiple state machines to track jobs, each of which runs on its own dedicated receive endpoint. Multiple service + /// instances will use the competing consumer pattern, so a shared saga repository should be configured. + /// + /// The transport receive endpoint configurator type + /// The service instance + /// + /// + [Obsolete("Use AddJobSagaStateMachines instead. Visit https://masstransit.io/obsolete for details.")] + public static IServiceInstanceConfigurator ConfigureJobServiceEndpoints(this IServiceInstanceConfigurator configurator, + IRegistrationContext context, Action configure = default) + where T : IReceiveEndpointConfigurator + { + var jobServiceConfigurator = new JobServiceConfigurator(configurator); + + configure?.Invoke(jobServiceConfigurator); + + jobServiceConfigurator.ConfigureJobServiceEndpoints(context); + + return configurator; + } + /// /// Configures support for job consumers on the service instance, which supports executing long-running jobs without blocking the consumer pipeline. /// Job consumers use multiple state machines to track jobs, each of which runs on its own dedicated receive endpoint. Multiple service @@ -27,6 +50,30 @@ public static IServiceInstanceConfigurator ConfigureJobServiceEndpoints(th return configurator; } + /// + /// Configures support for job consumers on the service instance, which supports executing long-running jobs without blocking the consumer pipeline. + /// Job consumers use multiple state machines to track jobs, each of which runs on its own dedicated receive endpoint. Multiple service + /// instances will use the competing consumer pattern, so a shared saga repository should be configured. + /// + /// The transport receive endpoint configurator type + /// The service instance + /// + /// + /// + [Obsolete("Use AddJobSagaStateMachines instead. Visit https://masstransit.io/obsolete for details.")] + public static IServiceInstanceConfigurator ConfigureJobServiceEndpoints(this IServiceInstanceConfigurator configurator, + JobServiceOptions options, IRegistrationContext context, Action configure = default) + where T : IReceiveEndpointConfigurator + { + var jobServiceConfigurator = new JobServiceConfigurator(configurator, options); + + configure?.Invoke(jobServiceConfigurator); + + jobServiceConfigurator.ConfigureJobServiceEndpoints(context); + + return configurator; + } + /// /// Configures support for job consumers on the service instance, which supports executing long-running jobs without blocking the consumer pipeline. /// Job consumers use multiple state machines to track jobs, each of which runs on its own dedicated receive endpoint. Multiple service @@ -36,6 +83,7 @@ public static IServiceInstanceConfigurator ConfigureJobServiceEndpoints(th /// The service instance /// /// + [Obsolete("Use AddJobSagaStateMachines instead. Visit https://masstransit.io/obsolete for details.")] public static IServiceInstanceConfigurator ConfigureJobServiceEndpoints(this IServiceInstanceConfigurator configurator, JobServiceOptions options, Action configure = default) where T : IReceiveEndpointConfigurator @@ -59,6 +107,7 @@ public static IServiceInstanceConfigurator ConfigureJobServiceEndpoints(th /// The transport receive endpoint configurator type /// The service instance /// + [Obsolete("Job Consumers no longer require a service instance. Visit https://masstransit.io/obsolete for details.")] public static IServiceInstanceConfigurator ConfigureJobService(this IServiceInstanceConfigurator configurator, Action configure = default) where T : IReceiveEndpointConfigurator @@ -81,6 +130,7 @@ public static IServiceInstanceConfigurator ConfigureJobService(this IServi /// The service instance /// /// + [Obsolete("Job Consumers no longer require a service instance. Visit https://masstransit.io/obsolete for details.")] public static IServiceInstanceConfigurator ConfigureJobService(this IServiceInstanceConfigurator configurator, JobServiceOptions options, Action configure = default) where T : IReceiveEndpointConfigurator diff --git a/src/MassTransit/Configuration/JobServiceContainerConfigurationExtensions.cs b/src/MassTransit/Configuration/JobServiceContainerConfigurationExtensions.cs index 3f9ef03f66c..008210ded06 100644 --- a/src/MassTransit/Configuration/JobServiceContainerConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/JobServiceContainerConfigurationExtensions.cs @@ -1,22 +1,38 @@ namespace MassTransit { using System; - using Microsoft.Extensions.DependencyInjection; + using DependencyInjection; public static class JobServiceContainerConfigurationExtensions { + /// + /// Configure the job server saga repositories to resolve from the container. + /// + /// + /// The bus registration context provided during configuration + /// + public static IJobServiceConfigurator ConfigureSagaRepositories(this IJobServiceConfigurator configurator, IRegistrationContext context) + { + configurator.Repository = new DependencyInjectionSagaRepository(context); + configurator.JobRepository = new DependencyInjectionSagaRepository(context); + configurator.JobAttemptRepository = new DependencyInjectionSagaRepository(context); + + return configurator; + } + /// /// Configure the job server saga repositories to resolve from the container. /// /// /// The bus registration context provided during configuration /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static IJobServiceConfigurator ConfigureSagaRepositories(this IJobServiceConfigurator configurator, IServiceProvider provider) { - configurator.Repository = provider.GetRequiredService>(); - configurator.JobRepository = provider.GetRequiredService>(); - configurator.JobAttemptRepository = provider.GetRequiredService>(); + configurator.Repository = new DependencyInjectionSagaRepository(provider, LegacySetScopedConsumeContext.Instance); + configurator.JobRepository = new DependencyInjectionSagaRepository(provider, LegacySetScopedConsumeContext.Instance); + configurator.JobAttemptRepository = new DependencyInjectionSagaRepository(provider, LegacySetScopedConsumeContext.Instance); return configurator; } diff --git a/src/MassTransit/Configuration/JobServiceOptions.cs b/src/MassTransit/Configuration/JobServiceOptions.cs index 88bcd875d1d..0ffdc92be4e 100644 --- a/src/MassTransit/Configuration/JobServiceOptions.cs +++ b/src/MassTransit/Configuration/JobServiceOptions.cs @@ -7,6 +7,7 @@ namespace MassTransit public class JobServiceOptions : + JobSagaSettings, IOptions, ISpecification { @@ -18,8 +19,6 @@ public JobServiceOptions() { StatusCheckInterval = TimeSpan.FromMinutes(1); SlotWaitTime = TimeSpan.FromSeconds(30); - StartJobTimeout = TimeSpan.Zero; - SlotRequestTimeout = TimeSpan.Zero; HeartbeatInterval = TimeSpan.FromMinutes(1); HeartbeatTimeout = TimeSpan.FromMinutes(5); @@ -58,50 +57,67 @@ public string JobAttemptSagaEndpointName } /// - /// The endpoint for the JobAttemptStateMachine + /// The job service for the endpoint /// - public Uri JobSagaEndpointAddress { get; set; } + public IJobService JobService { get; set; } /// - /// The endpoint for the JobAttemptStateMachine + /// How often a job instance should send a heartbeat /// - public Uri JobTypeSagaEndpointAddress { get; set; } + public TimeSpan HeartbeatInterval { get; set; } /// - /// The endpoint for the JobAttemptStateMachine + /// If specified, overrides the default saga partition count to reduce conflicts when using optimistic concurrency. + /// If using a saga repository with pessimistic concurrency, this is not recommended. /// - public Uri JobAttemptSagaEndpointAddress { get; set; } + public int? SagaPartitionCount { get; set; } + + public IReceiveEndpointConfigurator InstanceEndpointConfigurator { get; set; } + + public Action OnConfigureEndpoint { get; set; } + + public int? ConcurrentMessageLimit => SagaPartitionCount; + + IEnumerable ISpecification.Validate() + { + if (SlotWaitTime < TimeSpan.FromSeconds(1)) + yield return this.Failure(nameof(SlotWaitTime), "must be >= 1 second"); + if (StatusCheckInterval < TimeSpan.FromSeconds(30)) + yield return this.Failure(nameof(StatusCheckInterval), "must be >= 30 seconds"); + + if (string.IsNullOrWhiteSpace(JobTypeSagaEndpointName)) + yield return this.Failure(nameof(JobTypeSagaEndpointName), "must not be null or empty"); + if (string.IsNullOrWhiteSpace(JobStateSagaEndpointName)) + yield return this.Failure(nameof(JobStateSagaEndpointName), "must not be null or empty"); + if (string.IsNullOrWhiteSpace(JobAttemptSagaEndpointName)) + yield return this.Failure(nameof(JobAttemptSagaEndpointName), "must not be null or empty"); + } /// - /// The job service for the endpoint + /// The endpoint for the JobAttemptStateMachine /// - public IJobService JobService { get; set; } + public Uri JobSagaEndpointAddress { get; set; } /// - /// Timeout for the Allocate Job Slot Request + /// The endpoint for the JobAttemptStateMachine /// - public TimeSpan SlotRequestTimeout { get; set; } + public Uri JobTypeSagaEndpointAddress { get; set; } /// - /// The time to wait for a job slot when one is unavailable + /// The endpoint for the JobAttemptStateMachine /// - public TimeSpan SlotWaitTime { get; set; } + public Uri JobAttemptSagaEndpointAddress { get; set; } /// - /// The time to wait for a job to start + /// The time to wait for a job slot when one is unavailable /// - public TimeSpan StartJobTimeout { get; set; } + public TimeSpan SlotWaitTime { get; set; } /// /// The time after which the status of a job should be checked /// public TimeSpan StatusCheckInterval { get; set; } - /// - /// How often a job instance should send a heartbeat - /// - public TimeSpan HeartbeatInterval { get; set; } - /// /// The time after which an instance will automatically be purged from the instance list /// @@ -117,32 +133,9 @@ public string JobAttemptSagaEndpointName /// public TimeSpan? SuspectJobRetryDelay { get; set; } - /// - /// If specified, overrides the default saga partition count to reduce conflicts when using optimistic concurrency. - /// If using a saga repository with pessimistic concurrency, this is not recommended. - /// - public int? SagaPartitionCount { get; set; } - /// /// If true, completed jobs will be finalized, removing the saga from the repository /// public bool FinalizeCompleted { get; set; } - - public IReceiveEndpointConfigurator InstanceEndpointConfigurator { get; set; } - - IEnumerable ISpecification.Validate() - { - if (SlotWaitTime < TimeSpan.FromSeconds(1)) - yield return this.Failure(nameof(SlotWaitTime), "must be >= 1 second"); - if (StatusCheckInterval < TimeSpan.FromSeconds(30)) - yield return this.Failure(nameof(StatusCheckInterval), "must be >= 30 seconds"); - - if (string.IsNullOrWhiteSpace(JobTypeSagaEndpointName)) - yield return this.Failure(nameof(JobTypeSagaEndpointName), "must not be null or empty"); - if (string.IsNullOrWhiteSpace(JobStateSagaEndpointName)) - yield return this.Failure(nameof(JobStateSagaEndpointName), "must not be null or empty"); - if (string.IsNullOrWhiteSpace(JobAttemptSagaEndpointName)) - yield return this.Failure(nameof(JobAttemptSagaEndpointName), "must not be null or empty"); - } } } diff --git a/src/MassTransit/Configuration/JobServiceRegistrationExtensions.cs b/src/MassTransit/Configuration/JobServiceRegistrationExtensions.cs new file mode 100644 index 00000000000..6d5da5e39f4 --- /dev/null +++ b/src/MassTransit/Configuration/JobServiceRegistrationExtensions.cs @@ -0,0 +1,58 @@ +#nullable enable +namespace MassTransit +{ + using System; + using Configuration; + using DependencyInjection.Registration; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + + + public static class JobServiceRegistrationExtensions + { + /// + /// Set the job consumer options (optional, not required to use job consumers) + /// + /// + /// Configure the job consumer options using this callback + /// + public static IJobServiceRegistrationConfigurator SetJobConsumerOptions(this IBusRegistrationConfigurator configurator, + Action? configure = null) + { + var registration = configurator.RegisterJobService(configurator.Registrar); + + registration.AddConfigureAction(configure); + + var registrationConfigurator = new JobServiceRegistrationConfigurator(configurator, registration); + + return registrationConfigurator; + } + + /// + /// Add registrations for the job service saga state machines + /// + /// + /// Configure the job saga options + public static IJobSagaRegistrationConfigurator AddJobSagaStateMachines(this IBusRegistrationConfigurator configurator, + Action? configure = null) + { + var registrationConfigurator = new JobSagaRegistrationConfigurator(configurator, configure); + + return registrationConfigurator; + } + + /// + /// Register a custom job distribution strategy for the job saga state machines + /// + /// + /// + /// + public static IServiceCollection TryAddJobDistributionStrategy(this IServiceCollection services) + where T : class, IJobDistributionStrategy + { + services.TryAddScoped(); + + return services; + } + } +} diff --git a/src/MassTransit/Configuration/JsonSerializerConfigurationExtensions.cs b/src/MassTransit/Configuration/JsonSerializerConfigurationExtensions.cs index 310a7206d25..08581f5ae4c 100644 --- a/src/MassTransit/Configuration/JsonSerializerConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/JsonSerializerConfigurationExtensions.cs @@ -1,15 +1,18 @@ +#nullable enable namespace MassTransit { using System; + using System.Linq; using System.Text.Json; using Configuration; using Serialization; + using Serialization.JsonConverters; public static class JsonSerializerConfigurationExtensions { /// - /// Serialize messages using the raw JSON message serializer + /// Serialize and deserialize messages using the raw JSON message serializer /// /// public static void UseJsonSerializer(this IBusFactoryConfigurator configurator) @@ -21,7 +24,7 @@ public static void UseJsonSerializer(this IBusFactoryConfigurator configurator) } /// - /// Serialize messages using the raw JSON message serializer + /// Deserialize messages using the raw JSON message serializer /// /// /// If true, set the default content type to the content type of the deserializer @@ -33,7 +36,7 @@ public static void UseJsonDeserializer(this IBusFactoryConfigurator configurator } /// - /// Serialize messages using the raw JSON message serializer + /// Serialize and deserialize messages using the raw JSON message serializer /// /// public static void UseJsonSerializer(this IReceiveEndpointConfigurator configurator) @@ -45,7 +48,7 @@ public static void UseJsonSerializer(this IReceiveEndpointConfigurator configura } /// - /// Serialize messages using the raw JSON message serializer + /// Deserialize messages using the raw JSON message serializer /// /// /// If true, set the default content type to the content type of the deserializer @@ -62,10 +65,30 @@ public static void UseJsonDeserializer(this IReceiveEndpointConfigurator configu /// /// public static void ConfigureJsonSerializerOptions(this IBusFactoryConfigurator configurator, - Func configure = null) + Func? configure = null) { if (configure != null) SystemTextJsonMessageSerializer.Options = configure(new JsonSerializerOptions(SystemTextJsonMessageSerializer.Options)); } + + /// + /// Specify custom for a message type, removing any previous configured options for the same message type. + /// + /// + /// + /// + public static void SetMessageSerializerOptions(this JsonSerializerOptions options, + Func? configure = null) + where T : class + { + var existingConverter = options.Converters.FirstOrDefault(x => x is CustomMessageTypeJsonConverter); + if(existingConverter != null) + options.Converters.Remove(existingConverter); + + var messageSerializerOptions = new JsonSerializerOptions(); + configure?.Invoke(messageSerializerOptions); + + options.Converters.Insert(0, new CustomMessageTypeJsonConverter(messageSerializerOptions)); + } } } diff --git a/src/MassTransit/Configuration/KebabCaseEndpointNameFormatter.cs b/src/MassTransit/Configuration/KebabCaseEndpointNameFormatter.cs index 33043930b43..942bdb2ccdf 100644 --- a/src/MassTransit/Configuration/KebabCaseEndpointNameFormatter.cs +++ b/src/MassTransit/Configuration/KebabCaseEndpointNameFormatter.cs @@ -20,6 +20,15 @@ public KebabCaseEndpointNameFormatter(bool includeNamespace) { } + /// + /// Kebab case endpoint formatter, which uses dashes between words + /// + /// Prefix to start the name, should match the casing of the formatter (such as Dev or PreProd) + public KebabCaseEndpointNameFormatter(string prefix) + : base(KebabCaseSeparator, prefix, false) + { + } + /// /// Kebab case endpoint formatter, which uses dashes between words /// diff --git a/src/MassTransit/Configuration/MediatorConfigurationExtensions.cs b/src/MassTransit/Configuration/MediatorConfigurationExtensions.cs index 81531a7eac5..1b3dd023f6e 100644 --- a/src/MassTransit/Configuration/MediatorConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/MediatorConfigurationExtensions.cs @@ -17,12 +17,27 @@ public static class MediatorConfigurationExtensions /// /// public static IMediator CreateMediator(this IBusFactorySelector selector, Action configure) + { + return CreateMediator(selector, null, configure); + } + + /// + /// Create a mediator, which sends messages to consumers, handlers, and sagas. Messages are dispatched to the consumers asynchronously. + /// Consumers are not directly coupled to the sender. Can be used entirely in-memory without a broker. + /// + /// + /// + /// + /// + /// + public static IMediator CreateMediator(this IBusFactorySelector selector, Uri baseAddress, Action configure) { if (configure == null) throw new ArgumentNullException(nameof(configure)); - var topologyConfiguration = new InMemoryTopologyConfiguration(InMemoryBus.MessageTopology); - var busConfiguration = new InMemoryBusConfiguration(topologyConfiguration, new Uri("loopback://localhost")); + baseAddress ??= new Uri("loopback://localhost/"); + var topologyConfiguration = new InMemoryTopologyConfiguration(InMemoryBus.CreateMessageTopology()); + var busConfiguration = new InMemoryBusConfiguration(topologyConfiguration, baseAddress); if (LogContext.Current != null) busConfiguration.HostConfiguration.LogContext = LogContext.Current; diff --git a/src/MassTransit/Configuration/MessageSchedulerRegistrationExtensions.cs b/src/MassTransit/Configuration/MessageSchedulerRegistrationExtensions.cs index 74df6de9bb9..1d73095c89d 100644 --- a/src/MassTransit/Configuration/MessageSchedulerRegistrationExtensions.cs +++ b/src/MassTransit/Configuration/MessageSchedulerRegistrationExtensions.cs @@ -1,6 +1,7 @@ namespace MassTransit { using System; + using DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Scheduling; @@ -14,7 +15,7 @@ public static class MessageSchedulerRegistrationExtensions /// /// /// The endpoint address where the scheduler is running - public static void AddMessageScheduler(this IRegistrationConfigurator configurator, Uri schedulerEndpointAddress) + public static void AddMessageScheduler(this IBusRegistrationConfigurator configurator, Uri schedulerEndpointAddress) { if (schedulerEndpointAddress == null) throw new ArgumentNullException(nameof(schedulerEndpointAddress)); @@ -25,6 +26,41 @@ public static void AddMessageScheduler(this IRegistrationConfigurator configurat var sendEndpointProvider = provider.GetRequiredService(); return sendEndpointProvider.CreateMessageScheduler(bus.Topology, schedulerEndpointAddress); }); + + configurator.TryAddScoped(provider => + { + var bus = provider.GetRequiredService(); + var sendEndpointProvider = provider.GetRequiredService(); + return new EndpointRecurringMessageScheduler(sendEndpointProvider, schedulerEndpointAddress, bus.Topology); + }); + } + + /// + /// Add a to the container that sends + /// to an external message scheduler on the specified endpoint address, such as Quartz or Hangfire. + /// + /// + /// The endpoint address where the scheduler is running + public static void AddMessageScheduler(this IBusRegistrationConfigurator configurator, Uri schedulerEndpointAddress) + where TBus : class, IBus + { + if (schedulerEndpointAddress == null) + throw new ArgumentNullException(nameof(schedulerEndpointAddress)); + + configurator.TryAddScoped(provider => + { + var bus = provider.GetRequiredService(); + var sendEndpointProvider = provider.GetRequiredService>().Value; + return Bind.Create(sendEndpointProvider.CreateMessageScheduler(bus.Topology, schedulerEndpointAddress)); + }); + + configurator.TryAddScoped(provider => + { + var bus = provider.GetRequiredService(); + var sendEndpointProvider = provider.GetRequiredService>().Value; + return Bind.Create( + new EndpointRecurringMessageScheduler(sendEndpointProvider, schedulerEndpointAddress, bus.Topology)); + }); } /// @@ -32,7 +68,7 @@ public static void AddMessageScheduler(this IRegistrationConfigurator configurat /// to an external message scheduler, such as Quartz or Hangfire. /// /// - public static void AddPublishMessageScheduler(this IRegistrationConfigurator configurator) + public static void AddPublishMessageScheduler(this IBusRegistrationConfigurator configurator) { configurator.TryAddScoped(provider => { @@ -40,6 +76,36 @@ public static void AddPublishMessageScheduler(this IRegistrationConfigurator con var publishEndpoint = provider.GetRequiredService(); return publishEndpoint.CreateMessageScheduler(bus.Topology); }); + + configurator.TryAddScoped(provider => + { + var bus = provider.GetRequiredService(); + var publishEndpoint = provider.GetRequiredService(); + return new PublishRecurringMessageScheduler(publishEndpoint, bus.Topology); + }); + } + + /// + /// Add a to the container that publishes + /// to an external message scheduler, such as Quartz or Hangfire. + /// + /// + public static void AddPublishMessageScheduler(this IBusRegistrationConfigurator configurator) + where TBus : class, IBus + { + configurator.TryAddScoped(provider => + { + var bus = provider.GetRequiredService(); + var publishEndpoint = provider.GetRequiredService>().Value; + return Bind.Create(publishEndpoint.CreateMessageScheduler(bus.Topology)); + }); + + configurator.TryAddScoped(provider => + { + var bus = provider.GetRequiredService(); + var publishEndpoint = provider.GetRequiredService>().Value; + return Bind.Create(new PublishRecurringMessageScheduler(publishEndpoint, bus.Topology)); + }); } } } diff --git a/src/MassTransit/Configuration/ObserverRegistrationExtensions.cs b/src/MassTransit/Configuration/ObserverRegistrationExtensions.cs index c27ac0311a4..3a33066dcd6 100644 --- a/src/MassTransit/Configuration/ObserverRegistrationExtensions.cs +++ b/src/MassTransit/Configuration/ObserverRegistrationExtensions.cs @@ -178,7 +178,7 @@ public static IServiceCollection AddPublishObserver(this IServiceCollection s /// public static IServiceCollection AddEventObserver(this IServiceCollection services) where T : class, IEventObserver - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { services.TryAddEnumerable(ServiceDescriptor.Singleton, T>()); return services; @@ -194,7 +194,7 @@ public static IServiceCollection AddEventObserver(this IServiceCol /// public static IServiceCollection AddEventObserver(this IServiceCollection services, Func factory) where T : class, IEventObserver - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { services.TryAddEnumerable(ServiceDescriptor.Singleton, T>(factory)); return services; @@ -209,7 +209,7 @@ public static IServiceCollection AddEventObserver(this IServiceCol /// public static IServiceCollection AddStateObserver(this IServiceCollection services) where T : class, IStateObserver - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { services.TryAddEnumerable(ServiceDescriptor.Singleton, T>()); return services; @@ -225,7 +225,7 @@ public static IServiceCollection AddStateObserver(this IServiceCol /// public static IServiceCollection AddStateObserver(this IServiceCollection services, Func factory) where T : class, IStateObserver - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { services.TryAddEnumerable(ServiceDescriptor.Singleton, T>(factory)); return services; diff --git a/src/MassTransit/Configuration/PartitionKeyConventionExtensions.cs b/src/MassTransit/Configuration/PartitionKeyConventionExtensions.cs new file mode 100644 index 00000000000..c18a7129360 --- /dev/null +++ b/src/MassTransit/Configuration/PartitionKeyConventionExtensions.cs @@ -0,0 +1,57 @@ +namespace MassTransit +{ + using System; + using Configuration; + using Transports; + + + public static class PartitionKeyConventionExtensions + { + public static void UsePartitionKeyFormatter(this IMessageSendTopologyConfigurator configurator, IMessagePartitionKeyFormatter formatter) + where T : class + { + configurator.UpdateConvention>(update => + { + update.SetFormatter(formatter); + + return update; + }); + } + + /// + /// Use the partition key formatter for the specified message type + /// + /// + /// + /// + public static void UsePartitionKeyFormatter(this ISendTopologyConfigurator configurator, IMessagePartitionKeyFormatter formatter) + where T : class + { + configurator.GetMessageTopology().UsePartitionKeyFormatter(formatter); + } + + /// + /// Use the delegate to format the partition key, using Empty if the string is null upon return + /// + /// + /// + /// + public static void UsePartitionKeyFormatter(this ISendTopologyConfigurator configurator, Func, string> formatter) + where T : class + { + configurator.GetMessageTopology().UsePartitionKeyFormatter(new DelegatePartitionKeyFormatter(formatter)); + } + + /// + /// Use the delegate to format the partition key, using Empty if the string is null upon return + /// + /// + /// + /// + public static void UsePartitionKeyFormatter(this IMessageSendTopologyConfigurator configurator, Func, string> formatter) + where T : class + { + configurator.UsePartitionKeyFormatter(new DelegatePartitionKeyFormatter(formatter)); + } + } +} diff --git a/src/MassTransit/Configuration/ProgressBufferSettings.cs b/src/MassTransit/Configuration/ProgressBufferSettings.cs new file mode 100644 index 00000000000..d29c81684f4 --- /dev/null +++ b/src/MassTransit/Configuration/ProgressBufferSettings.cs @@ -0,0 +1,27 @@ +namespace MassTransit; + +using System; + + +/// +/// Specifies the settings for the progress buffer, which defers updating the job progress until the +/// thresholds (steps or duration) have been reached. +/// +public class ProgressBufferSettings +{ + public ProgressBufferSettings() + { + UpdateLimit = 1000; + TimeLimit = TimeSpan.FromSeconds(30); + } + + /// + /// The number of progress updates reported before the value is sent to the job saga + /// + public int UpdateLimit { get; set; } + + /// + /// The time period after which the progress should be reported + /// + public TimeSpan TimeLimit { get; set; } +} diff --git a/src/MassTransit/Configuration/RawJsonSerializerConfigurationExtensions.cs b/src/MassTransit/Configuration/RawJsonSerializerConfigurationExtensions.cs index e2b0ccc2cae..93f6b392567 100644 --- a/src/MassTransit/Configuration/RawJsonSerializerConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/RawJsonSerializerConfigurationExtensions.cs @@ -1,13 +1,12 @@ namespace MassTransit { using Configuration; - using Serialization; public static class RawJsonSerializerConfigurationExtensions { /// - /// Serialize messages using the raw JSON message serializer + /// Serialize and deserialize messages using the raw JSON message serializer /// /// /// Options for the raw serializer behavior @@ -22,7 +21,21 @@ public static void UseRawJsonSerializer(this IBusFactoryConfigurator configurato } /// - /// Serialize messages using the raw JSON message serializer + /// Add support for RAW JSON message serialization and deserialization (does not change the default serializer) + /// + /// + /// Options for the raw serializer behavior + public static void AddRawJsonSerializer(this IBusFactoryConfigurator configurator, RawSerializerOptions options = + RawSerializerOptions.AddTransportHeaders | RawSerializerOptions.CopyHeaders) + { + var factory = new SystemTextJsonRawMessageSerializerFactory(options); + + configurator.AddSerializer(factory, false); + configurator.AddDeserializer(factory); + } + + /// + /// Deserialize messages using the raw JSON message serializer /// /// /// Options for the raw serializer behavior @@ -36,7 +49,7 @@ public static void UseRawJsonDeserializer(this IBusFactoryConfigurator configura } /// - /// Serialize messages using the raw JSON message serializer + /// Serialize and deserialize messages using the raw JSON message serializer /// /// /// Options for the raw serializer behavior @@ -51,7 +64,7 @@ public static void UseRawJsonSerializer(this IReceiveEndpointConfigurator config } /// - /// Serialize messages using the raw JSON message serializer + /// Deserialize messages using the raw JSON message serializer /// /// /// Options for the raw serializer behavior diff --git a/src/MassTransit/Configuration/RegistrationContextExtensions.cs b/src/MassTransit/Configuration/RegistrationContextExtensions.cs index 06838d13c0b..333d2e9f0f4 100644 --- a/src/MassTransit/Configuration/RegistrationContextExtensions.cs +++ b/src/MassTransit/Configuration/RegistrationContextExtensions.cs @@ -16,7 +16,7 @@ public static class RegistrationContextExtensions /// The registration for this bus instance /// Optional, the endpoint name formatter /// The bus factory type (depends upon the transport) - public static void ConfigureEndpoints(this IReceiveConfigurator configurator, IBusRegistrationContext registration, + public static void ConfigureEndpoints(this IBusFactoryConfigurator configurator, IBusRegistrationContext registration, IEndpointNameFormatter endpointNameFormatter = null) where T : IReceiveEndpointConfigurator { @@ -34,7 +34,44 @@ public static void ConfigureEndpoints(this IReceiveConfigurator configurat /// Filter the configured consumers, sagas, and activities /// Optional, the endpoint name formatter /// The bus factory type (depends upon the transport) - public static void ConfigureEndpoints(this IReceiveConfigurator configurator, IBusRegistrationContext registration, + public static void ConfigureEndpoints(this IBusFactoryConfigurator configurator, IBusRegistrationContext registration, + Action configureFilter, IEndpointNameFormatter endpointNameFormatter = null) + where T : IReceiveEndpointConfigurator + { + registration.ConfigureEndpoints(configurator, endpointNameFormatter, configureFilter); + } + + /// + /// Configure the endpoints for all defined consumer, saga, and activity types using an optional + /// endpoint name formatter. If no endpoint name formatter is specified and an + /// is registered in the container, it is resolved from the container. Otherwise, the + /// is used. + /// + /// The for the bus being configured + /// The registration for this bus instance + /// Optional, the endpoint name formatter + /// The bus factory type (depends upon the transport) + [Obsolete("Job Consumers no longer require a service instance. Visit https://masstransit.io/obsolete for details.")] + public static void ConfigureEndpoints(this IServiceInstanceConfigurator configurator, IBusRegistrationContext registration, + IEndpointNameFormatter endpointNameFormatter = null) + where T : IReceiveEndpointConfigurator + { + registration.ConfigureEndpoints(configurator, endpointNameFormatter); + } + + /// + /// Configure the endpoints for all defined consumer, saga, and activity types using an optional + /// endpoint name formatter. If no endpoint name formatter is specified and an + /// is registered in the container, it is resolved from the container. Otherwise, the + /// is used. + /// + /// The for the bus being configured + /// The registration for this bus instance + /// Filter the configured consumers, sagas, and activities + /// Optional, the endpoint name formatter + /// The bus factory type (depends upon the transport) + [Obsolete("Job Consumers no longer require a service instance. Visit https://masstransit.io/obsolete for details.")] + public static void ConfigureEndpoints(this IServiceInstanceConfigurator configurator, IBusRegistrationContext registration, Action configureFilter, IEndpointNameFormatter endpointNameFormatter = null) where T : IReceiveEndpointConfigurator { @@ -49,6 +86,7 @@ public static void ConfigureEndpoints(this IReceiveConfigurator configurat /// Filter the configured consumers, sagas, and activities /// Optional service instance options to start /// The bus factory type (depends upon the transport) + [Obsolete("Job Consumers no longer require a service instance. Visit https://masstransit.io/obsolete for details.")] public static void ConfigureServiceEndpoints(this IBusFactoryConfigurator configurator, IBusRegistrationContext registration, Action configureFilter, ServiceInstanceOptions options = null) where T : IReceiveEndpointConfigurator @@ -64,7 +102,7 @@ public static void ConfigureServiceEndpoints(this IBusFactoryConfigurator configurator.ServiceInstance(options, instanceConfigurator => { if (options.TryGetOptions(out JobServiceOptions jobServiceOptions)) - instanceConfigurator.ConfigureJobServiceEndpoints(jobServiceOptions); + instanceConfigurator.ConfigureJobServiceEndpoints(jobServiceOptions, registration); registration.ConfigureEndpoints(instanceConfigurator, instanceConfigurator.EndpointNameFormatter, configureFilter); }); @@ -77,6 +115,7 @@ public static void ConfigureServiceEndpoints(this IBusFactoryConfigurator /// The registration for this bus instance /// Optional service instance options to start /// The bus factory type (depends upon the transport) + [Obsolete("Job Consumers no longer require a service instance. Visit https://masstransit.io/obsolete for details.")] public static void ConfigureServiceEndpoints(this IBusFactoryConfigurator configurator, IBusRegistrationContext registration, ServiceInstanceOptions options = null) where T : IReceiveEndpointConfigurator diff --git a/src/MassTransit/Configuration/RegistrationExtensions.cs b/src/MassTransit/Configuration/RegistrationExtensions.cs index 89cf904b61b..fdba8abd0f2 100644 --- a/src/MassTransit/Configuration/RegistrationExtensions.cs +++ b/src/MassTransit/Configuration/RegistrationExtensions.cs @@ -78,20 +78,7 @@ public static void AddConsumersFromNamespaceContaining(this IRegistrationConfigu if (type.Assembly == null || type.Namespace == null) throw new ArgumentException($"The type {TypeCache.GetShortName(type)} is not in an assembly with a valid namespace", nameof(type)); - IEnumerable types; - if (filter != null) - { - bool IsAllowed(Type candidate) - { - return RegistrationMetadata.IsConsumerOrDefinition(candidate) && filter(candidate); - } - - types = FindTypesInNamespace(type, IsAllowed); - } - else - types = FindTypesInNamespace(type, RegistrationMetadata.IsConsumerOrDefinition); - - AddConsumers(configurator, types.ToArray()); + AddConsumers(configurator, filter, FindTypesInNamespace(type, RegistrationMetadata.IsConsumerOrDefinition)); } /// @@ -115,7 +102,7 @@ public static void AddConsumers(this IRegistrationConfigurator configurator, Fun { filter ??= t => true; - IEnumerable consumerTypes = types.Where(MessageTypeCache.HasConsumerInterfaces); + IEnumerable consumerTypes = types.Where(RegistrationMetadata.IsConsumer); IEnumerable consumerDefinitionTypes = types.Where(x => x.HasInterface(typeof(IConsumerDefinition<>))); var consumers = from c in consumerTypes @@ -148,6 +135,22 @@ public static ISagaRegistrationConfigurator AddSaga(this IReg return configurator.AddSaga(typeof(TDefinition), configure); } + /// + /// Adds all sagas in the specified assemblies. If using state machine sagas, they should be added first using AddSagaStateMachines. + /// + /// + /// + /// The assemblies to scan for consumers + public static void AddSagas(this IRegistrationConfigurator configurator, Func filter, params Assembly[] assemblies) + { + if (assemblies.Length == 0) + assemblies = AppDomain.CurrentDomain.GetAssemblies(); + + var types = AssemblyTypeCache.FindTypes(assemblies, RegistrationMetadata.IsSagaOrDefinition).GetAwaiter().GetResult(); + + AddSagas(configurator, filter, types.FindTypes(TypeClassification.Concrete | TypeClassification.Closed).ToArray()); + } + /// /// Adds all sagas in the specified assemblies. If using state machine sagas, they should be added first using AddSagaStateMachines. /// @@ -189,35 +192,36 @@ public static void AddSagasFromNamespaceContaining(this IRegistrationConfigurato if (type.Assembly == null || type.Namespace == null) throw new ArgumentException($"The type {TypeCache.GetShortName(type)} is not in an assembly with a valid namespace", nameof(type)); - IEnumerable types; - if (filter != null) - { - bool IsAllowed(Type candidate) - { - return RegistrationMetadata.IsSagaOrDefinition(candidate) && filter(candidate); - } - - types = FindTypesInNamespace(type, IsAllowed); - } - else - types = FindTypesInNamespace(type, RegistrationMetadata.IsSagaOrDefinition); + AddSagas(configurator, filter, FindTypesInNamespace(type, RegistrationMetadata.IsSagaOrDefinition)); + } - AddSagas(configurator, types.ToArray()); + /// + /// Adds the specified saga and saga definition types + /// + /// + /// The state machine types to add + public static void AddSagas(this IRegistrationConfigurator configurator, params Type[] types) + { + AddSagas(configurator, null, types); } /// /// Adds the specified saga types /// /// + /// /// The state machine types to add - public static void AddSagas(this IRegistrationConfigurator configurator, params Type[] types) + public static void AddSagas(this IRegistrationConfigurator configurator, Func filter, params Type[] types) { + filter ??= t => true; + IEnumerable sagaTypes = types.Where(x => x.HasInterface() && !x.HasInterface()); IEnumerable sagaDefinitionTypes = types.Where(x => x.HasInterface(typeof(ISagaDefinition<>))); var sagas = from c in sagaTypes join d in sagaDefinitionTypes on c equals d.GetClosingArgument(typeof(ISagaDefinition<>)) into dc from d in dc.DefaultIfEmpty() + where filter(c) select new { SagaType = c, @@ -290,22 +294,8 @@ public static void AddSagaStateMachinesFromNamespaceContaining(this IRegistratio if (type.Assembly == null || type.Namespace == null) throw new ArgumentException($"The type {TypeCache.GetShortName(type)} is not in an assembly with a valid namespace", nameof(type)); - IEnumerable types; - if (filter != null) - { - bool IsAllowed(Type candidate) - { - return RegistrationMetadata.IsSagaStateMachineOrDefinition(candidate) - && !RegistrationMetadata.IsFutureOrDefinition(type) - && filter(candidate); - } - - types = FindTypesInNamespace(type, IsAllowed); - } - else - types = FindTypesInNamespace(type, RegistrationMetadata.IsSagaStateMachineOrDefinition); - - AddSagaStateMachines(configurator, types.ToArray()); + AddSagaStateMachines(configurator, filter, + FindTypesInNamespace(type, x => RegistrationMetadata.IsSagaStateMachineOrDefinition(x) && !RegistrationMetadata.IsFutureOrDefinition(x))); } /// @@ -316,13 +306,28 @@ bool IsAllowed(Type candidate) /// The state machine types to add public static void AddSagaStateMachines(this IRegistrationConfigurator configurator, params Type[] types) { + AddSagaStateMachines(configurator, null, types); + } + + /// + /// Adds SagaStateMachines to the registry, using the factory method, and updates the registrar prior to registering so that the default + /// saga registrar isn't notified. + /// + /// + /// + /// The state machine types to add + public static void AddSagaStateMachines(this IRegistrationConfigurator configurator, Func filter, params Type[] types) + { + filter ??= t => true; + IEnumerable sagaTypes = types.Where(x => x.HasInterface(typeof(SagaStateMachine<>))); IEnumerable sagaDefinitionTypes = types.Where(x => x.HasInterface(typeof(ISagaDefinition<>))); var sagas = from c in sagaTypes - join d in sagaDefinitionTypes on c.GetClosingArgument(typeof(SagaStateMachine<>)) - equals d.GetClosingArguments(typeof(ISagaDefinition<>)).Single() into dc + let it = c.GetClosingArgument(typeof(SagaStateMachine<>)) + join d in sagaDefinitionTypes on it equals d.GetClosingArgument(typeof(ISagaDefinition<>)) into dc from d in dc.DefaultIfEmpty() + where filter(c) || filter(it) select new { SagaType = c, @@ -407,26 +412,15 @@ public static void AddActivitiesFromNamespaceContaining(this IRegistrationCon /// public static void AddActivitiesFromNamespaceContaining(this IRegistrationConfigurator configurator, Type type, Func filter = null) { + filter ??= _ => true; + if (type == null) throw new ArgumentNullException(nameof(type)); if (type.Assembly == null || type.Namespace == null) throw new ArgumentException($"The type {TypeCache.GetShortName(type)} is not in an assembly with a valid namespace", nameof(type)); - IEnumerable types; - if (filter != null) - { - bool IsAllowed(Type candidate) - { - return RegistrationMetadata.IsActivityOrDefinition(candidate) && filter(candidate); - } - - types = FindTypesInNamespace(type, IsAllowed); - } - else - types = FindTypesInNamespace(type, RegistrationMetadata.IsActivityOrDefinition); - - AddActivities(configurator, types.ToArray()); + AddActivities(configurator, filter, FindTypesInNamespace(type, RegistrationMetadata.IsActivityOrDefinition)); } /// @@ -436,12 +430,20 @@ bool IsAllowed(Type candidate) /// The state machine types to add public static void AddActivities(this IRegistrationConfigurator configurator, params Type[] types) { + AddActivities(configurator, null, types); + } + + public static void AddActivities(this IRegistrationConfigurator configurator, Func filter, params Type[] types) + { + filter ??= _ => true; + IEnumerable activityTypes = types.Where(x => x.HasInterface(typeof(IActivity<,>))).ToList(); IEnumerable activityDefinitionTypes = types.Where(x => x.HasInterface(typeof(IActivityDefinition<,,>))).ToList(); var activities = from c in activityTypes join d in activityDefinitionTypes on c equals d.GetClosingArguments(typeof(IActivityDefinition<,,>)).First() into dc from d in dc.DefaultIfEmpty() + where filter(c) select new { ActivityType = c, @@ -457,6 +459,7 @@ from d in dc.DefaultIfEmpty() var executeActivities = from c in executeActivityTypes join d in executeActivityDefinitionTypes on c equals d.GetClosingArguments(typeof(IExecuteActivityDefinition<,>)).First() into dc from d in dc.DefaultIfEmpty() + where filter(c) select new { ActivityType = c, @@ -490,24 +493,6 @@ public static void SetKebabCaseEndpointNameFormatter(this IRegistrationConfigura configurator.SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); } - static IEnumerable FindTypesInNamespace(Type type, Func typeFilter) - { - if (type.Namespace == null) - throw new ArgumentException("The type must have a valid namespace", nameof(type)); - - var dottedNamespace = type.Namespace + "."; - - bool Filter(Type candidate) - { - return typeFilter(candidate) - && candidate.Namespace != null - && (candidate.Namespace.StartsWith(dottedNamespace, StringComparison.OrdinalIgnoreCase) - || candidate.Namespace.Equals(type.Namespace, StringComparison.OrdinalIgnoreCase)); - } - - return AssemblyTypeCache.FindTypes(type.Assembly, TypeClassification.Concrete | TypeClassification.Closed, Filter).GetAwaiter().GetResult(); - } - /// /// Adds the consumer, allowing configuration when it is configured on an endpoint /// @@ -594,20 +579,7 @@ public static void AddFuturesFromNamespaceContaining(this IRegistrationConfigura if (type.Assembly == null || type.Namespace == null) throw new ArgumentException($"The type {TypeCache.GetShortName(type)} is not in an assembly with a valid namespace", nameof(type)); - IEnumerable types; - if (filter != null) - { - bool IsAllowed(Type candidate) - { - return RegistrationMetadata.IsFutureOrDefinition(candidate) && filter(candidate); - } - - types = FindTypesInNamespace(type, IsAllowed); - } - else - types = FindTypesInNamespace(type, RegistrationMetadata.IsFutureOrDefinition); - - AddFutures(configurator, types.ToArray()); + AddFutures(configurator, filter, FindTypesInNamespace(type, RegistrationMetadata.IsFutureOrDefinition)); } /// @@ -647,5 +619,24 @@ where filter(c) foreach (var future in futures) configurator.AddFuture(future.FutureType, future.DefinitionType); } + + static Type[] FindTypesInNamespace(Type type, Func typeFilter) + { + if (type.Namespace == null) + throw new ArgumentException("The type must have a valid namespace", nameof(type)); + + var dottedNamespace = type.Namespace + "."; + + bool Filter(Type candidate) + { + return typeFilter(candidate) + && candidate.Namespace != null + && (candidate.Namespace.StartsWith(dottedNamespace, StringComparison.OrdinalIgnoreCase) + || candidate.Namespace.Equals(type.Namespace, StringComparison.OrdinalIgnoreCase)); + } + + return AssemblyTypeCache.FindTypes(type.Assembly, TypeClassification.Concrete | TypeClassification.Closed, Filter) + .GetAwaiter().GetResult().ToArray(); + } } } diff --git a/src/MassTransit/Configuration/RetryConfigurationExtensions.cs b/src/MassTransit/Configuration/RetryConfigurationExtensions.cs index bc1146c45e9..8fd7585e5cc 100644 --- a/src/MassTransit/Configuration/RetryConfigurationExtensions.cs +++ b/src/MassTransit/Configuration/RetryConfigurationExtensions.cs @@ -9,6 +9,7 @@ namespace MassTransit public static class RetryConfigurationExtensions { + [Obsolete("Use UseMessageRetry instead. Visit https://masstransit.io/obsolete for details.")] public static void UseRetry(this IPipeConfigurator configurator, Action configure) { if (configurator == null) @@ -21,6 +22,7 @@ public static void UseRetry(this IPipeConfigurator configurator, configurator.AddPipeSpecification(specification); } + [Obsolete("Use UseMessageRetry instead. Visit https://masstransit.io/obsolete for details.")] public static void UseRetry(this IPipeConfigurator> configurator, Action configure) where T : class { @@ -34,6 +36,20 @@ public static void UseRetry(this IPipeConfigurator> configu configurator.AddPipeSpecification(specification); } + public static void UseMessageRetry(this IPipeConfigurator> configurator, Action configure) + where T : class + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + + var specification = new ConsumeContextRetryPipeSpecification, RetryConsumeContext>(Factory); + + configure?.Invoke(specification); + + configurator.AddPipeSpecification(specification); + } + + [Obsolete("Use UseMessageRetry instead. Visit https://masstransit.io/obsolete for details.")] public static void UseRetry(this IConsumePipeConfigurator configurator, Action configure) where T : class { @@ -53,6 +69,7 @@ static RetryConsumeContext Factory(ConsumeContext context, IRetryPolicy return new RetryConsumeContext(context, retryPolicy, retryContext); } + [Obsolete("Use UseMessageRetry instead. Visit https://masstransit.io/obsolete for details.")] public static void UseRetry(this IPipeConfigurator> configurator, Action configure) where TConsumer : class { @@ -74,6 +91,7 @@ static RetryConsumerConsumeContext Factory(ConsumerConsume return new RetryConsumerConsumeContext(context, retryPolicy, retryContext); } + [Obsolete("Use UseMessageRetry instead. Visit https://masstransit.io/obsolete for details.")] public static void UseRetry(this IPipeConfigurator> configurator, Action configure) where TSaga : class, ISaga { @@ -106,6 +124,7 @@ public static void UseRetry(this IPipeConfigurator configurator, Action configurator, IBusFactoryConfigurator connector, Action configure) { @@ -122,6 +141,7 @@ public static void UseRetry(this IPipeConfigurator configurator, configurator.AddPipeSpecification(specification); } + [Obsolete("Use UseMessageRetry instead. Visit https://masstransit.io/obsolete for details.")] public static void UseRetry(this IBusFactoryConfigurator configurator, Action configure) { if (configurator == null) @@ -137,6 +157,7 @@ public static void UseRetry(this IBusFactoryConfigurator configurator, Action(this IPipeConfigurator> configurator, IBusFactoryConfigurator connector, Action configure) where T : class @@ -154,6 +175,7 @@ public static void UseRetry(this IPipeConfigurator> configu configurator.AddPipeSpecification(specification); } + [Obsolete("Use UseMessageRetry instead. Visit https://masstransit.io/obsolete for details.")] public static void UseRetry(this IPipeConfigurator> configurator, IBusFactoryConfigurator connector, Action configure) where TConsumer : class @@ -172,6 +194,7 @@ public static void UseRetry(this IPipeConfigurator(this IPipeConfigurator> configurator, IBusFactoryConfigurator connector, Action configure) where TSaga : class, ISaga @@ -190,8 +213,7 @@ public static void UseRetry(this IPipeConfigurator - /// Create an immediate retry policy with the specified number of retries, with no - /// delay between attempts. + /// Create a policy that does not retry any messages. /// /// /// @@ -244,7 +266,7 @@ public static IRetryConfigurator Intervals(this IRetryConfigurator configurator, /// the number of intervals provided /// /// - /// The intervals before each subsequent retry attempt + /// The intervals in milliseconds before each subsequent retry attempt /// public static IRetryConfigurator Intervals(this IRetryConfigurator configurator, params int[] intervals) { @@ -278,7 +300,7 @@ public static IRetryConfigurator Interval(this IRetryConfigurator configurator, /// /// /// The number of retry attempts - /// The interval between each retry attempt + /// The interval in milliseconds between each retry attempt /// public static IRetryConfigurator Interval(this IRetryConfigurator configurator, int retryCount, int interval) { diff --git a/src/MassTransit/Configuration/RoutingKeyConventionExtensions.cs b/src/MassTransit/Configuration/RoutingKeyConventionExtensions.cs new file mode 100644 index 00000000000..d5fcdc61bcc --- /dev/null +++ b/src/MassTransit/Configuration/RoutingKeyConventionExtensions.cs @@ -0,0 +1,57 @@ +namespace MassTransit +{ + using System; + using Configuration; + using Transports; + + + public static class RoutingKeyConventionExtensions + { + public static void UseRoutingKeyFormatter(this IMessageSendTopologyConfigurator configurator, IMessageRoutingKeyFormatter formatter) + where T : class + { + configurator.UpdateConvention>(update => + { + update.SetFormatter(formatter); + + return update; + }); + } + + /// + /// Use the routing key formatter for the specified message type + /// + /// + /// + /// + public static void UseRoutingKeyFormatter(this ISendTopologyConfigurator configurator, IMessageRoutingKeyFormatter formatter) + where T : class + { + configurator.GetMessageTopology().UseRoutingKeyFormatter(formatter); + } + + /// + /// Use the delegate to format the routing key + /// + /// + /// + /// + public static void UseRoutingKeyFormatter(this ISendTopologyConfigurator configurator, Func, string> formatter) + where T : class + { + configurator.GetMessageTopology().UseRoutingKeyFormatter(new DelegateRoutingKeyFormatter(formatter)); + } + + /// + /// Use the delegate to format the routing key + /// + /// + /// + /// + public static void UseRoutingKeyFormatter(this IMessageSendTopologyConfigurator configurator, Func, string> formatter) + where T : class + { + configurator.UseRoutingKeyFormatter(new DelegateRoutingKeyFormatter(formatter)); + } + } +} diff --git a/src/MassTransit/Configuration/SagaStateMachineReceiveEndpointExtensions.cs b/src/MassTransit/Configuration/SagaStateMachineReceiveEndpointExtensions.cs index 9c65b8b25b6..f9d61ac0759 100644 --- a/src/MassTransit/Configuration/SagaStateMachineReceiveEndpointExtensions.cs +++ b/src/MassTransit/Configuration/SagaStateMachineReceiveEndpointExtensions.cs @@ -24,7 +24,7 @@ public static void StateMachineSaga(this IReceiveEndpointConfigurator throw new ArgumentNullException(nameof(stateMachine)); if (repository == null) throw new ArgumentNullException(nameof(repository)); - var stateMachineConfigurator = new StateMachineSagaConfigurator(stateMachine, repository, configurator); + var stateMachineConfigurator = new MassTransitStateMachine.StateMachineSagaConfigurator(stateMachine, repository, configurator); configure?.Invoke(stateMachineConfigurator); @@ -35,7 +35,7 @@ public static ConnectHandle ConnectStateMachineSaga(this IConsumePipe ISagaRepository repository, Action>? configure = null) where TInstance : class, SagaStateMachineInstance { - var connector = new StateMachineConnector(stateMachine); + var connector = new MassTransitStateMachine.StateMachineConnector(stateMachine); ISagaSpecification specification = connector.CreateSagaSpecification(); diff --git a/src/MassTransit/Configuration/SetSerializerSendConventionExtensions.cs b/src/MassTransit/Configuration/SetSerializerSendConventionExtensions.cs new file mode 100644 index 00000000000..41125de9189 --- /dev/null +++ b/src/MassTransit/Configuration/SetSerializerSendConventionExtensions.cs @@ -0,0 +1,52 @@ +namespace MassTransit +{ + using System; + using System.Net.Mime; + using Configuration; + + + public static class SetSerializerSendConventionExtensions + { + /// + /// Use the message serializer identified by the specified content type to serialize messages of this type + /// + /// The message type + /// + /// + public static void UseSerializer(this IMessageSendTopologyConfigurator configurator, ContentType contentType) + where T : class + { + configurator.AddOrUpdateConvention>( + () => + { + var convention = new SetSerializerMessageSendTopologyConvention(); + convention.SetSerializer(contentType); + + return convention; + }, + update => + { + update.SetSerializer(contentType); + + return update; + }); + } + + /// + /// Use the message serializer identified by the specified content type to serialize messages of this type + /// + /// The message type + /// + /// + public static void UseSerializer(this IMessageSendTopologyConfigurator configurator, string contentType) + where T : class + { + if (contentType == null) + throw new ArgumentNullException(nameof(contentType)); + + var type = new ContentType(contentType); + + UseSerializer(configurator, type); + } + } +} diff --git a/src/MassTransit/Configuration/SnakeCaseEndpointNameFormatter.cs b/src/MassTransit/Configuration/SnakeCaseEndpointNameFormatter.cs index cf5b98ad7f5..566bab001c0 100644 --- a/src/MassTransit/Configuration/SnakeCaseEndpointNameFormatter.cs +++ b/src/MassTransit/Configuration/SnakeCaseEndpointNameFormatter.cs @@ -43,6 +43,18 @@ public SnakeCaseEndpointNameFormatter(string prefix, bool includeNamespace) Separator = _separator.ToString(); } + /// + /// Snake case endpoint formatter, which uses underscores between words + /// + /// Prefix to start the name, should match the casing of the formatter (such as Dev or PreProd) + public SnakeCaseEndpointNameFormatter(string prefix) + : base(prefix, false) + { + _separator = SnakeCaseSeparator; + + Separator = _separator.ToString(); + } + /// /// Snake case endpoint formatter, which uses underscores between words /// diff --git a/src/MassTransit/Consumers/Batching/BatchCollector.cs b/src/MassTransit/Consumers/Batching/BatchCollector.cs index 37257615d3f..31ee7c3a4a1 100644 --- a/src/MassTransit/Consumers/Batching/BatchCollector.cs +++ b/src/MassTransit/Consumers/Batching/BatchCollector.cs @@ -10,9 +10,9 @@ public class BatchCollector : IBatchCollector where TMessage : class { - readonly ChannelExecutor _collector; + readonly TaskExecutor _collector; readonly IPipe>> _consumerPipe; - readonly ChannelExecutor _dispatcher; + readonly TaskExecutor _dispatcher; readonly BatchOptions _options; BatchConsumer _currentConsumer; @@ -21,8 +21,8 @@ public BatchCollector(BatchOptions options, IPipe _options = options; _consumerPipe = consumerPipe; - _collector = new ChannelExecutor(1); - _dispatcher = new ChannelExecutor(options.ConcurrencyLimit); + _collector = new TaskExecutor(); + _dispatcher = new TaskExecutor(options.ConcurrencyLimit); } public ValueTask DisposeAsync() @@ -79,10 +79,10 @@ public class BatchCollector : IBatchCollector where TMessage : class { - readonly ChannelExecutor _collector; + readonly TaskExecutor _collector; readonly IDictionary> _collectors; readonly IPipe>> _consumerPipe; - readonly ChannelExecutor _dispatcher; + readonly TaskExecutor _dispatcher; readonly IGroupKeyProvider _keyProvider; readonly BatchOptions _options; BatchConsumer _currentConsumer; @@ -93,8 +93,8 @@ public BatchCollector(BatchOptions options, IPipe _consumerPipe = consumerPipe; _keyProvider = keyProvider; - _collector = new ChannelExecutor(1); - _dispatcher = new ChannelExecutor(options.ConcurrencyLimit); + _collector = new TaskExecutor(); + _dispatcher = new TaskExecutor(options.ConcurrencyLimit); _collectors = new Dictionary>(); } diff --git a/src/MassTransit/Consumers/Batching/BatchConsumer.cs b/src/MassTransit/Consumers/Batching/BatchConsumer.cs index bc4fb6e8d12..235d7436993 100644 --- a/src/MassTransit/Consumers/Batching/BatchConsumer.cs +++ b/src/MassTransit/Consumers/Batching/BatchConsumer.cs @@ -17,22 +17,22 @@ public class BatchConsumer : { readonly TaskCompletionSource _completed; readonly IPipe>> _consumerPipe; - readonly ChannelExecutor _dispatcher; - readonly ChannelExecutor _executor; + readonly TaskExecutor _dispatcher; + readonly TaskExecutor _executor; readonly DateTime _firstMessage; - readonly SortedDictionary> _messages; + readonly Dictionary _messages; readonly BatchOptions _options; readonly Timer _timer; Activity _currentActivity; DateTime _lastMessage; ILogContext _logContext; - public BatchConsumer(BatchOptions options, ChannelExecutor executor, ChannelExecutor dispatcher, IPipe>> consumerPipe) + public BatchConsumer(BatchOptions options, TaskExecutor executor, TaskExecutor dispatcher, IPipe>> consumerPipe) { _executor = executor; _consumerPipe = consumerPipe; _dispatcher = dispatcher; - _messages = new SortedDictionary>(); + _messages = new Dictionary(); _completed = TaskUtil.GetTask(); _firstMessage = DateTime.UtcNow; _options = options; @@ -42,12 +42,16 @@ public BatchConsumer(BatchOptions options, ChannelExecutor executor, ChannelExec public bool IsCompleted { get; private set; } - async Task IConsumer.Consume(ConsumeContext context) + public async Task Consume(ConsumeContext context) { try { await _completed.Task.ConfigureAwait(false); } + catch (OperationCanceledException) when (context.CancellationToken.IsCancellationRequested) + { + throw new OperationCanceledException(context.CancellationToken); + } catch { // if this message was marked as successfully delivered, do not fault it @@ -83,7 +87,19 @@ public Task Add(ConsumeContext context, Activity currentActivity) _currentActivity = currentActivity; var messageId = context.MessageId ?? NewId.NextGuid(); - _messages.Add(messageId, context); + + ulong? sequenceNumber = context.ReceiveContext.TryGetPayload(out var payload) ? payload.SequenceNumber : null; + ulong sentTimeAsSequenceFallback() => (ulong)(context.SentTime ?? context.ReceiveContext.GetSentTime() ?? DateTime.UtcNow).Ticks; + + var batchEntry = new BatchEntry( + context, + sequenceNumber ?? sentTimeAsSequenceFallback(), + () => RemoveCanceledMessage(messageId)); + + if (!_messages.ContainsKey(messageId)) + _messages.Add(messageId, batchEntry); + else + batchEntry.Unregister(); if (_options.TimeLimitStart == BatchTimeLimitStart.FromLast) _timer.Change(_options.TimeLimit, TimeSpan.FromMilliseconds(-1)); @@ -96,12 +112,39 @@ public Task Add(ConsumeContext context, Activity currentActivity) List> messageList = GetMessageBatchInOrder(); - return _dispatcher.Push(() => Deliver(context, messageList, BatchCompletionMode.Size)); + return messageList.Count == 0 + ? Task.CompletedTask + : _dispatcher.Push(() => Deliver(context, messageList, BatchCompletionMode.Size)); } return Task.CompletedTask; } + void RemoveCanceledMessage(Guid messageId) + { + Task.Run(() => _executor.Push(() => + { + if (IsCompleted) + return Task.CompletedTask; + + if (_messages.TryGetValue(messageId, out var batchEntry)) + { + batchEntry.Unregister(); + + _messages.Remove(messageId); + + if (_messages.Count == 0) + { + IsCompleted = true; + + _completed.TrySetCanceled(); + } + } + + return Task.CompletedTask; + })); + } + bool IsReadyToDeliver(ConsumeContext context) { if (context.GetRetryAttempt() > 0) @@ -124,6 +167,9 @@ async Task Deliver(ConsumeContext context, IReadOnlyList> GetMessageBatchInOrder() { - return _messages.Values - .OrderBy(x => x.SentTime ?? x.MessageId?.ToNewId().Timestamp ?? default) - .ToList(); + return _messages.Values.OrderBy(x => x.Index).Select(x => x.Context).ToList(); + } + + + readonly struct BatchEntry + { + public readonly ConsumeContext Context; + public readonly ulong Index; + readonly CancellationTokenRegistration _registration; + + public BatchEntry(ConsumeContext context, ulong index, Action canceled) + { + Context = context; + Index = index; + + if (context.CancellationToken.CanBeCanceled) + _registration = context.CancellationToken.Register(() => canceled()); + } + + public void Unregister() + { + _registration.Dispose(); + } } } } diff --git a/src/MassTransit/Consumers/Configuration/ActivityPipeConfiguratorExtensions.cs b/src/MassTransit/Consumers/Configuration/ActivityPipeConfiguratorExtensions.cs index 0c2e5c5af43..31e4a848b79 100644 --- a/src/MassTransit/Consumers/Configuration/ActivityPipeConfiguratorExtensions.cs +++ b/src/MassTransit/Consumers/Configuration/ActivityPipeConfiguratorExtensions.cs @@ -14,8 +14,8 @@ public static void AddPipeSpecification(this IPipeConfigu throw new ArgumentNullException(nameof(specification)); IPipeSpecification> filterSpecification = - new SplitFilterPipeSpecification, ExecuteActivityContext>(specification, - (input, context) => input, context => context); + new PipeConfigurator>.SplitFilterPipeSpecification>( + specification, (input, context) => input, context => context); configurator.AddPipeSpecification(filterSpecification); } @@ -29,7 +29,7 @@ public static void AddPipeSpecification(this IPipeConfigurator< throw new ArgumentNullException(nameof(specification)); IPipeSpecification> filterSpecification = - new SplitFilterPipeSpecification, CompensateActivityContext>(specification, + new PipeConfigurator>.SplitFilterPipeSpecification>(specification, (input, context) => input, context => context); configurator.AddPipeSpecification(filterSpecification); diff --git a/src/MassTransit/Consumers/Configuration/ConsumerConnector.cs b/src/MassTransit/Consumers/Configuration/ConsumerConnector.cs index 46fe594b2c7..a1e3eb97edd 100644 --- a/src/MassTransit/Consumers/Configuration/ConsumerConnector.cs +++ b/src/MassTransit/Consumers/Configuration/ConsumerConnector.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; + using Metadata; using Util; @@ -10,15 +11,14 @@ public class ConsumerConnector : IConsumerConnector where T : class { - readonly IEnumerable> _connectors; + readonly List> _connectors; public ConsumerConnector() { - if (MessageTypeCache.HasSagaInterfaces) + if (RegistrationMetadata.IsSaga(typeof(T))) throw new ConfigurationException("A saga cannot be registered as a consumer"); - _connectors = Consumes() - .ToList(); + _connectors = Consumes().ToList(); } public IEnumerable Connectors => _connectors; @@ -26,7 +26,7 @@ public ConsumerConnector() ConnectHandle IConsumerConnector.ConnectConsumer(IConsumePipeConnector consumePipe, IConsumerFactory consumerFactory, IConsumerSpecification specification) { - var handles = new List(); + var handles = new List(_connectors.Count); try { foreach (IConsumerMessageConnector connector in _connectors.Cast>()) diff --git a/src/MassTransit/Consumers/Configuration/ConsumerConnectorCache.cs b/src/MassTransit/Consumers/Configuration/ConsumerConnectorCache.cs index 2037ac59f4f..1adf1d38704 100644 --- a/src/MassTransit/Consumers/Configuration/ConsumerConnectorCache.cs +++ b/src/MassTransit/Consumers/Configuration/ConsumerConnectorCache.cs @@ -2,7 +2,6 @@ { using System; using System.Collections.Concurrent; - using System.Threading; using Consumer; @@ -14,8 +13,7 @@ public class ConsumerConnectorCache : ConsumerConnectorCache() { - _connector = new Lazy>(() => new ConsumerConnector(), - LazyThreadSafetyMode.PublicationOnly); + _connector = new Lazy>(() => new ConsumerConnector()); } public static IConsumerConnector Connector => Cached.Instance.Value.Connector; @@ -25,8 +23,7 @@ public class ConsumerConnectorCache : static class Cached { - internal static readonly Lazy Instance = new Lazy( - () => new ConsumerConnectorCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy Instance = new Lazy(() => new ConsumerConnectorCache()); } } diff --git a/src/MassTransit/Consumers/Configuration/ConsumerFactoryConfiguratorExtensions.cs b/src/MassTransit/Consumers/Configuration/ConsumerFactoryConfiguratorExtensions.cs index b3b9c81b014..715bba49ab7 100644 --- a/src/MassTransit/Consumers/Configuration/ConsumerFactoryConfiguratorExtensions.cs +++ b/src/MassTransit/Consumers/Configuration/ConsumerFactoryConfiguratorExtensions.cs @@ -20,8 +20,8 @@ public static IEnumerable ValidateConsumer(this ISp IEnumerable warningForMessages = ConsumerMetadataCache .ConsumerTypes - .Where(x => !IntrospectionExtensions.GetTypeInfo(x.MessageType).IsInterface) - .Where(x => !(HasProtectedDefaultConstructor(x.MessageType) || HasSinglePublicConstructor(x.MessageType))) + .Where(x => !x.MessageType.IsInterface) + .Where(x => !HasProtectedDefaultConstructor(x.MessageType)) .Select(x => $"The {TypeCache.GetShortName(x.MessageType)} message should have a public or protected default constructor." + " Without an available constructor, MassTransit will initialize new message instances" @@ -45,15 +45,8 @@ public static IEnumerable Validate(this IConsumerFa static bool HasProtectedDefaultConstructor(Type type) { - return type.GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance) + return type.GetConstructors(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) .Any(constructorInfo => !constructorInfo.GetParameters().Any()); } - - static bool HasSinglePublicConstructor(Type type) - { - return type.GetConstructors(BindingFlags.Public | BindingFlags.Instance) - .All(constructorInfo => !constructorInfo.GetParameters().Any()) - && type.GetConstructors().Length == 1; - } } } diff --git a/src/MassTransit/Consumers/Configuration/ConsumerMetadataCache.cs b/src/MassTransit/Consumers/Configuration/ConsumerMetadataCache.cs index 5d399b2045f..d5ef55b4b95 100644 --- a/src/MassTransit/Consumers/Configuration/ConsumerMetadataCache.cs +++ b/src/MassTransit/Consumers/Configuration/ConsumerMetadataCache.cs @@ -2,7 +2,6 @@ namespace MassTransit.Configuration { using System; using System.Linq; - using System.Threading; public class ConsumerMetadataCache : @@ -27,8 +26,7 @@ public class ConsumerMetadataCache : static class Cached { - internal static readonly Lazy> Metadata = new Lazy>( - () => new ConsumerMetadataCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy> Metadata = new Lazy>(() => new ConsumerMetadataCache()); } } } diff --git a/src/MassTransit/Consumers/Configuration/Conventions/AsyncConsumerMessageConvention.cs b/src/MassTransit/Consumers/Configuration/Conventions/AsyncConsumerMessageConvention.cs index cbe885d08c7..d3dc4c0de57 100644 --- a/src/MassTransit/Consumers/Configuration/Conventions/AsyncConsumerMessageConvention.cs +++ b/src/MassTransit/Consumers/Configuration/Conventions/AsyncConsumerMessageConvention.cs @@ -2,7 +2,6 @@ namespace MassTransit.Configuration { using System.Collections.Generic; using System.Linq; - using System.Reflection; /// @@ -15,18 +14,18 @@ public class AsyncConsumerMessageConvention : { public IEnumerable GetMessageTypes() { - var typeInfo = typeof(T).GetTypeInfo(); - if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IConsumer<>)) + var consumerType = typeof(T); + if (consumerType.IsGenericType && consumerType.GetGenericTypeDefinition() == typeof(IConsumer<>)) { - var interfaceType = new ConsumerInterfaceType(typeof(T).GetGenericArguments()[0], typeof(T)); + var interfaceType = new ConsumerInterfaceType(consumerType.GetGenericArguments()[0], consumerType); if (MessageTypeCache.IsValidMessageType(interfaceType.MessageType)) yield return interfaceType; } - IEnumerable types = typeof(T).GetInterfaces() - .Where(x => IntrospectionExtensions.GetTypeInfo(x).IsGenericType) + IEnumerable types = consumerType.GetInterfaces() + .Where(x => x.IsGenericType) .Where(x => x.GetGenericTypeDefinition() == typeof(IConsumer<>)) - .Select(x => new ConsumerInterfaceType(x.GetGenericArguments()[0], typeof(T))) + .Select(x => new ConsumerInterfaceType(x.GetGenericArguments()[0], consumerType)) .Where(x => MessageTypeCache.IsValidMessageType(x.MessageType)); foreach (var type in types) diff --git a/src/MassTransit/Consumers/Configuration/Conventions/BatchConsumerMessageConvention.cs b/src/MassTransit/Consumers/Configuration/Conventions/BatchConsumerMessageConvention.cs index f1b2a63fc8c..a47a2585b84 100644 --- a/src/MassTransit/Consumers/Configuration/Conventions/BatchConsumerMessageConvention.cs +++ b/src/MassTransit/Consumers/Configuration/Conventions/BatchConsumerMessageConvention.cs @@ -3,7 +3,6 @@ namespace MassTransit.Configuration using System; using System.Collections.Generic; using System.Linq; - using System.Reflection; using Internals; @@ -17,20 +16,20 @@ public class BatchConsumerMessageConvention : { public IEnumerable GetMessageTypes() { - var typeInfo = typeof(T).GetTypeInfo(); - if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IConsumer<>)) + var consumerType = typeof(T); + if (consumerType.IsGenericType && consumerType.GetGenericTypeDefinition() == typeof(IConsumer<>)) { - var messageType = typeof(T).GetGenericArguments()[0]; + var messageType = consumerType.GetGenericArguments()[0]; if (messageType.ClosesType(typeof(Batch<>), out Type[] batchTypes)) { - var interfaceType = new BatchConsumerInterfaceType(messageType, batchTypes[0], typeof(T)); + var interfaceType = new BatchConsumerInterfaceType(messageType, batchTypes[0], consumerType); if (MessageTypeCache.IsValidMessageType(interfaceType.MessageType)) yield return interfaceType; } } - IEnumerable types = typeof(T).GetInterfaces() - .Where(x => IntrospectionExtensions.GetTypeInfo(x).IsGenericType) + IEnumerable types = consumerType.GetInterfaces() + .Where(x => x.IsGenericType) .Where(x => x.GetGenericTypeDefinition() == typeof(IConsumer<>)) .Select(x => new { @@ -44,7 +43,7 @@ public IEnumerable GetMessageTypes() BatchMessageType = x.MessageType, MessageType = x.MessageType.GetClosingArgument(typeof(Batch<>)) }) - .Select(x => new BatchConsumerInterfaceType(x.BatchMessageType, x.MessageType, typeof(T))) + .Select(x => new BatchConsumerInterfaceType(x.BatchMessageType, x.MessageType, consumerType)) .Where(x => MessageTypeCache.IsValidMessageType(x.MessageType)); foreach (var type in types) diff --git a/src/MassTransit/Consumers/Configuration/Conventions/ConsumerConventionCache.cs b/src/MassTransit/Consumers/Configuration/Conventions/ConsumerConventionCache.cs index dc1cf262558..c3f4429f1de 100644 --- a/src/MassTransit/Consumers/Configuration/Conventions/ConsumerConventionCache.cs +++ b/src/MassTransit/Consumers/Configuration/Conventions/ConsumerConventionCache.cs @@ -53,7 +53,7 @@ public static IEnumerable GetConventions() static class Cached { - internal static readonly IList Registered = new List(); + internal static readonly List Registered = new List(); } } } diff --git a/src/MassTransit/Consumers/Configuration/Conventions/JobConsumerMessageConvention.cs b/src/MassTransit/Consumers/Configuration/Conventions/JobConsumerMessageConvention.cs index 9d1b0d0ea30..4712e9788b7 100644 --- a/src/MassTransit/Consumers/Configuration/Conventions/JobConsumerMessageConvention.cs +++ b/src/MassTransit/Consumers/Configuration/Conventions/JobConsumerMessageConvention.cs @@ -2,7 +2,6 @@ namespace MassTransit.Configuration { using System.Collections.Generic; using System.Linq; - using System.Reflection; using Internals; @@ -12,18 +11,18 @@ public class JobConsumerMessageConvention : { public IEnumerable GetMessageTypes() { - var typeInfo = typeof(T).GetTypeInfo(); - if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IJobConsumer<>)) + var consumerType = typeof(T); + if (consumerType.IsGenericType && consumerType.GetGenericTypeDefinition() == typeof(IJobConsumer<>)) { - var interfaceType = new JobInterfaceType(typeof(T).GetGenericArguments()[0], typeof(T)); + var interfaceType = new JobInterfaceType(consumerType.GetGenericArguments()[0], consumerType); if (MessageTypeCache.IsValidMessageType(interfaceType.MessageType)) yield return interfaceType; } - IEnumerable types = typeof(T).GetInterfaces() - .Where(x => IntrospectionExtensions.GetTypeInfo(x).IsGenericType) + IEnumerable types = consumerType.GetInterfaces() + .Where(x => x.IsGenericType) .Where(x => x.GetGenericTypeDefinition() == typeof(IJobConsumer<>)) - .Select(x => new JobInterfaceType(x.GetGenericArguments()[0], typeof(T))) + .Select(x => new JobInterfaceType(x.GetGenericArguments()[0], consumerType)) .Where(x => MessageTypeCache.IsValidMessageType(x.MessageType)) .Where(x => !x.MessageType.ClosesType(typeof(Batch<>))); diff --git a/src/MassTransit/Consumers/Configuration/InstanceConnector.cs b/src/MassTransit/Consumers/Configuration/InstanceConnector.cs index 985b99ec52c..071b1840c15 100644 --- a/src/MassTransit/Consumers/Configuration/InstanceConnector.cs +++ b/src/MassTransit/Consumers/Configuration/InstanceConnector.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; + using Metadata; using Util; @@ -10,11 +11,11 @@ public class InstanceConnector : IInstanceConnector where TConsumer : class { - readonly IEnumerable> _connectors; + readonly List> _connectors; public InstanceConnector() { - if (MessageTypeCache.HasSagaInterfaces) + if (RegistrationMetadata.IsSaga(typeof(TConsumer))) throw new ConfigurationException("A saga cannot be registered as a consumer"); _connectors = Consumes() @@ -24,7 +25,7 @@ public InstanceConnector() public ConnectHandle ConnectInstance(IConsumePipeConnector pipeConnector, T instance, IConsumerSpecification specification) where T : class { - var handles = new List(); + var handles = new List(_connectors.Count); try { foreach (IInstanceMessageConnector connector in _connectors.Cast>()) diff --git a/src/MassTransit/Consumers/Configuration/InstanceConnectorCache.cs b/src/MassTransit/Consumers/Configuration/InstanceConnectorCache.cs index a5737047486..3d882bf8d7c 100644 --- a/src/MassTransit/Consumers/Configuration/InstanceConnectorCache.cs +++ b/src/MassTransit/Consumers/Configuration/InstanceConnectorCache.cs @@ -2,7 +2,6 @@ { using System; using System.Collections.Concurrent; - using System.Threading; public class InstanceConnectorCache : @@ -13,8 +12,7 @@ public class InstanceConnectorCache : InstanceConnectorCache() { - _connector = new Lazy>(() => new InstanceConnector(), - LazyThreadSafetyMode.PublicationOnly); + _connector = new Lazy>(() => new InstanceConnector()); } public static IInstanceConnector Connector => InstanceCache.Cached.Value.Connector; @@ -24,8 +22,7 @@ public class InstanceConnectorCache : static class InstanceCache { - internal static readonly Lazy> Cached = new Lazy>( - () => new InstanceConnectorCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy> Cached = new Lazy>(() => new InstanceConnectorCache()); } } @@ -49,8 +46,7 @@ public static IInstanceConnector GetInstanceConnector(Type type) static class InstanceCache { internal static readonly Lazy>> Cached = - new Lazy>>( - () => new ConcurrentDictionary>(), LazyThreadSafetyMode.PublicationOnly); + new Lazy>>(() => new ConcurrentDictionary>()); } } } diff --git a/src/MassTransit/Consumers/Configuration/JobConsumerMessageConnector.cs b/src/MassTransit/Consumers/Configuration/JobConsumerMessageConnector.cs index a9a8d518b1b..222e5d41a3f 100644 --- a/src/MassTransit/Consumers/Configuration/JobConsumerMessageConnector.cs +++ b/src/MassTransit/Consumers/Configuration/JobConsumerMessageConnector.cs @@ -15,14 +15,12 @@ public class JobConsumerMessageConnector : readonly IConsumerConnector _finalizeJobConsumerConnector; readonly IConsumerConnector _startJobConsumerConnector; readonly IConsumerConnector _submitJobConsumerConnector; - readonly IConsumerConnector _superviseJobConsumerConnector; public JobConsumerMessageConnector() { _submitJobConsumerConnector = ConsumerConnectorCache>.Connector; _startJobConsumerConnector = ConsumerConnectorCache>.Connector; _finalizeJobConsumerConnector = ConsumerConnectorCache>.Connector; - _superviseJobConsumerConnector = ConsumerConnectorCache.Connector; } public Type MessageType => typeof(TJob); @@ -35,33 +33,29 @@ public IConsumerMessageSpecification CreateConsumerMessageSpecificati public ConnectHandle ConnectConsumer(IConsumePipeConnector consumePipe, IConsumerFactory consumerFactory, IConsumerSpecification specification) { - var jobServiceOptions = specification.Options(); - var jobService = jobServiceOptions.JobService; - - if (jobService == null || jobServiceOptions.InstanceEndpointConfigurator == null) + if (!specification.TryGetOptions(out JobServiceSettings settings) || settings.JobService == null || settings.InstanceEndpointConfigurator == null) { throw new ConfigurationException( - "The job service must be configured prior to configuring a job consumer, using either ConfigureJobServiceEndpoints or ConfigureJobService"); + "The job service must be configured when adding job consumers. See https://masstransit.io/documentation/patterns/job-consumers"); } var options = specification.Options>(); - var jobTypeId = jobService.GetJobTypeId(); + var jobTypeId = settings.JobService.GetJobTypeId(); IConsumerMessageSpecification messageSpecification = specification.GetMessageSpecification(); - var jobSpecification = messageSpecification as JobConsumerMessageSpecification; - if (jobSpecification == null) + if (!(messageSpecification is JobConsumerMessageSpecification jobSpecification)) throw new ArgumentException("The consumer specification did not match the message specification type"); var submitJobHandle = ConnectSubmitJobConsumer(consumePipe, jobSpecification.SubmitJobSpecification, options, jobTypeId); IPipe> jobPipe = CreateJobPipe(consumerFactory, specification); - var startJobHandle = ConnectStartJobConsumer(consumePipe, jobSpecification.StartJobSpecification, options, jobTypeId, jobService, jobPipe); + var startJobHandle = ConnectStartJobConsumer(consumePipe, jobSpecification.StartJobSpecification, options, jobTypeId, settings.JobService, jobPipe); - ConfigureInstanceStartJobConsumer(jobServiceOptions.InstanceEndpointConfigurator, options, jobTypeId, jobService, jobPipe); + ConfigureInstanceStartJobConsumer(settings.InstanceEndpointConfigurator, options, jobTypeId, settings.JobService, jobPipe); - var finalizeJobHandle = ConnectFinalizeJobConsumer(consumePipe, jobSpecification.FinalizeJobSpecification, options, jobTypeId, jobService); + var finalizeJobHandle = ConnectFinalizeJobConsumer(consumePipe, jobSpecification.FinalizeJobSpecification, jobTypeId); return new MultipleConnectHandle(submitJobHandle, startJobHandle, finalizeJobHandle); } @@ -103,7 +97,7 @@ ConnectHandle ConnectStartJobConsumer(IConsumePipeConnector consumePipe, IConsum } ConnectHandle ConnectFinalizeJobConsumer(IConsumePipeConnector consumePipe, IConsumerSpecification> specification, - JobOptions options, Guid jobTypeId, IJobService jobService) + Guid jobTypeId) { var consumerFactory = new DelegateConsumerFactory>(() => new FinalizeJobConsumer(jobTypeId, TypeCache.ShortName)); @@ -112,8 +106,7 @@ ConnectHandle ConnectFinalizeJobConsumer(IConsumePipeConnector consumePipe, ICon } static void ConfigureInstanceStartJobConsumer(IReceiveEndpointConfigurator configurator, JobOptions options, Guid jobTypeId, - IJobService jobService, - IPipe> pipe) + IJobService jobService, IPipe> pipe) { var consumerFactory = new DelegateConsumerFactory>(() => new StartJobConsumer(jobService, options, jobTypeId, pipe)); diff --git a/src/MassTransit/Consumers/Configuration/ObserverConnectorCache.cs b/src/MassTransit/Consumers/Configuration/ObserverConnectorCache.cs index c8c667b2ed6..d0a8594df6a 100644 --- a/src/MassTransit/Consumers/Configuration/ObserverConnectorCache.cs +++ b/src/MassTransit/Consumers/Configuration/ObserverConnectorCache.cs @@ -1,7 +1,6 @@ namespace MassTransit.Configuration { using System; - using System.Threading; public class ObserverConnectorCache : @@ -22,8 +21,8 @@ public class ObserverConnectorCache : static class InstanceCache { - internal static readonly Lazy> Cached = new Lazy>( - () => new ObserverConnectorCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy> Cached = + new Lazy>(() => new ObserverConnectorCache()); } } } diff --git a/src/MassTransit/Consumers/Configuration/ServiceInstanceConfigurator.cs b/src/MassTransit/Consumers/Configuration/ServiceInstanceConfigurator.cs index 9efd4686102..7374c9a5d93 100644 --- a/src/MassTransit/Consumers/Configuration/ServiceInstanceConfigurator.cs +++ b/src/MassTransit/Consumers/Configuration/ServiceInstanceConfigurator.cs @@ -11,7 +11,7 @@ public class ServiceInstanceConfigurator : { readonly ServiceInstanceOptions _options; - public ServiceInstanceConfigurator(IBusFactoryConfigurator configurator, ServiceInstanceOptions options, + public ServiceInstanceConfigurator(IReceiveConfigurator configurator, ServiceInstanceOptions options, TEndpointConfigurator instanceEndpointConfigurator) { if (instanceEndpointConfigurator == null) @@ -24,10 +24,10 @@ public ServiceInstanceConfigurator(IBusFactoryConfigurator InstanceEndpointConfigurator.InputAddress; - IBusFactoryConfigurator IServiceInstanceConfigurator.BusConfigurator => BusConfigurator; + IReceiveConfigurator IServiceInstanceConfigurator.BusConfigurator => BusConfigurator; IReceiveEndpointConfigurator IServiceInstanceConfigurator.InstanceEndpointConfigurator => InstanceEndpointConfigurator; - public IBusFactoryConfigurator BusConfigurator { get; } + public IReceiveConfigurator BusConfigurator { get; } public TEndpointConfigurator InstanceEndpointConfigurator { get; } public void AddSpecification(ISpecification specification) diff --git a/src/MassTransit/Contexts/Context/BaseConsumeContext.cs b/src/MassTransit/Contexts/Context/BaseConsumeContext.cs index 5fa8b645ab1..bab2b27e1ae 100644 --- a/src/MassTransit/Contexts/Context/BaseConsumeContext.cs +++ b/src/MassTransit/Contexts/Context/BaseConsumeContext.cs @@ -1,7 +1,9 @@ +#nullable enable namespace MassTransit.Context { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading; using System.Threading.Tasks; @@ -15,14 +17,8 @@ public abstract class BaseConsumeContext : PublishEndpoint, ConsumeContext { - protected BaseConsumeContext(ReceiveContext receiveContext) - : base(receiveContext?.PublishEndpointProvider) - { - ReceiveContext = receiveContext; - } - protected BaseConsumeContext(ReceiveContext receiveContext, SerializerContext serializerContext) - : base(receiveContext?.PublishEndpointProvider) + : base(receiveContext.PublishEndpointProvider) { ReceiveContext = receiveContext; SerializerContext = serializerContext; @@ -32,7 +28,7 @@ protected BaseConsumeContext(ReceiveContext receiveContext, SerializerContext se public abstract bool HasPayloadType(Type payloadType); - public abstract bool TryGetPayload(out T payload) + public abstract bool TryGetPayload([NotNullWhen(true)] out T? payload) where T : class; public abstract T GetOrAddPayload(PayloadFactory payloadFactory) @@ -63,7 +59,7 @@ public abstract T AddOrUpdatePayload(PayloadFactory addFactory, UpdatePayl public abstract IEnumerable SupportedMessageTypes { get; } public abstract bool HasMessageType(Type messageType); - public abstract bool TryGetMessage(out ConsumeContext consumeContext) + public abstract bool TryGetMessage([NotNullWhen(true)] out ConsumeContext? consumeContext) where T : class; public virtual Task RespondAsync(T message) @@ -186,11 +182,12 @@ public virtual async Task NotifyFaulted(ConsumeContext context, TimeSpan d { switch (exception) { - case OperationCanceledException canceledException when canceledException.CancellationToken == context.CancellationToken: + case OperationCanceledException canceled when canceled.CancellationToken == context.CancellationToken: break; default: - await GenerateFault(context, exception).ConfigureAwait(false); + if (!context.CancellationToken.IsCancellationRequested) + await GenerateFault(context, exception).ConfigureAwait(false); break; } @@ -204,7 +201,7 @@ public ConnectHandle ConnectSendObserver(ISendObserver observer) public abstract void AddConsumeTask(Task task); - Task RespondInternal(T message, IPipe> pipe = null) + Task RespondInternal(T message, IPipe>? pipe = null) where T : class { Task sendEndpointTask = this.GetResponseEndpoint(); @@ -213,7 +210,7 @@ Task RespondInternal(T message, IPipe> pipe = null) var sendEndpoint = sendEndpointTask.Result; return pipe.IsNotEmpty() - ? sendEndpoint.Send(message, pipe, CancellationToken) + ? sendEndpoint.Send(message, pipe!, CancellationToken) : sendEndpoint.Send(message, CancellationToken); } @@ -222,7 +219,7 @@ async Task RespondInternalAsync() var sendEndpoint = await sendEndpointTask.ConfigureAwait(false); if (pipe.IsNotEmpty()) - await sendEndpoint.Send(message, pipe, CancellationToken).ConfigureAwait(false); + await sendEndpoint.Send(message, pipe!, CancellationToken).ConfigureAwait(false); else await sendEndpoint.Send(message, CancellationToken).ConfigureAwait(false); } @@ -230,7 +227,7 @@ async Task RespondInternalAsync() return RespondInternalAsync(); } - Task RespondInternal(object values, IPipe> pipe = null) + Task RespondInternal(object values, IPipe>? pipe = null) where T : class { Task sendEndpointTask = this.GetResponseEndpoint(); @@ -239,7 +236,7 @@ Task RespondInternal(object values, IPipe> pipe = null) var sendEndpoint = sendEndpointTask.Result; return pipe.IsNotEmpty() - ? sendEndpoint.Send(values, pipe, CancellationToken) + ? sendEndpoint.Send(values, pipe!, CancellationToken) : sendEndpoint.Send(values, CancellationToken); } @@ -248,7 +245,7 @@ async Task RespondInternalAsync() var sendEndpoint = await sendEndpointTask.ConfigureAwait(false); if (pipe.IsNotEmpty()) - await sendEndpoint.Send(values, pipe, CancellationToken).ConfigureAwait(false); + await sendEndpoint.Send(values, pipe!, CancellationToken).ConfigureAwait(false); else await sendEndpoint.Send(values, CancellationToken).ConfigureAwait(false); } @@ -270,7 +267,7 @@ protected virtual async Task GenerateFault(ConsumeContext context, Excepti var faultEndpoint = await faultContext.GetFaultEndpoint().ConfigureAwait(false); - await faultEndpoint.Send(fault, faultPipe, CancellationToken).ConfigureAwait(false); + await faultEndpoint.Send(fault, faultPipe, context.CancellationToken).ConfigureAwait(false); } } @@ -307,9 +304,9 @@ public Task Send(SendContext> context) context.CorrelationId = _context.CorrelationId; context.RequestId = _context.RequestId; - if (_context.TryGetPayload(out ConsumeRetryContext consumeRetryContext) && consumeRetryContext.RetryCount > 0) + if (_context.TryGetPayload(out ConsumeRetryContext? consumeRetryContext) && consumeRetryContext.RetryCount > 0) context.Headers.Set(MessageHeaders.FaultRetryCount, consumeRetryContext.RetryCount); - else if (_context.TryGetPayload(out RetryContext retryContext) && retryContext.RetryCount > 0) + else if (_context.TryGetPayload(out RetryContext? retryContext) && retryContext.RetryCount > 0) context.Headers.Set(MessageHeaders.FaultRetryCount, retryContext.RetryCount); var redeliveryCount = _context.Headers.Get(MessageHeaders.RedeliveryCount); diff --git a/src/MassTransit/Contexts/Context/BaseCourierContext.cs b/src/MassTransit/Contexts/Context/BaseCourierContext.cs index 600cfdc7910..2aaf15352ef 100644 --- a/src/MassTransit/Contexts/Context/BaseCourierContext.cs +++ b/src/MassTransit/Contexts/Context/BaseCourierContext.cs @@ -26,10 +26,10 @@ protected BaseCourierContext(ConsumeContext consumeContext) _executionId = newId.ToGuid(); _timestamp = newId.Timestamp; - // TODO move this to the deserializer! One for JSON, one for SystemTextJson RoutingSlip = new SanitizedRoutingSlip(consumeContext); - Publisher = new RoutingSlipEventPublisher(this, RoutingSlip); + // ReSharper disable once VirtualMemberCallInConstructor + Publisher = new RoutingSlipEventPublisher(this, RoutingSlip, CancellationToken); } protected IRoutingSlipEventPublisher Publisher { get; } @@ -43,5 +43,17 @@ protected BaseCourierContext(ConsumeContext consumeContext) RoutingSlip ConsumeContext.Message => RoutingSlip; public abstract string ActivityName { get; } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Contexts/Context/BatchConsumeContext.cs b/src/MassTransit/Contexts/Context/BatchConsumeContext.cs index b83acc4fa3e..4c114be9722 100644 --- a/src/MassTransit/Contexts/Context/BatchConsumeContext.cs +++ b/src/MassTransit/Contexts/Context/BatchConsumeContext.cs @@ -39,5 +39,17 @@ public Task NotifyFaulted(TimeSpan duration, string consumerType, Exception exce { return Task.CompletedTask; } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Contexts/Context/BindContextProxy.cs b/src/MassTransit/Contexts/Context/BindContextProxy.cs index 325b389cffb..3bbc0696ead 100644 --- a/src/MassTransit/Contexts/Context/BindContextProxy.cs +++ b/src/MassTransit/Contexts/Context/BindContextProxy.cs @@ -1,7 +1,6 @@ namespace MassTransit.Context { using System; - using System.Reflection; using System.Threading; @@ -29,7 +28,7 @@ public BindContextProxy(TLeft left, TRight source) public bool HasPayloadType(Type payloadType) { - return payloadType.GetTypeInfo().IsInstanceOfType(Right) || Left.HasPayloadType(payloadType); + return payloadType.IsInstanceOfType(Right) || Left.HasPayloadType(payloadType); } public bool TryGetPayload(out T payload) diff --git a/src/MassTransit/Contexts/Context/ConsumeContextProxy.cs b/src/MassTransit/Contexts/Context/ConsumeContextProxy.cs index 81a6f21046e..6b74ff1b6cb 100644 --- a/src/MassTransit/Contexts/Context/ConsumeContextProxy.cs +++ b/src/MassTransit/Contexts/Context/ConsumeContextProxy.cs @@ -2,6 +2,7 @@ { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; @@ -82,7 +83,8 @@ public override bool HasPayloadType(Type payloadType) /// /// /// - public override bool TryGetPayload(out T payload) + public override bool TryGetPayload([NotNullWhen(true)] out T payload) + where T : class { if (this is T context) { @@ -163,5 +165,17 @@ public virtual Task NotifyFaulted(TimeSpan duration, string consumerType, Except { return NotifyFaulted(this, duration, consumerType, exception); } + + public void Method1() + { + } + + public void Method2() + { + } + + public void Method3() + { + } } } diff --git a/src/MassTransit/Contexts/Context/ConsumeContextScope.cs b/src/MassTransit/Contexts/Context/ConsumeContextScope.cs index bf5fd7c5940..c6a4bee815e 100644 --- a/src/MassTransit/Contexts/Context/ConsumeContextScope.cs +++ b/src/MassTransit/Contexts/Context/ConsumeContextScope.cs @@ -1,7 +1,7 @@ namespace MassTransit.Context { using System; - using System.Reflection; + using System.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Payloads; @@ -45,10 +45,11 @@ IPayloadCache PayloadCache public override bool HasPayloadType(Type payloadType) { - return payloadType.GetTypeInfo().IsInstanceOfType(this) || PayloadCache.HasPayloadType(payloadType) || _context.HasPayloadType(payloadType); + return payloadType.IsInstanceOfType(this) || PayloadCache.HasPayloadType(payloadType) || _context.HasPayloadType(payloadType); } - public override bool TryGetPayload(out T payload) + public override bool TryGetPayload([NotNullWhen(true)] out T payload) + where T : class { if (this is T context) { @@ -126,5 +127,17 @@ public virtual Task NotifyFaulted(TimeSpan duration, string consumerType, Except { return NotifyFaulted(this, duration, consumerType, exception); } + + public void Method1() + { + } + + public void Method2() + { + } + + public void Method3() + { + } } } diff --git a/src/MassTransit/Contexts/Context/ConsumeMessageSchedulerContext.cs b/src/MassTransit/Contexts/Context/ConsumeMessageSchedulerContext.cs index 7a05d3ac8ec..d9b7ff2b4fe 100644 --- a/src/MassTransit/Contexts/Context/ConsumeMessageSchedulerContext.cs +++ b/src/MassTransit/Contexts/Context/ConsumeMessageSchedulerContext.cs @@ -151,9 +151,9 @@ public Task> ScheduleSend(DateTime scheduledTime, object return _scheduler.Value.ScheduleSend(_inputAddress, scheduledTime, values, pipe, cancellationToken); } - Task IMessageScheduler.CancelScheduledSend(Uri destinationAddress, Guid tokenId) + Task IMessageScheduler.CancelScheduledSend(Uri destinationAddress, Guid tokenId, CancellationToken cancellationToken) { - return _scheduler.Value.CancelScheduledSend(destinationAddress, tokenId); + return _scheduler.Value.CancelScheduledSend(destinationAddress, tokenId, cancellationToken); } public Task> SchedulePublish(DateTime scheduledTime, T message, CancellationToken cancellationToken) @@ -218,15 +218,15 @@ public Task> SchedulePublish(DateTime scheduledTime, obje return _scheduler.Value.SchedulePublish(scheduledTime, values, pipe, cancellationToken); } - public Task CancelScheduledPublish(Guid tokenId) + public Task CancelScheduledPublish(Guid tokenId, CancellationToken cancellationToken) where T : class { - return _scheduler.Value.CancelScheduledPublish(tokenId); + return _scheduler.Value.CancelScheduledPublish(tokenId, cancellationToken); } - public Task CancelScheduledPublish(Type messageType, Guid tokenId) + public Task CancelScheduledPublish(Type messageType, Guid tokenId, CancellationToken cancellationToken) { - return _scheduler.Value.CancelScheduledPublish(messageType, tokenId); + return _scheduler.Value.CancelScheduledPublish(messageType, tokenId, cancellationToken); } } } diff --git a/src/MassTransit/Contexts/Context/ConsumerConsumeContextProxy.cs b/src/MassTransit/Contexts/Context/ConsumerConsumeContextProxy.cs index e36c3777957..2c7185ed244 100644 --- a/src/MassTransit/Contexts/Context/ConsumerConsumeContextProxy.cs +++ b/src/MassTransit/Contexts/Context/ConsumerConsumeContextProxy.cs @@ -18,5 +18,17 @@ public ConsumerConsumeContextProxy(ConsumeContext context, TConsumer c } public TConsumer Consumer { get; } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Contexts/Context/ConsumerConsumeContextScope.cs b/src/MassTransit/Contexts/Context/ConsumerConsumeContextScope.cs index b628b251c39..6e46f36b835 100644 --- a/src/MassTransit/Contexts/Context/ConsumerConsumeContextScope.cs +++ b/src/MassTransit/Contexts/Context/ConsumerConsumeContextScope.cs @@ -24,5 +24,17 @@ public ConsumerConsumeContextScope(ConsumeContext context, TConsumer c } public TConsumer Consumer { get; } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Contexts/Context/CorrelationIdConsumeContextProxy.cs b/src/MassTransit/Contexts/Context/CorrelationIdConsumeContextProxy.cs index ea5308930b9..3f1814786b2 100644 --- a/src/MassTransit/Contexts/Context/CorrelationIdConsumeContextProxy.cs +++ b/src/MassTransit/Contexts/Context/CorrelationIdConsumeContextProxy.cs @@ -20,5 +20,17 @@ public CorrelationIdConsumeContextProxy(ConsumeContext context, Guid c } public override Guid? CorrelationId => _correlationId; + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Contexts/Context/CourierContextProxy.cs b/src/MassTransit/Contexts/Context/CourierContextProxy.cs index 81db6afe743..23f721c5d8f 100644 --- a/src/MassTransit/Contexts/Context/CourierContextProxy.cs +++ b/src/MassTransit/Contexts/Context/CourierContextProxy.cs @@ -21,5 +21,17 @@ protected CourierContextProxy(CourierContext courierContext) Guid CourierContext.TrackingNumber => _courierContext.TrackingNumber; Guid CourierContext.ExecutionId => _courierContext.ExecutionId; string CourierContext.ActivityName => _courierContext.ActivityName; + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Contexts/Context/CourierContextScope.cs b/src/MassTransit/Contexts/Context/CourierContextScope.cs index c48039be16a..4c280396ad2 100644 --- a/src/MassTransit/Contexts/Context/CourierContextScope.cs +++ b/src/MassTransit/Contexts/Context/CourierContextScope.cs @@ -21,5 +21,17 @@ protected CourierContextScope(CourierContext courierContext, params object[] pay Guid CourierContext.TrackingNumber => _courierContext.TrackingNumber; Guid CourierContext.ExecutionId => _courierContext.ExecutionId; string CourierContext.ActivityName => _courierContext.ActivityName; + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Contexts/Context/DefaultSagaConsumeContext.cs b/src/MassTransit/Contexts/Context/DefaultSagaConsumeContext.cs index 26a4fd15417..883ea87a913 100644 --- a/src/MassTransit/Contexts/Context/DefaultSagaConsumeContext.cs +++ b/src/MassTransit/Contexts/Context/DefaultSagaConsumeContext.cs @@ -27,5 +27,17 @@ public Task SetCompleted() return Task.CompletedTask; } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Contexts/Context/DeserializerConsumeContext.cs b/src/MassTransit/Contexts/Context/DeserializerConsumeContext.cs index aa06e6ff1f4..51ed6407a8e 100644 --- a/src/MassTransit/Contexts/Context/DeserializerConsumeContext.cs +++ b/src/MassTransit/Contexts/Context/DeserializerConsumeContext.cs @@ -1,6 +1,8 @@ +#nullable enable namespace MassTransit.Context { using System; + using System.Diagnostics.CodeAnalysis; using System.Threading.Tasks; using Util; @@ -10,12 +12,6 @@ public abstract class DeserializerConsumeContext : { readonly PendingTaskCollection _consumeTasks; - protected DeserializerConsumeContext(ReceiveContext receiveContext) - : base(receiveContext) - { - _consumeTasks = new PendingTaskCollection(4); - } - protected DeserializerConsumeContext(ReceiveContext receiveContext, SerializerContext serializerContext) : base(receiveContext, serializerContext) { @@ -40,7 +36,8 @@ public override bool HasPayloadType(Type payloadType) /// /// /// - public override bool TryGetPayload(out T payload) + public override bool TryGetPayload([NotNullWhen(true)] out T? payload) + where T : class { if (this is T context) { diff --git a/src/MassTransit/Contexts/Context/HostCompensateActivityContext.cs b/src/MassTransit/Contexts/Context/HostCompensateActivityContext.cs index 8c4882b72a3..665495f7cb1 100644 --- a/src/MassTransit/Contexts/Context/HostCompensateActivityContext.cs +++ b/src/MassTransit/Contexts/Context/HostCompensateActivityContext.cs @@ -15,5 +15,17 @@ public HostCompensateActivityContext(TActivity activity, CompensateContext } TActivity CompensateActivityContext.Activity => _activity; + + public void Method7() + { + } + + public void Method8() + { + } + + public void Method9() + { + } } } diff --git a/src/MassTransit/Contexts/Context/HostExecuteActivityContext.cs b/src/MassTransit/Contexts/Context/HostExecuteActivityContext.cs index da82254dc03..01d0e8b3d0f 100644 --- a/src/MassTransit/Contexts/Context/HostExecuteActivityContext.cs +++ b/src/MassTransit/Contexts/Context/HostExecuteActivityContext.cs @@ -15,5 +15,17 @@ public HostExecuteActivityContext(TActivity activity, ExecuteContext } TActivity ExecuteActivityContext.Activity => _activity; + + public void Method7() + { + } + + public void Method8() + { + } + + public void Method9() + { + } } } diff --git a/src/MassTransit/Contexts/Context/MessageConsumeContext.cs b/src/MassTransit/Contexts/Context/MessageConsumeContext.cs index 5a3c4584941..f997271c7e1 100644 --- a/src/MassTransit/Contexts/Context/MessageConsumeContext.cs +++ b/src/MassTransit/Contexts/Context/MessageConsumeContext.cs @@ -2,7 +2,6 @@ namespace MassTransit.Context { using System; using System.Collections.Generic; - using System.Reflection; using System.Threading; using System.Threading.Tasks; using Initializers; @@ -35,7 +34,7 @@ public Task NotifyFaulted(TimeSpan duration, string consumerType, Exception exce public bool HasPayloadType(Type payloadType) { - return payloadType.GetTypeInfo().IsInstanceOfType(this) || _context.HasPayloadType(payloadType); + return payloadType.IsInstanceOfType(this) || _context.HasPayloadType(payloadType); } public bool TryGetPayload(out T payload) diff --git a/src/MassTransit/Contexts/Context/MessageSendContext.cs b/src/MassTransit/Contexts/Context/MessageSendContext.cs index d3b67f03f1e..d485cfa20fb 100644 --- a/src/MassTransit/Contexts/Context/MessageSendContext.cs +++ b/src/MassTransit/Contexts/Context/MessageSendContext.cs @@ -35,6 +35,8 @@ public MessageSendContext(TMessage message, CancellationToken cancellationToken MessageId = messageId.ToGuid(); SentTime = messageId.Timestamp; + SupportedMessageTypes = MessageTypeCache.MessageTypeNames; + _body = new Lazy(() => GetMessageBody()); } @@ -84,6 +86,8 @@ public IMessageSerializer Serializer public ISerialization Serialization { get; set; } + public string[] SupportedMessageTypes { get; set; } + public long? BodyLength => _body.IsValueCreated ? _body.Value.Length : default; public SendContext CreateProxy(T message) @@ -137,6 +141,23 @@ protected static string ReadString(IReadOnlyDictionary propertie return defaultValue; } + protected static string[] ReadStringArray(IReadOnlyDictionary properties, string key) + { + if (properties.TryGetValue(key, out var value)) + { + if (value is string text) + return text.Split(';'); + + if (value is byte[] bytes) + { + text = Encoding.UTF8.GetString(bytes); + return text.Split(';'); + } + } + + return Array.Empty(); + } + protected static TimeSpan? ReadTimeSpan(IReadOnlyDictionary properties, string key, TimeSpan? defaultValue = null) { var value = ReadString(properties, key); @@ -179,6 +200,13 @@ protected static byte ReadByte(IReadOnlyDictionary properties, s return longValue.HasValue ? (int)longValue.Value : defaultValue; } + protected static short? ReadShort(IReadOnlyDictionary properties, string key, short? defaultValue = null) + { + var longValue = ReadLong(properties, key); + + return longValue.HasValue ? (short)longValue.Value : defaultValue; + } + protected static long? ReadLong(IReadOnlyDictionary properties, string key, long? defaultValue = null) { if (properties.TryGetValue(key, out var value)) diff --git a/src/MassTransit/Contexts/Context/NoLockReceiveContext.cs b/src/MassTransit/Contexts/Context/NoLockReceiveContext.cs new file mode 100644 index 00000000000..be35ddc1d2e --- /dev/null +++ b/src/MassTransit/Contexts/Context/NoLockReceiveContext.cs @@ -0,0 +1,33 @@ +#nullable enable +namespace MassTransit.Context +{ + using System; + using System.Threading.Tasks; + using Transports; + + + public class NoLockReceiveContext : + ReceiveLockContext + { + public static readonly ReceiveLockContext Instance = new NoLockReceiveContext(); + + NoLockReceiveContext() + { + } + + public Task Complete() + { + return Task.CompletedTask; + } + + public Task Faulted(Exception exception) + { + return Task.CompletedTask; + } + + public Task ValidateLockStatus() + { + return Task.CompletedTask; + } + } +} diff --git a/src/MassTransit/Contexts/Context/RetryCompensateContext.cs b/src/MassTransit/Contexts/Context/RetryCompensateContext.cs index 8ef9be5b0fc..0b42644b2ac 100644 --- a/src/MassTransit/Contexts/Context/RetryCompensateContext.cs +++ b/src/MassTransit/Contexts/Context/RetryCompensateContext.cs @@ -10,8 +10,8 @@ public class RetryCompensateContext : where TLog : class { readonly CompensateContext _context; - readonly IRetryPolicy _retryPolicy; readonly CompensationResult _existingResult; + readonly IRetryPolicy _retryPolicy; public RetryCompensateContext(CompensateContext context, IRetryPolicy retryPolicy, RetryContext retryContext) : base(context) diff --git a/src/MassTransit/Contexts/Context/SagaConsumeContextProxy.cs b/src/MassTransit/Contexts/Context/SagaConsumeContextProxy.cs index 316cbb5537a..f867c3824f2 100644 --- a/src/MassTransit/Contexts/Context/SagaConsumeContextProxy.cs +++ b/src/MassTransit/Contexts/Context/SagaConsumeContextProxy.cs @@ -33,5 +33,17 @@ public Task SetCompleted() } public bool IsCompleted => _sagaContext.IsCompleted; + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/Contexts/Context/TransportReceiveContext.cs b/src/MassTransit/Contexts/Context/TransportReceiveContext.cs new file mode 100644 index 00000000000..57d8910938b --- /dev/null +++ b/src/MassTransit/Contexts/Context/TransportReceiveContext.cs @@ -0,0 +1,14 @@ +namespace MassTransit.Context +{ + using System.Collections.Generic; + + + public interface TransportReceiveContext + { + /// + /// Write any transport-specific properties to the dictionary so that they can be + /// restored on subsequent outgoing messages (scheduled) + /// + IDictionary GetTransportProperties(); + } +} diff --git a/src/MassTransit/Courier/CompensateActivityHost.cs b/src/MassTransit/Courier/CompensateActivityHost.cs index b01879271a5..a2cfc397f4d 100644 --- a/src/MassTransit/Courier/CompensateActivityHost.cs +++ b/src/MassTransit/Courier/CompensateActivityHost.cs @@ -21,9 +21,11 @@ public CompensateActivityHost(IPipe> compensatePipe) public async Task Send(ConsumeContext context, IPipe> next) { + var timer = Stopwatch.StartNew(); + StartedActivity? activity = LogContext.Current?.StartCompensateActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartActivityCompensateInstrument(context, timer); - var timer = Stopwatch.StartNew(); try { CompensateContext compensateContext = new HostCompensateContext(context); @@ -40,32 +42,46 @@ public async Task Send(ConsumeContext context, IPipe.ShortName, exception).ConfigureAwait(false); + + activity?.AddExceptionEvent(exception); + + instrument?.AddException(exception); + + await compensateContext.Failed(exception).Evaluate().ConfigureAwait(false); } await context.NotifyConsumed(timer.Elapsed, TypeCache.ShortName).ConfigureAwait(false); await next.Send(context).ConfigureAwait(false); } - catch (OperationCanceledException exception) + catch (Exception exception) when ((exception is OperationCanceledException || exception.GetBaseException() is OperationCanceledException) + && !context.CancellationToken.IsCancellationRequested) { await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); - if (exception.CancellationToken == context.CancellationToken) - throw; + activity?.AddExceptionEvent(exception); + + instrument?.AddException(exception); throw new ConsumerCanceledException($"The operation was canceled by the activity: {TypeCache.ShortName}"); } - catch (Exception ex) + catch (Exception exception) { - await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, ex).ConfigureAwait(false); + await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); + + activity?.AddExceptionEvent(exception); + + instrument?.AddException(exception); + throw; } finally { activity?.Stop(); + instrument?.Stop(); } } diff --git a/src/MassTransit/Courier/ExecuteActivityHost.cs b/src/MassTransit/Courier/ExecuteActivityHost.cs index 3c8171a9d65..90bbed1eba3 100644 --- a/src/MassTransit/Courier/ExecuteActivityHost.cs +++ b/src/MassTransit/Courier/ExecuteActivityHost.cs @@ -23,9 +23,11 @@ public ExecuteActivityHost(IPipe> executePipe, Uri co public async Task Send(ConsumeContext context, IPipe> next) { + var timer = Stopwatch.StartNew(); + StartedActivity? activity = LogContext.Current?.StartExecuteActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartActivityExecuteInstrument(context, timer); - var timer = Stopwatch.StartNew(); try { ExecuteContext executeContext = new HostExecuteContext(_compensateAddress, context); @@ -47,6 +49,11 @@ public async Task Send(ConsumeContext context, IPipe.ShortName, exception).ConfigureAwait(false); + + activity?.AddExceptionEvent(exception); + instrument?.AddException(exception); + await executeContext.Result.Evaluate().ConfigureAwait(false); } @@ -54,23 +61,31 @@ public async Task Send(ConsumeContext context, IPipe.ShortName, exception).ConfigureAwait(false); - if (exception.CancellationToken == context.CancellationToken) - throw; + activity?.AddExceptionEvent(exception); + + instrument?.AddException(exception); throw new ConsumerCanceledException($"The operation was canceled by the activity: {TypeCache.ShortName}"); } - catch (Exception ex) + catch (Exception exception) { - await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, ex).ConfigureAwait(false); + await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); + + activity?.AddExceptionEvent(exception); + + instrument?.AddException(exception); + throw; } finally { activity?.Stop(); + instrument?.Stop(); } } diff --git a/src/MassTransit/Courier/IRoutingSlipSendEndpointTarget.cs b/src/MassTransit/Courier/IRoutingSlipSendEndpointTarget.cs index 264bfadbaab..9cc9596ef55 100644 --- a/src/MassTransit/Courier/IRoutingSlipSendEndpointTarget.cs +++ b/src/MassTransit/Courier/IRoutingSlipSendEndpointTarget.cs @@ -2,7 +2,7 @@ { using System; using Contracts; - using MassTransit.Serialization; + using Serialization; public interface IRoutingSlipSendEndpointTarget diff --git a/src/MassTransit/Courier/RoutingSlipBuilderSendEndpoint.cs b/src/MassTransit/Courier/RoutingSlipBuilderSendEndpoint.cs index 4d370322c4a..22c08b84284 100644 --- a/src/MassTransit/Courier/RoutingSlipBuilderSendEndpoint.cs +++ b/src/MassTransit/Courier/RoutingSlipBuilderSendEndpoint.cs @@ -169,7 +169,7 @@ public RoutingSlipSendContext(T message, CancellationToken cancellationToken, Ur public MessageEnvelope GetMessageEnvelope() { - var envelope = new JsonMessageEnvelope(this, Message, MessageTypeCache.MessageTypeNames); + var envelope = new JsonMessageEnvelope(this, Message); return envelope; } diff --git a/src/MassTransit/Courier/RoutingSlipRequestInfo.cs b/src/MassTransit/Courier/RoutingSlipRequestInfo.cs new file mode 100644 index 00000000000..6de87b0214b --- /dev/null +++ b/src/MassTransit/Courier/RoutingSlipRequestInfo.cs @@ -0,0 +1,35 @@ +#nullable enable +namespace MassTransit.Courier +{ + using System; + using System.Collections.Generic; + + + public readonly struct RoutingSlipRequestInfo + where T : class + { + public readonly Guid RequestId; + public readonly Uri ResponseAddress; + public readonly Uri? FaultAddress; + public readonly Uri? RequestAddress; + public readonly int? RetryAttempt; + public readonly T Request; + + public RoutingSlipRequestInfo(IObjectDeserializer context, IDictionary variables) + { + Request = context.GetValue(variables, RoutingSlipRequestVariableNames.Request) + ?? throw new ArgumentException($"Routing Slip Request variable was not found: {RoutingSlipRequestVariableNames.Request}"); + + RequestId = context.GetValue(variables, RoutingSlipRequestVariableNames.RequestId) + ?? throw new ArgumentException($"Routing Slip RequestId variable was not found: {RoutingSlipRequestVariableNames.RequestId}"); + + ResponseAddress = context.GetValue(variables, RoutingSlipRequestVariableNames.ResponseAddress) + ?? throw new ArgumentException($"Routing Slip ResponseAddress variable was not found: {RoutingSlipRequestVariableNames.ResponseAddress}"); + + FaultAddress = context.GetValue(variables, RoutingSlipRequestVariableNames.FaultAddress); + + RequestAddress = context.GetValue(variables, RoutingSlipRequestVariableNames.RequestAddress); + RetryAttempt = context.GetValue(variables, RoutingSlipRequestVariableNames.RetryAttempt); + } + } +} diff --git a/src/MassTransit/Courier/RoutingSlipRequestProxy.cs b/src/MassTransit/Courier/RoutingSlipRequestProxy.cs index 931ddcddd89..3eff8d843bc 100644 --- a/src/MassTransit/Courier/RoutingSlipRequestProxy.cs +++ b/src/MassTransit/Courier/RoutingSlipRequestProxy.cs @@ -1,5 +1,6 @@ namespace MassTransit.Courier { + using System; using System.Threading.Tasks; using Contracts; @@ -12,20 +13,36 @@ public virtual async Task Consume(ConsumeContext context) { var builder = new RoutingSlipBuilder(NewId.NextGuid()); - builder.AddSubscription(context.ReceiveContext.InputAddress, RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); + builder.AddSubscription(GetResponseEndpointAddress(context), RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted); - builder.AddVariable("RequestId", context.RequestId); - builder.AddVariable("ResponseAddress", context.ResponseAddress); - builder.AddVariable("FaultAddress", context.FaultAddress); - builder.AddVariable("Request", context.Message); + builder.AddVariable(RoutingSlipRequestVariableNames.RequestId, context.RequestId); + builder.AddVariable(RoutingSlipRequestVariableNames.ResponseAddress, context.ResponseAddress); + builder.AddVariable(RoutingSlipRequestVariableNames.FaultAddress, context.FaultAddress); + builder.AddVariable(RoutingSlipRequestVariableNames.Request, context.Message); + builder.AddVariable(RoutingSlipRequestVariableNames.RequestAddress, context.ReceiveContext.InputAddress); + + var retryAttempt = context.Headers.Get(MessageHeaders.Request.RoutingSlipRetryCount); + if (retryAttempt > 0) + builder.AddVariable(RoutingSlipRequestVariableNames.RetryAttempt, retryAttempt); await BuildRoutingSlip(builder, context); var routingSlip = builder.Build(); - await context.Execute(routingSlip).ConfigureAwait(false); + await context.Execute(routingSlip, context.CancellationToken).ConfigureAwait(false); } protected abstract Task BuildRoutingSlip(RoutingSlipBuilder builder, ConsumeContext request); + + /// + /// By default, returns the input address of the request consumer which assumes the response consumer is on the same receive endpoint. + /// Override to specify the endpoint address of the response consumer if it is configured on a separate receive endpoint. + /// + /// + /// + protected virtual Uri GetResponseEndpointAddress(ConsumeContext context) + { + return context.ReceiveContext.InputAddress; + } } } diff --git a/src/MassTransit/Courier/RoutingSlipRequestVariableNames.cs b/src/MassTransit/Courier/RoutingSlipRequestVariableNames.cs new file mode 100644 index 00000000000..8fcb672d59f --- /dev/null +++ b/src/MassTransit/Courier/RoutingSlipRequestVariableNames.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace MassTransit.Courier +{ + public static class RoutingSlipRequestVariableNames + { + public static string RequestId = "RequestId"; + public static string Request = "Request"; + public static string FaultAddress = "FaultAddress"; + public static string ResponseAddress = "ResponseAddress"; + public static string RequestAddress = "RequestAddress"; + public static string RetryAttempt = "RetryAttempt"; + } +} diff --git a/src/MassTransit/Courier/RoutingSlipResponseProxy.cs b/src/MassTransit/Courier/RoutingSlipResponseProxy.cs index 63cb001dac9..c28d9d7a5e3 100644 --- a/src/MassTransit/Courier/RoutingSlipResponseProxy.cs +++ b/src/MassTransit/Courier/RoutingSlipResponseProxy.cs @@ -8,50 +8,89 @@ using Events; - public abstract class RoutingSlipResponseProxy : + public abstract class RoutingSlipResponseProxy : IConsumer, IConsumer where TRequest : class where TResponse : class - where TFaultResponse : class + where TFault : class { + protected virtual IRetryPolicy RetryPolicy => null; + public virtual async Task Consume(ConsumeContext context) { - var request = context.GetVariable("Request"); - - Guid? requestId = context.GetVariable("RequestId") - ?? throw new ArgumentException($"The RequestId variable was not found on the completed routing slip: {context.Message.TrackingNumber}"); + var requestInfo = new RoutingSlipRequestInfo(context.SerializerContext, context.Message.Variables); - var responseAddress = context.GetVariable("ResponseAddress") - ?? throw new ArgumentException($"The ResponseAddress variable was not found on the completed routing slip: {context.Message.TrackingNumber}"); + var endpoint = await context.GetResponseEndpoint(requestInfo.ResponseAddress, requestInfo.RequestId).ConfigureAwait(false); - var endpoint = await context.GetResponseEndpoint(responseAddress, requestId).ConfigureAwait(false); - - var response = await CreateResponseMessage(context, request); + var response = await CreateResponseMessage(context, requestInfo.Request).ConfigureAwait(false); await endpoint.Send(response).ConfigureAwait(false); } public virtual async Task Consume(ConsumeContext context) { - var request = context.GetVariable("Request"); + var requestInfo = new RoutingSlipRequestInfo(context.SerializerContext, context.Message.Variables); - Guid? requestId = context.GetVariable("RequestId") - ?? throw new ArgumentException($"The RequestId variable was not found on the faulted routing slip: {context.Message.TrackingNumber}"); + if (CanRetry(requestInfo, context, out TimeSpan? delay)) + { + var retryAttempt = requestInfo.RetryAttempt ?? 0; - var faultAddress = context.GetVariable("FaultAddress") ?? context.GetVariable("ResponseAddress") - ?? throw new ArgumentException($"The (Fault|Response)Address was not found on the faulted routing slip: {context.Message.TrackingNumber}"); + var schedulerContext = context.GetPayload(); - var endpoint = await context.GetFaultEndpoint(faultAddress, requestId).ConfigureAwait(false); + await schedulerContext.ScheduleSend(requestInfo.RequestAddress, delay.Value, requestInfo.Request, x => + { + x.RequestId = requestInfo.RequestId; + x.ResponseAddress = requestInfo.ResponseAddress; + x.FaultAddress = requestInfo.FaultAddress; + x.Delay = delay; + x.Headers.Set(MessageHeaders.Request.RoutingSlipRetryCount, retryAttempt + 1); + }).ConfigureAwait(false); - var response = await CreateFaultedResponseMessage(context, request, requestId.Value); + return; + } - await endpoint.Send(response).ConfigureAwait(false); + var endpoint = await context.GetFaultEndpoint(requestInfo.FaultAddress ?? requestInfo.ResponseAddress, requestInfo.RequestId) + .ConfigureAwait(false); + + var response = await CreateFaultedResponseMessage(context, requestInfo.Request, requestInfo.RequestId); + + await endpoint.Send(response, x => + { + if (requestInfo.RetryAttempt > 0) + x.Headers.Set(MessageHeaders.FaultRetryCount, requestInfo.RetryAttempt.Value); + }).ConfigureAwait(false); + } + + bool CanRetry(RoutingSlipRequestInfo requestInfo, ConsumeContext context, out TimeSpan? delay) + { + delay = default; + + var retryPolicy = RetryPolicy; + if (retryPolicy == null) + return false; + + RetryPolicyContext> policyContext = retryPolicy.CreatePolicyContext(context); + + var exception = new RoutingSlipRequestFaultedException(context.Message); + + if (!policyContext.CanRetry(exception, out RetryContext> retryContext)) + return false; + + var retryAttempt = requestInfo.RetryAttempt ?? 0; + for (var retryIndex = 0; retryIndex < retryAttempt; retryIndex++) + { + if (!retryContext.CanRetry(exception, out retryContext)) + return false; + } + + delay = retryContext.Delay ?? TimeSpan.Zero; + return true; } protected abstract Task CreateResponseMessage(ConsumeContext context, TRequest request); - protected abstract Task CreateFaultedResponseMessage(ConsumeContext context, TRequest request, Guid requestId); + protected abstract Task CreateFaultedResponseMessage(ConsumeContext context, TRequest request, Guid requestId); } diff --git a/src/MassTransit/DependencyInjection/Configuration/BusRegistrationContext.cs b/src/MassTransit/DependencyInjection/Configuration/BusRegistrationContext.cs index a08b43c676c..8fbd07b226a 100644 --- a/src/MassTransit/DependencyInjection/Configuration/BusRegistrationContext.cs +++ b/src/MassTransit/DependencyInjection/Configuration/BusRegistrationContext.cs @@ -5,7 +5,7 @@ namespace MassTransit.Configuration using System.Collections.Generic; using System.Linq; using DependencyInjection.Registration; - using Microsoft.Extensions.DependencyInjection; + using Internals; public class BusRegistrationContext : @@ -14,12 +14,12 @@ public class BusRegistrationContext : { IConfigureReceiveEndpoint? _configureReceiveEndpoints; - public BusRegistrationContext(IServiceProvider provider, IContainerSelector selector) - : base(provider, selector) + public BusRegistrationContext(IServiceProvider provider, IContainerSelector selector, ISetScopedConsumeContext setScopedConsumeContext) + : base(provider, selector, setScopedConsumeContext) { } - public IEndpointNameFormatter EndpointNameFormatter => this.GetService() ?? DefaultEndpointNameFormatter.Instance; + public IEndpointNameFormatter EndpointNameFormatter => Selector.GetEndpointNameFormatter(this); public void ConfigureEndpoints(IReceiveConfigurator configurator, IEndpointNameFormatter? endpointNameFormatter = null) where T : IReceiveEndpointConfigurator @@ -33,8 +33,6 @@ public void ConfigureEndpoints(IReceiveConfigurator configurator, IEndpoin { endpointNameFormatter ??= EndpointNameFormatter; - var configureReceiveEndpoint = GetConfigureReceiveEndpoints(); - var builder = new RegistrationFilterConfigurator(); configureFilter?.Invoke(builder); @@ -92,6 +90,11 @@ public void ConfigureEndpoints(IReceiveConfigurator configurator, IEndpoin }) .ToList(); + IEndpointDefinition? GetEndpointDefinitionByName(string name) + { + return endpointsWithName.SingleOrDefault(x => x.Name == name)?.Definition; + } + IEnumerable endpointNames = consumersByEndpoint.Select(x => x.Key) .Union(sagasByEndpoint.Select(x => x.Key)) .Union(activitiesByExecuteEndpoint.Select(x => x.Key)) @@ -100,7 +103,7 @@ public void ConfigureEndpoints(IReceiveConfigurator configurator, IEndpoin .Union(endpointsWithName.Select(x => x.Name)) .Except(activitiesByCompensateEndpoint.Select(x => x.Key)); - var endpoints = + IList endpoints = ( from e in endpointNames join c in consumersByEndpoint on e equals c.Key into cs from c in cs.DefaultIfEmpty() @@ -120,115 +123,124 @@ from ep in eps.Select(x => x.Definition) ?? ea?.Select(x => (IEndpointDefinition)new DelegateEndpointDefinition(e, x, x.ExecuteEndpointDefinition)).Combine() ?? f?.Select(x => (IEndpointDefinition)new DelegateEndpointDefinition(e, x, x.EndpointDefinition)).Combine() ?? new NamedEndpointDefinition(e)) - select new + select new Endpoint(ep, c, s, a, ea, f)).ToList(); + + var needsServiceInstance = !(configurator is IServiceInstanceConfigurator) && endpoints.Any(endpoint => endpoint.HasJobConsumers); + if (needsServiceInstance) + { + var registration = Selector.GetRegistrations(this).SingleOrDefault(); + registration ??= new JobServiceRegistration(); + + configurator.ReceiveEndpoint(registration.EndpointDefinition, endpointNameFormatter, endpointConfigurator => { - Name = e, - Definition = ep, - Consumers = c, - Sagas = s, - Activities = a, - ExecuteActivities = ea, - Futures = f - }; + var options = new ServiceInstanceOptions().SetEndpointNameFormatter(endpointNameFormatter); + + var instanceConfigurator = new ServiceInstanceConfigurator(configurator, options, endpointConfigurator); + + registration.Configure(instanceConfigurator, this); + + ConfigureTheEndpoints(endpoints, endpointNameFormatter, GetEndpointDefinitionByName, configurator, instanceConfigurator); + }); + } + else + ConfigureTheEndpoints(endpoints, endpointNameFormatter, GetEndpointDefinitionByName, configurator); + } + + public IConfigureReceiveEndpoint GetConfigureReceiveEndpoints() + { + if (_configureReceiveEndpoints != null) + return _configureReceiveEndpoints; + + _configureReceiveEndpoints = Selector.GetConfigureReceiveEndpoints(this); + + return _configureReceiveEndpoints; + } + + void ConfigureTheEndpoints(IEnumerable endpoints, IEndpointNameFormatter endpointNameFormatter, + Func getEndpointDefinitionByName, + IReceiveConfigurator configurator, IReceiveConfigurator? instanceConfigurator = null) + where T : IReceiveEndpointConfigurator + { + var configureReceiveEndpoint = GetConfigureReceiveEndpoints(); foreach (var endpoint in endpoints) { - configurator.ReceiveEndpoint(endpoint.Definition, endpointNameFormatter, cfg => + IReceiveConfigurator useConfigurator = instanceConfigurator != null && endpoint.HasJobConsumers + ? instanceConfigurator + : configurator; + + useConfigurator.ReceiveEndpoint(endpoint.Definition, endpointNameFormatter, cfg => { configureReceiveEndpoint.Configure(endpoint.Definition.GetEndpointName(endpointNameFormatter), cfg); - if (endpoint.Consumers != null) - { - foreach (var consumer in endpoint.Consumers) - ConfigureConsumer(consumer.ConsumerType, cfg); - } + foreach (var consumer in endpoint.Consumers) + ConfigureConsumer(consumer.ConsumerType, cfg); - if (endpoint.Sagas != null) - { - foreach (var saga in endpoint.Sagas) - ConfigureSaga(saga.SagaType, cfg); - } + foreach (var saga in endpoint.Sagas) + ConfigureSaga(saga.SagaType, cfg); - if (endpoint.Activities != null) + foreach (var activity in endpoint.Activities) { - foreach (var activity in endpoint.Activities) - { - var compensateEndpointName = activity.GetCompensateEndpointName(endpointNameFormatter); - - var compensateDefinition = activity.CompensateEndpointDefinition ?? - endpointsWithName.SingleOrDefault(x => x.Name == compensateEndpointName)?.Definition; + var compensateEndpointName = activity.GetCompensateEndpointName(endpointNameFormatter); - if (compensateDefinition != null) + var compensateDefinition = activity.CompensateEndpointDefinition ?? getEndpointDefinitionByName(compensateEndpointName); + if (compensateDefinition != null) + { + configurator.ReceiveEndpoint(compensateDefinition, endpointNameFormatter, compensateEndpointConfigurator => { - configurator.ReceiveEndpoint(compensateDefinition, endpointNameFormatter, compensateEndpointConfigurator => - { - configureReceiveEndpoint.Configure(compensateDefinition.GetEndpointName(endpointNameFormatter), - compensateEndpointConfigurator); - - ConfigureActivity(activity.ActivityType, cfg, compensateEndpointConfigurator); - }); - } - else + configureReceiveEndpoint.Configure(compensateDefinition.GetEndpointName(endpointNameFormatter), + compensateEndpointConfigurator); + + ConfigureActivity(activity.ActivityType, cfg, compensateEndpointConfigurator); + }); + } + else + { + configurator.ReceiveEndpoint(compensateEndpointName, compensateEndpointConfigurator => { - configurator.ReceiveEndpoint(compensateEndpointName, compensateEndpointConfigurator => - { - configureReceiveEndpoint.Configure(compensateEndpointName, compensateEndpointConfigurator); + configureReceiveEndpoint.Configure(compensateEndpointName, compensateEndpointConfigurator); - ConfigureActivity(activity.ActivityType, cfg, compensateEndpointConfigurator); - }); - } + ConfigureActivity(activity.ActivityType, cfg, compensateEndpointConfigurator); + }); } } - if (endpoint.ExecuteActivities != null) - { - foreach (var activity in endpoint.ExecuteActivities) - ConfigureExecuteActivity(activity.ActivityType, cfg); - } + foreach (var activity in endpoint.ExecuteActivities) + ConfigureExecuteActivity(activity.ActivityType, cfg); - if (endpoint.Futures != null) - { - foreach (var future in endpoint.Futures) - ConfigureFuture(future.FutureType, cfg); - } + foreach (var future in endpoint.Futures) + ConfigureFuture(future.FutureType, cfg); }); } } - public IConfigureReceiveEndpoint GetConfigureReceiveEndpoints() - { - if (_configureReceiveEndpoints != null) - return _configureReceiveEndpoints; - - IEnumerable configureReceiveEndpoints = this.GetServices(); - - _configureReceiveEndpoints = configureReceiveEndpoints == null - ? new ConfigureReceiveEndpoint(Array.Empty()) - : new ConfigureReceiveEndpoint(configureReceiveEndpoints.ToArray()); - - return _configureReceiveEndpoints; - } - static void NoFilter(IRegistrationFilterConfigurator configurator) { } - class ConfigureReceiveEndpoint : - IConfigureReceiveEndpoint + class Endpoint { - readonly IConfigureReceiveEndpoint[] _configurators; - - public ConfigureReceiveEndpoint(IConfigureReceiveEndpoint[] configurators) + public Endpoint(IEndpointDefinition definition, IEnumerable? consumers, IEnumerable? sagas, + IEnumerable? activities, IEnumerable? executeActivities, + IEnumerable? futures) { - _configurators = configurators; + Definition = definition; + Consumers = consumers?.ToList() ?? new List(); + Sagas = sagas?.ToList() ?? new List(); + Activities = activities?.ToList() ?? new List(); + ExecuteActivities = executeActivities?.ToList() ?? new List(); + Futures = futures?.ToList() ?? new List(); } - public void Configure(string name, IReceiveEndpointConfigurator configurator) - { - for (var i = 0; i < _configurators.Length; i++) - _configurators[i].Configure(name, configurator); - } + public IEndpointDefinition Definition { get; } + public List Consumers { get; } + public List Sagas { get; } + public List Activities { get; } + public List ExecuteActivities { get; } + public List Futures { get; } + + public bool HasJobConsumers => Consumers.Any(c => c.ConsumerType.ClosesType(typeof(IJobConsumer<>))); } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/ConfigureReceiveEndpointDelegateProvider.cs b/src/MassTransit/DependencyInjection/Configuration/ConfigureReceiveEndpointDelegateProvider.cs index 15ebfa3094b..4c68a33f126 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ConfigureReceiveEndpointDelegateProvider.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ConfigureReceiveEndpointDelegateProvider.cs @@ -6,21 +6,18 @@ namespace MassTransit.Configuration public class ConfigureReceiveEndpointDelegateProvider : IConfigureReceiveEndpoint { - readonly IServiceProvider _provider; readonly ConfigureEndpointsProviderCallback _callback; + readonly IRegistrationContext _context; - public ConfigureReceiveEndpointDelegateProvider(IServiceProvider provider, ConfigureEndpointsProviderCallback callback) + public ConfigureReceiveEndpointDelegateProvider(IRegistrationContext context, ConfigureEndpointsProviderCallback callback) { - if (callback == null) - throw new ArgumentNullException(nameof(callback)); - - _provider = provider; - _callback = callback; + _context = context; + _callback = callback ?? throw new ArgumentNullException(nameof(callback)); } public void Configure(string name, IReceiveEndpointConfigurator configurator) { - _callback(_provider, name, configurator); + _callback(_context, name, configurator); } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionActivityRegistrationExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionActivityRegistrationExtensions.cs index 24b6f78ce61..798f542415d 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionActivityRegistrationExtensions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionActivityRegistrationExtensions.cs @@ -1,7 +1,6 @@ namespace MassTransit.Configuration { using System; - using DependencyInjection; using DependencyInjection.Registration; using Internals; using Microsoft.Extensions.DependencyInjection; @@ -75,6 +74,37 @@ public static IActivityRegistration RegisterActivity), out Type[] argumentTypes)) + { + throw new ArgumentException($" activities must implement IActivity: {TypeCache.GetShortName(activityType)}", + nameof(activityType)); + } + + if (activityDefinitionType != null) + { + if (!activityDefinitionType.ClosesType(typeof(IActivityDefinition<,,>), out Type[] types) || types[0] != activityType) + { + throw new ArgumentException( + $"{TypeCache.GetShortName(activityDefinitionType)} is not an activity definition of {TypeCache.GetShortName(activityType)}", + nameof(activityDefinitionType)); + } + + var activityRegistrar = (IActivityRegistrar)Activator.CreateInstance(typeof(ActivityDefinitionRegistrar<,,,>) + .MakeGenericType(activityType, argumentTypes[0], argumentTypes[1], activityDefinitionType)); + + return activityRegistrar.Register(collection, registrar); + } + + + var register = (IActivityRegistrar)Activator.CreateInstance(typeof(ActivityRegistrar<,,>) + .MakeGenericType(activityType, argumentTypes[0], argumentTypes[1])); + + return register.Register(collection, registrar); + } + interface IActivityRegistrar { @@ -92,12 +122,6 @@ public virtual IActivityRegistration Register(IServiceCollection collection, ICo { collection.TryAddScoped(); - collection.TryAddTransient, - ExecuteActivityScopeProvider>(); - - collection.TryAddTransient, - CompensateActivityScopeProvider>(); - return registrar.GetOrAdd(typeof(TActivity), _ => new ActivityRegistration()); } } diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionConsumerRegistrationExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionConsumerRegistrationExtensions.cs index 431d9563f63..35c51568e0a 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionConsumerRegistrationExtensions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionConsumerRegistrationExtensions.cs @@ -3,6 +3,7 @@ namespace MassTransit.Configuration using System; using DependencyInjection.Registration; using Internals; + using Metadata; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -18,7 +19,7 @@ public static IConsumerRegistration RegisterConsumer(this IServiceCollection public static IConsumerRegistration RegisterConsumer(this IServiceCollection collection, IContainerRegistrar registrar) where T : class, IConsumer { - if (MessageTypeCache.HasSagaInterfaces) + if (RegistrationMetadata.IsSaga(typeof(T))) throw new ArgumentException($"{TypeCache.ShortName} is a saga, and cannot be registered as a consumer", nameof(T)); return new ConsumerRegistrar().Register(collection, registrar); @@ -35,7 +36,7 @@ public static IConsumerRegistration RegisterConsumer(this IServi where T : class, IConsumer where TDefinition : class, IConsumerDefinition { - if (MessageTypeCache.HasSagaInterfaces) + if (RegistrationMetadata.IsSaga(typeof(T))) throw new ArgumentException($"{TypeCache.ShortName} is a saga, and cannot be registered as a consumer", nameof(T)); return new ConsumerDefinitionRegistrar().Register(collection, registrar); @@ -53,7 +54,7 @@ public static IConsumerRegistration RegisterConsumer(this IServiceCollection if (consumerDefinitionType == null) return RegisterConsumer(collection, registrar); - if (MessageTypeCache.HasSagaInterfaces) + if (RegistrationMetadata.IsSaga(typeof(T))) throw new ArgumentException($"{TypeCache.ShortName} is a saga, and cannot be registered as a consumer", nameof(T)); if (!consumerDefinitionType.ClosesType(typeof(IConsumerDefinition<>), out Type[] types) || types[0] != typeof(T)) @@ -68,6 +69,32 @@ public static IConsumerRegistration RegisterConsumer(this IServiceCollection return register.Register(collection, registrar); } + public static IConsumerRegistration RegisterConsumer(this IServiceCollection collection, IContainerRegistrar registrar, Type consumerType, + Type consumerDefinitionType = null) + { + if (RegistrationMetadata.IsSaga(consumerType)) + throw new ArgumentException($"{TypeCache.GetShortName(consumerType)} is a saga, and cannot be registered as a consumer", nameof(consumerType)); + + if (consumerDefinitionType != null) + { + if (!consumerDefinitionType.ClosesType(typeof(IConsumerDefinition<>), out Type[] types) || types[0] != consumerType) + { + throw new ArgumentException( + $"{TypeCache.GetShortName(consumerDefinitionType)} is not a consumer definition of {TypeCache.GetShortName(consumerType)}", + nameof(consumerDefinitionType)); + } + + var consumerRegistrar = (IConsumerRegistrar)Activator.CreateInstance( + typeof(ConsumerDefinitionRegistrar<,>).MakeGenericType(consumerType, consumerDefinitionType)); + + return consumerRegistrar.Register(collection, registrar); + } + + var register = (IConsumerRegistrar)Activator.CreateInstance(typeof(ConsumerRegistrar<>).MakeGenericType(consumerType)); + + return register.Register(collection, registrar); + } + interface IConsumerRegistrar { diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionContainerRegistrar.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionContainerRegistrar.cs index c24efcfbeb5..3d45c9457f0 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionContainerRegistrar.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionContainerRegistrar.cs @@ -35,6 +35,11 @@ public void RegisterScopedClientFactory() Collection.TryAddScoped(provider => GetScopedBusContext(provider)); } + public virtual void RegisterEndpointNameFormatter(IEndpointNameFormatter endpointNameFormatter) + { + Collection.TryAddSingleton(endpointNameFormatter); + } + public T GetOrAdd(Type type, Func missingRegistrationFactory = default) where T : class, IRegistration { @@ -70,6 +75,23 @@ public virtual IEnumerable GetRegistrations(IServiceProvider provider) return provider.GetService>() ?? Array.Empty(); } + public IConfigureReceiveEndpoint GetConfigureReceiveEndpoints(IServiceProvider provider) + { + IConfigureReceiveEndpoint[] globalConfigureReceiveEndpoints = provider.GetServices().ToArray(); + + return new ConfigureReceiveEndpoint(globalConfigureReceiveEndpoints, GetBusConfigureReceiveEndpoints(provider)); + } + + public virtual IEndpointNameFormatter GetEndpointNameFormatter(IServiceProvider provider) + { + return provider.GetService() ?? DefaultEndpointNameFormatter.Instance; + } + + protected virtual IConfigureReceiveEndpoint[] GetBusConfigureReceiveEndpoints(IServiceProvider provider) + { + return provider.GetServices>().Select(x => x.Value).ToArray(); + } + bool TryGetValue(Type type, out T value) where T : class, IRegistration { @@ -88,6 +110,29 @@ protected virtual IScopedClientFactory GetScopedBusContext(IServiceProvider prov { return provider.GetRequiredService>().Context.ClientFactory; } + + + class ConfigureReceiveEndpoint : + IConfigureReceiveEndpoint + { + readonly IConfigureReceiveEndpoint[] _global; + readonly IConfigureReceiveEndpoint[] _typed; + + public ConfigureReceiveEndpoint(IConfigureReceiveEndpoint[] global, IConfigureReceiveEndpoint[] typed) + { + _global = global; + _typed = typed; + } + + public void Configure(string name, IReceiveEndpointConfigurator configurator) + { + for (var i = 0; i < _global.Length; i++) + _global[i].Configure(name, configurator); + + for (var i = 0; i < _typed.Length; i++) + _typed[i].Configure(name, configurator); + } + } } @@ -117,9 +162,27 @@ protected override void AddRegistration(T value) Collection.Add(ServiceDescriptor.Singleton(Bind.Create(value))); } + public override void RegisterEndpointNameFormatter(IEndpointNameFormatter endpointNameFormatter) + { + Collection.TryAddSingleton(Bind.Create(endpointNameFormatter)); + } + protected override IScopedClientFactory GetScopedBusContext(IServiceProvider provider) { return provider.GetRequiredService>().Context.ClientFactory; } + + protected override IConfigureReceiveEndpoint[] GetBusConfigureReceiveEndpoints(IServiceProvider provider) + { + return provider.GetServices>().Select(x => x.Value).ToArray(); + } + + public override IEndpointNameFormatter GetEndpointNameFormatter(IServiceProvider provider) + { + var bind = provider.GetService>(); + return bind != null + ? bind.Value + : base.GetEndpointNameFormatter(provider); + } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionEndpointRegistrationExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionEndpointRegistrationExtensions.cs index c7c6d6b53a4..88afae30e21 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionEndpointRegistrationExtensions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionEndpointRegistrationExtensions.cs @@ -9,20 +9,20 @@ namespace MassTransit.Configuration public static class DependencyInjectionEndpointRegistrationExtensions { - public static IEndpointRegistration RegisterEndpoint(this IServiceCollection collection, + public static IEndpointRegistration RegisterEndpoint(this IServiceCollection collection, IRegistration registration, IEndpointSettings> settings = null) where TDefinition : class, IEndpointDefinition where T : class { - return RegisterEndpoint(collection, new DependencyInjectionContainerRegistrar(collection), settings); + return RegisterEndpoint(collection, new DependencyInjectionContainerRegistrar(collection), registration, settings); } public static IEndpointRegistration RegisterEndpoint(this IServiceCollection collection, IContainerRegistrar registrar, - IEndpointSettings> settings = null) + IRegistration registration, IEndpointSettings> settings = null) where T : class where TDefinition : class, IEndpointDefinition { - return new EndpointRegistrar().Register(collection, registrar, settings); + return new EndpointRegistrar(registration).Register(collection, registrar, settings); } public static IEndpointRegistration RegisterEndpoint(this IServiceCollection collection, Type endpointDefinitionType) @@ -52,11 +52,18 @@ class EndpointRegistrar : where TDefinition : class, IEndpointDefinition where T : class { + readonly IRegistration _registration; + + public EndpointRegistrar(IRegistration registration) + { + _registration = registration; + } + public IEndpointRegistration Register(IServiceCollection collection, IContainerRegistrar registrar) { collection.TryAddTransient, TDefinition>(); - return registrar.GetOrAdd(typeof(T), _ => new EndpointRegistration()); + return registrar.GetOrAdd(typeof(T), _ => new EndpointRegistration(_registration)); } public IEndpointRegistration Register(IServiceCollection collection, IContainerRegistrar registrar, @@ -66,7 +73,7 @@ public IEndpointRegistration Register(IServiceCollection collection, IContainerR if (settings != null) collection.AddSingleton(settings); - return registrar.GetOrAdd(typeof(T), _ => new EndpointRegistration()); + return registrar.GetOrAdd(typeof(T), _ => new EndpointRegistration(_registration)); } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionExecuteActivityRegistrationExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionExecuteActivityRegistrationExtensions.cs index f8d2ecc831b..6a52984bfce 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionExecuteActivityRegistrationExtensions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionExecuteActivityRegistrationExtensions.cs @@ -1,7 +1,6 @@ namespace MassTransit.Configuration { using System; - using DependencyInjection; using DependencyInjection.Registration; using Internals; using Microsoft.Extensions.DependencyInjection; @@ -71,6 +70,43 @@ public static IExecuteActivityRegistration RegisterExecuteActivity), out Type[] _)) + { + throw new ArgumentException($"Activities must be registered using RegisterActivity: {TypeCache.GetShortName(activityType)}", + nameof(activityType)); + } + + if (!activityType.ClosesType(typeof(IExecuteActivity<>), out Type[] argumentTypes)) + { + throw new ArgumentException($"Execute activities must implement IExecuteActivity: {TypeCache.GetShortName(activityType)}", + nameof(activityType)); + } + + if (activityDefinitionType != null) + { + if (!activityDefinitionType.ClosesType(typeof(IExecuteActivityDefinition<,>), out Type[] types) || types[0] != activityType) + { + throw new ArgumentException( + $"{TypeCache.GetShortName(activityDefinitionType)} is not an activity definition of {TypeCache.GetShortName(activityType)}", + nameof(activityDefinitionType)); + } + + var activityRegistrar = (IExecuteActivityRegistrar)Activator.CreateInstance(typeof(ExecuteActivityDefinitionRegistrar<,,>) + .MakeGenericType(activityType, argumentTypes[0], activityDefinitionType)); + + return activityRegistrar.Register(collection, registrar); + } + + + var register = (IExecuteActivityRegistrar)Activator.CreateInstance(typeof(ExecuteActivityRegistrar<,>) + .MakeGenericType(activityType, argumentTypes[0])); + + return register.Register(collection, registrar); + } + interface IExecuteActivityRegistrar { @@ -87,9 +123,6 @@ public virtual IExecuteActivityRegistration Register(IServiceCollection collecti { collection.TryAddScoped(); - collection.TryAddTransient, - ExecuteActivityScopeProvider>(); - return registrar.GetOrAdd(typeof(TActivity), _ => new ExecuteActivityRegistration()); } } diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionFutureRegistrationExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionFutureRegistrationExtensions.cs index 7cd1241bac6..480436742ee 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionFutureRegistrationExtensions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionFutureRegistrationExtensions.cs @@ -58,6 +58,27 @@ public static IFutureRegistration RegisterFuture(this IServiceCollection coll return register.Register(collection, registrar); } + public static IFutureRegistration RegisterFuture(this IServiceCollection collection, IContainerRegistrar registrar, Type futureType, + Type futureDefinitionType = null) + { + if (!futureType.HasInterface>()) + throw new ArgumentException($"The registered type must be a future: {TypeCache.GetShortName(futureType)}"); + + futureDefinitionType ??= typeof(DefaultFutureDefinition<>).MakeGenericType(futureType); + + if (!futureDefinitionType.ClosesType(typeof(ISagaDefinition<>), out Type[] types) || types[0] != futureType) + { + throw new ArgumentException( + $"{TypeCache.GetShortName(futureDefinitionType)} is not a future definition of {TypeCache.GetShortName(futureType)}", + nameof(futureDefinitionType)); + } + + var sagaRegistrar = + (IFutureRegistrar)Activator.CreateInstance(typeof(FutureDefinitionRegistrar<,>).MakeGenericType(futureType, futureDefinitionType)); + + return sagaRegistrar.Register(collection, registrar); + } + interface IFutureRegistrar { diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionHandlerRegistrationExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionHandlerRegistrationExtensions.cs new file mode 100644 index 00000000000..5afee12c3ee --- /dev/null +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionHandlerRegistrationExtensions.cs @@ -0,0 +1,430 @@ +namespace MassTransit.Configuration +{ + using System; + using System.Threading.Tasks; + using DependencyInjection; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + + + public static class DependencyInjectionHandlerRegistrationExtensions + { + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection) + where T : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection)); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar) + where T : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + collection.TryAddSingleton(new MessageHandlerMethod((ConsumeContext _) => Task.CompletedTask)); + + return collection.RegisterConsumer, MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func, Task> handler) + where T : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func, Task> handler) + where T : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + collection.TryAddSingleton(new MessageHandlerMethod(handler)); + + return collection.RegisterConsumer, MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func handler) + where T : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, Func handler) + where T : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + collection.TryAddSingleton(new MessageHandlerMethod(handler)); + + return collection.RegisterConsumer, MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func, Task> handler) + where T : class + where TResponse : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func, Task> handler) + where T : class + where TResponse : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(TResponse)); + + collection.TryAddSingleton(new RequestHandlerMethod(handler)); + + return collection + .RegisterConsumer, MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func> handler) + where T : class + where TResponse : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func> handler) + where T : class + where TResponse : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(TResponse)); + + collection.TryAddSingleton(new RequestHandlerMethod(handler)); + + return collection + .RegisterConsumer, MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func, T1, Task> handler) + where T : class + where T1 : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func, T1, Task> handler) + where T : class + where T1 : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + collection.TryAddSingleton(new MessageHandlerMethod(handler)); + + return collection.RegisterConsumer, MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func handler) + where T : class + where T1 : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, Func handler) + where T : class + where T1 : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + collection.TryAddSingleton(new MessageHandlerMethod(handler)); + + return collection.RegisterConsumer, MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func, T1, Task> + handler) + where T : class + where T1 : class + where TResponse : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func, T1, Task> handler) + where T : class + where T1 : class + where TResponse : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(TResponse)); + + collection.TryAddSingleton(new RequestHandlerMethod(handler)); + + return collection + .RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func> handler) + where T : class + where T1 : class + where TResponse : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func> handler) + where T : class + where T1 : class + where TResponse : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(TResponse)); + + collection.TryAddSingleton(new RequestHandlerMethod(handler)); + + return collection + .RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func, T1, T2, Task> handler) + where T : class + where T1 : class + where T2 : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func, T1, T2, Task> handler) + where T : class + where T1 : class + where T2 : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + collection.TryAddSingleton(new MessageHandlerMethod(handler)); + + return collection.RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func handler) + where T : class + where T1 : class + where T2 : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, Func + handler) + where T : class + where T1 : class + where T2 : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + collection.TryAddSingleton(new MessageHandlerMethod(handler)); + + return collection.RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func, T1, T2, + Task> + handler) + where T : class + where T1 : class + where T2 : class + where TResponse : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func, T1, T2, Task> handler) + where T : class + where T1 : class + where T2 : class + where TResponse : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(TResponse)); + + collection.TryAddSingleton(new RequestHandlerMethod(handler)); + + return collection + .RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func> handler) + where T : class + where T1 : class + where T2 : class + where TResponse : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func> handler) + where T : class + where T1 : class + where T2 : class + where TResponse : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(TResponse)); + + collection.TryAddSingleton(new RequestHandlerMethod(handler)); + + return collection + .RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func, T1, T2, T3, Task> + handler) + where T : class + where T1 : class + where T2 : class + where T3 : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func, T1, T2, T3, Task> handler) + where T : class + where T1 : class + where T2 : class + where T3 : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + collection.TryAddSingleton(new MessageHandlerMethod(handler)); + + return collection.RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func handler) + where T : class + where T1 : class + where T2 : class + where T3 : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, Func + handler) + where T : class + where T1 : class + where T2 : class + where T3 : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + collection.TryAddSingleton(new MessageHandlerMethod(handler)); + + return collection.RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func, T1, T2, T3, + Task> + handler) + where T : class + where T1 : class + where T2 : class + where T3 : class + where TResponse : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func, T1, T2, T3, Task> handler) + where T : class + where T1 : class + where T2 : class + where T3 : class + where TResponse : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(TResponse)); + + collection.TryAddSingleton(new RequestHandlerMethod(handler)); + + return collection + .RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, Func> + handler) + where T : class + where T1 : class + where T2 : class + where T3 : class + where TResponse : class + { + return RegisterHandler(collection, new DependencyInjectionContainerRegistrar(collection), handler); + } + + public static IConsumerRegistration RegisterHandler(this IServiceCollection collection, IContainerRegistrar registrar, + Func> handler) + where T : class + where T1 : class + where T2 : class + where T3 : class + where TResponse : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(TResponse)); + + collection.TryAddSingleton(new RequestHandlerMethod(handler)); + + return collection + .RegisterConsumer, + MessageHandlerConsumerDefinition, T>>(registrar); + } + } +} diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionJobServiceRegistrationExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionJobServiceRegistrationExtensions.cs new file mode 100644 index 00000000000..c1488f83d0e --- /dev/null +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionJobServiceRegistrationExtensions.cs @@ -0,0 +1,16 @@ +namespace MassTransit.Configuration +{ + using DependencyInjection.Registration; + using JobService; + using Microsoft.Extensions.DependencyInjection; + + + public static class DependencyInjectionJobServiceRegistrationExtensions + { + public static IJobServiceRegistration RegisterJobService(this IServiceCollection collection, IContainerRegistrar registrar) + { + collection.AddOptions(); + return registrar.GetOrAdd(typeof(JobService), _ => new JobServiceRegistration()); + } + } +} diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionMediatorContainerRegistrar.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionMediatorContainerRegistrar.cs index 122c3bfffeb..5c34b33c7de 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionMediatorContainerRegistrar.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionMediatorContainerRegistrar.cs @@ -37,10 +37,10 @@ protected override void AddRegistration(T value) protected override IScopedClientFactory GetScopedBusContext(IServiceProvider provider) { var clientFactory = provider.GetRequiredService(); - var consumeContext = provider.GetRequiredService().GetContext(); + var consumeContextProvider = provider.GetRequiredService>().Value; - return consumeContext != null - ? new ScopedClientFactory(clientFactory, consumeContext) + return consumeContextProvider.HasContext + ? new ScopedClientFactory(clientFactory, consumeContextProvider.GetContext()) : new ScopedClientFactory(new ClientFactory(new ScopedClientFactoryContext(clientFactory, provider)), null); } } diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionRiderContainerRegistrar.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionRiderContainerRegistrar.cs index fa5bab21519..98691e56a1b 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionRiderContainerRegistrar.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionRiderContainerRegistrar.cs @@ -7,33 +7,6 @@ namespace MassTransit.Configuration using Microsoft.Extensions.DependencyInjection; - public class DependencyInjectionRiderContainerRegistrar : - DependencyInjectionContainerRegistrar - { - public DependencyInjectionRiderContainerRegistrar(IServiceCollection collection) - : base(collection) - { - } - - public override IEnumerable GetRegistrations() - { - return Collection.Where(x => x.ServiceType == typeof(Bind)) - .Select(x => x.ImplementationInstance).Cast>() - .Select(x => x.Value); - } - - public override IEnumerable GetRegistrations(IServiceProvider provider) - { - return provider.GetService>>().Select(x => x.Value) ?? Array.Empty(); - } - - protected override void AddRegistration(T value) - { - Collection.Add(ServiceDescriptor.Singleton(Bind.Create(value))); - } - } - - abstract class Rider { } diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionSagaRegistrationExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionSagaRegistrationExtensions.cs index 0790184002a..be8b7c633ce 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionSagaRegistrationExtensions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionSagaRegistrationExtensions.cs @@ -67,6 +67,30 @@ public static ISagaRegistration RegisterSaga(this IServiceCollection collecti return register.Register(collection, registrar); } + public static ISagaRegistration RegisterSaga(this IServiceCollection collection, IContainerRegistrar registrar, Type sagaType, + Type sagaDefinitionType = null) + { + if (sagaType.HasInterface()) + throw new ArgumentException($"State machine sagas must be registered using RegisterSagaStateMachine: {TypeCache.GetShortName(sagaType)}"); + + if (sagaDefinitionType != null) + { + if (!sagaDefinitionType.ClosesType(typeof(ISagaDefinition<>), out Type[] types) || types[0] != sagaType) + { + throw new ArgumentException($"{TypeCache.GetShortName(sagaDefinitionType)} is not a saga definition of {TypeCache.GetShortName(sagaType)}", + nameof(sagaDefinitionType)); + } + + var sagaRegistrar = (ISagaRegistrar)Activator.CreateInstance(typeof(SagaDefinitionRegistrar<,>).MakeGenericType(sagaType, sagaDefinitionType)); + + return sagaRegistrar.Register(collection, registrar); + } + + var register = (ISagaRegistrar)Activator.CreateInstance(typeof(SagaRegistrar<>).MakeGenericType(sagaType)); + + return register.Register(collection, registrar); + } + interface ISagaRegistrar { diff --git a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionSagaStateMachineRegistrationExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionSagaStateMachineRegistrationExtensions.cs index 06eb7b8e78d..e4276064c5b 100644 --- a/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionSagaStateMachineRegistrationExtensions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/DependencyInjectionSagaStateMachineRegistrationExtensions.cs @@ -66,6 +66,35 @@ public static ISagaRegistration RegisterSagaStateMachine(this IService return register.Register(collection, registrar); } + public static ISagaRegistration RegisterSagaStateMachine(this IServiceCollection collection, IContainerRegistrar registrar, Type sagaType, + Type sagaDefinitionType = null) + { + if (!sagaType.ClosesType(typeof(SagaStateMachine<>), out Type[] instanceTypes)) + throw new ArgumentException($"The saga type must be a saga state machine: {TypeCache.GetShortName(sagaType)}"); + + if (!instanceTypes[0].HasInterface()) + throw new ArgumentException($"The instance type must be a saga state machine instance: {TypeCache.GetShortName(instanceTypes[0])}"); + + if (sagaDefinitionType != null) + { + if (!sagaDefinitionType.ClosesType(typeof(ISagaDefinition<>), out Type[] types) || types[0] != instanceTypes[0]) + { + throw new ArgumentException( + $"{TypeCache.GetShortName(sagaDefinitionType)} is not a saga definition of {TypeCache.GetShortName(instanceTypes[0])}", + nameof(sagaDefinitionType)); + } + + var sagaRegistrar = (ISagaRegistrar)Activator.CreateInstance(typeof(SagaDefinitionRegistrar<,,>).MakeGenericType(sagaType, + instanceTypes[0], sagaDefinitionType)); + + return sagaRegistrar.Register(collection, registrar); + } + + var register = (ISagaRegistrar)Activator.CreateInstance(typeof(SagaRegistrar<,>).MakeGenericType(sagaType, instanceTypes[0])); + + return register.Register(collection, registrar); + } + interface ISagaRegistrar { diff --git a/src/MassTransit/DependencyInjection/Configuration/IActivityRegistration.cs b/src/MassTransit/DependencyInjection/Configuration/IActivityRegistration.cs index c42137db760..536d8b70a61 100644 --- a/src/MassTransit/DependencyInjection/Configuration/IActivityRegistration.cs +++ b/src/MassTransit/DependencyInjection/Configuration/IActivityRegistration.cs @@ -9,21 +9,21 @@ namespace MassTransit.Configuration public interface IActivityRegistration : IRegistration { - void AddConfigureAction(Action> configure) + void AddConfigureAction(Action> configure) where T : class, IExecuteActivity where TArguments : class; - void AddConfigureAction(Action> configure) + void AddConfigureAction(Action> configure) where T : class, ICompensateActivity where TLog : class; void Configure(IReceiveEndpointConfigurator executeEndpointConfigurator, IReceiveEndpointConfigurator compensateEndpointConfigurator, - IServiceProvider scopeProvider); + IRegistrationContext context); - IActivityDefinition GetDefinition(IServiceProvider provider); + IActivityDefinition GetDefinition(IRegistrationContext context); - void ConfigureCompensate(IReceiveEndpointConfigurator configurator, IServiceProvider configurationServiceProvider); + void ConfigureCompensate(IReceiveEndpointConfigurator configurator, IRegistrationContext context); - void ConfigureExecute(IReceiveEndpointConfigurator configurator, IServiceProvider configurationServiceProvider, Uri compensateAddress); + void ConfigureExecute(IReceiveEndpointConfigurator configurator, IRegistrationContext context, Uri compensateAddress); } } diff --git a/src/MassTransit/DependencyInjection/Configuration/IConsumerRegistration.cs b/src/MassTransit/DependencyInjection/Configuration/IConsumerRegistration.cs index 41b1174562b..a6a88317a35 100644 --- a/src/MassTransit/DependencyInjection/Configuration/IConsumerRegistration.cs +++ b/src/MassTransit/DependencyInjection/Configuration/IConsumerRegistration.cs @@ -6,12 +6,12 @@ namespace MassTransit.Configuration public interface IConsumerRegistration : IRegistration { - void AddConfigureAction(Action> configure) + void AddConfigureAction(Action> configure) where T : class, IConsumer; - void Configure(IReceiveEndpointConfigurator configurator, IServiceProvider scopeProvider); + void Configure(IReceiveEndpointConfigurator configurator, IRegistrationContext context); - IConsumerDefinition GetDefinition(IServiceProvider provider); + IConsumerDefinition GetDefinition(IRegistrationContext context); IConsumerRegistrationConfigurator GetConsumerRegistrationConfigurator(IRegistrationConfigurator registrationConfigurator); } diff --git a/src/MassTransit/DependencyInjection/Configuration/IContainerRegistrar.cs b/src/MassTransit/DependencyInjection/Configuration/IContainerRegistrar.cs index 989f2b2eb6d..11d635e58a1 100644 --- a/src/MassTransit/DependencyInjection/Configuration/IContainerRegistrar.cs +++ b/src/MassTransit/DependencyInjection/Configuration/IContainerRegistrar.cs @@ -15,6 +15,8 @@ void RegisterRequestClient(Uri destinationAddress, RequestTimeout timeout = d void RegisterScopedClientFactory(); + void RegisterEndpointNameFormatter(IEndpointNameFormatter endpointNameFormatter); + /// /// Gets or adds a registration from the service collection /// diff --git a/src/MassTransit/DependencyInjection/Configuration/IContainerSelector.cs b/src/MassTransit/DependencyInjection/Configuration/IContainerSelector.cs index 4e5f176e7f2..b2623ac39d1 100644 --- a/src/MassTransit/DependencyInjection/Configuration/IContainerSelector.cs +++ b/src/MassTransit/DependencyInjection/Configuration/IContainerSelector.cs @@ -5,7 +5,7 @@ namespace MassTransit.Configuration /// - /// Used to pull registrations from the container, scoped to the bus, multi-bus, or mediator + /// Used to pull configuration from the container, scoped to the bus, multi-bus, or mediator /// public interface IContainerSelector { @@ -22,5 +22,14 @@ bool TryGetValue(IServiceProvider provider, Type type, out T value) IEnumerable GetRegistrations(IServiceProvider provider) where T : class, IRegistration; + + IConfigureReceiveEndpoint GetConfigureReceiveEndpoints(IServiceProvider provider); + + /// + /// Returns the endpoint name formatter registered for the bus instance + /// + /// + /// + IEndpointNameFormatter GetEndpointNameFormatter(IServiceProvider provider); } } diff --git a/src/MassTransit/DependencyInjection/Configuration/IExecuteActivityRegistration.cs b/src/MassTransit/DependencyInjection/Configuration/IExecuteActivityRegistration.cs index f6ec159f2d8..7a359d7db4b 100644 --- a/src/MassTransit/DependencyInjection/Configuration/IExecuteActivityRegistration.cs +++ b/src/MassTransit/DependencyInjection/Configuration/IExecuteActivityRegistration.cs @@ -9,12 +9,12 @@ namespace MassTransit.Configuration public interface IExecuteActivityRegistration : IRegistration { - void AddConfigureAction(Action> configure) + void AddConfigureAction(Action> configure) where T : class, IExecuteActivity where TArguments : class; - void Configure(IReceiveEndpointConfigurator configurator, IServiceProvider scopeProvider); + void Configure(IReceiveEndpointConfigurator configurator, IRegistrationContext context); - IExecuteActivityDefinition GetDefinition(IServiceProvider provider); + IExecuteActivityDefinition GetDefinition(IRegistrationContext context); } } diff --git a/src/MassTransit/DependencyInjection/Configuration/IFutureRegistration.cs b/src/MassTransit/DependencyInjection/Configuration/IFutureRegistration.cs index 5fee80896ed..31160b4b5c4 100644 --- a/src/MassTransit/DependencyInjection/Configuration/IFutureRegistration.cs +++ b/src/MassTransit/DependencyInjection/Configuration/IFutureRegistration.cs @@ -1,13 +1,10 @@ namespace MassTransit.Configuration { - using System; - - public interface IFutureRegistration : IRegistration { - void Configure(IReceiveEndpointConfigurator configurator, IServiceProvider provider); + void Configure(IReceiveEndpointConfigurator configurator, IRegistrationContext context); - IFutureDefinition GetDefinition(IServiceProvider provider); + IFutureDefinition GetDefinition(IRegistrationContext context); } } diff --git a/src/MassTransit/DependencyInjection/Configuration/IHealthCheckOptions.cs b/src/MassTransit/DependencyInjection/Configuration/IHealthCheckOptions.cs index 3f500411fac..39c8375ef2a 100644 --- a/src/MassTransit/DependencyInjection/Configuration/IHealthCheckOptions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/IHealthCheckOptions.cs @@ -1,6 +1,7 @@ #nullable enable namespace MassTransit.Configuration { + using System; using System.Collections.Generic; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -16,8 +17,15 @@ public interface IHealthCheckOptions /// The that should be reported when the health check fails. /// If null then the default status of will be reported. /// + [Obsolete("Use MinimalFailureStatus instead.", true)] public HealthStatus? FailureStatus { get; } + /// + /// The minimal that should be reported when the health check fails. + /// If null then all statuses from to will be reported depending on app health. + /// + public HealthStatus? MinimalFailureStatus { get; } + /// /// A list of tags that can be used to filter sets of health checks /// diff --git a/src/MassTransit/DependencyInjection/Configuration/IJobServiceRegistration.cs b/src/MassTransit/DependencyInjection/Configuration/IJobServiceRegistration.cs new file mode 100644 index 00000000000..a4fcd224645 --- /dev/null +++ b/src/MassTransit/DependencyInjection/Configuration/IJobServiceRegistration.cs @@ -0,0 +1,19 @@ +namespace MassTransit.Configuration +{ + using System; + + + public interface IJobServiceRegistration : + IRegistration + { + IEndpointRegistrationConfigurator EndpointRegistrationConfigurator { get; } + + IEndpointDefinition EndpointDefinition { get; } + + void AddConfigureAction(Action configure); + + void AddReceiveEndpointDependency(IReceiveEndpointConfigurator dependency); + + void Configure(IServiceInstanceConfigurator instanceConfigurator, IRegistrationContext context); + } +} diff --git a/src/MassTransit/DependencyInjection/Configuration/ISagaRegistration.cs b/src/MassTransit/DependencyInjection/Configuration/ISagaRegistration.cs index f58516a2633..fe21d465568 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ISagaRegistration.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ISagaRegistration.cs @@ -6,11 +6,11 @@ namespace MassTransit.Configuration public interface ISagaRegistration : IRegistration { - void AddConfigureAction(Action> configure) + void AddConfigureAction(Action> configure) where T : class, ISaga; - void Configure(IReceiveEndpointConfigurator configurator, IServiceProvider provider); + void Configure(IReceiveEndpointConfigurator configurator, IRegistrationContext context); - ISagaDefinition GetDefinition(IServiceProvider provider); + ISagaDefinition GetDefinition(IRegistrationContext context); } } diff --git a/src/MassTransit/DependencyInjection/Configuration/ISagaRepositoryDecoratorRegistration.cs b/src/MassTransit/DependencyInjection/Configuration/ISagaRepositoryDecoratorRegistration.cs index 1a1fec19269..3803c022126 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ISagaRepositoryDecoratorRegistration.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ISagaRepositoryDecoratorRegistration.cs @@ -8,6 +8,11 @@ namespace MassTransit.Configuration public interface ISagaRepositoryDecoratorRegistration where TSaga : class, ISaga { + TimeSpan TestTimeout { get; } + ReceivedMessageList Consumed { get; } + SagaList Created { get; } + SagaList Sagas { get; } + /// /// Decorate the container-based saga repository, returning the saga repository that should be /// used for receive endpoint registration @@ -15,10 +20,5 @@ public interface ISagaRepositoryDecoratorRegistration /// /// ISagaRepository DecorateSagaRepository(ISagaRepository repository); - - TimeSpan TestTimeout { get; } - ReceivedMessageList Consumed { get; } - SagaList Created { get; } - SagaList Sagas { get; } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/JobServiceEndpointDefinition.cs b/src/MassTransit/DependencyInjection/Configuration/JobServiceEndpointDefinition.cs new file mode 100644 index 00000000000..d213ffbb9d9 --- /dev/null +++ b/src/MassTransit/DependencyInjection/Configuration/JobServiceEndpointDefinition.cs @@ -0,0 +1,48 @@ +namespace MassTransit.Configuration +{ + using System.Text; + using JobService; + using NewIdFormatters; + + + public class JobServiceEndpointDefinition : + IEndpointDefinition + { + readonly InstanceJobServiceSettings _jobServiceSettings; + readonly IEndpointSettings> _settings; + + public JobServiceEndpointDefinition(IEndpointSettings> settings, InstanceJobServiceSettings jobServiceSettings) + { + _settings = settings; + _jobServiceSettings = jobServiceSettings; + + var instanceId = NewId.Next(); + + InstanceName = instanceId.ToString(ZBase32Formatter.LowerCase); + } + + string InstanceName { get; } + + public bool IsTemporary => true; + public int? PrefetchCount => _settings.PrefetchCount; + public int? ConcurrentMessageLimit => _settings.ConcurrentMessageLimit; + public bool ConfigureConsumeTopology => _settings.ConfigureConsumeTopology; + + public void Configure(T configurator) + where T : IReceiveEndpointConfigurator + { + _jobServiceSettings.ApplyConfiguration(configurator); + } + + public string GetEndpointName(IEndpointNameFormatter formatter) + { + var sb = new StringBuilder(InstanceName.Length + 9); + + sb.Append("Instance"); + sb.Append('_'); + sb.Append(InstanceName); + + return formatter.SanitizeName(sb.ToString()); + } + } +} diff --git a/src/MassTransit/DependencyInjection/Configuration/MassTransitHealthCheckOptions.cs b/src/MassTransit/DependencyInjection/Configuration/MassTransitHealthCheckOptions.cs index fbd926ec94b..fa48ab364df 100644 --- a/src/MassTransit/DependencyInjection/Configuration/MassTransitHealthCheckOptions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/MassTransitHealthCheckOptions.cs @@ -25,8 +25,15 @@ public MassTransitHealthCheckOptions() /// The that should be reported when the health check fails. /// If null then the default status of will be reported. /// + [Obsolete("Use MinimalFailureStatus instead.", true)] public HealthStatus? FailureStatus { get; set; } + /// + /// The minimal that should be reported when the health check fails. + /// If null then all statuses from to will be reported depending on app health. + /// + public HealthStatus? MinimalFailureStatus { get; set; } + /// /// A list of tags that can be used to filter sets of health checks. If empty, the default tags /// will be used. diff --git a/src/MassTransit/DependencyInjection/Configuration/MediatorRegistrationContext.cs b/src/MassTransit/DependencyInjection/Configuration/MediatorRegistrationContext.cs index e71329fd724..a0bbdd4cc25 100644 --- a/src/MassTransit/DependencyInjection/Configuration/MediatorRegistrationContext.cs +++ b/src/MassTransit/DependencyInjection/Configuration/MediatorRegistrationContext.cs @@ -1,14 +1,16 @@ namespace MassTransit.Configuration { using System; + using Microsoft.Extensions.DependencyInjection; public class MediatorRegistrationContext : - IMediatorRegistrationContext + IMediatorRegistrationContext, + ISetScopedConsumeContext { - readonly IRegistrationContext _registration; + readonly RegistrationContext _registration; - public MediatorRegistrationContext(IRegistrationContext registration) + public MediatorRegistrationContext(RegistrationContext registration) { _registration = registration; } @@ -81,5 +83,10 @@ public void ConfigureFuture(IReceiveEndpointConfigurator configurator) { _registration.ConfigureFuture(configurator); } + + public IDisposable PushContext(IServiceScope scope, ConsumeContext context) + { + return _registration.PushContext(scope, context); + } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/MessageScopeConfigurationObserver.cs b/src/MassTransit/DependencyInjection/Configuration/MessageScopeConfigurationObserver.cs index 5eab82f8743..e3e55c28918 100644 --- a/src/MassTransit/DependencyInjection/Configuration/MessageScopeConfigurationObserver.cs +++ b/src/MassTransit/DependencyInjection/Configuration/MessageScopeConfigurationObserver.cs @@ -10,11 +10,19 @@ public class MessageScopeConfigurationObserver : IMessageConfigurationObserver { readonly IServiceProvider _serviceProvider; + readonly ISetScopedConsumeContext _setScopedConsumeContext; public MessageScopeConfigurationObserver(IConsumePipeConfigurator receiveEndpointConfigurator, IServiceProvider serviceProvider) + : this(receiveEndpointConfigurator, serviceProvider, LegacySetScopedConsumeContext.Instance) + { + } + + public MessageScopeConfigurationObserver(IConsumePipeConfigurator receiveEndpointConfigurator, IServiceProvider serviceProvider, + ISetScopedConsumeContext setScopedConsumeContext) : base(receiveEndpointConfigurator) { _serviceProvider = serviceProvider; + _setScopedConsumeContext = setScopedConsumeContext; Connect(this); } @@ -22,7 +30,7 @@ public MessageScopeConfigurationObserver(IConsumePipeConfigurator receiveEndpoin public void MessageConfigured(IConsumePipeConfigurator configurator) where TMessage : class { - var scopeProvider = new ConsumeScopeProvider(_serviceProvider); + var scopeProvider = new ConsumeScopeProvider(_serviceProvider, _setScopedConsumeContext); var scopeFilter = new ScopeMessageFilter(scopeProvider); var specification = new FilterPipeSpecification>(scopeFilter); @@ -31,11 +39,10 @@ public void MessageConfigured(IConsumePipeConfigurator configurator) public override void BatchConsumerConfigured(IConsumerMessageConfigurator> configurator) { - var consumerSpecification = configurator as IConsumerMessageSpecification>; - if (consumerSpecification == null) + if (!(configurator is IConsumerMessageSpecification> consumerSpecification)) throw new ArgumentException("The configurator must be a consumer specification"); - var scopeProvider = new ConsumeScopeProvider(_serviceProvider); + var scopeProvider = new ConsumeScopeProvider(_serviceProvider, _setScopedConsumeContext); var scopeFilter = new ScopeMessageFilter>(scopeProvider); var specification = new FilterPipeSpecification>>(scopeFilter); @@ -44,7 +51,7 @@ public override void BatchConsumerConfigured(IConsumerMessa public override void ActivityConfigured(IExecuteActivityConfigurator configurator, Uri compensateAddress) { - var scopeProvider = new ExecuteActivityScopeProvider(_serviceProvider); + var scopeProvider = new ExecuteActivityScopeProvider(_serviceProvider, _setScopedConsumeContext); var scopeFilter = new ScopeExecuteFilter(scopeProvider); var specification = new FilterPipeSpecification>(scopeFilter); @@ -53,7 +60,7 @@ public override void ActivityConfigured(IExecuteActivityC public override void ExecuteActivityConfigured(IExecuteActivityConfigurator configurator) { - var scopeProvider = new ExecuteActivityScopeProvider(_serviceProvider); + var scopeProvider = new ExecuteActivityScopeProvider(_serviceProvider, _setScopedConsumeContext); var scopeFilter = new ScopeExecuteFilter(scopeProvider); var specification = new FilterPipeSpecification>(scopeFilter); @@ -62,11 +69,23 @@ public override void ExecuteActivityConfigured(IExecuteAc public override void CompensateActivityConfigured(ICompensateActivityConfigurator configurator) { - var scopeProvider = new CompensateActivityScopeProvider(_serviceProvider); + var scopeProvider = new CompensateActivityScopeProvider(_serviceProvider, _setScopedConsumeContext); var scopeFilter = new ScopeCompensateFilter(scopeProvider); var specification = new FilterPipeSpecification>(scopeFilter); configurator.Log(x => x.AddPipeSpecification(specification)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/RegistrationConfigurator.cs b/src/MassTransit/DependencyInjection/Configuration/RegistrationConfigurator.cs index 6a55adc4fb0..79910f7da98 100644 --- a/src/MassTransit/DependencyInjection/Configuration/RegistrationConfigurator.cs +++ b/src/MassTransit/DependencyInjection/Configuration/RegistrationConfigurator.cs @@ -7,13 +7,13 @@ namespace MassTransit.Configuration using DependencyInjection.Registration; using Internals; using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; + using Saga; /// /// Used for registration of consumers and sagas /// - public class RegistrationConfigurator : + public abstract class RegistrationConfigurator : IRegistrationConfigurator { readonly IServiceCollection _collection; @@ -31,13 +31,16 @@ protected RegistrationConfigurator(IServiceCollection collection, IContainerRegi public IContainerRegistrar Registrar { get; } - public IConsumerRegistrationConfigurator AddConsumer(Action> configure) + protected RequestTimeout DefaultRequestTimeout { get; private set; } = RequestTimeout.Default; + + public IConsumerRegistrationConfigurator AddConsumer(Action> configure = null) where T : class, IConsumer { return AddConsumer(null, configure); } - public IConsumerRegistrationConfigurator AddConsumer(Type consumerDefinitionType, Action> configure = null) + public IConsumerRegistrationConfigurator AddConsumer(Type consumerDefinitionType, + Action> configure = null) where T : class, IConsumer { var registration = _collection.RegisterConsumer(Registrar, consumerDefinitionType); @@ -47,13 +50,13 @@ public IConsumerRegistrationConfigurator AddConsumer(Type consumerDefiniti return new ConsumerRegistrationConfigurator(this, registration); } - public ISagaRegistrationConfigurator AddSaga(Action> configure) + public ISagaRegistrationConfigurator AddSaga(Action> configure) where T : class, ISaga { return AddSaga(null, configure); } - public ISagaRegistrationConfigurator AddSaga(Type sagaDefinitionType, Action> configure = null) + public ISagaRegistrationConfigurator AddSaga(Type sagaDefinitionType, Action> configure = null) where T : class, ISaga { if (typeof(T).HasInterface()) @@ -66,14 +69,15 @@ public ISagaRegistrationConfigurator AddSaga(Type sagaDefinitionType, Acti return new SagaRegistrationConfigurator(this, registration); } - public ISagaRegistrationConfigurator AddSagaStateMachine(Action> configure = null) + public ISagaRegistrationConfigurator AddSagaStateMachine(Action> configure = null) where TStateMachine : class, SagaStateMachine where T : class, SagaStateMachineInstance { return AddSagaStateMachine(null, configure); } - public ISagaRegistrationConfigurator AddSagaStateMachine(Type sagaDefinitionType, Action> configure = null) + public ISagaRegistrationConfigurator AddSagaStateMachine(Type sagaDefinitionType, + Action> configure = null) where TStateMachine : class, SagaStateMachine where T : class, SagaStateMachineInstance { @@ -85,7 +89,7 @@ public ISagaRegistrationConfigurator AddSagaStateMachine(Ty } public IExecuteActivityRegistrationConfigurator AddExecuteActivity( - Action> configure) + Action> configure) where TActivity : class, IExecuteActivity where TArguments : class { @@ -93,7 +97,7 @@ public IExecuteActivityRegistrationConfigurator AddExecut } public IExecuteActivityRegistrationConfigurator AddExecuteActivity(Type executeActivityDefinitionType, - Action> configure = null) + Action> configure = null) where TActivity : class, IExecuteActivity where TArguments : class { @@ -105,8 +109,8 @@ public IExecuteActivityRegistrationConfigurator AddExecut } public IActivityRegistrationConfigurator AddActivity( - Action> configureExecute, - Action> configureCompensate) + Action> configureExecute, + Action> configureCompensate) where TActivity : class, IActivity where TArguments : class where TLog : class @@ -115,8 +119,8 @@ public IActivityRegistrationConfigurator AddActivit } public IActivityRegistrationConfigurator AddActivity(Type activityDefinitionType, - Action> configureExecute = null, - Action> configureCompensate = null) + Action> configureExecute = null, + Action> configureCompensate = null) where TActivity : class, IActivity where TArguments : class where TLog : class @@ -137,38 +141,22 @@ public IFutureRegistrationConfigurator AddFuture(Type futureDe return new FutureRegistrationConfigurator(this, registration); } - public void AddConfigureEndpointsCallback(ConfigureEndpointsCallback callback) - { - if (callback == null) - throw new ArgumentNullException(nameof(callback)); - - _collection.TryAddSingleton(provider => new ConfigureReceiveEndpointDelegate(callback)); - } - - public void AddConfigureEndpointsCallback(ConfigureEndpointsProviderCallback callback) - { - if (callback == null) - throw new ArgumentNullException(nameof(callback)); - - _collection.TryAddSingleton(provider => new ConfigureReceiveEndpointDelegateProvider(provider, callback)); - } - public void AddEndpoint(Type definitionType) { _collection.RegisterEndpoint(Registrar, definitionType); } - public void AddEndpoint(IEndpointSettings> settings) + public void AddEndpoint(IRegistration registration, IEndpointSettings> settings) where TDefinition : class, IEndpointDefinition where T : class { - _collection.RegisterEndpoint(Registrar, settings); + _collection.RegisterEndpoint(Registrar, registration, settings); } public void AddRequestClient(RequestTimeout timeout) where T : class { - Registrar.RegisterRequestClient(timeout); + Registrar.RegisterRequestClient(GetRequestTimeout(timeout)); } public void AddRequestClient(Uri destinationAddress, RequestTimeout timeout) @@ -179,17 +167,31 @@ public void AddRequestClient(Uri destinationAddress, RequestTimeout timeout) public void AddRequestClient(Type requestType, RequestTimeout timeout = default) { - RequestClientRegistrationCache.Register(requestType, timeout, Registrar); + RequestClientRegistrationCache.Register(requestType, GetRequestTimeout(timeout), Registrar); } public void AddRequestClient(Type requestType, Uri destinationAddress, RequestTimeout timeout = default) { - RequestClientRegistrationCache.Register(requestType, destinationAddress, timeout, Registrar); + RequestClientRegistrationCache.Register(requestType, destinationAddress, GetRequestTimeout(timeout), Registrar); + } + + public void SetDefaultRequestTimeout(RequestTimeout timeout) + { + DefaultRequestTimeout = timeout; + } + + public void SetDefaultRequestTimeout(int? d = null, int? h = null, int? m = null, int? s = null, int? ms = null) + { + var timeout = new TimeSpan(d ?? 0, h ?? 0, m ?? 0, s ?? 0, ms ?? 0); + if (timeout <= TimeSpan.Zero) + throw new ArgumentException("The timeout must be > 0"); + + DefaultRequestTimeout = timeout; } public void SetEndpointNameFormatter(IEndpointNameFormatter endpointNameFormatter) { - _collection.TryAddSingleton(endpointNameFormatter); + Registrar.RegisterEndpointNameFormatter(endpointNameFormatter); } public ISagaRegistrationConfigurator AddSagaRepository() @@ -263,6 +265,11 @@ public ServiceDescriptor this[int index] set => _collection[index] = value; } + RequestTimeout GetRequestTimeout(RequestTimeout timeout) + { + return timeout == RequestTimeout.Default ? DefaultRequestTimeout : timeout; + } + public void Complete() { if (_sagaRepositoryRegistrationProvider != null) @@ -271,7 +278,7 @@ public void Complete() foreach (var registration in registrations) { - if (_collection.Any(x => x.ServiceType == typeof(ISagaRepository<>).MakeGenericType(registration.Type))) + if (_collection.Any(x => x.ServiceType == typeof(ISagaRepositoryContextFactory<>).MakeGenericType(registration.Type))) continue; var register = (IConfigureSagaRepository)Activator.CreateInstance(typeof(ConfigureSagaRepository<>).MakeGenericType(registration.Type)); @@ -279,14 +286,15 @@ public void Complete() register.Configure(this, _sagaRepositoryRegistrationProvider, registration); } - if (Registrar.GetRegistrations().Any() && _collection.All(x => x.ServiceType != typeof(ISagaRepository))) + if (Registrar.GetRegistrations().Any() + && _collection.All(x => x.ServiceType != typeof(ISagaRepositoryContextFactory))) new ConfigureSagaRepository().Configure(this, _sagaRepositoryRegistrationProvider, null); } } - protected IRegistrationContext CreateRegistration(IServiceProvider provider) + protected RegistrationContext CreateRegistration(IServiceProvider provider, ISetScopedConsumeContext setScopedConsumeContext) { - return new RegistrationContext(provider, Registrar); + return new RegistrationContext(provider, Registrar, setScopedConsumeContext); } protected void ThrowIfAlreadyConfigured(string methodName) diff --git a/src/MassTransit/DependencyInjection/Configuration/RegistrationContext.cs b/src/MassTransit/DependencyInjection/Configuration/RegistrationContext.cs index 520dc46c5ea..a13bc5d88e1 100644 --- a/src/MassTransit/DependencyInjection/Configuration/RegistrationContext.cs +++ b/src/MassTransit/DependencyInjection/Configuration/RegistrationContext.cs @@ -2,17 +2,21 @@ namespace MassTransit.Configuration { using System; using System.Linq; + using Microsoft.Extensions.DependencyInjection; public class RegistrationContext : - IRegistrationContext + IRegistrationContext, + ISetScopedConsumeContext { readonly IServiceProvider _provider; + readonly ISetScopedConsumeContext _setScopedConsumeContext; - public RegistrationContext(IServiceProvider provider, IContainerSelector selector) + public RegistrationContext(IServiceProvider provider, IContainerSelector selector, ISetScopedConsumeContext setScopedConsumeContext) { Selector = selector; _provider = provider; + _setScopedConsumeContext = setScopedConsumeContext; } protected IContainerSelector Selector { get; } @@ -31,16 +35,15 @@ public void ConfigureConsumer(IReceiveEndpointConfigurator configurator, Acti if (!Selector.TryGetValue(_provider, typeof(T), out var consumer)) throw new ArgumentException($"The consumer type was not found: {TypeCache.GetShortName(typeof(T))}", nameof(T)); - consumer.AddConfigureAction(configure); + if (configure != null) + consumer.AddConfigureAction((_, cfg) => configure.Invoke(cfg)); consumer.Configure(configurator, this); } public void ConfigureConsumers(IReceiveEndpointConfigurator configurator) { foreach (var consumer in Selector.GetRegistrations(_provider).Where(x => x.IncludeInConfigureEndpoints)) - { consumer.Configure(configurator, this); - } } public void ConfigureSaga(Type sagaType, IReceiveEndpointConfigurator configurator) @@ -57,16 +60,15 @@ public void ConfigureSaga(IReceiveEndpointConfigurator configurator, Action(_provider, typeof(T), out var saga)) throw new ArgumentException($"The saga type was not found: {TypeCache.GetShortName(typeof(T))}", nameof(T)); - saga.AddConfigureAction(configure); + if (configure != null) + saga.AddConfigureAction((_, cfg) => configure.Invoke(cfg)); saga.Configure(configurator, this); } public void ConfigureSagas(IReceiveEndpointConfigurator configurator) { foreach (var saga in Selector.GetRegistrations(_provider).Where(x => x.IncludeInConfigureEndpoints)) - { saga.Configure(configurator, this); - } } public void ConfigureExecuteActivity(Type activityType, IReceiveEndpointConfigurator configurator) @@ -121,7 +123,15 @@ public void ConfigureFuture(IReceiveEndpointConfigurator configurator) public object GetService(Type serviceType) { + if (serviceType == typeof(IContainerSelector)) + return Selector; + return _provider.GetService(serviceType); } + + public IDisposable PushContext(IServiceScope scope, ConsumeContext context) + { + return _setScopedConsumeContext.PushContext(scope, context); + } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/RegistrationServiceCollectionExtensions.cs b/src/MassTransit/DependencyInjection/Configuration/RegistrationServiceCollectionExtensions.cs index 311ec709d18..f4b9f944963 100644 --- a/src/MassTransit/DependencyInjection/Configuration/RegistrationServiceCollectionExtensions.cs +++ b/src/MassTransit/DependencyInjection/Configuration/RegistrationServiceCollectionExtensions.cs @@ -1,5 +1,8 @@ namespace MassTransit.Configuration { + using System; + using System.Collections.Generic; + using System.Threading.Tasks; using DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -16,19 +19,112 @@ public static void RegisterSagaRepository, TConsumeContextFactory>(); collection.AddScoped, TRepositoryContextFactory>(); + collection.TryAddSingleton, TempSagaRepository>(); + collection.TryAddSingleton, NotSupportedSagaRepository>(); + collection.TryAddSingleton, NotSupportedSagaRepository>(); + } - collection.AddSingleton>(); - collection.AddSingleton>(provider => - new SagaRepository(provider.GetRequiredService>())); + public static void RegisterQuerySagaRepository(this IServiceCollection collection) + where TSaga : class, ISaga + where TQueryRepositoryContextFactory : class, IQuerySagaRepositoryContextFactory + { + collection.AddSingleton, DependencyInjectionQuerySagaRepository>(); + collection.AddScoped, TQueryRepositoryContextFactory>(); + } + + public static void RegisterLoadSagaRepository(this IServiceCollection collection) + where TSaga : class, ISaga + where TLoadRepositoryContextFactory : class, ILoadSagaRepositoryContextFactory + { + collection.AddSingleton, DependencyInjectionLoadSagaRepository>(); + collection.AddScoped, TLoadRepositoryContextFactory>(); } internal static void RemoveSagaRepositories(this IServiceCollection collection) { collection.RemoveAll(typeof(ISagaConsumeContextFactory<,>)); collection.RemoveAll(typeof(ISagaRepositoryContextFactory<>)); + collection.RemoveAll(typeof(IQuerySagaRepositoryContextFactory<>)); + collection.RemoveAll(typeof(ILoadSagaRepositoryContextFactory<>)); - collection.RemoveAll(typeof(DependencyInjectionSagaRepositoryContextFactory<>)); + collection.RemoveAll(typeof(IQuerySagaRepository<>)); + collection.RemoveAll(typeof(ILoadSagaRepository<>)); collection.RemoveAll(typeof(ISagaRepository<>)); } + + + class TempSagaRepository : + ISagaRepository, + IQuerySagaRepository, + ILoadSagaRepository + where TSaga : class, ISaga + { + const string SendSagaIsNotAvailableInIocAnymore = "Send saga is not available in IoC anymore"; + readonly ILoadSagaRepository _loadSagaRepository; + readonly IQuerySagaRepository _querySagaRepository; + + public TempSagaRepository(IQuerySagaRepository querySagaRepository, ILoadSagaRepository loadSagaRepository) + { + _querySagaRepository = querySagaRepository; + _loadSagaRepository = loadSagaRepository; + } + + public Task Load(Guid correlationId) + { + return _loadSagaRepository.Load(correlationId); + } + + public Task> Find(ISagaQuery query) + { + return _querySagaRepository.Find(query); + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateScope("sagaRepository"); + + _querySagaRepository.Probe(scope); + _loadSagaRepository.Probe(scope); + } + + public Task Send(ConsumeContext context, ISagaPolicy policy, IPipe> next) + where T : class + { + throw new NotSupportedException(SendSagaIsNotAvailableInIocAnymore); + } + + public Task SendQuery(ConsumeContext context, ISagaQuery query, ISagaPolicy policy, IPipe> next) + where T : class + { + throw new NotSupportedException(SendSagaIsNotAvailableInIocAnymore); + } + } + + + class NotSupportedSagaRepository : + IQuerySagaRepository, + ILoadSagaRepository + where TSaga : class, ISaga + { + static readonly string QueryErrorMessage = + $"Query-based saga correlation is not available when using current saga repository implementation: {TypeCache.ShortName}"; + + static readonly string LoadErrorMessage = + $"Load-based saga correlation is not available when using current saga repository implementation: {TypeCache.ShortName}"; + + public Task Load(Guid correlationId) + { + throw new NotSupportedException(LoadErrorMessage); + } + + public Task> Find(ISagaQuery query) + { + throw new NotSupportedException(QueryErrorMessage); + } + + public void Probe(ProbeContext context) + { + } + } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/RiderRegistrationContext.cs b/src/MassTransit/DependencyInjection/Configuration/RiderRegistrationContext.cs index 2d059e85ec1..6fbad8df150 100644 --- a/src/MassTransit/DependencyInjection/Configuration/RiderRegistrationContext.cs +++ b/src/MassTransit/DependencyInjection/Configuration/RiderRegistrationContext.cs @@ -2,15 +2,18 @@ namespace MassTransit.Configuration { using System; using System.Collections.Generic; + using Microsoft.Extensions.DependencyInjection; public class RiderRegistrationContext : + ISetScopedConsumeContext, IRiderRegistrationContext { - readonly IRegistrationContext _registration; + readonly RegistrationContext _registration; + readonly IContainerSelector _selector; - public RiderRegistrationContext(IRegistrationContext registration, IContainerSelector selector) + public RiderRegistrationContext(RegistrationContext registration, IContainerSelector selector) { _registration = registration; _selector = selector; @@ -90,5 +93,10 @@ public void ConfigureFuture(IReceiveEndpointConfigurator configurator) { _registration.ConfigureFuture(configurator); } + + public IDisposable PushContext(IServiceScope scope, ConsumeContext context) + { + return _registration.PushContext(scope, context); + } } } diff --git a/src/MassTransit/DependencyInjection/Configuration/ScopedCompensateActivityPipeSpecificationObserver.cs b/src/MassTransit/DependencyInjection/Configuration/ScopedCompensateActivityPipeSpecificationObserver.cs index 71b1f284ed7..174eb3daf68 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ScopedCompensateActivityPipeSpecificationObserver.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ScopedCompensateActivityPipeSpecificationObserver.cs @@ -9,13 +9,16 @@ namespace MassTransit.Configuration public class ScopedCompensateActivityPipeSpecificationObserver : IActivityConfigurationObserver { + readonly IRegistrationContext _context; readonly Type _filterType; - readonly IServiceProvider _provider; + readonly CompositeFilter _messageTypeFilter; - public ScopedCompensateActivityPipeSpecificationObserver(Type filterType, IServiceProvider provider) + public ScopedCompensateActivityPipeSpecificationObserver(Type filterType, IRegistrationContext context, + CompositeFilter messageTypeFilter) { _filterType = filterType; - _provider = provider; + _context = context; + _messageTypeFilter = messageTypeFilter; } public void ActivityConfigured(IExecuteActivityConfigurator configurator, Uri compensateAddress) @@ -34,15 +37,15 @@ public void CompensateActivityConfigured(ICompensateActivityCon where TActivity : class, ICompensateActivity where TLog : class { - if (!_filterType.IsGenericType || !_filterType.IsGenericTypeDefinition) - throw new ConfigurationException("The scoped filter must be a generic type definition"); + if (!_messageTypeFilter.Matches(typeof(TLog))) + return; var filterType = _filterType.MakeGenericType(typeof(TLog)); if (!filterType.HasInterface(typeof(IFilter>))) throw new ConfigurationException($"The scoped filter must implement {TypeCache>>.ShortName} "); - var scopeProvider = new CompensateActivityScopeProvider(_provider); + var scopeProvider = new CompensateActivityScopeProvider(_context); var scopedFilterType = typeof(ScopedCompensateFilter<,,>).MakeGenericType(typeof(TActivity), typeof(TLog), filterType); diff --git a/src/MassTransit/DependencyInjection/Configuration/ScopedConsumePipeSpecificationObserver.cs b/src/MassTransit/DependencyInjection/Configuration/ScopedConsumePipeSpecificationObserver.cs index 2caaa365ea5..7b77f033328 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ScopedConsumePipeSpecificationObserver.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ScopedConsumePipeSpecificationObserver.cs @@ -11,13 +11,17 @@ public class ScopedConsumePipeSpecificationObserver : IConsumerConfigurationObserver, ISagaConfigurationObserver { + readonly IRegistrationContext _context; readonly Type _filterType; - readonly IServiceProvider _provider; + readonly CompositeFilter _messageTypeFilter; - public ScopedConsumePipeSpecificationObserver(Type filterType, IServiceProvider provider) + public ScopedConsumePipeSpecificationObserver(Type filterType, IRegistrationContext context, CompositeFilter messageTypeFilter) { _filterType = filterType; - _provider = provider; + _context = context; + _messageTypeFilter = messageTypeFilter; + // do not create filters for scheduled/outbox messages + _messageTypeFilter.Excludes += type => type == typeof(SerializedMessageBody); } public void ConsumerConfigured(IConsumerConfigurator configurator) @@ -58,19 +62,17 @@ public void SagaMessageConfigured(ISagaMessageConfigurator(IPipeConfigurator> messageConfigurator) where TMessage : class { - if (!_filterType.IsGenericType || !_filterType.IsGenericTypeDefinition) - throw new ConfigurationException("The scoped filter must be a generic type definition"); - - // do not create filters for scheduled/outbox messages - if (typeof(TMessage) == typeof(SerializedMessageBody)) + if (!_messageTypeFilter.Matches(typeof(TMessage))) return; - var filterType = _filterType.MakeGenericType(typeof(TMessage)); + var filterType = _filterType.HasInterface>>() + ? _filterType + : _filterType.MakeGenericType(typeof(TMessage)); if (!filterType.HasInterface(typeof(IFilter>))) throw new ConfigurationException($"The scoped filter must implement {TypeCache>>.ShortName} "); - var scopeProvider = new ConsumeScopeProvider(_provider); + var scopeProvider = new ConsumeScopeProvider(_context); var scopedFilterType = typeof(ScopedConsumeFilter<,>).MakeGenericType(typeof(TMessage), filterType); diff --git a/src/MassTransit/DependencyInjection/Configuration/ScopedExecuteActivityPipeSpecificationObserver.cs b/src/MassTransit/DependencyInjection/Configuration/ScopedExecuteActivityPipeSpecificationObserver.cs index 0ebbe2f1639..0158613aac7 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ScopedExecuteActivityPipeSpecificationObserver.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ScopedExecuteActivityPipeSpecificationObserver.cs @@ -9,13 +9,16 @@ namespace MassTransit.Configuration public class ScopedExecuteActivityPipeSpecificationObserver : IActivityConfigurationObserver { + readonly IRegistrationContext _context; readonly Type _filterType; - readonly IServiceProvider _provider; + readonly CompositeFilter _messageTypeFilter; - public ScopedExecuteActivityPipeSpecificationObserver(Type filterType, IServiceProvider provider) + public ScopedExecuteActivityPipeSpecificationObserver(Type filterType, IRegistrationContext context, + CompositeFilter messageTypeFilter) { _filterType = filterType; - _provider = provider; + _context = context; + _messageTypeFilter = messageTypeFilter; } public void ActivityConfigured(IExecuteActivityConfigurator configurator, Uri compensateAddress) @@ -29,15 +32,15 @@ public void ExecuteActivityConfigured(IExecuteActivityCon where TActivity : class, IExecuteActivity where TArguments : class { - if (!_filterType.IsGenericType || !_filterType.IsGenericTypeDefinition) - throw new ConfigurationException("The scoped filter must be a generic type definition"); + if (!_messageTypeFilter.Matches(typeof(TArguments))) + return; var filterType = _filterType.MakeGenericType(typeof(TArguments)); if (!filterType.HasInterface(typeof(IFilter>))) throw new ConfigurationException($"The scoped filter must implement {TypeCache>>.ShortName} "); - var scopeProvider = new ExecuteActivityScopeProvider(_provider); + var scopeProvider = new ExecuteActivityScopeProvider(_context); var scopedFilterType = typeof(ScopedExecuteFilter<,,>).MakeGenericType(typeof(TActivity), typeof(TArguments), filterType); diff --git a/src/MassTransit/DependencyInjection/Configuration/ScopedFilterSpecificationObserver.cs b/src/MassTransit/DependencyInjection/Configuration/ScopedFilterSpecificationObserver.cs index 05868486c4a..8b4509c244e 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ScopedFilterSpecificationObserver.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ScopedFilterSpecificationObserver.cs @@ -12,12 +12,18 @@ public class ScopedFilterSpecificationObserver : IPublishPipeSpecificationObserver { readonly Type _filterType; + readonly CompositeFilter _messageTypeFilter; readonly IServiceProvider _provider; - public ScopedFilterSpecificationObserver(Type filterType, IServiceProvider provider) + public ScopedFilterSpecificationObserver(Type filterType, IServiceProvider provider, CompositeFilter messageTypeFilter) { _filterType = filterType; _provider = provider; + _messageTypeFilter = messageTypeFilter; + _messageTypeFilter.Excludes += type => type.HasInterface(); + _messageTypeFilter.Excludes += type => type.HasInterface(); + // do not create filters for scheduled/outbox messages + _messageTypeFilter.Excludes += type => type == typeof(SerializedMessageBody); } public void MessageSpecificationCreated(IMessagePublishPipeSpecification specification) @@ -36,17 +42,12 @@ void AddScopedFilter(IPipeConfigurator configurator) where TContext : class, PipeContext where T : class { - if (typeof(T).HasInterface()) - return; - - if (!_filterType.IsGenericType || !_filterType.IsGenericTypeDefinition) - throw new ConfigurationException("The scoped filter must be a generic type definition"); - - // do not create filters for scheduled/outbox messages - if (typeof(T) == typeof(SerializedMessageBody)) + if (!_messageTypeFilter.Matches(typeof(T))) return; - var filterType = _filterType.MakeGenericType(typeof(T)); + var filterType = _filterType.HasInterface>() + ? _filterType + : _filterType.MakeGenericType(typeof(T)); if (!filterType.HasInterface(typeof(IFilter))) throw new ConfigurationException($"The scoped filter must implement {TypeCache>.ShortName} "); diff --git a/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionBusConfigurator.cs b/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionBusConfigurator.cs index 12c7442a0ff..c1c5afe23ad 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionBusConfigurator.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionBusConfigurator.cs @@ -3,7 +3,9 @@ namespace MassTransit.Configuration using System; using System.Collections.Generic; using System.Linq; + using Clients; using Context; + using Courier; using DependencyInjection; using DependencyInjection.Registration; using Microsoft.Extensions.DependencyInjection; @@ -20,27 +22,42 @@ public ServiceCollectionBusConfigurator(IServiceCollection collection) { IBusRegistrationContext CreateRegistrationContext(IServiceProvider provider) { - return new BusRegistrationContext(provider, Registrar); + var setter = provider.GetRequiredService>(); + return new BusRegistrationContext(provider, Registrar, setter.Value); } + static Bind CreateScopeProvider(IServiceProvider provider) + { + var global = provider.GetRequiredService(); + return Bind.Create((IScopedConsumeContextProvider)new TypedScopedConsumeContextProvider(global)); + } + + collection.AddScoped(CreateScopeProvider); + collection.AddSingleton(_ => + Bind.Create((ISetScopedConsumeContext)new SetScopedConsumeContext(provider => + provider.GetRequiredService>().Value))); + collection.AddSingleton(provider => Bind.Create(CreateRegistrationContext(provider))); collection.AddSingleton(provider => provider.GetRequiredService>().Value); collection.TryAdd(ServiceDescriptor.Singleton(typeof(IReceiveEndpointDispatcher<>), typeof(ReceiveEndpointDispatcher<>))); collection.TryAddSingleton(provider => { - var registrationContext = provider.GetRequiredService>().Value; + var context = provider.GetRequiredService>().Value; var busInstance = provider.GetRequiredService>().Value; - return new ReceiveEndpointDispatcherFactory(registrationContext, busInstance); + return new ReceiveEndpointDispatcherFactory(context, busInstance); }); - collection.TryAddSingleton(provider => Bind.Create(provider.GetRequiredService().CreateClientFactory())); + collection.TryAddSingleton(provider => Bind.Create(CreateClientFactory(provider.GetRequiredService(), DefaultRequestTimeout))); collection.TryAddSingleton(provider => provider.GetRequiredService>().Value); - collection.TryAddScoped, ScopedBusContextProvider>(); + collection.TryAddScoped, ScopedBusContextProvider>(); collection.TryAddScoped(provider => provider.GetRequiredService>().Context.SendEndpointProvider); collection.TryAddScoped(provider => provider.GetRequiredService>().Context.PublishEndpoint); + collection.TryAddScoped(provider => new RoutingSlipExecutor( + provider.GetRequiredService>().Context.SendEndpointProvider, + provider.GetRequiredService>().Context.PublishEndpoint)); } protected ServiceCollectionBusConfigurator(IServiceCollection collection, IContainerRegistrar registrar) @@ -49,6 +66,9 @@ protected ServiceCollectionBusConfigurator(IServiceCollection collection, IConta AddMassTransitComponents(collection); } + protected Func CreateClientFactory { get; private set; } = DefaultClientFactory; + + [Obsolete("Use 'Using[TransportName]' instead. Visit https://masstransit.io/obsolete for details.", true)] public virtual void AddBus(Func busFactory) { SetBusFactory(new RegistrationBusFactory(busFactory)); @@ -74,10 +94,35 @@ public virtual void SetBusFactory(T busFactory) public virtual void AddRider(Action configure) { - var configurator = new ServiceCollectionRiderConfigurator(this, new DependencyInjectionRiderContainerRegistrar(this)); + var configurator = new ServiceCollectionRiderConfigurator(this, new DependencyInjectionRiderContainerRegistrar(this)); configure?.Invoke(configurator); } + public virtual void AddConfigureEndpointsCallback(ConfigureEndpointsCallback callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + this.AddSingleton(provider => Bind.Create((IConfigureReceiveEndpoint)new ConfigureReceiveEndpointDelegate(callback))); + } + + public virtual void AddConfigureEndpointsCallback(ConfigureEndpointsProviderCallback callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + this.AddSingleton(provider => Bind.Create((IConfigureReceiveEndpoint)new ConfigureReceiveEndpointDelegateProvider( + provider.GetRequiredService>().Value, callback))); + } + + public virtual void SetRequestClientFactory(Func clientFactory) + { + if (clientFactory == null) + throw new ArgumentNullException(nameof(clientFactory)); + + CreateClientFactory = clientFactory; + } + static IBusInstance CreateBus(T busFactory, IServiceProvider provider) where T : IRegistrationBusFactory { @@ -93,12 +138,23 @@ static void AddMassTransitComponents(IServiceCollection collection) collection.TryAddSingleton(); collection.TryAddScoped(); - collection.TryAddScoped(provider => provider.GetRequiredService().GetContext() ?? MissingConsumeContext.Instance); + collection.TryAddScoped(provider => provider.GetRequiredService()); - collection.TryAddSingleton(provider => new ConsumeScopeProvider(provider)); + collection.TryAddScoped(provider => provider.GetRequiredService().GetContext() ?? MissingConsumeContext.Instance); collection.TryAddScoped(typeof(IRequestClient<>), typeof(GenericRequestClient<>)); } + + /// + /// This is the default client factory, which can be overridden by configuration + /// + /// + /// + /// + static IClientFactory DefaultClientFactory(IBus bus, RequestTimeout timeout = default) + { + return new ClientFactory(new BusClientFactoryContext(bus, timeout)); + } } @@ -113,17 +169,34 @@ public ServiceCollectionBusConfigurator(IServiceCollection collection) { IBusRegistrationContext CreateRegistrationContext(IServiceProvider provider) { - return new BusRegistrationContext(provider, Registrar); + var setter = provider.GetRequiredService>(); + return new BusRegistrationContext(provider, Registrar, setter.Value); } - collection.TryAddSingleton(provider => Bind.Create(provider.GetRequiredService().CreateClientFactory())); + static Bind CreateScopeProvider(IServiceProvider provider) + { + var global = provider.GetRequiredService(); + return Bind.Create((IScopedConsumeContextProvider)new TypedScopedConsumeContextProvider(global)); + } + + collection.TryAddScoped(CreateScopeProvider); + + collection.AddSingleton(_ => + Bind.Create((ISetScopedConsumeContext)new SetScopedConsumeContext(provider => + provider.GetRequiredService>().Value))); + collection.TryAddSingleton(provider => Bind.Create(CreateClientFactory(provider.GetRequiredService(), DefaultRequestTimeout))); collection.TryAddScoped, ScopedBusContextProvider>(); collection.TryAddScoped(provider => Bind.Create(provider.GetRequiredService>().Context.SendEndpointProvider)); collection.TryAddScoped(provider => Bind.Create(provider.GetRequiredService>().Context.PublishEndpoint)); + collection.TryAddScoped(provider => Bind.Create(new RoutingSlipExecutor( + provider.GetRequiredService>().Context.SendEndpointProvider, + provider.GetRequiredService>().Context.PublishEndpoint))); + collection.AddSingleton(provider => Bind.Create(CreateRegistrationContext(provider))); } + [Obsolete("This method is deprecated, please use 'Using[TransportName]' instead", true)] public override void AddBus(Func busFactory) { SetBusFactory(new RegistrationBusFactory(busFactory)); @@ -156,6 +229,21 @@ public void AddRider(Action> configure) configure?.Invoke(configurator); } + public override void AddConfigureEndpointsCallback(ConfigureEndpointsCallback callback) + { + this.AddSingleton(provider => Bind.Create((IConfigureReceiveEndpoint)new ConfigureReceiveEndpointDelegate(callback))); + } + + public override void AddConfigureEndpointsCallback(ConfigureEndpointsProviderCallback callback) + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + this.AddSingleton(provider => Bind.Create((IConfigureReceiveEndpoint)new ConfigureReceiveEndpointDelegateProvider( + provider.GetRequiredService>().Value, + callback))); + } + static IBusInstance CreateBus(T busFactory, IServiceProvider provider) where T : IRegistrationBusFactory { diff --git a/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionMediatorConfigurator.cs b/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionMediatorConfigurator.cs index 949b3854017..6fe9aefc199 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionMediatorConfigurator.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionMediatorConfigurator.cs @@ -14,16 +14,17 @@ public class ServiceCollectionMediatorConfigurator : { Action _configure; - public ServiceCollectionMediatorConfigurator(IServiceCollection collection) + public ServiceCollectionMediatorConfigurator(IServiceCollection collection, Uri baseAddress) : base(collection, new DependencyInjectionMediatorContainerRegistrar(collection)) { IMediatorRegistrationContext CreateRegistrationContext(IServiceProvider provider) { - var registration = CreateRegistration(provider); + var setter = provider.GetRequiredService>(); + var registration = CreateRegistration(provider, setter.Value); return new MediatorRegistrationContext(registration); } - collection.AddSingleton(MediatorFactory); + collection.AddSingleton(e => MediatorFactory(e, baseAddress)); collection.AddSingleton(CreateRegistrationContext); AddMassTransitComponents(collection); @@ -43,20 +44,30 @@ static void AddMassTransitComponents(IServiceCollection collection) collection.AddScoped(); collection.TryAddScoped(); - collection.TryAddScoped(provider => provider.GetRequiredService().GetContext() ?? MissingConsumeContext.Instance); + collection.TryAddScoped(provider => provider.GetRequiredService()); + collection.AddSingleton(_ => + Bind.Create((ISetScopedConsumeContext)new SetScopedConsumeContext(provider => + provider.GetRequiredService>().Value))); - collection.TryAddSingleton(provider => new ConsumeScopeProvider(provider)); + static Bind CreateScopeProvider(IServiceProvider provider) + { + var global = provider.GetRequiredService(); + return Bind.Create((IScopedConsumeContextProvider)new TypedScopedConsumeContextProvider(global)); + } + + collection.TryAddScoped(CreateScopeProvider); + collection.TryAddScoped(provider => provider.GetRequiredService().GetContext() ?? MissingConsumeContext.Instance); collection.TryAddScoped(typeof(IRequestClient<>), typeof(GenericRequestClient<>)); } - IMediator MediatorFactory(IServiceProvider provider) + IMediator MediatorFactory(IServiceProvider provider, Uri baseAddress) { ConfigureLogContext(provider); var context = provider.GetRequiredService(); - return Bus.Factory.CreateMediator(cfg => + return Bus.Factory.CreateMediator(baseAddress, cfg => { _configure?.Invoke(context, cfg); diff --git a/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionRiderConfigurator.cs b/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionRiderConfigurator.cs index 477445bcc46..354110e721f 100644 --- a/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionRiderConfigurator.cs +++ b/src/MassTransit/DependencyInjection/Configuration/ServiceCollectionRiderConfigurator.cs @@ -34,10 +34,21 @@ public virtual void SetRiderFactory(IRegistrationRiderFactory ri IRiderRegistrationContext CreateRegistrationContext(IServiceProvider provider) { - var registration = CreateRegistration(provider); + var setter = provider.GetRequiredService>(); + var registration = CreateRegistration(provider, setter.Value); return new RiderRegistrationContext(registration, Registrar); } + static Bind CreateScopeProvider(IServiceProvider provider) + { + var global = provider.GetRequiredService(); + return Bind.Create((IScopedConsumeContextProvider)new TypedScopedConsumeContextProvider(global)); + } + + this.TryAddScoped(CreateScopeProvider); + + this.AddSingleton(_ => Bind.Create((ISetScopedConsumeContext)new SetScopedConsumeContext(provider => + provider.GetRequiredService>().Value))); this.AddSingleton(provider => Bind.Create(CreateRegistrationContext(provider))); this.AddSingleton(provider => Bind.Create(riderFactory.CreateRider(provider.GetRequiredService>().Value))); @@ -78,10 +89,21 @@ public override void SetRiderFactory(IRegistrationRiderFactory r IRiderRegistrationContext CreateRegistrationContext(IServiceProvider provider) { - var registration = CreateRegistration(provider); + var setter = provider.GetRequiredService>(); + var registration = CreateRegistration(provider, setter.Value); return new RiderRegistrationContext(registration, Registrar); } + static Bind CreateScopeProvider(IServiceProvider provider) + { + var global = provider.GetRequiredService(); + return Bind.Create((IScopedConsumeContextProvider)new TypedScopedConsumeContextProvider(global)); + } + + this.TryAddScoped(CreateScopeProvider); + + this.AddSingleton(_ => Bind.Create((ISetScopedConsumeContext)new SetScopedConsumeContext(provider => + provider.GetRequiredService>().Value))); this.AddSingleton(provider => Bind.Create(CreateRegistrationContext(provider))); this.AddSingleton(provider => Bind.Create(riderFactory.CreateRider(provider.GetRequiredService>().Value))); diff --git a/src/MassTransit/DependencyInjection/Configuration/TestHarnessRegistrationConfigurator.cs b/src/MassTransit/DependencyInjection/Configuration/TestHarnessRegistrationConfigurator.cs index 6d580741540..17dc410f598 100644 --- a/src/MassTransit/DependencyInjection/Configuration/TestHarnessRegistrationConfigurator.cs +++ b/src/MassTransit/DependencyInjection/Configuration/TestHarnessRegistrationConfigurator.cs @@ -78,13 +78,14 @@ public ServiceDescriptor this[int index] set => _configurator[index] = value; } - public IConsumerRegistrationConfigurator AddConsumer(Action> configure = null) + public IConsumerRegistrationConfigurator AddConsumer(Action> configure = null) where T : class, IConsumer { return AddConsumer(null, configure); } - public IConsumerRegistrationConfigurator AddConsumer(Type consumerDefinitionType, Action> configure = null) + public IConsumerRegistrationConfigurator AddConsumer(Type consumerDefinitionType, + Action> configure = null) where T : class, IConsumer { IConsumerRegistrationConfigurator registrationConfigurator = _configurator.AddConsumer(consumerDefinitionType, configure); @@ -94,13 +95,13 @@ public IConsumerRegistrationConfigurator AddConsumer(Type consumerDefiniti return registrationConfigurator; } - public ISagaRegistrationConfigurator AddSaga(Action> configure = null) + public ISagaRegistrationConfigurator AddSaga(Action> configure = null) where T : class, ISaga { return AddSaga(null, configure); } - public ISagaRegistrationConfigurator AddSaga(Type sagaDefinitionType, Action> configure = null) + public ISagaRegistrationConfigurator AddSaga(Type sagaDefinitionType, Action> configure = null) where T : class, ISaga { ISagaRegistrationConfigurator registrationConfigurator = _configurator.AddSaga(sagaDefinitionType, configure); @@ -110,14 +111,15 @@ public ISagaRegistrationConfigurator AddSaga(Type sagaDefinitionType, Acti return registrationConfigurator; } - public ISagaRegistrationConfigurator AddSagaStateMachine(Action> configure = null) + public ISagaRegistrationConfigurator AddSagaStateMachine(Action> configure = null) where TStateMachine : class, SagaStateMachine where T : class, SagaStateMachineInstance { return AddSagaStateMachine(null, configure); } - public ISagaRegistrationConfigurator AddSagaStateMachine(Type sagaDefinitionType, Action> configure = null) + public ISagaRegistrationConfigurator AddSagaStateMachine(Type sagaDefinitionType, + Action> configure = null) where TStateMachine : class, SagaStateMachine where T : class, SagaStateMachineInstance { @@ -129,7 +131,7 @@ public ISagaRegistrationConfigurator AddSagaStateMachine(Ty } public IExecuteActivityRegistrationConfigurator AddExecuteActivity( - Action> configure = null) + Action> configure = null) where TActivity : class, IExecuteActivity where TArguments : class { @@ -137,7 +139,7 @@ public IExecuteActivityRegistrationConfigurator AddExecut } public IExecuteActivityRegistrationConfigurator AddExecuteActivity(Type executeActivityDefinitionType, - Action> configure = null) + Action> configure = null) where TActivity : class, IExecuteActivity where TArguments : class { @@ -145,8 +147,8 @@ public IExecuteActivityRegistrationConfigurator AddExecut } public IActivityRegistrationConfigurator AddActivity( - Action> configureExecute = null, - Action> configureCompensate = null) + Action> configureExecute = null, + Action> configureCompensate = null) where TActivity : class, IActivity where TArguments : class where TLog : class @@ -155,8 +157,8 @@ public IActivityRegistrationConfigurator AddActivit } public IActivityRegistrationConfigurator AddActivity(Type activityDefinitionType, - Action> configureExecute = null, - Action> configureCompensate = null) + Action> configureExecute = null, + Action> configureCompensate = null) where TActivity : class, IActivity where TArguments : class where TLog : class @@ -169,11 +171,11 @@ public void AddEndpoint(Type endpointDefinition) _configurator.AddEndpoint(endpointDefinition); } - public void AddEndpoint(IEndpointSettings> settings = null) + public void AddEndpoint(IRegistration registration, IEndpointSettings> settings = null) where TDefinition : class, IEndpointDefinition where T : class { - _configurator.AddEndpoint(settings); + _configurator.AddEndpoint(registration, settings); } public void AddRequestClient(RequestTimeout timeout = default) @@ -198,6 +200,16 @@ public void AddRequestClient(Type requestType, Uri destinationAddress, RequestTi _configurator.AddRequestClient(requestType, destinationAddress, timeout); } + public void SetDefaultRequestTimeout(RequestTimeout timeout) + { + _configurator.SetDefaultRequestTimeout(timeout); + } + + public void SetDefaultRequestTimeout(int? d = null, int? h = null, int? m = null, int? s = null, int? ms = null) + { + _configurator.SetDefaultRequestTimeout(d, h, m, s, ms); + } + public void SetEndpointNameFormatter(IEndpointNameFormatter endpointNameFormatter) { _configurator.SetEndpointNameFormatter(endpointNameFormatter); @@ -233,8 +245,14 @@ public void AddConfigureEndpointsCallback(ConfigureEndpointsProviderCallback cal _configurator.AddConfigureEndpointsCallback(callback); } + public void SetRequestClientFactory(Func clientFactory) + { + _configurator.SetRequestClientFactory(clientFactory); + } + public IContainerRegistrar Registrar => _configurator.Registrar; + [Obsolete("Use 'Using[TransportName]' instead. Visit https://masstransit.io/obsolete for details.")] public void AddBus(Func busFactory) { _configurator.AddBus(busFactory); diff --git a/src/MassTransit/DependencyInjection/Configuration/TransportRegistrationBusFactory.cs b/src/MassTransit/DependencyInjection/Configuration/TransportRegistrationBusFactory.cs index 07475c5e8c1..8bbc9c3d954 100644 --- a/src/MassTransit/DependencyInjection/Configuration/TransportRegistrationBusFactory.cs +++ b/src/MassTransit/DependencyInjection/Configuration/TransportRegistrationBusFactory.cs @@ -4,6 +4,7 @@ namespace MassTransit.Configuration using System.Collections.Generic; using System.Linq; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; using Transports; @@ -28,6 +29,7 @@ protected IBusInstance CreateBus(T configurator, IBusRegistrat LogContext.ConfigureCurrentLogContextIfNull(context); _hostConfiguration.LogContext = LogContext.Current; + _hostConfiguration.ConsumerStopTimeout = context.GetService>()?.Value.ConsumerStopTimeout; ConnectBusObservers(context, configurator); diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/BaseConsumeScopeProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/BaseConsumeScopeProvider.cs index 770750c19f5..b905b279760 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/BaseConsumeScopeProvider.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/BaseConsumeScopeProvider.cs @@ -10,10 +10,17 @@ namespace MassTransit.DependencyInjection public abstract class BaseConsumeScopeProvider { readonly IServiceProvider _serviceProvider; + protected readonly ISetScopedConsumeContext SetScopedConsumeContext; - protected BaseConsumeScopeProvider(IServiceProvider serviceProvider) + protected BaseConsumeScopeProvider(IRegistrationContext context) + : this(context, context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context))) + { + } + + protected BaseConsumeScopeProvider(IServiceProvider serviceProvider, ISetScopedConsumeContext setScopedConsumeContext) { _serviceProvider = serviceProvider; + SetScopedConsumeContext = setScopedConsumeContext; } protected ValueTask GetScopeContext(TPipeContext context, @@ -25,7 +32,7 @@ protected ValueTask GetScopeContext( if (context.TryGetPayload(out var existingServiceScope)) { return new ValueTask(existingScopeContextFactory(context, existingServiceScope, - existingServiceScope.SetCurrentConsumeContext(context))); + SetScopedConsumeContext.PushContext(existingServiceScope, context))); } var serviceProvider = context.GetPayload(_serviceProvider); @@ -43,7 +50,7 @@ protected ValueTask GetScopeContext( } return new ValueTask(createdScopeContextFactory(scopeContext, serviceScope, - serviceScope.SetCurrentConsumeContext(scopeContext))); + SetScopedConsumeContext.PushContext(serviceScope, scopeContext))); } catch (Exception ex) { diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/BusInstanceBuilder.cs b/src/MassTransit/DependencyInjection/DependencyInjection/BusInstanceBuilder.cs index 412c163270a..6f917716d96 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/BusInstanceBuilder.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/BusInstanceBuilder.cs @@ -50,15 +50,13 @@ public TResult GetBusInstanceType(IBusInstanceBuilderCallback()) + if (!interfaceType.HasInterface()) throw new ArgumentException("Bus instance types must include the IBus interface: " + interfaceType.Name, nameof(interfaceType)); return GetModuleBuilderForType(interfaceType, moduleBuilder => CreateTypeFromInterface(moduleBuilder, interfaceType)); @@ -98,7 +96,7 @@ Type CreateTypeFromInterface(ModuleBuilder builder, Type interfaceType) il.Emit(OpCodes.Call, ctorParent); il.Emit(OpCodes.Ret); - Type[] extraInterfaces = interfaceType.GetTypeInfo().GetAllInterfaces().Except(typeof(IBus).GetTypeInfo().GetAllInterfaces()).ToArray(); + Type[] extraInterfaces = interfaceType.GetAllInterfaces().Except(typeof(IBus).GetAllInterfaces()).ToArray(); IEnumerable properties = interfaceType.GetAllProperties(); foreach (var property in properties) diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/CompensateActivityScopeProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/CompensateActivityScopeProvider.cs index 96e0c824ba1..7d7f4c0c121 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/CompensateActivityScopeProvider.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/CompensateActivityScopeProvider.cs @@ -12,8 +12,13 @@ public class CompensateActivityScopeProvider : where TActivity : class, ICompensateActivity where TLog : class { - public CompensateActivityScopeProvider(IServiceProvider serviceProvider) - : base(serviceProvider) + public CompensateActivityScopeProvider(IRegistrationContext context) + : base(context) + { + } + + public CompensateActivityScopeProvider(IServiceProvider serviceProvider, ISetScopedConsumeContext setScopedConsumeContext) + : base(serviceProvider, setScopedConsumeContext) { } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/ConsumeScopeProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/ConsumeScopeProvider.cs index 50f5d3faff2..0668f06b421 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/ConsumeScopeProvider.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/ConsumeScopeProvider.cs @@ -10,8 +10,13 @@ public class ConsumeScopeProvider : BaseConsumeScopeProvider, IConsumeScopeProvider { - public ConsumeScopeProvider(IServiceProvider serviceProvider) - : base(serviceProvider) + public ConsumeScopeProvider(IRegistrationContext context) + : base(context) + { + } + + public ConsumeScopeProvider(IServiceProvider serviceProvider, ISetScopedConsumeContext setScopedConsumeContext) + : base(serviceProvider, setScopedConsumeContext) { } @@ -59,16 +64,16 @@ static ConsumeContext PipeContextFactory(ConsumeContext consumeContext, return new ConsumeContextScope(consumeContext, serviceScope, serviceScope.ServiceProvider, serviceProvider); } - static IConsumeScopeContext ExistingScopeContextFactory(ConsumeContext consumeContext, IServiceScope serviceScope, IDisposable disposable) + IConsumeScopeContext ExistingScopeContextFactory(ConsumeContext consumeContext, IServiceScope serviceScope, IDisposable disposable) where T : class { - return new ExistingConsumeScopeContext(consumeContext, serviceScope, disposable); + return new ExistingConsumeScopeContext(consumeContext, serviceScope, disposable, SetScopedConsumeContext); } - static IConsumeScopeContext CreatedScopeContextFactory(ConsumeContext consumeContext, IServiceScope serviceScope, IDisposable disposable) + IConsumeScopeContext CreatedScopeContextFactory(ConsumeContext consumeContext, IServiceScope serviceScope, IDisposable disposable) where T : class { - return new CreatedConsumeScopeContext(serviceScope, consumeContext, disposable); + return new CreatedConsumeScopeContext(serviceScope, consumeContext, disposable, SetScopedConsumeContext); } static IConsumerConsumeScopeContext ExistingScopeContextFactory(ConsumeContext consumeContext, diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/CreatedConsumeScopeContext.cs b/src/MassTransit/DependencyInjection/DependencyInjection/CreatedConsumeScopeContext.cs index 116418d751b..34a01082067 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/CreatedConsumeScopeContext.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/CreatedConsumeScopeContext.cs @@ -39,11 +39,13 @@ public class CreatedConsumeScopeContext : { readonly IDisposable _disposable; readonly IServiceScope _scope; + readonly ISetScopedConsumeContext _setter; - public CreatedConsumeScopeContext(IServiceScope scope, ConsumeContext context, IDisposable disposable) + public CreatedConsumeScopeContext(IServiceScope scope, ConsumeContext context, IDisposable disposable, ISetScopedConsumeContext setter) { _scope = scope; _disposable = disposable; + _setter = setter; Context = context; } @@ -63,7 +65,7 @@ public T CreateInstance(params object[] arguments) public IDisposable PushConsumeContext(ConsumeContext context) { - return _scope.SetCurrentConsumeContext(context); + return _setter.PushContext(_scope, context); } public ValueTask DisposeAsync() diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionLoadSagaRepository.cs b/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionLoadSagaRepository.cs new file mode 100644 index 00000000000..fc9a0dc2a85 --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionLoadSagaRepository.cs @@ -0,0 +1,56 @@ +namespace MassTransit.DependencyInjection +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Saga; + + + public class DependencyInjectionLoadSagaRepository : + LoadSagaRepository + where TSaga : class, ISaga + { + public DependencyInjectionLoadSagaRepository(IServiceProvider provider) + : base(new DependencyInjectionLoadSagaRepositoryContextFactory(provider)) + { + } + + + class DependencyInjectionLoadSagaRepositoryContextFactory : + ILoadSagaRepositoryContextFactory + { + readonly IServiceProvider _serviceProvider; + + public DependencyInjectionLoadSagaRepositoryContextFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + var serviceScope = _serviceProvider.CreateScope(); + + try + { + var factory = serviceScope.ServiceProvider.GetRequiredService>(); + + return await factory.Execute(asyncMethod, cancellationToken).ConfigureAwait(false); + } + finally + { + if (serviceScope is IAsyncDisposable asyncDisposable) + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + else + serviceScope.Dispose(); + } + } + + public void Probe(ProbeContext context) + { + context.Add("provider", "dependencyInjection"); + } + } + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionQuerySagaRepository.cs b/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionQuerySagaRepository.cs new file mode 100644 index 00000000000..29f6bbb7f67 --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionQuerySagaRepository.cs @@ -0,0 +1,56 @@ +namespace MassTransit.DependencyInjection +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Saga; + + + public class DependencyInjectionQuerySagaRepository : + QuerySagaRepository + where TSaga : class, ISaga + { + public DependencyInjectionQuerySagaRepository(IServiceProvider provider) + : base(new DependencyInjectionQuerySagaRepositoryContextFactory(provider)) + { + } + + + class DependencyInjectionQuerySagaRepositoryContextFactory : + IQuerySagaRepositoryContextFactory + { + readonly IServiceProvider _serviceProvider; + + public DependencyInjectionQuerySagaRepositoryContextFactory(IServiceProvider serviceProvider) + { + _serviceProvider = serviceProvider; + } + + public void Probe(ProbeContext context) + { + context.Add("provider", "dependencyInjection"); + } + + public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken) + where T : class + { + var serviceScope = _serviceProvider.CreateScope(); + + try + { + var factory = serviceScope.ServiceProvider.GetRequiredService>(); + + return await factory.Execute(asyncMethod, cancellationToken).ConfigureAwait(false); + } + finally + { + if (serviceScope is IAsyncDisposable asyncDisposable) + await asyncDisposable.DisposeAsync().ConfigureAwait(false); + else + serviceScope.Dispose(); + } + } + } + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionSagaRepository.cs b/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionSagaRepository.cs new file mode 100644 index 00000000000..a8216f87af2 --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionSagaRepository.cs @@ -0,0 +1,53 @@ +namespace MassTransit.DependencyInjection +{ + using System; + using System.Threading.Tasks; + using Middleware; + using Saga; + + + public class DependencyInjectionSagaRepository : + ISagaRepository + where TSaga : class, ISaga + { + readonly ISagaRepositoryContextFactory _repositoryContextFactory; + + public DependencyInjectionSagaRepository(IRegistrationContext context) + : this(new DependencyInjectionSagaRepositoryContextFactory(context)) + { + } + + public DependencyInjectionSagaRepository(IServiceProvider serviceProvider, ISetScopedConsumeContext setter) + : this(new DependencyInjectionSagaRepositoryContextFactory(serviceProvider, setter)) + { + } + + DependencyInjectionSagaRepository(ISagaRepositoryContextFactory repositoryContextFactory) + { + _repositoryContextFactory = repositoryContextFactory; + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateScope("dependencyInjectionSagaRepository"); + + _repositoryContextFactory.Probe(scope); + } + + public Task Send(ConsumeContext context, ISagaPolicy policy, IPipe> next) + where T : class + { + var correlationId = context.CorrelationId ?? + throw new SagaException("The CorrelationId was not specified", typeof(TSaga), typeof(T)); + + return _repositoryContextFactory.Send(context, new SendSagaPipe(policy, next, correlationId)); + } + + public Task SendQuery(ConsumeContext context, ISagaQuery query, ISagaPolicy policy, + IPipe> next) + where T : class + { + return _repositoryContextFactory.SendQuery(context, query, new SendQuerySagaPipe(policy, next)); + } + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionSagaRepositoryContextFactory.cs b/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionSagaRepositoryContextFactory.cs index f4991c0798e..f9f5a92af9c 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionSagaRepositoryContextFactory.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/DependencyInjectionSagaRepositoryContextFactory.cs @@ -1,7 +1,6 @@ namespace MassTransit.DependencyInjection { using System; - using System.Threading; using System.Threading.Tasks; using Context; using Microsoft.Extensions.DependencyInjection; @@ -13,13 +12,20 @@ public class DependencyInjectionSagaRepositoryContextFactory : where TSaga : class, ISaga { readonly IServiceProvider _serviceProvider; + readonly ISetScopedConsumeContext _setter; - public DependencyInjectionSagaRepositoryContextFactory(IServiceProvider serviceProvider) + public DependencyInjectionSagaRepositoryContextFactory(IRegistrationContext context) + : this(context, context as ISetScopedConsumeContext ?? throw new ArgumentException(nameof(context))) + { + } + + public DependencyInjectionSagaRepositoryContextFactory(IServiceProvider serviceProvider, ISetScopedConsumeContext setter) { _serviceProvider = serviceProvider; + _setter = setter; } - void IProbeSite.Probe(ProbeContext context) + public void Probe(ProbeContext context) { context.Add("provider", "dependencyInjection"); } @@ -36,26 +42,6 @@ public Task SendQuery(ConsumeContext context, ISagaQuery query, IPi return Send(context, (consumeContext, factory) => factory.SendQuery(consumeContext, query, next)); } - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken) - where T : class - { - var serviceScope = _serviceProvider.CreateScope(); - - try - { - var factory = serviceScope.ServiceProvider.GetRequiredService>(); - - return await factory.Execute(asyncMethod, cancellationToken).ConfigureAwait(false); - } - finally - { - if (serviceScope is IAsyncDisposable asyncDisposable) - await asyncDisposable.DisposeAsync().ConfigureAwait(false); - else - serviceScope.Dispose(); - } - } - async Task Send(ConsumeContext context, Func, ISagaRepositoryContextFactory, Task> send) where T : class { @@ -65,7 +51,7 @@ async Task Send(ConsumeContext context, Func, ISagaRepos if (context.TryGetPayload(out var existingScope)) { - disposable = existingScope.SetCurrentConsumeContext(context); + disposable = _setter.PushContext(existingScope, context); try { @@ -92,7 +78,7 @@ async Task Send(ConsumeContext context, Func, ISagaRepos existing => new ConsumeMessageSchedulerContext(scopeContext, existing.SchedulerFactory)); } - disposable = serviceScope.SetCurrentConsumeContext(scopeContext); + disposable = _setter.PushContext(serviceScope, scopeContext); var consumeContextScope = new ConsumeContextScope(scopeContext); diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/ExecuteActivityScopeProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/ExecuteActivityScopeProvider.cs index 10b048e6671..5baad4daf64 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/ExecuteActivityScopeProvider.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/ExecuteActivityScopeProvider.cs @@ -12,8 +12,13 @@ public class ExecuteActivityScopeProvider : where TActivity : class, IExecuteActivity where TArguments : class { - public ExecuteActivityScopeProvider(IServiceProvider serviceProvider) - : base(serviceProvider) + public ExecuteActivityScopeProvider(IRegistrationContext context) + : base(context) + { + } + + public ExecuteActivityScopeProvider(IServiceProvider serviceProvider, ISetScopedConsumeContext setScopedConsumeContext) + : base(serviceProvider, setScopedConsumeContext) { } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/ExistingConsumeScopeContext.cs b/src/MassTransit/DependencyInjection/DependencyInjection/ExistingConsumeScopeContext.cs index a8c0079449d..b49fed56d6e 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/ExistingConsumeScopeContext.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/ExistingConsumeScopeContext.cs @@ -32,12 +32,14 @@ public class ExistingConsumeScopeContext : { readonly IDisposable _disposable; readonly IServiceScope _scope; + readonly ISetScopedConsumeContext _setter; - public ExistingConsumeScopeContext(ConsumeContext context, IServiceScope scope, IDisposable disposable) + public ExistingConsumeScopeContext(ConsumeContext context, IServiceScope scope, IDisposable disposable, ISetScopedConsumeContext setter) { Context = context; _scope = scope; _disposable = disposable; + _setter = setter; } public ValueTask DisposeAsync() @@ -60,7 +62,7 @@ public T CreateInstance(params object[] arguments) public IDisposable PushConsumeContext(ConsumeContext context) { - return _scope.SetCurrentConsumeContext(context); + return _setter.PushContext(_scope, context); } public ConsumeContext Context { get; } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/FilterScopeProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/FilterScopeProvider.cs index 9fe14fabb32..8e9f865a25d 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/FilterScopeProvider.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/FilterScopeProvider.cs @@ -44,7 +44,7 @@ public DependencyInjectionFilterScopeContext(TContext context, IServiceProvider { Context = context; _scope = context.TryGetPayload(out IServiceProvider provider) - || context.TryGetPayload(out ConsumeContext consumeContext) && consumeContext.TryGetPayload(out provider) + || (context.TryGetPayload(out ConsumeContext consumeContext) && consumeContext.TryGetPayload(out provider)) ? new NoopScope(provider) : serviceProvider.CreateScope(); } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/GenericRequestClient.cs b/src/MassTransit/DependencyInjection/DependencyInjection/GenericRequestClient.cs index 978fab784a3..d0774e597bc 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/GenericRequestClient.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/GenericRequestClient.cs @@ -3,7 +3,6 @@ namespace MassTransit.DependencyInjection using System; using System.Threading; using System.Threading.Tasks; - using Clients; using Mediator; using Microsoft.Extensions.DependencyInjection; @@ -124,8 +123,6 @@ public Task> GetResponse(object values, Request static IRequestClient GetRequestClient(IServiceProvider provider) { - var consumeContext = provider.GetRequiredService().GetContext(); - var clientFactory = provider.GetService(); if (clientFactory != null) return clientFactory.CreateRequestClient(); @@ -133,9 +130,8 @@ static IRequestClient GetRequestClient(IServiceProvider provider) var mediator = provider.GetService(); if (mediator != null) { - return consumeContext != null - ? mediator.CreateRequestClient(consumeContext) - : new ClientFactory(new ScopedClientFactoryContext(mediator, provider)).CreateRequestClient(default); + var consumeContext = provider.GetRequiredService>().Value.GetContext(); + return mediator.CreateRequestClient(consumeContext); } throw new MassTransitException($"Unable to resolve client factory or mediator for request client: {TypeCache.ShortName}"); diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/IScopedConsumeContextProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/IScopedConsumeContextProvider.cs new file mode 100644 index 00000000000..22d1ea1506a --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/IScopedConsumeContextProvider.cs @@ -0,0 +1,12 @@ +namespace MassTransit.DependencyInjection +{ + using System; + + + public interface IScopedConsumeContextProvider + { + bool HasContext { get; } + ConsumeContext GetContext(); + IDisposable PushContext(ConsumeContext context); + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/InternalScopeExtensions.cs b/src/MassTransit/DependencyInjection/DependencyInjection/InternalScopeExtensions.cs deleted file mode 100644 index 55b33f82af5..00000000000 --- a/src/MassTransit/DependencyInjection/DependencyInjection/InternalScopeExtensions.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MassTransit.DependencyInjection -{ - using System; - using Microsoft.Extensions.DependencyInjection; - - - static class InternalScopeExtensions - { - public static IDisposable SetCurrentConsumeContext(this IServiceScope scope, ConsumeContext context) - { - return scope.ServiceProvider.GetRequiredService().PushContext(context); - } - } -} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/LegacySetScopedConsumeContext.cs b/src/MassTransit/DependencyInjection/DependencyInjection/LegacySetScopedConsumeContext.cs new file mode 100644 index 00000000000..2d72a188791 --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/LegacySetScopedConsumeContext.cs @@ -0,0 +1,21 @@ +namespace MassTransit.DependencyInjection +{ + using System; + using Microsoft.Extensions.DependencyInjection; + + + public class LegacySetScopedConsumeContext : + ISetScopedConsumeContext + { + public static readonly ISetScopedConsumeContext Instance = new LegacySetScopedConsumeContext(); + + LegacySetScopedConsumeContext() + { + } + + public IDisposable PushContext(IServiceScope scope, ConsumeContext context) + { + return scope.ServiceProvider.GetRequiredService().PushContext(context); + } + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/MessageHandlerConsumerDefinition.cs b/src/MassTransit/DependencyInjection/DependencyInjection/MessageHandlerConsumerDefinition.cs index 9906bae3c84..4061fe7f214 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/MessageHandlerConsumerDefinition.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/MessageHandlerConsumerDefinition.cs @@ -12,7 +12,8 @@ public class MessageHandlerConsumerDefinition : public int? ConcurrentMessageLimit => default; public Type ConsumerType => typeof(TConsumer); - public void Configure(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + public void Configure(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ActivityRegistration.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ActivityRegistration.cs index df103dd968f..63ca70abf6f 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ActivityRegistration.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ActivityRegistration.cs @@ -14,14 +14,14 @@ public class ActivityRegistration : where TArguments : class where TLog : class { - readonly List>> _compensateActions; - readonly List>> _executeActions; + readonly List>> _compensateActions; + readonly List>> _executeActions; IActivityDefinition _definition; public ActivityRegistration() { - _executeActions = new List>>(); - _compensateActions = new List>>(); + _executeActions = new List>>(); + _compensateActions = new List>>(); IncludeInConfigureEndpoints = !Type.HasAttribute(); } @@ -29,38 +29,38 @@ public ActivityRegistration() public bool IncludeInConfigureEndpoints { get; set; } - public void AddConfigureAction(Action> configure) + public void AddConfigureAction(Action> configure) where T : class, IExecuteActivity where TA : class { - if (configure is Action> action) + if (configure is Action> action) _executeActions.Add(action); } - public void AddConfigureAction(Action> configure) + public void AddConfigureAction(Action> configure) where T : class, ICompensateActivity where TL : class { - if (configure is Action> action) + if (configure is Action> action) _compensateActions.Add(action); } public void Configure(IReceiveEndpointConfigurator executeEndpointConfigurator, IReceiveEndpointConfigurator compensateEndpointConfigurator, - IServiceProvider scopeProvider) + IRegistrationContext context) { - ConfigureCompensate(compensateEndpointConfigurator, scopeProvider); + ConfigureCompensate(compensateEndpointConfigurator, context); - ConfigureExecute(executeEndpointConfigurator, scopeProvider, compensateEndpointConfigurator.InputAddress); + ConfigureExecute(executeEndpointConfigurator, context, compensateEndpointConfigurator.InputAddress); } - IActivityDefinition IActivityRegistration.GetDefinition(IServiceProvider provider) + IActivityDefinition IActivityRegistration.GetDefinition(IRegistrationContext context) { - return GetActivityDefinition(provider); + return GetActivityDefinition(context); } - public void ConfigureCompensate(IReceiveEndpointConfigurator configurator, IServiceProvider configurationServiceProvider) + public void ConfigureCompensate(IReceiveEndpointConfigurator configurator, IRegistrationContext context) { - var activityScopeProvider = configurationServiceProvider.GetRequiredService>(); + var activityScopeProvider = new CompensateActivityScopeProvider(context); var activityFactory = new ScopeCompensateActivityFactory(activityScopeProvider); @@ -68,11 +68,11 @@ public void ConfigureCompensate(IReceiveEndpointConfigurator configurator, IServ configurator.ConfigureConsumeTopology = false; - GetActivityDefinition(configurationServiceProvider) - .Configure(configurator, specification); + GetActivityDefinition(context) + .Configure(configurator, specification, context); - foreach (Action> action in _compensateActions) - action(specification); + foreach (Action> action in _compensateActions) + action(context, specification); LogContext.Info?.Log("Configured endpoint {Endpoint}, Compensate Activity: {ActivityType}", configurator.InputAddress.GetEndpointName(), TypeCache.ShortName); @@ -82,10 +82,10 @@ public void ConfigureCompensate(IReceiveEndpointConfigurator configurator, IServ IncludeInConfigureEndpoints = false; } - public void ConfigureExecute(IReceiveEndpointConfigurator configurator, IServiceProvider configurationServiceProvider, + public void ConfigureExecute(IReceiveEndpointConfigurator configurator, IRegistrationContext context, Uri compensateAddress) { - var activityScopeProvider = configurationServiceProvider.GetRequiredService>(); + var activityScopeProvider = new ExecuteActivityScopeProvider(context); var activityFactory = new ScopeExecuteActivityFactory(activityScopeProvider); @@ -93,11 +93,11 @@ public void ConfigureExecute(IReceiveEndpointConfigurator configurator, IService configurator.ConfigureConsumeTopology = false; - GetActivityDefinition(configurationServiceProvider) - .Configure(configurator, specification); + GetActivityDefinition(context) + .Configure(configurator, specification, context); - foreach (Action> action in _executeActions) - action(specification); + foreach (Action> action in _executeActions) + action(context, specification); LogContext.Info?.Log("Configured endpoint {Endpoint}, Execute Activity: {ActivityType}", configurator.InputAddress.GetEndpointName(), TypeCache.ShortName); diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ActivityRegistrationConfigurator.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ActivityRegistrationConfigurator.cs index 59c4cc45179..2712d82e756 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ActivityRegistrationConfigurator.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ActivityRegistrationConfigurator.cs @@ -34,7 +34,8 @@ public IActivityRegistrationConfigurator ExecuteEndpoint(Action, IExecuteActivity>(configurator.Settings); + _configurator.AddEndpoint, IExecuteActivity>(_registration, + configurator.Settings); return this; } @@ -48,7 +49,8 @@ public IActivityRegistrationConfigurator CompensateEndpoint(Action, ICompensateActivity>(compensateConfigurator.Settings); + _configurator.AddEndpoint, ICompensateActivity>(_registration, + compensateConfigurator.Settings); return this; } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ExecuteActivityRegistration.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ExecuteActivityRegistration.cs index 1f0e50c5969..831c59c663f 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ExecuteActivityRegistration.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ExecuteActivityRegistration.cs @@ -13,12 +13,12 @@ public class ExecuteActivityRegistration : where TActivity : class, IExecuteActivity where TArguments : class { - readonly List>> _configureActions; + readonly List>> _configureActions; IExecuteActivityDefinition _definition; public ExecuteActivityRegistration() { - _configureActions = new List>>(); + _configureActions = new List>>(); IncludeInConfigureEndpoints = !Type.HasAttribute(); } @@ -26,15 +26,15 @@ public ExecuteActivityRegistration() public bool IncludeInConfigureEndpoints { get; set; } - void IExecuteActivityRegistration.AddConfigureAction(Action> configure) + void IExecuteActivityRegistration.AddConfigureAction(Action> configure) { - if (configure is Action> action) + if (configure is Action> action) _configureActions.Add(action); } - public void Configure(IReceiveEndpointConfigurator configurator, IServiceProvider configurationServiceProvider) + public void Configure(IReceiveEndpointConfigurator configurator, IRegistrationContext context) { - var executeActivityScopeProvider = configurationServiceProvider.GetRequiredService>(); + var executeActivityScopeProvider = new ExecuteActivityScopeProvider(context); var executeActivityFactory = new ScopeExecuteActivityFactory(executeActivityScopeProvider); @@ -42,11 +42,11 @@ public void Configure(IReceiveEndpointConfigurator configurator, IServiceProvide configurator.ConfigureConsumeTopology = false; - GetActivityDefinition(configurationServiceProvider) - .Configure(configurator, specification); + GetActivityDefinition(context) + .Configure(configurator, specification, context); - foreach (Action> action in _configureActions) - action(specification); + foreach (Action> action in _configureActions) + action(context, specification); LogContext.Info?.Log("Configured endpoint {Endpoint}, Execute Activity: {ActivityType}", configurator.InputAddress.GetEndpointName(), TypeCache.ShortName); @@ -56,9 +56,9 @@ public void Configure(IReceiveEndpointConfigurator configurator, IServiceProvide IncludeInConfigureEndpoints = false; } - IExecuteActivityDefinition IExecuteActivityRegistration.GetDefinition(IServiceProvider provider) + IExecuteActivityDefinition IExecuteActivityRegistration.GetDefinition(IRegistrationContext context) { - return GetActivityDefinition(provider); + return GetActivityDefinition(context); } IExecuteActivityDefinition GetActivityDefinition(IServiceProvider provider) diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ExecuteActivityRegistrationConfigurator.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ExecuteActivityRegistrationConfigurator.cs index ef7c34920c3..191ff9a4eb3 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ExecuteActivityRegistrationConfigurator.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/ExecuteActivityRegistrationConfigurator.cs @@ -27,7 +27,8 @@ public void Endpoint(Action configure) configure?.Invoke(configurator); - _configurator.AddEndpoint, IExecuteActivity>(configurator.Settings); + _configurator.AddEndpoint, IExecuteActivity>(_registration, + configurator.Settings); } public void ExcludeFromConfigureEndpoints() diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/JobServiceRegistration.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/JobServiceRegistration.cs new file mode 100644 index 00000000000..7e96d38b71d --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Activites/JobServiceRegistration.cs @@ -0,0 +1,86 @@ +#nullable enable +namespace MassTransit.DependencyInjection.Registration +{ + using System; + using System.Collections.Generic; + using Configuration; + using Internals; + using JobService; + using NewIdFormatters; + + + public class JobServiceRegistration : + IJobServiceRegistration + { + readonly List> _configureActions; + readonly List _dependencies; + readonly EndpointRegistrationConfigurator _endpointConfigurator; + readonly Lazy _settings; + + public JobServiceRegistration() + { + _configureActions = new List>(); + _dependencies = new List(4); + + _settings = new Lazy(GetJobServiceSettings); + + _endpointConfigurator = new EndpointRegistrationConfigurator + { + InstanceId = NewId.Next().ToString(ZBase32Formatter.LowerCase), + Temporary = true + }; + + IncludeInConfigureEndpoints = !Type.HasAttribute(); + } + + JobServiceSettings Settings => _settings.Value; + + public Type Type => typeof(JobService); + + public bool IncludeInConfigureEndpoints { get; set; } + + public IEndpointRegistrationConfigurator EndpointRegistrationConfigurator => _endpointConfigurator; + public IEndpointDefinition EndpointDefinition => new JobServiceEndpointDefinition(_endpointConfigurator.Settings, _settings.Value); + + public void AddConfigureAction(Action? configure) + { + if (_settings.IsValueCreated) + throw new ConfigurationException("The settings were already computed"); + + if (configure != null) + _configureActions.Add(configure); + } + + public void AddReceiveEndpointDependency(IReceiveEndpointConfigurator dependency) + { + _dependencies.Add(dependency); + } + + public void Configure(IServiceInstanceConfigurator instanceConfigurator, IRegistrationContext context) + { + AddReceiveEndpointDependency(instanceConfigurator.InstanceEndpointConfigurator); + + Settings.JobService.ConfigureSuperviseJobConsumer(instanceConfigurator.InstanceEndpointConfigurator); + + if (instanceConfigurator.BusConfigurator is IBusObserverConnector connector) + connector.ConnectBusObserver(new JobServiceBusObserver(Settings.JobService)); + + instanceConfigurator.ConnectEndpointConfigurationObserver(new JobServiceEndpointConfigurationObserver(Settings, ConfigureJobConsumerEndpoint)); + } + + void ConfigureJobConsumerEndpoint(IReceiveEndpointConfigurator configurator) + { + foreach (var dependency in _dependencies) + configurator.AddDependency(dependency); + } + + InstanceJobServiceSettings GetJobServiceSettings() + { + var options = new JobConsumerOptions(); + foreach (Action configure in _configureActions) + configure(options); + + return new InstanceJobServiceSettings(options); + } + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/ConsumerRegistration.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/ConsumerRegistration.cs index ceaa8d9a171..36e8a9a0a0a 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/ConsumerRegistration.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/ConsumerRegistration.cs @@ -18,12 +18,12 @@ public class ConsumerRegistration : IConsumerRegistration where TConsumer : class, IConsumer { - readonly List>> _configureActions; + readonly List>> _configureActions; IConsumerDefinition _definition; public ConsumerRegistration() { - _configureActions = new List>>(); + _configureActions = new List>>(); IncludeInConfigureEndpoints = !Type.HasAttribute(); } @@ -31,28 +31,28 @@ public ConsumerRegistration() public bool IncludeInConfigureEndpoints { get; set; } - void IConsumerRegistration.AddConfigureAction(Action> configure) + void IConsumerRegistration.AddConfigureAction(Action> configure) { - if (configure is Action> action) + if (configure is Action> action) _configureActions.Add(action); } - void IConsumerRegistration.Configure(IReceiveEndpointConfigurator configurator, IServiceProvider provider) + void IConsumerRegistration.Configure(IReceiveEndpointConfigurator configurator, IRegistrationContext context) { - var scopeProvider = provider.GetRequiredService(); + IConsumeScopeProvider scopeProvider = new ConsumeScopeProvider(context); IConsumerFactory consumerFactory = new ScopeConsumerFactory(scopeProvider); - var decoratorRegistration = provider.GetService>(); + var decoratorRegistration = context.GetService>(); if (decoratorRegistration != null) consumerFactory = decoratorRegistration.DecorateConsumerFactory(consumerFactory); var consumerConfigurator = new ConsumerConfigurator(consumerFactory, configurator); - GetConsumerDefinition(provider) - .Configure(configurator, consumerConfigurator); + GetConsumerDefinition(context) + .Configure(configurator, consumerConfigurator, context); - foreach (Action> action in _configureActions) - action(consumerConfigurator); + foreach (Action> action in _configureActions) + action(context, consumerConfigurator); var endpointName = configurator.InputAddress.GetEndpointName(); @@ -66,9 +66,9 @@ void IConsumerRegistration.Configure(IReceiveEndpointConfigurator configurator, IncludeInConfigureEndpoints = false; } - IConsumerDefinition IConsumerRegistration.GetDefinition(IServiceProvider provider) + IConsumerDefinition IConsumerRegistration.GetDefinition(IRegistrationContext context) { - return GetConsumerDefinition(provider); + return GetConsumerDefinition(context); } public IConsumerRegistrationConfigurator GetConsumerRegistrationConfigurator(IRegistrationConfigurator registrationConfigurator) diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/ConsumerRegistrationConfigurator.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/ConsumerRegistrationConfigurator.cs index 4c11a3ec9ab..37a806deeee 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/ConsumerRegistrationConfigurator.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/ConsumerRegistrationConfigurator.cs @@ -26,7 +26,7 @@ public void Endpoint(Action configure) configure?.Invoke(configurator); - _configurator.AddEndpoint, TConsumer>(configurator.Settings); + _configurator.AddEndpoint, TConsumer>(_registration, configurator.Settings); } public void ExcludeFromConfigureEndpoints() diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/JobSagaRegistrationConfigurator.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/JobSagaRegistrationConfigurator.cs new file mode 100644 index 00000000000..2292959bdf4 --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/JobSagaRegistrationConfigurator.cs @@ -0,0 +1,64 @@ +#nullable enable +namespace MassTransit.DependencyInjection.Registration +{ + using System; + using Configuration; + using Microsoft.Extensions.DependencyInjection; + + + public class JobSagaRegistrationConfigurator : + IJobSagaRegistrationConfigurator + { + readonly IBusRegistrationConfigurator _configurator; + ISagaRegistrationConfigurator _jobAttemptConfigurator; + ISagaRegistrationConfigurator _jobConfigurator; + ISagaRegistrationConfigurator _jobTypeConfigurator; + + public JobSagaRegistrationConfigurator(IBusRegistrationConfigurator configurator, Action? configure) + { + _configurator = configurator; + + configurator.AddOptions() + .Configure(options => configure?.Invoke(options)); + + _jobTypeConfigurator = configurator.AddSagaStateMachine(); + _jobConfigurator = configurator.AddSagaStateMachine(); + _jobAttemptConfigurator = configurator.AddSagaStateMachine(); + } + + public IJobSagaRegistrationConfigurator Endpoints(Action configure) + { + _jobAttemptConfigurator = _jobAttemptConfigurator.Endpoint(configure); + _jobConfigurator = _jobConfigurator.Endpoint(configure); + _jobTypeConfigurator = _jobTypeConfigurator.Endpoint(configure); + return this; + } + + public IJobSagaRegistrationConfigurator JobAttemptEndpoint(Action configure) + { + _jobAttemptConfigurator = _jobAttemptConfigurator.Endpoint(configure); + return this; + } + + public IJobSagaRegistrationConfigurator JobEndpoint(Action configure) + { + _jobConfigurator = _jobConfigurator.Endpoint(configure); + return this; + } + + public IJobSagaRegistrationConfigurator JobTypeEndpoint(Action configure) + { + _jobTypeConfigurator = _jobTypeConfigurator.Endpoint(configure); + return this; + } + + public IJobSagaRegistrationConfigurator UseRepositoryRegistrationProvider(ISagaRepositoryRegistrationProvider registrationProvider) + { + registrationProvider.Configure(_jobAttemptConfigurator); + registrationProvider.Configure(_jobConfigurator); + registrationProvider.Configure(_jobTypeConfigurator); + + return this; + } + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/JobServiceRegistrationConfigurator.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/JobServiceRegistrationConfigurator.cs new file mode 100644 index 00000000000..09934dfd0cc --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Consumers/JobServiceRegistrationConfigurator.cs @@ -0,0 +1,31 @@ +namespace MassTransit.DependencyInjection.Registration +{ + using System; + using Configuration; + + + public class JobServiceRegistrationConfigurator : + IJobServiceRegistrationConfigurator + { + readonly IBusRegistrationConfigurator _configurator; + readonly IJobServiceRegistration _registration; + + public JobServiceRegistrationConfigurator(IBusRegistrationConfigurator configurator, IJobServiceRegistration registration) + { + _configurator = configurator; + _registration = registration; + } + + public IJobServiceRegistrationConfigurator Options(Action configure) + { + _registration.AddConfigureAction(configure); + + return this; + } + + public void Endpoint(Action configure) + { + configure?.Invoke(_registration.EndpointRegistrationConfigurator); + } + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Endpoints/EndpointRegistration.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Endpoints/EndpointRegistration.cs index c9821d9646c..4aee31a8980 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Endpoints/EndpointRegistration.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Endpoints/EndpointRegistration.cs @@ -9,11 +9,18 @@ public class EndpointRegistration : IEndpointRegistration where T : class { + readonly IRegistration _registration; + + public EndpointRegistration(IRegistration registration) + { + _registration = registration; + } + public Type Type => typeof(T); public bool IncludeInConfigureEndpoints { - get => true; + get => _registration.IncludeInConfigureEndpoints; set { } } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/DefaultFutureDefinition.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/DefaultFutureDefinition.cs index e87eecda8d5..ce6a3bd7aed 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/DefaultFutureDefinition.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/DefaultFutureDefinition.cs @@ -4,11 +4,12 @@ public class DefaultFutureDefinition : FutureDefinition where TFuture : class, SagaStateMachine { - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) { endpointConfigurator.UseDelayedRedelivery(r => r.Intervals(5000, 30000, 120000)); endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 200, 500)); - endpointConfigurator.UseInMemoryOutbox(); + endpointConfigurator.UseInMemoryOutbox(context); } } } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRegistration.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRegistration.cs index 278b579fa82..4611818c6d3 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRegistration.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRegistration.cs @@ -22,19 +22,19 @@ public FutureRegistration() public bool IncludeInConfigureEndpoints { get; set; } - public void Configure(IReceiveEndpointConfigurator configurator, IServiceProvider provider) + public void Configure(IReceiveEndpointConfigurator configurator, IRegistrationContext context) { - var stateMachine = provider.GetRequiredService(); - var repository = provider.GetRequiredService>(); + var stateMachine = context.GetRequiredService(); + ISagaRepository repository = new DependencyInjectionSagaRepository(context); - var decoratorRegistration = provider.GetService>(); + var decoratorRegistration = context.GetService>(); if (decoratorRegistration != null) repository = decoratorRegistration.DecorateSagaRepository(repository); - var sagaConfigurator = new StateMachineSagaConfigurator(stateMachine, repository, configurator); + var sagaConfigurator = new MassTransitStateMachine.StateMachineSagaConfigurator(stateMachine, repository, configurator); - GetFutureDefinition(provider) - .Configure(configurator, sagaConfigurator); + GetFutureDefinition(context) + .Configure(configurator, sagaConfigurator, context); LogContext.Info?.Log("Configured endpoint {Endpoint}, Future: {FutureType}", configurator.InputAddress.GetEndpointName(), TypeCache.ShortName); @@ -44,9 +44,9 @@ public void Configure(IReceiveEndpointConfigurator configurator, IServiceProvide IncludeInConfigureEndpoints = false; } - public IFutureDefinition GetDefinition(IServiceProvider provider) + public IFutureDefinition GetDefinition(IRegistrationContext context) { - return GetFutureDefinition(provider); + return GetFutureDefinition(context); } IFutureDefinition GetFutureDefinition(IServiceProvider provider) diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRegistrationConfigurator.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRegistrationConfigurator.cs index 5644280647e..a1f9b65be86 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRegistrationConfigurator.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRegistrationConfigurator.cs @@ -36,7 +36,7 @@ public IFutureRegistrationConfigurator Endpoint(Action, TFuture>(configurator.Settings); + _configurator.AddEndpoint, TFuture>(_registration, configurator.Settings); return this; } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRequestConsumerDefinition.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRequestConsumerDefinition.cs index 1ddbad5c15a..232cba17b6f 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRequestConsumerDefinition.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/FutureRequestConsumerDefinition.cs @@ -15,13 +15,14 @@ public class FutureRequestConsumerDefinition : _requestAddress?.Value ?? throw new ConfigurationException($"The future consumer definition was not configured: {TypeCache.ShortName}"); - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator) + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { endpointConfigurator.ConfigureConsumeTopology = false; _requestAddress = new Lazy(() => endpointConfigurator.InputAddress); - base.ConfigureConsumer(endpointConfigurator, consumerConfigurator); + base.ConfigureConsumer(endpointConfigurator, consumerConfigurator, context); } } } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/RequestConsumerFutureDefinition.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/RequestConsumerFutureDefinition.cs index 90215ebeea0..3510e04eb01 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/RequestConsumerFutureDefinition.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Futures/RequestConsumerFutureDefinition.cs @@ -26,10 +26,11 @@ public RequestConsumerFutureDefinition(IConsumerDefinition consumerDe _requestDefinition?.RequestAddress ?? throw new ConfigurationException($"The consumer definition was not a FutureConsumerDefinition: {TypeCache.ShortName}"); - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 200, 500, 1000, 5000, 10000)); - endpointConfigurator.UseInMemoryOutbox(); + endpointConfigurator.UseInMemoryOutbox(context); } } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/RegistrationBusFactory.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/RegistrationBusFactory.cs index f8c17d74c2a..8e047216c12 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/RegistrationBusFactory.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/RegistrationBusFactory.cs @@ -64,7 +64,8 @@ public HostReceiveEndpointHandle ConnectReceiveEndpoint(IEndpointDefinition defi { return BusControl.ConnectReceiveEndpoint(definition, endpointNameFormatter, configurator => { - _busRegistrationContext.GetConfigureReceiveEndpoints().Configure(definition.GetEndpointName(endpointNameFormatter), configurator); + _busRegistrationContext.GetConfigureReceiveEndpoints() + .Configure(definition.GetEndpointName(endpointNameFormatter), configurator); configure?.Invoke(_busRegistrationContext, configurator); }); diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaRegistration.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaRegistration.cs index 2e9579fd4a1..8a04ae9ef7d 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaRegistration.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaRegistration.cs @@ -17,12 +17,12 @@ public class SagaRegistration : ISagaRegistration where TSaga : class, ISaga { - readonly List>> _configureActions; + readonly List>> _configureActions; ISagaDefinition _definition; public SagaRegistration() { - _configureActions = new List>>(); + _configureActions = new List>>(); IncludeInConfigureEndpoints = !Type.HasAttribute(); } @@ -30,27 +30,27 @@ public SagaRegistration() public bool IncludeInConfigureEndpoints { get; set; } - void ISagaRegistration.AddConfigureAction(Action> configure) + void ISagaRegistration.AddConfigureAction(Action> configure) { - if (configure is Action> action) + if (configure is Action> action) _configureActions.Add(action); } - void ISagaRegistration.Configure(IReceiveEndpointConfigurator configurator, IServiceProvider provider) + void ISagaRegistration.Configure(IReceiveEndpointConfigurator configurator, IRegistrationContext context) { - var repository = provider.GetRequiredService>(); + ISagaRepository repository = new DependencyInjectionSagaRepository(context); - var decoratorRegistration = provider.GetService>(); + var decoratorRegistration = context.GetService>(); if (decoratorRegistration != null) repository = decoratorRegistration.DecorateSagaRepository(repository); var sagaConfigurator = new SagaConfigurator(repository, configurator); - GetSagaDefinition(provider) - .Configure(configurator, sagaConfigurator); + GetSagaDefinition(context) + .Configure(configurator, sagaConfigurator, context); - foreach (Action> action in _configureActions) - action(sagaConfigurator); + foreach (Action> action in _configureActions) + action(context, sagaConfigurator); LogContext.Info?.Log("Configured endpoint {Endpoint}, Saga: {SagaType}", configurator.InputAddress.GetEndpointName(), TypeCache.ShortName); @@ -60,9 +60,9 @@ void ISagaRegistration.Configure(IReceiveEndpointConfigurator configurator, ISer IncludeInConfigureEndpoints = false; } - ISagaDefinition ISagaRegistration.GetDefinition(IServiceProvider provider) + ISagaDefinition ISagaRegistration.GetDefinition(IRegistrationContext context) { - return GetSagaDefinition(provider); + return GetSagaDefinition(context); } ISagaDefinition GetSagaDefinition(IServiceProvider provider) diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaRegistrationConfigurator.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaRegistrationConfigurator.cs index feb47e9fd2b..69a40a21ce8 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaRegistrationConfigurator.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaRegistrationConfigurator.cs @@ -37,7 +37,7 @@ public ISagaRegistrationConfigurator Endpoint(Action, TSaga>(configurator.Settings); + _configurator.AddEndpoint, TSaga>(_registration, configurator.Settings); return this; } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaStateMachineRegistration.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaStateMachineRegistration.cs index dff462952c4..e236f94d01e 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaStateMachineRegistration.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Registration/Sagas/SagaStateMachineRegistration.cs @@ -19,12 +19,12 @@ public class SagaStateMachineRegistration : where TStateMachine : class, SagaStateMachine where TInstance : class, SagaStateMachineInstance { - readonly List>> _configureActions; + readonly List>> _configureActions; ISagaDefinition _definition; public SagaStateMachineRegistration() { - _configureActions = new List>>(); + _configureActions = new List>>(); IncludeInConfigureEndpoints = !Type.HasAttribute(); } @@ -32,35 +32,35 @@ public SagaStateMachineRegistration() public bool IncludeInConfigureEndpoints { get; set; } - public void AddConfigureAction(Action> configure) + public void AddConfigureAction(Action> configure) where T : class, ISaga { - if (configure is Action> action) + if (configure is Action> action) _configureActions.Add(action); } - public void Configure(IReceiveEndpointConfigurator configurator, IServiceProvider provider) + public void Configure(IReceiveEndpointConfigurator configurator, IRegistrationContext context) { - var stateMachine = provider.GetRequiredService>(); - var repository = provider.GetRequiredService>(); + var stateMachine = context.GetRequiredService>(); + ISagaRepository repository = new DependencyInjectionSagaRepository(context); - var decoratorRegistration = provider.GetService>(); + var decoratorRegistration = context.GetService>(); if (decoratorRegistration != null) repository = decoratorRegistration.DecorateSagaRepository(repository); - var stateMachineConfigurator = new StateMachineSagaConfigurator(stateMachine, repository, configurator); + var stateMachineConfigurator = new MassTransitStateMachine.StateMachineSagaConfigurator(stateMachine, repository, configurator); - GetSagaDefinition(provider) - .Configure(configurator, stateMachineConfigurator); + GetSagaDefinition(context) + .Configure(configurator, stateMachineConfigurator, context); - foreach (Action> action in _configureActions) - action(stateMachineConfigurator); + foreach (Action> action in _configureActions) + action(context, stateMachineConfigurator); - IEnumerable> eventObservers = provider.GetServices>(); + IEnumerable> eventObservers = context.GetServices>(); foreach (IEventObserver eventObserver in eventObservers) stateMachine.ConnectEventObserver(eventObserver); - IEnumerable> stateObservers = provider.GetServices>(); + IEnumerable> stateObservers = context.GetServices>(); foreach (IStateObserver stateObserver in stateObservers) stateMachine.ConnectStateObserver(stateObserver); @@ -73,9 +73,9 @@ public void Configure(IReceiveEndpointConfigurator configurator, IServiceProvide IncludeInConfigureEndpoints = false; } - ISagaDefinition ISagaRegistration.GetDefinition(IServiceProvider provider) + ISagaDefinition ISagaRegistration.GetDefinition(IRegistrationContext context) { - return GetSagaDefinition(provider); + return GetSagaDefinition(context); } ISagaDefinition GetSagaDefinition(IServiceProvider provider) diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/ScopedBusContextProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/ScopedBusContextProvider.cs index ad3086c67c0..4b01d5c8d64 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/ScopedBusContextProvider.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/ScopedBusContextProvider.cs @@ -3,26 +3,6 @@ namespace MassTransit.DependencyInjection using System; - /// - /// Captures the bus context for the current scope as a scoped provider, so that it can be resolved - /// by components at runtime (since MS DI doesn't support runtime configuration of scopes) - /// - public class ScopedBusContextProvider : - IScopedBusContextProvider - { - public ScopedBusContextProvider(IBus bus, Bind clientFactory, ScopedConsumeContextProvider consumeContextProvider, - IServiceProvider provider) - { - if (consumeContextProvider.HasContext) - Context = new ConsumeContextScopedBusContext(consumeContextProvider.GetContext(), clientFactory.Value); - else - Context = new BusScopedBusContext(bus, clientFactory.Value, provider); - } - - public ScopedBusContext Context { get; } - } - - /// /// Captures the bus context for the current scope as a scoped provider, so that it can be resolved /// by components at runtime (since MS DI doesn't support runtime configuration of scopes) @@ -31,11 +11,15 @@ public class ScopedBusContextProvider : IScopedBusContextProvider where TBus : class, IBus { - public ScopedBusContextProvider(TBus bus, Bind clientFactory, ScopedConsumeContextProvider consumeContextProvider, + public ScopedBusContextProvider(TBus bus, Bind clientFactory, + Bind consumeContextProvider, + IScopedConsumeContextProvider globalConsumeContextProvider, IServiceProvider provider) { - if (consumeContextProvider.HasContext) - Context = new ConsumeContextScopedBusContext(bus, consumeContextProvider.GetContext(), clientFactory.Value, provider); + if (consumeContextProvider.Value.HasContext) + Context = new ConsumeContextScopedBusContext(consumeContextProvider.Value.GetContext(), clientFactory.Value); + else if (globalConsumeContextProvider.HasContext) + Context = new ConsumeContextScopedBusContext(bus, globalConsumeContextProvider.GetContext(), clientFactory.Value, provider); else Context = new BusScopedBusContext(bus, clientFactory.Value, provider); } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/ScopedConsumeContextProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/ScopedConsumeContextProvider.cs index 375bf500ddb..ab3523160e1 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/ScopedConsumeContextProvider.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/ScopedConsumeContextProvider.cs @@ -9,13 +9,14 @@ namespace MassTransit.DependencyInjection /// Captures the for the current message as a scoped provider, so that it can be resolved /// by components at runtime (since MS DI doesn't support runtime configuration of scopes) /// - public class ScopedConsumeContextProvider + public class ScopedConsumeContextProvider : + IScopedConsumeContextProvider { ConsumeContext _context; public bool HasContext => _context != null && !(_context is MissingConsumeContext); - public IDisposable PushContext(ConsumeContext context) + public virtual IDisposable PushContext(ConsumeContext context) { if (context == null) throw new ArgumentNullException(nameof(context)); @@ -30,14 +31,14 @@ public IDisposable PushContext(ConsumeContext context) } } - void PopContext(ConsumeContext context, ConsumeContext originalContext) + public ConsumeContext GetContext() { - Interlocked.CompareExchange(ref _context, originalContext, context); + return _context; } - public ConsumeContext GetContext() + void PopContext(ConsumeContext context, ConsumeContext originalContext) { - return _context; + Interlocked.CompareExchange(ref _context, originalContext, context); } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/SetScopedConsumeContext.cs b/src/MassTransit/DependencyInjection/DependencyInjection/SetScopedConsumeContext.cs new file mode 100644 index 00000000000..bf435ae41bc --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/SetScopedConsumeContext.cs @@ -0,0 +1,22 @@ +namespace MassTransit.DependencyInjection +{ + using System; + using Microsoft.Extensions.DependencyInjection; + + + public class SetScopedConsumeContext : + ISetScopedConsumeContext + { + readonly Func _setterProvider; + + public SetScopedConsumeContext(Func setterProvider) + { + _setterProvider = setterProvider; + } + + public IDisposable PushContext(IServiceScope scope, ConsumeContext context) + { + return _setterProvider(scope.ServiceProvider).PushContext(context); + } + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/Testing/ContainerTestHarness.cs b/src/MassTransit/DependencyInjection/DependencyInjection/Testing/ContainerTestHarness.cs index 6156c51d980..03a010382c8 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/Testing/ContainerTestHarness.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/Testing/ContainerTestHarness.cs @@ -97,6 +97,7 @@ public async ValueTask DisposeAsync() public ISentMessageList Sent => _sent.Value.Messages; public IServiceScope Scope => _scope.Value; + public IServiceProvider Provider => _provider; public IEndpointNameFormatter EndpointNameFormatter => _provider.GetService() ?? DefaultEndpointNameFormatter.Instance; @@ -142,9 +143,25 @@ public Task GetConsumerEndpoint() { var provider = _scope.Value.ServiceProvider.GetRequiredService(); - var shortName = new Uri($"queue:{EndpointNameFormatter.Consumer()}"); + return provider.GetSendEndpoint(GetConsumerAddress()); + } + + public Task GetHandlerEndpoint() + where T : class + { + return GetConsumerEndpoint>(); + } + + public Uri GetConsumerAddress() + where T : class, IConsumer + { + return new Uri($"queue:{EndpointNameFormatter.Consumer()}"); + } - return provider.GetSendEndpoint(shortName); + public Uri GetHandlerAddress() + where T : class + { + return GetConsumerAddress>(); } public Task GetSagaEndpoint() @@ -152,9 +169,13 @@ public Task GetSagaEndpoint() { var provider = _scope.Value.ServiceProvider.GetRequiredService(); - var shortName = new Uri($"queue:{EndpointNameFormatter.Saga()}"); + return provider.GetSendEndpoint(GetSagaAddress()); + } - return provider.GetSendEndpoint(shortName); + public Uri GetSagaAddress() + where T : class, ISaga + { + return new Uri($"queue:{EndpointNameFormatter.Saga()}"); } public Task GetExecuteActivityEndpoint() @@ -163,9 +184,14 @@ public Task GetExecuteActivityEndpoint() { var provider = _scope.Value.ServiceProvider.GetRequiredService(); - var shortName = new Uri($"queue:{EndpointNameFormatter.ExecuteActivity()}"); + return provider.GetSendEndpoint(GetExecuteActivityAddress()); + } - return provider.GetSendEndpoint(shortName); + public Uri GetExecuteActivityAddress() + where T : class, IExecuteActivity + where TArguments : class + { + return new Uri($"queue:{EndpointNameFormatter.ExecuteActivity()}"); } public async Task Start() diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/TransactionalScopedBusContextProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/TransactionalScopedBusContextProvider.cs index ef0c093a724..ca1e17307a7 100644 --- a/src/MassTransit/DependencyInjection/DependencyInjection/TransactionalScopedBusContextProvider.cs +++ b/src/MassTransit/DependencyInjection/DependencyInjection/TransactionalScopedBusContextProvider.cs @@ -9,10 +9,13 @@ public class TransactionalScopedBusContextProvider : where TBus : class, IBus { public TransactionalScopedBusContextProvider(ITransactionalBus bus, Bind clientFactory, - ScopedConsumeContextProvider consumeContextProvider, IServiceProvider provider) + Bind consumeContextProvider, IScopedConsumeContextProvider globalConsumeContextProvider, + IServiceProvider provider) { - if (consumeContextProvider.HasContext) - Context = new ConsumeContextScopedBusContext(consumeContextProvider.GetContext(), clientFactory.Value); + if (consumeContextProvider.Value.HasContext) + Context = new ConsumeContextScopedBusContext(consumeContextProvider.Value.GetContext(), clientFactory.Value); + else if (globalConsumeContextProvider.HasContext) + Context = new ConsumeContextScopedBusContext(bus, globalConsumeContextProvider.GetContext(), clientFactory.Value, provider); else Context = new BusScopedBusContext(bus, clientFactory.Value, provider); } diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/TypedScopedConsumeContextProvider.cs b/src/MassTransit/DependencyInjection/DependencyInjection/TypedScopedConsumeContextProvider.cs new file mode 100644 index 00000000000..74eb6daa7fb --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/TypedScopedConsumeContextProvider.cs @@ -0,0 +1,39 @@ +namespace MassTransit.DependencyInjection +{ + using System; + + + public class TypedScopedConsumeContextProvider : + ScopedConsumeContextProvider + { + readonly IScopedConsumeContextProvider _global; + + public TypedScopedConsumeContextProvider(IScopedConsumeContextProvider global) + { + _global = global; + } + + public override IDisposable PushContext(ConsumeContext context) + { + return new CombinedDisposable(_global.PushContext(context), base.PushContext(context)); + } + + + class CombinedDisposable : + IDisposable + { + readonly IDisposable[] _disposables; + + public CombinedDisposable(params IDisposable[] disposables) + { + _disposables = disposables; + } + + public void Dispose() + { + for (var i = 0; i < _disposables.Length; i++) + _disposables[i].Dispose(); + } + } + } +} diff --git a/src/MassTransit/DependencyInjection/DependencyInjection/ValidateMassTransitHostOptions.cs b/src/MassTransit/DependencyInjection/DependencyInjection/ValidateMassTransitHostOptions.cs new file mode 100644 index 00000000000..6ada9ed7a1f --- /dev/null +++ b/src/MassTransit/DependencyInjection/DependencyInjection/ValidateMassTransitHostOptions.cs @@ -0,0 +1,17 @@ +namespace MassTransit.DependencyInjection +{ + using Microsoft.Extensions.Options; + + + public class ValidateMassTransitHostOptions : + IValidateOptions + { + public ValidateOptionsResult Validate(string name, MassTransitHostOptions options) + { + if (options.StopTimeout < options.ConsumerStopTimeout) + return ValidateOptionsResult.Fail($"{nameof(options.ConsumerStopTimeout)} should be less than or equals to ${nameof(options.StopTimeout)}"); + + return ValidateOptionsResult.Success; + } + } +} diff --git a/src/MassTransit/EndpointConventionCache.cs b/src/MassTransit/EndpointConventionCache.cs index 17980cf9115..e135e8ad902 100644 --- a/src/MassTransit/EndpointConventionCache.cs +++ b/src/MassTransit/EndpointConventionCache.cs @@ -3,7 +3,6 @@ namespace MassTransit using System; using System.Collections.Concurrent; using System.Linq; - using System.Threading; public static class EndpointConventionCache @@ -154,7 +153,7 @@ bool IEndpointConventionCache.TryGetEndpointAddress(out Uri address) static class Cached { internal static Lazy> Metadata = new Lazy>( - () => new EndpointConventionCache(), LazyThreadSafetyMode.PublicationOnly); + () => new EndpointConventionCache()); } } } diff --git a/src/MassTransit/Exceptions/InvalidLicenseException.cs b/src/MassTransit/Exceptions/InvalidLicenseException.cs new file mode 100644 index 00000000000..7b26c6d770b --- /dev/null +++ b/src/MassTransit/Exceptions/InvalidLicenseException.cs @@ -0,0 +1,20 @@ +namespace MassTransit +{ + using System; + + + [Serializable] + public class InvalidLicenseException : + Exception + { + public InvalidLicenseException() + : this("The license was not valid") + { + } + + public InvalidLicenseException(string message) + : base(message) + { + } + } +} diff --git a/src/MassTransit/Exceptions/InvalidLicenseFormatException.cs b/src/MassTransit/Exceptions/InvalidLicenseFormatException.cs new file mode 100644 index 00000000000..e5de2ce1de5 --- /dev/null +++ b/src/MassTransit/Exceptions/InvalidLicenseFormatException.cs @@ -0,0 +1,20 @@ +namespace MassTransit +{ + using System; + + + [Serializable] + public class InvalidLicenseFormatException : + Exception + { + public InvalidLicenseFormatException() + : this("The license format was not recognized") + { + } + + public InvalidLicenseFormatException(string message) + : base(message) + { + } + } +} diff --git a/src/MassTransit/Futures/Future.cs b/src/MassTransit/Futures/Future.cs index b9b091b5bda..6928f301390 100644 --- a/src/MassTransit/Futures/Future.cs +++ b/src/MassTransit/Futures/Future.cs @@ -417,6 +417,18 @@ protected void WhenAnyFaulted(Action> configure configure?.Invoke(configurator); } + /// + /// When all requests have either completed or faulted, Set the future Faulted + /// + /// + protected void WhenAllCompletedOrFaulted(Action> configure) + { + _fault.WaitForPending = true; + var configurator = new FutureFaultConfigurator(_fault); + + configure?.Invoke(configurator); + } + static Task GetResult(BehaviorContext context) { if (context.TryGetResult(context.Saga.CorrelationId, out TResult completed)) diff --git a/src/MassTransit/Futures/Futures/FutureFault.cs b/src/MassTransit/Futures/Futures/FutureFault.cs index 52039513dfa..e28c71edfc8 100644 --- a/src/MassTransit/Futures/Futures/FutureFault.cs +++ b/src/MassTransit/Futures/Futures/FutureFault.cs @@ -26,6 +26,8 @@ public ContextMessageFactory, TFault> Facto set => _factory = value; } + public bool WaitForPending { get; set; } + public IEnumerable Validate() { yield break; @@ -33,12 +35,15 @@ public IEnumerable Validate() public async Task SetFaulted(BehaviorContext context) { - context.SetFaulted(context.Saga.CorrelationId); + if (!WaitForPending || !context.Saga.HasPending()) + { + context.SetFaulted(context.Saga.CorrelationId); - var fault = await context.SendMessageToSubscriptions(_factory, - context.Saga.HasSubscriptions() ? context.Saga.Subscriptions.ToArray() : Array.Empty()); + var fault = await context.SendMessageToSubscriptions(_factory, + context.Saga.HasSubscriptions() ? context.Saga.Subscriptions.ToArray() : Array.Empty()); - context.SetFault(context.Saga.CorrelationId, fault); + context.SetFault(context.Saga.CorrelationId, fault); + } } static Task> DefaultFactory(BehaviorContext context) @@ -86,6 +91,8 @@ public ContextMessageFactory, TFault> Factory set => _factory = value; } + public bool WaitForPending { get; set; } + public IEnumerable Validate() { yield break; @@ -93,12 +100,15 @@ public IEnumerable Validate() public async Task SetFaulted(BehaviorContext context) { - context.SetFaulted(context.Saga.CorrelationId); + if (!WaitForPending || !context.Saga.HasPending()) + { + context.SetFaulted(context.Saga.CorrelationId); - var fault = await context.SendMessageToSubscriptions(_factory, - context.Saga.HasSubscriptions() ? context.Saga.Subscriptions.ToArray() : Array.Empty()); + var fault = await context.SendMessageToSubscriptions(_factory, + context.Saga.HasSubscriptions() ? context.Saga.Subscriptions.ToArray() : Array.Empty()); - context.SetFault(context.Saga.CorrelationId, fault); + context.SetFault(context.Saga.CorrelationId, fault); + } } static Task> DefaultFactory(BehaviorContext context) diff --git a/src/MassTransit/Futures/Futures/PlanRoutingSlipExecutor.cs b/src/MassTransit/Futures/Futures/PlanRoutingSlipExecutor.cs index 3da862a9609..52277637f2f 100644 --- a/src/MassTransit/Futures/Futures/PlanRoutingSlipExecutor.cs +++ b/src/MassTransit/Futures/Futures/PlanRoutingSlipExecutor.cs @@ -24,7 +24,7 @@ public async Task Execute(BehaviorContext context) var routingSlip = builder.Build(); - await context.Execute(routingSlip).ConfigureAwait(false); + await context.Execute(routingSlip, context.CancellationToken).ConfigureAwait(false); if (TrackRoutingSlip) context.Saga.Pending.Add(trackingNumber); diff --git a/src/MassTransit/InMemoryTransport/InMemoryBus.cs b/src/MassTransit/InMemoryTransport/InMemoryBus.cs index e26fff7bde1..0df66b068bb 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryBus.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryBus.cs @@ -1,7 +1,6 @@ namespace MassTransit { using System; - using System.Threading; using Configuration; using InMemoryTransport.Configuration; using Topology; @@ -9,8 +8,6 @@ namespace MassTransit public static class InMemoryBus { - public static IMessageTopologyConfigurator MessageTopology => Cached.MessageTopologyValue.Value; - /// /// Configure and create an in-memory bus /// @@ -29,7 +26,7 @@ public static IBusControl Create(Action configu /// public static IBusControl Create(Uri baseAddress, Action configure) { - var topologyConfiguration = new InMemoryTopologyConfiguration(MessageTopology); + var topologyConfiguration = new InMemoryTopologyConfiguration(CreateMessageTopology()); var busConfiguration = new InMemoryBusConfiguration(topologyConfiguration, baseAddress); var configurator = new InMemoryBusFactoryConfigurator(busConfiguration); @@ -39,17 +36,19 @@ public static IBusControl Create(Uri baseAddress, Action MessageTopologyValue = - new Lazy(() => new MessageTopology(_entityNameFormatter), LazyThreadSafetyMode.PublicationOnly); - - static readonly IEntityNameFormatter _entityNameFormatter; + internal static readonly IEntityNameFormatter EntityNameFormatter; static Cached() { - _entityNameFormatter = new MessageUrnEntityNameFormatter(); + EntityNameFormatter = new MessageUrnEntityNameFormatter(); } } } diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryBusFactoryConfigurator.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryBusFactoryConfigurator.cs index bed21a4aa92..ccd4e599fa6 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryBusFactoryConfigurator.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryBusFactoryConfigurator.cs @@ -22,6 +22,11 @@ public InMemoryBusFactoryConfigurator(IInMemoryBusConfiguration busConfiguration busConfiguration.BusEndpointConfiguration.Consume.Configurator.AutoStart = true; } + public int TransportConcurrencyLimit + { + set => ConcurrentMessageLimit = value; + } + public IReceiveEndpointConfiguration CreateBusEndpointConfiguration(Action configure) { var queueName = _busConfiguration.Topology.Consume.CreateTemporaryQueueName("bus"); @@ -84,10 +89,5 @@ public void ReceiveEndpoint(string queueName, Action ConcurrentMessageLimit = value; - } } } diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryRegistrationBusFactory.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryRegistrationBusFactory.cs index 64d9a2eac15..7ab49bc66ac 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryRegistrationBusFactory.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryRegistrationBusFactory.cs @@ -13,7 +13,7 @@ public class InMemoryRegistrationBusFactory : readonly Action _configure; public InMemoryRegistrationBusFactory(Uri baseAddress, Action configure) - : this(new InMemoryBusConfiguration(new InMemoryTopologyConfiguration(InMemoryBus.MessageTopology), baseAddress), configure) + : this(new InMemoryBusConfiguration(new InMemoryTopologyConfiguration(InMemoryBus.CreateMessageTopology()), baseAddress), configure) { } diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryTopologyConfiguration.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryTopologyConfiguration.cs index f7cbd834b20..a074b93e2bd 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryTopologyConfiguration.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/Configuration/InMemoryTopologyConfiguration.cs @@ -20,6 +20,7 @@ public InMemoryTopologyConfiguration(IMessageTopologyConfigurator messageTopolog _sendTopology = new SendTopology(); _sendTopology.ConnectSendTopologyConfigurationObserver(new DelegateSendTopologyConfigurationObserver(GlobalTopology.Send)); + _sendTopology.TryAddConvention(new RoutingKeySendTopologyConvention()); _publishTopology = new InMemoryPublishTopology(messageTopology); _publishTopology.ConnectPublishTopologyConfigurationObserver(new DelegatePublishTopologyConfigurationObserver(GlobalTopology.Publish)); diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryConsumeTopology.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryConsumeTopology.cs index 9d88f331008..c919b41b414 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryConsumeTopology.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryConsumeTopology.cs @@ -14,7 +14,7 @@ public class InMemoryConsumeTopology : { readonly IMessageTopology _messageTopology; readonly IInMemoryPublishTopologyConfigurator _publishTopology; - readonly IList _specifications; + readonly List _specifications; public InMemoryConsumeTopology(IMessageTopology messageTopology, IInMemoryPublishTopologyConfigurator publishTopology) { @@ -63,7 +63,7 @@ public override IEnumerable Validate() return base.Validate().Concat(_specifications.SelectMany(x => x.Validate())); } - protected override IMessageConsumeTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessageConsumeTopologyConfigurator CreateMessageTopology() { var topology = new InMemoryMessageConsumeTopology(_messageTopology.GetMessageTopology(), _publishTopology); diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryMessageConsumeTopology.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryMessageConsumeTopology.cs index 31744f536db..5a844b27bdd 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryMessageConsumeTopology.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryMessageConsumeTopology.cs @@ -16,7 +16,7 @@ public class InMemoryMessageConsumeTopology : { readonly IMessageTopology _messageTopology; readonly IInMemoryPublishTopology _publishTopology; - readonly IList _specifications; + readonly List _specifications; public InMemoryMessageConsumeTopology(IMessageTopology messageTopology, IInMemoryPublishTopologyConfigurator publishTopology) { diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryMessageMoveTransport.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryMessageMoveTransport.cs index a5524504ed7..57cc5e28610 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryMessageMoveTransport.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryMessageMoveTransport.cs @@ -23,11 +23,7 @@ protected async Task Move(ReceiveContext context, Action : IInMemoryMessagePublishTopologyConfigurator where TMessage : class { - readonly IList _implementedMessageTypes; + readonly List _implementedMessageTypes; readonly IMessageTopology _messageTopology; public InMemoryMessagePublishTopology(IPublishTopologyConfigurator publishTopology, IMessageTopology messageTopology) @@ -46,7 +47,7 @@ public void Apply(IMessageFabricPublishTopologyBuilder builder) configurator.Apply(builder); } - public override bool TryGetPublishAddress(Uri baseAddress, out Uri? publishAddress) + public override bool TryGetPublishAddress(Uri baseAddress, [NotNullWhen(true)] out Uri? publishAddress) { publishAddress = new InMemoryEndpointAddress(new InMemoryHostAddress(baseAddress), _messageTopology.EntityName, exchangeType: ExchangeType); return true; diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryPublishTopology.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryPublishTopology.cs index 9ab5ac96887..77b33c04247 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryPublishTopology.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryPublishTopology.cs @@ -31,7 +31,7 @@ IInMemoryMessagePublishTopologyConfigurator IInMemoryPublishTopologyConfigurator return GetMessageTopology(messageType) as IInMemoryMessagePublishTopologyConfigurator; } - protected override IMessagePublishTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessagePublishTopologyConfigurator CreateMessageTopology() { var topology = new InMemoryMessagePublishTopology(this, _messageTopology.GetMessageTopology()); diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryReceiveTransport.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryReceiveTransport.cs index ccf7922b0f4..ca810273a13 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryReceiveTransport.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryReceiveTransport.cs @@ -3,8 +3,8 @@ namespace MassTransit.InMemoryTransport using System; using System.Threading; using System.Threading.Tasks; + using Context; using Internals; - using Middleware; using Transports; using Transports.Fabric; using Util; @@ -78,24 +78,22 @@ ConnectHandle ISendObserverConnector.ConnectSendObserver(ISendObserver observer) class ReceiveTransportAgent : - Agent, + ConsumerAgent, ReceiveTransportHandle, IMessageReceiver { readonly InMemoryReceiveEndpointContext _context; - readonly IReceivePipeDispatcher _dispatcher; - readonly ChannelExecutor _executor; + readonly TaskExecutor _executor; readonly IMessageQueue _queue; TopologyHandle _topologyHandle; public ReceiveTransportAgent(InMemoryReceiveEndpointContext context, IMessageQueue queue) + : base(context) { _context = context; _queue = queue; - _dispatcher = context.CreateReceivePipeDispatcher(); - - _executor = new ChannelExecutor(context.ConcurrentMessageLimit ?? context.PrefetchCount, false); + _executor = new TaskExecutor(context.ConcurrentMessageLimit ?? context.PrefetchCount); Task.Run(() => Startup()); } @@ -110,9 +108,10 @@ public Task Deliver(InMemoryTransportMessage message, CancellationToken cancella LogContext.Current = _context.LogContext; var context = new InMemoryReceiveContext(message, _context); + try { - await _dispatcher.Dispatch(context).ConfigureAwait(false); + await Dispatch(message.SequenceNumber, context, NoLockReceiveContext.Instance).ConfigureAwait(false); } catch (Exception exception) { @@ -154,21 +153,17 @@ async Task Startup() } } - protected override async Task StopAgent(StopContext context) + protected override async Task ActiveAndActualAgentsCompleted(StopContext context) { - LogContext.SetCurrentIfNull(_context.LogContext); - _topologyHandle?.Disconnect(); - await _executor.DisposeAsync().ConfigureAwait(false); - - var metrics = _dispatcher.GetMetrics(); + await base.ActiveAndActualAgentsCompleted(context).ConfigureAwait(false); - await _context.TransportObservers.NotifyCompleted(_context.InputAddress, metrics).ConfigureAwait(false); + await _executor.DisposeAsync().ConfigureAwait(false); - _context.LogConsumerCompleted(metrics.DeliveryCount, metrics.ConcurrentDeliveryCount); + await _context.TransportObservers.NotifyCompleted(_context.InputAddress, this).ConfigureAwait(false); - await base.StopAgent(context).ConfigureAwait(false); + _context.LogConsumerCompleted(DeliveryCount, ConcurrentDeliveryCount); } } } diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemorySendTransportContext.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemorySendTransportContext.cs index 010c20afab1..60420d77b4e 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemorySendTransportContext.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemorySendTransportContext.cs @@ -55,7 +55,7 @@ public Task Send(PipeContext transportContext, SendContext sendContext) var messageId = context.MessageId ?? NewId.NextGuid(); - var transportMessage = new InMemoryTransportMessage(messageId, context.Body.GetBytes(), context.ContentType.ToString(), TypeCache.ShortName) + var transportMessage = new InMemoryTransportMessage(messageId, context.Body.GetBytes(), context.ContentType.ToString()) { Delay = context.Delay, RoutingKey = context.RoutingKey diff --git a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryTransportMessage.cs b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryTransportMessage.cs index 92a7664ef62..170a8160f11 100644 --- a/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryTransportMessage.cs +++ b/src/MassTransit/InMemoryTransport/InMemoryTransport/InMemoryTransportMessage.cs @@ -3,22 +3,26 @@ namespace MassTransit.InMemoryTransport { using System; using System.Collections.Generic; + using System.Threading; public class InMemoryTransportMessage { - public InMemoryTransportMessage(Guid messageId, byte[] body, string contentType, string messageType) + static long _nextSequenceNumber; + + public InMemoryTransportMessage(Guid messageId, byte[] body, string contentType) { Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); MessageId = messageId; Body = body; - MessageType = messageType; Headers[MessageHeaders.MessageId] = messageId.ToString(); Headers[MessageHeaders.ContentType] = contentType; + + SequenceNumber = Interlocked.Increment(ref _nextSequenceNumber); } - public string MessageType { get; } + public long SequenceNumber { get; } public Guid MessageId { get; } diff --git a/src/MassTransit/Initializers/Contexts/DynamicInitializeContext.cs b/src/MassTransit/Initializers/Contexts/DynamicInitializeContext.cs index 37acb00e8e1..a0b63e31655 100644 --- a/src/MassTransit/Initializers/Contexts/DynamicInitializeContext.cs +++ b/src/MassTransit/Initializers/Contexts/DynamicInitializeContext.cs @@ -35,7 +35,7 @@ public InitializeContext CreateInputContext(T input) public bool TryGetParent(out InitializeContext parentContext) where T : class { - if (this is InitializeContext parent || Parent != null && Parent.TryGetParent(out parent)) + if (this is InitializeContext parent || (Parent != null && Parent.TryGetParent(out parent))) { parentContext = parent; return true; diff --git a/src/MassTransit/Initializers/Factories/MessageInitializerBuilder.cs b/src/MassTransit/Initializers/Factories/MessageInitializerBuilder.cs index 1aa9c6222dd..cb1c24c27fb 100644 --- a/src/MassTransit/Initializers/Factories/MessageInitializerBuilder.cs +++ b/src/MassTransit/Initializers/Factories/MessageInitializerBuilder.cs @@ -10,7 +10,7 @@ public class MessageInitializerBuilder : where TMessage : class where TInput : class { - readonly IList> _headerInitializers; + readonly List> _headerInitializers; readonly IDictionary> _initializers; readonly HashSet _inputPropertyUsed; readonly IMessageFactory _messageFactory; diff --git a/src/MassTransit/Initializers/MessageInitializer.cs b/src/MassTransit/Initializers/MessageInitializer.cs index ba24f150208..640b69b2c4e 100644 --- a/src/MassTransit/Initializers/MessageInitializer.cs +++ b/src/MassTransit/Initializers/MessageInitializer.cs @@ -12,7 +12,7 @@ namespace MassTransit.Initializers public static class MessageInitializer { - static readonly IList _conventions; + static readonly List _conventions; static IInitializerConvention[]? _conventionsArray; static MessageInitializer() diff --git a/src/MassTransit/Initializers/PropertyConverters/ListPropertyConverter.cs b/src/MassTransit/Initializers/PropertyConverters/ListPropertyConverter.cs index d41c1b69ae1..b8543337504 100644 --- a/src/MassTransit/Initializers/PropertyConverters/ListPropertyConverter.cs +++ b/src/MassTransit/Initializers/PropertyConverters/ListPropertyConverter.cs @@ -10,8 +10,23 @@ public class ListPropertyConverter : IPropertyConverter, IEnumerable>, IPropertyConverter, IEnumerable>, IPropertyConverter, IEnumerable>, - IPropertyConverter, IEnumerable> + IPropertyConverter, IEnumerable>, + IPropertyConverter, IEnumerable> { + Task> IPropertyConverter, IEnumerable>.Convert(InitializeContext context, + IEnumerable input) + { + switch (input) + { + case null: + return TaskUtil.Default>(); + case ICollection list: + return Task.FromResult(list); + default: + return Task.FromResult>(input.ToList()); + } + } + Task> IPropertyConverter, IEnumerable>.Convert(InitializeContext context, IEnumerable input) { @@ -71,7 +86,8 @@ public class ListPropertyConverter : IPropertyConverter, IEnumerable>, IPropertyConverter, IEnumerable>, IPropertyConverter, IEnumerable>, - IPropertyConverter, IEnumerable> + IPropertyConverter, IEnumerable>, + IPropertyConverter, IEnumerable> { readonly IPropertyConverter _converter; @@ -80,6 +96,21 @@ public ListPropertyConverter(IPropertyConverter convert _converter = converter; } + public Task> Convert(InitializeContext context, IEnumerable elements) + where T : class + { + Task> resultTask = ConvertSync(context, elements); + if (resultTask.Status == TaskStatus.RanToCompletion) + return Task.FromResult>(resultTask.Result); + + async Task> ConvertAsync() + { + return await resultTask.ConfigureAwait(false); + } + + return ConvertAsync(); + } + Task> IPropertyConverter, IEnumerable>.Convert(InitializeContext context, IEnumerable elements) { diff --git a/src/MassTransit/Initializers/PropertyProviders/PropertyProviderFactory.cs b/src/MassTransit/Initializers/PropertyProviders/PropertyProviderFactory.cs index 2b1a453965d..1a6e928534d 100644 --- a/src/MassTransit/Initializers/PropertyProviders/PropertyProviderFactory.cs +++ b/src/MassTransit/Initializers/PropertyProviders/PropertyProviderFactory.cs @@ -139,7 +139,7 @@ public Convert(IPropertyProviderFactory factory) bool IProviderFactory.TryGetProvider(PropertyInfo propertyInfo, out IPropertyProvider provider) { if (TryGetConverter(out IPropertyConverter propertyConverter) - && _factory.TryGetPropertyProvider(propertyInfo, out IPropertyProvider inputFactory)) + && _factory.TryGetPropertyProvider(propertyInfo, out IPropertyProvider inputFactory)) { provider = new PropertyConverterPropertyProvider(propertyConverter, inputFactory); return true; @@ -159,7 +159,7 @@ public bool TryGetConverter(out IPropertyConverter c return converter != default; } - if (TypeConverterCache.TryGetTypeConverter(out ITypeConverter typeConverter)) + if (TypeConverterCache.TryGetTypeConverter(out ITypeConverter typeConverter)) { converter = new TypePropertyConverter(typeConverter); return true; @@ -224,7 +224,7 @@ public bool TryGetConverter(out IPropertyConverter c return converter != default; } - if (_factory.TryGetPropertyConverter(out IPropertyConverter taskConverter)) + if (_factory.TryGetPropertyConverter(out IPropertyConverter taskConverter)) { converter = new TaskPropertyConverter(taskConverter) as IPropertyConverter; return converter != default; @@ -250,7 +250,7 @@ bool IProviderFactory.TryGetProvider(PropertyInfo propertyInfo, out IProperty { if (typeof(T).IsTask(out var taskType) && taskType == typeof(TTask)) { - if (_factory.TryGetPropertyProvider(propertyInfo, out IPropertyProvider providerFactory)) + if (_factory.TryGetPropertyProvider(propertyInfo, out IPropertyProvider providerFactory)) { provider = new TaskPropertyProvider(providerFactory) as IPropertyProvider; return provider != null; @@ -269,7 +269,7 @@ public bool TryGetConverter(out IPropertyConverter c return converter != default; } - if (_factory.TryGetPropertyConverter(out IPropertyConverter taskConverter)) + if (_factory.TryGetPropertyConverter(out IPropertyConverter taskConverter)) { converter = new TaskPropertyConverter(taskConverter) as IPropertyConverter; return converter != default; @@ -296,7 +296,7 @@ bool IProviderFactory.TryGetProvider(PropertyInfo propertyInfo, out IProperty { if (typeof(T).IsNullable(out var underlyingType) && underlyingType == typeof(TValue)) { - if (_factory.TryGetPropertyProvider(propertyInfo, out IPropertyProvider providerFactory)) + if (_factory.TryGetPropertyProvider(propertyInfo, out IPropertyProvider providerFactory)) { provider = new ToNullablePropertyProvider(providerFactory) as IPropertyProvider; return provider != null; @@ -317,7 +317,7 @@ public bool TryGetConverter(out IPropertyConverter c return converter != default; } - if (TypeConverterCache.TryGetTypeConverter(out ITypeConverter typeConverter)) + if (TypeConverterCache.TryGetTypeConverter(out ITypeConverter typeConverter)) { converter = new TypePropertyConverter(typeConverter); return true; @@ -429,7 +429,7 @@ public bool TryGetConverter(out IPropertyConverter c return converter != default; } - if (TypeConverterCache.TryGetTypeConverter(out ITypeConverter typeConverter)) + if (TypeConverterCache.TryGetTypeConverter(out ITypeConverter typeConverter)) { var typePropertyConverter = new TypePropertyConverter(typeConverter); converter = new FromNullablePropertyConverter(typePropertyConverter) as IPropertyConverter; @@ -483,7 +483,7 @@ public bool TryGetConverter(out IPropertyConverter c return converter != null; } - if (_factory.TryGetPropertyConverter(out IPropertyConverter elementConverter)) + if (_factory.TryGetPropertyConverter(out IPropertyConverter elementConverter)) { converter = new VariablePropertyConverter(elementConverter) as IPropertyConverter; return converter != null; @@ -624,7 +624,7 @@ public bool TryGetConverter(out IPropertyConverter c return converter != null; } - if (_factory.TryGetPropertyConverter(out IPropertyConverter elementConverter)) + if (_factory.TryGetPropertyConverter(out IPropertyConverter elementConverter)) { converter = new ArrayPropertyConverter(elementConverter) as IPropertyConverter; return converter != null; @@ -668,7 +668,7 @@ public bool TryGetConverter(out IPropertyConverter c return converter != null; } - if (_factory.TryGetPropertyConverter(out IPropertyConverter elementConverter)) + if (_factory.TryGetPropertyConverter(out IPropertyConverter elementConverter)) { converter = new ListPropertyConverter(elementConverter) as IPropertyConverter; return converter != null; @@ -713,7 +713,8 @@ bool IsSupportedType(Type type, out IProviderFactory providerFactory) { if (type.ClosesType(typeof(IDictionary<,>), out Type[] types) || type.ClosesType(typeof(IReadOnlyDictionary<,>), out types) - || type.ClosesType(typeof(IEnumerable<>), out Type[] enumerableTypes) && enumerableTypes[0].ClosesType(typeof(KeyValuePair<,>), out types)) + || (type.ClosesType(typeof(IEnumerable<>), out Type[] enumerableTypes) + && enumerableTypes[0].ClosesType(typeof(KeyValuePair<,>), out types))) { var factoryType = typeof(DictionaryResult<,>).MakeGenericType(typeof(TInput), typeof(TInputProperty), typeof(TInputKey), typeof(TInputValue), types[0], types[1]); diff --git a/src/MassTransit/Initializers/TypeConverters/DateTimeOffsetTypeConverter.cs b/src/MassTransit/Initializers/TypeConverters/DateTimeOffsetTypeConverter.cs index 2a4f4a9d3fc..236e956ea9a 100644 --- a/src/MassTransit/Initializers/TypeConverters/DateTimeOffsetTypeConverter.cs +++ b/src/MassTransit/Initializers/TypeConverters/DateTimeOffsetTypeConverter.cs @@ -1,6 +1,7 @@ namespace MassTransit.Initializers.TypeConverters { using System; + using Internals; public class DateTimeOffsetTypeConverter : @@ -10,8 +11,6 @@ public class DateTimeOffsetTypeConverter : ITypeConverter, ITypeConverter { - readonly DateTime _epoch = new DateTime(1970, 1, 1); - public bool TryConvert(object input, out DateTimeOffset result) { switch (input) @@ -40,9 +39,9 @@ public bool TryConvert(string input, out DateTimeOffset result) public bool TryConvert(DateTimeOffset input, out int result) { - if (input >= _epoch) + if (input >= DateTimeConstants.Epoch) { - var timeSpan = input.UtcDateTime - _epoch; + var timeSpan = input.UtcDateTime - DateTimeConstants.Epoch; if (timeSpan.TotalMilliseconds <= int.MaxValue) { result = (int)timeSpan.TotalMilliseconds; @@ -56,9 +55,9 @@ public bool TryConvert(DateTimeOffset input, out int result) public bool TryConvert(DateTimeOffset input, out long result) { - if (input >= _epoch) + if (input >= DateTimeConstants.Epoch) { - var timeSpan = input.UtcDateTime - _epoch; + var timeSpan = input.UtcDateTime - DateTimeConstants.Epoch; if (timeSpan.TotalMilliseconds <= long.MaxValue) { result = (long)timeSpan.TotalMilliseconds; diff --git a/src/MassTransit/Initializers/TypeConverters/DateTimeTypeConverter.cs b/src/MassTransit/Initializers/TypeConverters/DateTimeTypeConverter.cs index f906b8736c2..e2c2ce5bafd 100644 --- a/src/MassTransit/Initializers/TypeConverters/DateTimeTypeConverter.cs +++ b/src/MassTransit/Initializers/TypeConverters/DateTimeTypeConverter.cs @@ -1,6 +1,8 @@ namespace MassTransit.Initializers.TypeConverters { using System; + using System.Globalization; + using Internals; public class DateTimeTypeConverter : @@ -13,8 +15,6 @@ public class DateTimeTypeConverter : ITypeConverter, ITypeConverter { - readonly DateTime _epoch = new DateTime(1970, 1, 1); - public bool TryConvert(DateTimeOffset input, out DateTime result) { result = input.UtcDateTime; @@ -23,13 +23,13 @@ public bool TryConvert(DateTimeOffset input, out DateTime result) public bool TryConvert(int input, out DateTime result) { - result = _epoch + TimeSpan.FromMilliseconds(input); + result = DateTimeConstants.Epoch + TimeSpan.FromMilliseconds(input); return true; } public bool TryConvert(long input, out DateTime result) { - result = _epoch + TimeSpan.FromMilliseconds(input); + result = DateTimeConstants.Epoch + TimeSpan.FromMilliseconds(input); return true; } @@ -56,7 +56,7 @@ public bool TryConvert(object input, out DateTime result) public bool TryConvert(string input, out DateTime result) { - if (DateTimeOffset.TryParse(input, out var value)) + if (DateTimeOffset.TryParse(input, null, DateTimeStyles.AssumeUniversal, out var value)) { result = value.Offset == TimeSpan.Zero ? value.UtcDateTime : value.LocalDateTime; return true; @@ -68,9 +68,9 @@ public bool TryConvert(string input, out DateTime result) public bool TryConvert(DateTime input, out int result) { - if (input >= _epoch) + if (input >= DateTimeConstants.Epoch) { - var timeSpan = input - _epoch; + var timeSpan = input - DateTimeConstants.Epoch; if (timeSpan.TotalMilliseconds <= int.MaxValue) { result = (int)timeSpan.TotalMilliseconds; @@ -84,9 +84,9 @@ public bool TryConvert(DateTime input, out int result) public bool TryConvert(DateTime input, out long result) { - if (input >= _epoch) + if (input >= DateTimeConstants.Epoch) { - var timeSpan = input - _epoch; + var timeSpan = input - DateTimeConstants.Epoch; if (timeSpan.TotalMilliseconds <= long.MaxValue) { result = (long)timeSpan.TotalMilliseconds; diff --git a/src/MassTransit/Initializers/TypeConverters/TypeConverterCache.cs b/src/MassTransit/Initializers/TypeConverters/TypeConverterCache.cs index db7dbc4456b..c3e4726dd90 100644 --- a/src/MassTransit/Initializers/TypeConverters/TypeConverterCache.cs +++ b/src/MassTransit/Initializers/TypeConverters/TypeConverterCache.cs @@ -10,7 +10,7 @@ namespace MassTransit.Initializers.TypeConverters public class TypeConverterCache : ITypeConverterCache { - readonly IList _converters; + readonly List _converters; readonly ConcurrentDictionary _typeConverters; TypeConverterCache() diff --git a/src/MassTransit/Internals/Caching/ValueFactoryException.cs b/src/MassTransit/Internals/Caching/ValueFactoryException.cs index 9cdd2a95c73..7d7c7112d52 100644 --- a/src/MassTransit/Internals/Caching/ValueFactoryException.cs +++ b/src/MassTransit/Internals/Caching/ValueFactoryException.cs @@ -17,6 +17,9 @@ public ValueFactoryException(string message) { } + #if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] + #endif protected ValueFactoryException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/MassTransit/Internals/Extensions/AsyncEnumerableExtensions.cs b/src/MassTransit/Internals/Extensions/AsyncEnumerableExtensions.cs index f3153fc05df..354e46e6c29 100644 --- a/src/MassTransit/Internals/Extensions/AsyncEnumerableExtensions.cs +++ b/src/MassTransit/Internals/Extensions/AsyncEnumerableExtensions.cs @@ -1,6 +1,7 @@ namespace MassTransit.Internals { using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; @@ -15,5 +16,15 @@ public static async Task> ToListAsync(this IAsyncEnume return elementsList; } + + public static async Task> ToListAsync(this IAsyncEnumerable elements, CancellationToken cancellationToken) + where TElement : class + { + var elementsList = new List(); + await foreach (var element in elements.WithCancellation(cancellationToken).ConfigureAwait(false)) + elementsList.Add(element); + + return elementsList; + } } } diff --git a/src/MassTransit/Internals/Extensions/StringExtensions.cs b/src/MassTransit/Internals/Extensions/StringExtensions.cs new file mode 100644 index 00000000000..45ba012af6b --- /dev/null +++ b/src/MassTransit/Internals/Extensions/StringExtensions.cs @@ -0,0 +1,127 @@ +#nullable enable +namespace MassTransit.Internals; + +using System; +using System.Runtime.InteropServices; + + +static class StringExtensions +{ + /// + /// Allows null-safe trimming of string. + /// + /// + /// + internal static string? NullSafeTrim(this string? s) + { + return s?.Trim(); + } + + /// + /// Trims string and if resulting string is empty, null is returned. + /// + /// + /// + internal static string? TrimEmptyToNull(this string? s) + { + if (s is null) + return null; + + s = s.Trim(); + + if (s.Length == 0) + return null; + + return s; + } + + // based on https://www.meziantou.net/split-a-string-into-lines-without-allocation.htm + internal static StringSplitEnumerator SpanSplit(this string str, char ch1, char ch2 = char.MinValue) + { + return SpanSplit(str.AsSpan(), ch1, ch2); + } + + internal static StringSplitEnumerator SpanSplit(this ReadOnlySpan span, char ch1, char ch2 = char.MinValue) + { + return new StringSplitEnumerator(span, ch1, ch2); + } + + + // Must be a ref struct as it contains a ReadOnlySpan + [StructLayout(LayoutKind.Auto)] + internal ref struct StringSplitEnumerator + { + ReadOnlySpan _str; + readonly char ch1; + readonly char ch2; + + public StringSplitEnumerator(ReadOnlySpan str, char ch1, char ch2) + { + _str = str; + this.ch1 = ch1; + this.ch2 = ch2; + Current = default; + } + + // Needed to be compatible with the foreach operator + public StringSplitEnumerator GetEnumerator() + { + return this; + } + + public bool MoveNext() + { + ReadOnlySpan span = _str; + if (span.Length == 0) // Reach the end of the string + return false; + + var index = ch2 != char.MinValue + ? span.IndexOfAny(ch1, ch2) + : span.IndexOf(ch1); + + if (index == -1) // The string is composed of only token + { + _str = ReadOnlySpan.Empty; // The remaining string is an empty string + Current = new StringSplitEntry(span, ReadOnlySpan.Empty); + return true; + } + + Current = new StringSplitEntry(span.Slice(0, index), span.Slice(index, 1)); + _str = span.Slice(index + 1); + return true; + } + + public StringSplitEntry Current { get; private set; } + } + + + [StructLayout(LayoutKind.Auto)] + internal readonly ref struct StringSplitEntry + { + public StringSplitEntry(ReadOnlySpan token, ReadOnlySpan separator) + { + Token = token; + Separator = separator; + } + + public ReadOnlySpan Token { get; } + public ReadOnlySpan Separator { get; } + + // This method allow to deconstruct the type, so you can write any of the following code + // foreach (var entry in str.SplitLines()) { _ = entry.Line; } + // foreach (var (line, endOfLine) in str.SplitLines()) { _ = line; } + // https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/deconstruct?WT.mc_id=DT-MVP-5003978#deconstructing-user-defined-types + public void Deconstruct(out ReadOnlySpan line, out ReadOnlySpan separator) + { + line = Token; + separator = Separator; + } + + // This method allow to implicitly cast the type into a ReadOnlySpan, so you can write the following code + // foreach (ReadOnlySpan entry in str.SplitLines()) + public static implicit operator ReadOnlySpan(StringSplitEntry entry) + { + return entry.Token; + } + } +} diff --git a/src/MassTransit/Internals/Reflection/DynamicImplementationBuilder.cs b/src/MassTransit/Internals/Reflection/DynamicImplementationBuilder.cs index c659cc4f43f..5cde46acc88 100644 --- a/src/MassTransit/Internals/Reflection/DynamicImplementationBuilder.cs +++ b/src/MassTransit/Internals/Reflection/DynamicImplementationBuilder.cs @@ -37,7 +37,7 @@ public Type GetImplementationType(Type interfaceType) Type CreateImplementation(Type interfaceType) { - if (!interfaceType.GetTypeInfo().IsInterface) + if (!interfaceType.IsInterface) throw new ArgumentException("Proxies can only be created for interfaces: " + interfaceType.Name, nameof(interfaceType)); return GetModuleBuilderForType(interfaceType, moduleBuilder => CreateTypeFromInterface(moduleBuilder, interfaceType)); @@ -52,8 +52,10 @@ static Type CreateTypeFromInterface(ModuleBuilder builder, Type interfaceType) try { var typeBuilder = builder.DefineType(typeName, - TypeAttributes.Serializable | TypeAttributes.Class | - TypeAttributes.Public | TypeAttributes.Sealed, + #pragma warning disable SYSLIB0050 // Formatter-based serialization is obsolete and should not be used. + TypeAttributes.Serializable | + #pragma warning restore SYSLIB0050 + TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.Sealed, typeof(object), new[] { interfaceType }); typeBuilder.DefineDefaultConstructor(MethodAttributes.Public); diff --git a/src/MassTransit/Internals/Reflection/IReadPropertyCache.cs b/src/MassTransit/Internals/Reflection/IReadPropertyCache.cs index c1fe94e18e9..c6099e7114f 100644 --- a/src/MassTransit/Internals/Reflection/IReadPropertyCache.cs +++ b/src/MassTransit/Internals/Reflection/IReadPropertyCache.cs @@ -1,12 +1,15 @@ -namespace MassTransit.Internals +#nullable enable +namespace MassTransit.Internals { + using System.Diagnostics.CodeAnalysis; using System.Reflection; - public interface IReadPropertyCache + public interface IReadPropertyCache where T : class { - IReadProperty GetProperty(string name); - IReadProperty GetProperty(PropertyInfo propertyInfo); + IReadProperty GetProperty(string? name); + IReadProperty GetProperty(PropertyInfo? propertyInfo); + bool TryGetProperty(string name, [NotNullWhen(true)] out IReadProperty? property); } } diff --git a/src/MassTransit/Internals/Reflection/ReadPropertyCache.cs b/src/MassTransit/Internals/Reflection/ReadPropertyCache.cs index 4b81266034e..98dafec16f9 100644 --- a/src/MassTransit/Internals/Reflection/ReadPropertyCache.cs +++ b/src/MassTransit/Internals/Reflection/ReadPropertyCache.cs @@ -1,7 +1,9 @@ -namespace MassTransit.Internals +#nullable enable +namespace MassTransit.Internals { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Reflection; @@ -16,36 +18,47 @@ public class ReadPropertyCache : ReadPropertyCache() { _properties = new Dictionary>(StringComparer.OrdinalIgnoreCase); - _propertyIndex = MessageTypeCache.Properties - .ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); + _propertyIndex = MessageTypeCache.Properties.ToDictionary(x => x.Name, StringComparer.OrdinalIgnoreCase); } - IReadProperty IReadPropertyCache.GetProperty(string name) + IReadProperty IReadPropertyCache.GetProperty(string? name) { - return GetReadProperty(name ?? throw new ArgumentNullException(nameof(name))); + return GetReadProperty(name ?? throw new ArgumentNullException(nameof(name))) + ?? throw new ArgumentException($"{TypeCache.ShortName} does not contain the property: {name}", nameof(name)); } - IReadProperty IReadPropertyCache.GetProperty(PropertyInfo propertyInfo) + IReadProperty IReadPropertyCache.GetProperty(PropertyInfo? propertyInfo) { var name = propertyInfo?.Name ?? throw new ArgumentNullException(nameof(propertyInfo)); - return GetReadProperty(name); + return GetReadProperty(name) + ?? throw new ArgumentException($"{TypeCache.ShortName} does not contain the property: {name}", nameof(name)); } - IReadProperty GetReadProperty(string name) + bool IReadPropertyCache.TryGetProperty(string name, [NotNullWhen(true)] out IReadProperty? property) + { + IReadProperty? readProperty = GetReadProperty(name ?? throw new ArgumentNullException(nameof(name))); + if (readProperty != null) + { + property = readProperty; + return true; + } + + property = null; + return false; + } + + IReadProperty? GetReadProperty(string name) { lock (_properties) { - if (_properties.TryGetValue(name, out IReadProperty property)) + if (_properties.TryGetValue(name, out IReadProperty? property)) return property as IReadProperty; if (_propertyIndex.TryGetValue(name, out var propertyInfo)) { if (propertyInfo.PropertyType != typeof(TProperty)) - { - throw new ArgumentException( - $"Property type mismatch, {TypeCache.ShortName} != {TypeCache.GetShortName(propertyInfo.PropertyType)}"); - } + return null; var readProperty = new ReadProperty(propertyInfo); @@ -55,7 +68,7 @@ IReadProperty GetReadProperty(string name) } } - throw new ArgumentException($"{TypeCache.ShortName} does not contain the property: {name}", nameof(name)); + return null; } public static IReadProperty GetProperty(string name) @@ -63,6 +76,11 @@ public static IReadProperty GetProperty(string name) return Cached.PropertyCache.Value.GetProperty(name); } + public static bool TryGetProperty(string name, [NotNullWhen(true)] out IReadProperty? property) + { + return Cached.PropertyCache.Value.TryGetProperty(name, out property); + } + public static IReadProperty GetProperty(PropertyInfo propertyInfo) { return Cached.PropertyCache.Value.GetProperty(propertyInfo); diff --git a/src/MassTransit/Internals/Reflection/WriteProperty.cs b/src/MassTransit/Internals/Reflection/WriteProperty.cs index a39c50fc177..115e3cf23c4 100644 --- a/src/MassTransit/Internals/Reflection/WriteProperty.cs +++ b/src/MassTransit/Internals/Reflection/WriteProperty.cs @@ -25,7 +25,7 @@ public WriteProperty(Type implementationType, PropertyInfo propertyInfo) void SetUsingReflection(T entity, TProperty property) { - setMethod.Invoke(entity, new object[] {property}); + setMethod.Invoke(entity, new object[] { property }); } void Initialize(T entity, TProperty property) diff --git a/src/MassTransit/Internals/Reflection/WritePropertyCache.cs b/src/MassTransit/Internals/Reflection/WritePropertyCache.cs index 1a31fa7a7ed..ef9dea5bbb6 100644 --- a/src/MassTransit/Internals/Reflection/WritePropertyCache.cs +++ b/src/MassTransit/Internals/Reflection/WritePropertyCache.cs @@ -17,7 +17,7 @@ public class WritePropertyCache : WritePropertyCache() { - if (MessageTypeCache.IsValidMessageType && typeof(T).GetTypeInfo().IsInterface) + if (MessageTypeCache.IsValidMessageType && typeof(T).IsInterface) { _implementationType = TypeMetadataCache.ImplementationType; _propertyIndex = _implementationType.GetAllProperties() diff --git a/src/MassTransit/Introspection/ScopeProbeContext.cs b/src/MassTransit/Introspection/ScopeProbeContext.cs index 09087e3cd6e..cc744948483 100644 --- a/src/MassTransit/Introspection/ScopeProbeContext.cs +++ b/src/MassTransit/Introspection/ScopeProbeContext.cs @@ -37,7 +37,7 @@ public void Add(string key, object value) if (key == null) throw new ArgumentNullException(nameof(key)); - if (value == null || value is string s && string.IsNullOrEmpty(s)) + if (value == null || (value is string s && string.IsNullOrEmpty(s))) _variables.Remove(key); else _variables[key] = value; @@ -96,7 +96,7 @@ void SetVariablesFromDictionary(IEnumerable> values { foreach (KeyValuePair value in values) { - if (value.Value == null || value.Value is string s && string.IsNullOrEmpty(s)) + if (value.Value == null || (value.Value is string s && string.IsNullOrEmpty(s))) _variables.Remove(value.Key); else _variables[value.Key] = value.Value; diff --git a/src/MassTransit/JobService/ActiveJob.cs b/src/MassTransit/JobService/ActiveJob.cs index 7d0827aa7f7..bace7df0357 100644 --- a/src/MassTransit/JobService/ActiveJob.cs +++ b/src/MassTransit/JobService/ActiveJob.cs @@ -1,43 +1,54 @@ -namespace MassTransit +#nullable enable +namespace MassTransit; + +using System; +using System.Collections.Generic; + + +/// +/// Active Jobs are allocated a concurrency slot, and are valid until the deadline is reached, after +/// which they may be automatically released. +/// +public class ActiveJob : + IEquatable { - using System; + public Guid JobId { get; set; } + /// + /// Calculated from the JobTimeout based on the time the job slot was requested, not currently used + /// + public DateTime Deadline { get; set; } /// - /// Active Jobs are allocated a concurrency slot, and are valid until the deadline is reached, after - /// which they may be automatically released. + /// The instance assigned to the job /// - public class ActiveJob : - IEquatable + public Uri InstanceAddress { get; set; } = null!; + + public Dictionary? Properties { get; set; } + + public bool Equals(ActiveJob? other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + return JobId.Equals(other.JobId); + } + + public override bool Equals(object? obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != GetType()) + return false; + return Equals((ActiveJob)obj); + } + + public override int GetHashCode() { - public Guid JobId { get; set; } - public DateTime Deadline { get; set; } - - public Uri InstanceAddress { get; set; } - - public bool Equals(ActiveJob other) - { - if (ReferenceEquals(null, other)) - return false; - if (ReferenceEquals(this, other)) - return true; - return JobId.Equals(other.JobId); - } - - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - return false; - if (ReferenceEquals(this, obj)) - return true; - if (obj.GetType() != GetType()) - return false; - return Equals((ActiveJob)obj); - } - - public override int GetHashCode() - { - return JobId.GetHashCode(); - } + // ReSharper disable once NonReadonlyMemberInGetHashCode + return JobId.GetHashCode(); } } diff --git a/src/MassTransit/JobService/Configuration/InstanceJobServiceSettings.cs b/src/MassTransit/JobService/Configuration/InstanceJobServiceSettings.cs new file mode 100644 index 00000000000..ada86bd6dd7 --- /dev/null +++ b/src/MassTransit/JobService/Configuration/InstanceJobServiceSettings.cs @@ -0,0 +1,53 @@ +#nullable enable +namespace MassTransit.Configuration +{ + using System; + using System.Collections.Generic; + using JobService; + using Microsoft.Extensions.Options; + + + public class InstanceJobServiceSettings : + JobServiceSettings + { + readonly List> _configureActions; + + readonly JobConsumerOptions _options; + + public InstanceJobServiceSettings(IOptions options) + : this(options.Value) + { + } + + public InstanceJobServiceSettings(JobConsumerOptions options) + { + _options = options; + _configureActions = new List>(); + + JobService = new JobService(this); + } + + public TimeSpan HeartbeatInterval => _options.HeartbeatInterval; + + public Uri? InstanceAddress { get; set; } + public IReceiveEndpointConfigurator? InstanceEndpointConfigurator { get; set; } + public IJobService JobService { get; } + + public void AddConfigureAction(Action? configure) + { + if (configure != null) + _configureActions.Add(configure); + } + + public void ApplyConfiguration(T configurator) + where T : IReceiveEndpointConfigurator + { + InstanceEndpointConfigurator = configurator; + + for (var i = 0; i < _configureActions.Count; i++) + _configureActions[i](configurator); + + InstanceAddress = configurator.InputAddress; + } + } +} diff --git a/src/MassTransit/JobService/Configuration/JobAttemptSagaDefinition.cs b/src/MassTransit/JobService/Configuration/JobAttemptSagaDefinition.cs new file mode 100644 index 00000000000..b3bfe1d0ba7 --- /dev/null +++ b/src/MassTransit/JobService/Configuration/JobAttemptSagaDefinition.cs @@ -0,0 +1,59 @@ +namespace MassTransit.Configuration +{ + using Contracts.JobService; + using JobService; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Middleware; + + + public class JobAttemptSagaDefinition : + SagaDefinition + { + readonly JobSagaOptions _options; + readonly JobSagaSettingsConfigurator _setOptions; + + public JobAttemptSagaDefinition(IOptions options) + { + _options = options.Value; + _setOptions = _options; + } + + protected override void ConfigureSaga(IReceiveEndpointConfigurator configurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) + { + configurator.UseMessageRetry(r => r.Intervals(100, 500, 1000, 1000, 2000, 2000, 5000, 5000)); + + configurator.UseMessageScope(context); + + configurator.UseInMemoryOutbox(context); + + if (_options.ConcurrentMessageLimit.HasValue) + { + configurator.ConcurrentMessageLimit = _options.ConcurrentMessageLimit; + + var partition = new Partitioner(_options.ConcurrentMessageLimit.Value, new Murmur3UnsafeHashGenerator()); + + configurator.UsePartitioner(partition, p => p.Message.AttemptId); + configurator.UsePartitioner(partition, p => p.Message.AttemptId); + configurator.UsePartitioner(partition, p => p.Message.AttemptId); + configurator.UsePartitioner>(partition, p => p.Message.Message.AttemptId); + + configurator.UsePartitioner(partition, p => p.Message.AttemptId); + configurator.UsePartitioner(partition, p => p.Message.AttemptId); + configurator.UsePartitioner(partition, p => p.Message.AttemptId); + configurator.UsePartitioner(partition, p => p.Message.AttemptId); + + configurator.UsePartitioner(partition, p => p.Message.AttemptId); + configurator.UsePartitioner(partition, p => p.Message.AttemptId); + } + + sagaConfigurator.UseFilter(new PayloadFilter, JobSagaSettings>(_options)); + + _setOptions.JobAttemptSagaEndpointAddress = configurator.InputAddress; + + if (context.GetRequiredService().TryGetValue(context, typeof(JobService), out IJobServiceRegistration registration)) + registration.AddReceiveEndpointDependency(configurator); + } + } +} diff --git a/src/MassTransit/JobService/Configuration/JobSagaDefinition.cs b/src/MassTransit/JobService/Configuration/JobSagaDefinition.cs new file mode 100644 index 00000000000..b51e46b923f --- /dev/null +++ b/src/MassTransit/JobService/Configuration/JobSagaDefinition.cs @@ -0,0 +1,72 @@ +namespace MassTransit.Configuration +{ + using Contracts.JobService; + using JobService; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Middleware; + + + public class JobSagaDefinition : + SagaDefinition + { + readonly JobSagaOptions _options; + readonly JobSagaSettingsConfigurator _setOptions; + + public JobSagaDefinition(IOptions options) + { + _options = options.Value; + _setOptions = _options; + } + + protected override void ConfigureSaga(IReceiveEndpointConfigurator configurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) + { + configurator.UseMessageRetry(r => r.Intervals(100, 500, 1000, 1000, 2000, 2000, 5000, 5000)); + + configurator.UseMessageScope(context); + + configurator.UseInMemoryOutbox(context); + + if (_options.ConcurrentMessageLimit.HasValue) + { + configurator.ConcurrentMessageLimit = _options.ConcurrentMessageLimit; + + var partition = new Partitioner(_options.ConcurrentMessageLimit.Value, new Murmur3UnsafeHashGenerator()); + + configurator.UsePartitioner(partition, p => p.Message.JobId); + + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner>(partition, p => p.Message.Message.JobId); + + configurator.UsePartitioner>(partition, p => p.Message.Message.JobId); + + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + + configurator.UsePartitioner(partition, p => p.Message.JobId); + + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + } + + sagaConfigurator.UseFilter(new PayloadFilter, JobSagaSettings>(_options)); + + _setOptions.JobSagaEndpointAddress = configurator.InputAddress; + + if (context.GetRequiredService().TryGetValue(context, typeof(JobService), out IJobServiceRegistration registration)) + registration.AddReceiveEndpointDependency(configurator); + } + } +} diff --git a/src/MassTransit/JobService/Configuration/JobSagaSettings.cs b/src/MassTransit/JobService/Configuration/JobSagaSettings.cs new file mode 100644 index 00000000000..62565c40d95 --- /dev/null +++ b/src/MassTransit/JobService/Configuration/JobSagaSettings.cs @@ -0,0 +1,26 @@ +namespace MassTransit.Configuration +{ + using System; + + + /// + /// Settings used by the job service sagas + /// + public interface JobSagaSettings + { + Uri JobAttemptSagaEndpointAddress { get; } + Uri JobSagaEndpointAddress { get; } + Uri JobTypeSagaEndpointAddress { get; } + + TimeSpan StatusCheckInterval { get; } + + int SuspectJobRetryCount { get; } + TimeSpan? SuspectJobRetryDelay { get; } + + TimeSpan SlotWaitTime { get; } + + TimeSpan HeartbeatTimeout { get; } + + bool FinalizeCompleted { get; } + } +} diff --git a/src/MassTransit/JobService/Configuration/JobSagaSettingsConfigurator.cs b/src/MassTransit/JobService/Configuration/JobSagaSettingsConfigurator.cs new file mode 100644 index 00000000000..675aacf2d29 --- /dev/null +++ b/src/MassTransit/JobService/Configuration/JobSagaSettingsConfigurator.cs @@ -0,0 +1,13 @@ +namespace MassTransit.Configuration +{ + using System; + + + public interface JobSagaSettingsConfigurator : + JobSagaSettings + { + new Uri JobAttemptSagaEndpointAddress { set; } + new Uri JobSagaEndpointAddress { set; } + new Uri JobTypeSagaEndpointAddress { set; } + } +} diff --git a/src/MassTransit/JobService/Configuration/JobServiceConfigurator.cs b/src/MassTransit/JobService/Configuration/JobServiceConfigurator.cs index f6441f36a08..a5f9f89881f 100644 --- a/src/MassTransit/JobService/Configuration/JobServiceConfigurator.cs +++ b/src/MassTransit/JobService/Configuration/JobServiceConfigurator.cs @@ -12,7 +12,7 @@ public class JobServiceConfigurator : ISpecification where TReceiveEndpointConfigurator : IReceiveEndpointConfigurator { - readonly IBusFactoryConfigurator _busConfigurator; + readonly IReceiveConfigurator _busConfigurator; readonly JobServiceOptions _options; bool _endpointsConfigured; ISagaRepository _jobAttemptRepository; @@ -30,19 +30,27 @@ public JobServiceConfigurator(IServiceInstanceConfigurator(); - JobService = new JobService(instanceConfigurator, _options); + var settings = new InstanceJobServiceSettings(new JobConsumerOptions { HeartbeatInterval = _options.HeartbeatInterval }) + { + InstanceEndpointConfigurator = instanceConfigurator.InstanceEndpointConfigurator, + InstanceAddress = instanceConfigurator.InstanceAddress + }; + + settings.JobService.ConfigureSuperviseJobConsumer(instanceConfigurator.InstanceEndpointConfigurator); + + if (instanceConfigurator.BusConfigurator is IBusObserverConnector connector) + connector.ConnectBusObserver(new JobServiceBusObserver(settings.JobService)); - instanceConfigurator.BusConfigurator.ConnectBusObserver(new JobServiceBusObserver(JobService)); instanceConfigurator.AddSpecification(this); - _options.JobService = JobService; + _options.JobService = settings.JobService; _options.InstanceEndpointConfigurator = instanceConfigurator.InstanceEndpointConfigurator; _options.JobTypeSagaEndpointName = instanceConfigurator.EndpointNameFormatter.Saga(); _options.JobStateSagaEndpointName = instanceConfigurator.EndpointNameFormatter.Saga(); _options.JobAttemptSagaEndpointName = instanceConfigurator.EndpointNameFormatter.Saga(); - instanceConfigurator.ConnectEndpointConfigurationObserver(new JobServiceEndpointConfigurationObserver(_options, cfg => + instanceConfigurator.ConnectEndpointConfigurationObserver(new JobServiceEndpointConfigurationObserver(settings, cfg => { if (_jobTypeSagaEndpointConfigurator != null) cfg.AddDependency(_jobTypeSagaEndpointConfigurator); @@ -53,8 +61,6 @@ public JobServiceConfigurator(IServiceInstanceConfigurator Repository { set => _jobTypeRepository = value; @@ -85,11 +91,6 @@ public string JobServiceJobAttemptStateEndpointName set => _options.JobAttemptSagaEndpointName = value; } - public TimeSpan SlotRequestTimeout - { - set => _options.SlotRequestTimeout = value; - } - public TimeSpan SlotWaitTime { set => _options.SlotWaitTime = value; @@ -100,11 +101,6 @@ public TimeSpan StatusCheckInterval set => _options.StatusCheckInterval = value; } - public TimeSpan StartJobTimeout - { - set => _options.StartJobTimeout = value; - } - public int SuspectJobRetryCount { set => _options.SuspectJobRetryCount = value; @@ -133,20 +129,39 @@ public bool FinalizeCompleted public IEnumerable Validate() { - ISpecification turnoutOptions = _options; + ISpecification options = _options; - return turnoutOptions.Validate(); + return options.Validate(); } - public void ConfigureJobServiceEndpoints() + public void OnConfigureEndpoint(Action callback) + { + _options.OnConfigureEndpoint = callback; + } + + public void ConfigureJobServiceEndpoints(IRegistrationContext context = null) { if (_endpointsConfigured) return; + void UseInMemoryOutbox(IReceiveEndpointConfigurator configurator) + { + if (context == null) + #pragma warning disable CS0618 + configurator.UseInMemoryOutbox(); + #pragma warning restore CS0618 + else + { + configurator.UseMessageScope(context); + configurator.UseInMemoryOutbox(context); + } + } + _busConfigurator.ReceiveEndpoint(_options.JobStateSagaEndpointName, e => { e.UseMessageRetry(r => r.Intervals(100, 1000, 2000, 5000)); - e.UseInMemoryOutbox(); + + UseInMemoryOutbox(e); if (_options.SagaPartitionCount.HasValue) { @@ -160,7 +175,6 @@ public void ConfigureJobServiceEndpoints() e.UsePartitioner(partition, p => p.Message.JobId); e.UsePartitioner>(partition, p => p.Message.Message.JobId); - e.UsePartitioner(partition, p => p.Message.JobId); e.UsePartitioner>(partition, p => p.Message.Message.JobId); e.UsePartitioner(partition, p => p.Message.JobId); @@ -168,15 +182,23 @@ public void ConfigureJobServiceEndpoints() e.UsePartitioner(partition, p => p.Message.JobId); e.UsePartitioner(partition, p => p.Message.JobId); + e.UsePartitioner(partition, p => p.Message.JobId); + e.UsePartitioner(partition, p => p.Message.JobId); e.UsePartitioner(partition, p => p.Message.JobId); + e.UsePartitioner(partition, p => p.Message.JobId); + e.UsePartitioner(partition, p => p.Message.JobId); + + e.UsePartitioner(partition, p => p.Message.JobId); + e.UsePartitioner(partition, p => p.Message.JobId); e.UsePartitioner(partition, p => p.Message.JobId); e.UsePartitioner(partition, p => p.Message.JobId); } - var stateMachine = new JobStateMachine(_options); - e.StateMachineSaga(stateMachine, _jobRepository ?? new InMemorySagaRepository()); + var stateMachine = new JobStateMachine(); + e.StateMachineSaga(stateMachine, _jobRepository ?? new InMemorySagaRepository(), + s => s.UseFilter(new PayloadFilter, JobSagaSettings>(_options))); _jobSagaEndpointConfigurator = e; @@ -186,7 +208,8 @@ public void ConfigureJobServiceEndpoints() _busConfigurator.ReceiveEndpoint(_options.JobAttemptSagaEndpointName, e => { e.UseMessageRetry(r => r.Intervals(100, 1000, 2000, 5000)); - e.UseInMemoryOutbox(); + + UseInMemoryOutbox(e); if (_options.SagaPartitionCount.HasValue) { @@ -195,6 +218,8 @@ public void ConfigureJobServiceEndpoints() var partition = new Partitioner(_options.SagaPartitionCount.Value, new Murmur3UnsafeHashGenerator()); e.UsePartitioner(partition, p => p.Message.AttemptId); + e.UsePartitioner(partition, p => p.Message.AttemptId); + e.UsePartitioner(partition, p => p.Message.AttemptId); e.UsePartitioner>(partition, p => p.Message.Message.AttemptId); e.UsePartitioner(partition, p => p.Message.AttemptId); @@ -206,8 +231,9 @@ public void ConfigureJobServiceEndpoints() e.UsePartitioner(partition, p => p.Message.AttemptId); } - var stateMachine = new JobAttemptStateMachine(_options); - e.StateMachineSaga(stateMachine, _jobAttemptRepository ?? new InMemorySagaRepository()); + var stateMachine = new JobAttemptStateMachine(); + e.StateMachineSaga(stateMachine, _jobAttemptRepository ?? new InMemorySagaRepository(), + s => s.UseFilter(new PayloadFilter, JobSagaSettings>(_options))); _jobAttemptSagaEndpointConfigurator = e; @@ -217,7 +243,8 @@ public void ConfigureJobServiceEndpoints() _busConfigurator.ReceiveEndpoint(_options.JobTypeSagaEndpointName, e => { e.UseMessageRetry(r => r.Intervals(100, 200, 300, 500, 1000, 2000, 5000)); - e.UseInMemoryOutbox(); + + UseInMemoryOutbox(e); if (_options.SagaPartitionCount.HasValue) { @@ -230,9 +257,10 @@ public void ConfigureJobServiceEndpoints() e.UsePartitioner(partition, p => p.Message.JobTypeId); } - var stateMachine = new JobTypeStateMachine(_options); + var stateMachine = new JobTypeStateMachine(); - e.StateMachineSaga(stateMachine, _jobTypeRepository ?? new InMemorySagaRepository()); + e.StateMachineSaga(stateMachine, _jobTypeRepository ?? new InMemorySagaRepository(), + s => s.UseFilter(new PayloadFilter, JobSagaSettings>(_options))); _jobTypeSagaEndpointConfigurator = e; diff --git a/src/MassTransit/JobService/Configuration/JobServiceConsumerConfigurationObserver.cs b/src/MassTransit/JobService/Configuration/JobServiceConsumerConfigurationObserver.cs index 7628435c83b..e6773fa780b 100644 --- a/src/MassTransit/JobService/Configuration/JobServiceConsumerConfigurationObserver.cs +++ b/src/MassTransit/JobService/Configuration/JobServiceConsumerConfigurationObserver.cs @@ -13,26 +13,28 @@ public class JobServiceConsumerConfigurationObserver : readonly IReceiveEndpointConfigurator _configurator; readonly Action _configureEndpoint; readonly Dictionary _consumerConfigurators; - readonly JobServiceOptions _jobServiceOptions; + readonly JobServiceSettings _settings; bool _endpointConfigured; - public JobServiceConsumerConfigurationObserver(IReceiveEndpointConfigurator configurator, JobServiceOptions jobServiceOptions, + public JobServiceConsumerConfigurationObserver(IReceiveEndpointConfigurator configurator, JobServiceSettings settings, Action configureEndpoint) { _configurator = configurator; - _jobServiceOptions = jobServiceOptions; _configureEndpoint = configureEndpoint; + _settings = settings; + _consumerConfigurators = new Dictionary(); } - void IConsumerConfigurationObserver.ConsumerConfigured(IConsumerConfigurator configurator) + public void ConsumerConfigured(IConsumerConfigurator configurator) + where T : class { if (typeof(T).HasInterface(typeof(IJobConsumer<>))) { _consumerConfigurators.Add(typeof(T), configurator); - configurator.Options(_jobServiceOptions); + configurator.Options(_settings); if (_endpointConfigured) return; @@ -43,7 +45,9 @@ void IConsumerConfigurationObserver.ConsumerConfigured(IConsumerConfigurator< } } - void IConsumerConfigurationObserver.ConsumerMessageConfigured(IConsumerMessageConfigurator configurator) + public void ConsumerMessageConfigured(IConsumerMessageConfigurator configurator) + where T : class + where TMessage : class { if (typeof(T).HasInterface>() && _consumerConfigurators.TryGetValue(typeof(T), out var value) @@ -52,8 +56,9 @@ void IConsumerConfigurationObserver.ConsumerMessageConfigured(ICons var options = consumerConfigurator.Options>(); var jobTypeId = JobMetadataCache.GenerateJobTypeId(_configurator.InputAddress.GetEndpointName()); + var jobTypeName = JobMetadataCache.GenerateJobTypeName(_configurator.InputAddress.GetEndpointName()); - _jobServiceOptions.JobService.RegisterJobType(_configurator, options, jobTypeId); + _settings.JobService.RegisterJobType(_configurator, options, jobTypeId, jobTypeName); } } } diff --git a/src/MassTransit/JobService/Configuration/JobServiceEndpointConfigurationObserver.cs b/src/MassTransit/JobService/Configuration/JobServiceEndpointConfigurationObserver.cs index 5ecc2685193..c8415ad8b5c 100644 --- a/src/MassTransit/JobService/Configuration/JobServiceEndpointConfigurationObserver.cs +++ b/src/MassTransit/JobService/Configuration/JobServiceEndpointConfigurationObserver.cs @@ -1,25 +1,25 @@ namespace MassTransit.Configuration { using System; + using JobService; public class JobServiceEndpointConfigurationObserver : IEndpointConfigurationObserver { readonly Action _configureEndpoint; - readonly JobServiceOptions _jobServiceOptions; + readonly JobServiceSettings _settings; - public JobServiceEndpointConfigurationObserver(JobServiceOptions jobServiceOptions, Action configureEndpoint) + public JobServiceEndpointConfigurationObserver(JobServiceSettings settings, Action configureEndpoint) { - _jobServiceOptions = jobServiceOptions; + _settings = settings; _configureEndpoint = configureEndpoint; } public void EndpointConfigured(T configurator) where T : IReceiveEndpointConfigurator { - var observer = new JobServiceConsumerConfigurationObserver(configurator, _jobServiceOptions, _configureEndpoint); - configurator.ConnectConsumerConfigurationObserver(observer); + configurator.ConnectConsumerConfigurationObserver(new JobServiceConsumerConfigurationObserver(configurator, _settings, _configureEndpoint)); } } } diff --git a/src/MassTransit/JobService/Configuration/JobTypeSagaDefinition.cs b/src/MassTransit/JobService/Configuration/JobTypeSagaDefinition.cs new file mode 100644 index 00000000000..e633078effc --- /dev/null +++ b/src/MassTransit/JobService/Configuration/JobTypeSagaDefinition.cs @@ -0,0 +1,50 @@ +namespace MassTransit.Configuration +{ + using Contracts.JobService; + using JobService; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Middleware; + + + public class JobTypeSagaDefinition : + SagaDefinition + { + readonly JobSagaOptions _options; + readonly JobSagaSettingsConfigurator _setOptions; + + public JobTypeSagaDefinition(IOptions options) + { + _options = options.Value; + _setOptions = _options; + } + + protected override void ConfigureSaga(IReceiveEndpointConfigurator configurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) + { + configurator.UseMessageRetry(r => r.Intervals(100, 500, 1000, 1000, 2000, 2000, 5000, 5000)); + + configurator.UseMessageScope(context); + + configurator.UseInMemoryOutbox(context); + + if (_options.ConcurrentMessageLimit.HasValue) + { + configurator.ConcurrentMessageLimit = _options.ConcurrentMessageLimit; + + var partition = new Partitioner(_options.ConcurrentMessageLimit.Value, new Murmur3UnsafeHashGenerator()); + + configurator.UsePartitioner(partition, p => p.Message.JobTypeId); + configurator.UsePartitioner(partition, p => p.Message.JobTypeId); + configurator.UsePartitioner(partition, p => p.Message.JobTypeId); + } + + sagaConfigurator.UseFilter(new PayloadFilter, JobSagaSettings>(_options)); + + _setOptions.JobTypeSagaEndpointAddress = configurator.InputAddress; + + if (context.GetRequiredService().TryGetValue(context, typeof(JobService), out IJobServiceRegistration registration)) + registration.AddReceiveEndpointDependency(configurator); + } + } +} diff --git a/src/MassTransit/JobService/IJobDistributionStrategy.cs b/src/MassTransit/JobService/IJobDistributionStrategy.cs new file mode 100644 index 00000000000..9071d53508b --- /dev/null +++ b/src/MassTransit/JobService/IJobDistributionStrategy.cs @@ -0,0 +1,21 @@ +#nullable enable +namespace MassTransit; + +using System.Threading.Tasks; +using Contracts.JobService; + + +/// +/// Used by the to determine if a job slot should be allocated to a job +/// +public interface IJobDistributionStrategy +{ + /// + /// Determine if a job slot is available and return an instance if the job can be assigned to a job consumer instance. + /// If no instance is available or the concurrency limits would be exceeded, return null. + /// + /// + /// + /// An if the job can be assigned to a job consumer instance, or null + Task IsJobSlotAvailable(ConsumeContext context, JobTypeInfo jobTypeInfo); +} diff --git a/src/MassTransit/JobService/IRecurringJobScheduleConfigurator.cs b/src/MassTransit/JobService/IRecurringJobScheduleConfigurator.cs new file mode 100644 index 00000000000..12dbd98fb7b --- /dev/null +++ b/src/MassTransit/JobService/IRecurringJobScheduleConfigurator.cs @@ -0,0 +1,33 @@ +#nullable enable +namespace MassTransit; + +using System; + + +/// +/// Configure the optional settings of a recurring job +/// +public interface IRecurringJobScheduleConfigurator +{ + /// + /// A valid cron expression specifying the job schedule + /// + public string CronExpression { set; } + + /// + /// If specified, the start date for the job. Otherwise, the current date/time will be used. + /// + public DateTimeOffset? Start { set; } + + /// + /// If specified, the end date for the job after which it will be removed from the job scheduler + /// + public DateTimeOffset? End { set; } + + /// + /// If specified, the time zone in which the cron expression should be evaluated, otherwise UTC is used. + /// + string? TimeZoneId { set; } +} + + diff --git a/src/MassTransit/JobService/JobAttemptStateMachine.cs b/src/MassTransit/JobService/JobAttemptStateMachine.cs index df46faffb0d..2713c6883fc 100644 --- a/src/MassTransit/JobService/JobAttemptStateMachine.cs +++ b/src/MassTransit/JobService/JobAttemptStateMachine.cs @@ -2,22 +2,17 @@ namespace MassTransit { using System; using System.Linq; + using Configuration; using Contracts.JobService; using Events; + using JobService.Messages; public sealed class JobAttemptStateMachine : MassTransitStateMachine { - readonly JobServiceOptions _options; - - public JobAttemptStateMachine(JobServiceOptions options) + public JobAttemptStateMachine() { - _options = options; - - SuspectJobRetryCount = options.SuspectJobRetryCount; - SuspectJobRetryDelay = options.SuspectJobRetryDelay ?? options.SlotWaitTime; - Event(() => StartJobAttempt, x => { x.CorrelateById(context => context.Message.AttemptId); @@ -28,6 +23,16 @@ public JobAttemptStateMachine(JobServiceOptions options) x.CorrelateById(context => context.Message.Message.AttemptId); x.ConfigureConsumeTopology = false; }); + Event(() => FinalizeJobAttempt, x => + { + x.CorrelateById(context => context.Message.AttemptId); + x.ConfigureConsumeTopology = false; + }); + Event(() => CancelJobAttempt, x => + { + x.CorrelateById(context => context.Message.AttemptId); + x.ConfigureConsumeTopology = false; + }); Event(() => AttemptCanceled, x => x.CorrelateById(context => context.Message.AttemptId)); Event(() => AttemptCompleted, x => x.CorrelateById(context => context.Message.AttemptId)); @@ -42,7 +47,7 @@ public JobAttemptStateMachine(JobServiceOptions options) Schedule(() => StatusCheckRequested, instance => instance.StatusCheckTokenId, x => { - x.Delay = options.StatusCheckInterval; + x.DelayProvider = context => context.GetPayload().StatusCheckInterval; x.Received = r => { r.CorrelateById(context => context.Message.AttemptId); @@ -61,8 +66,8 @@ public JobAttemptStateMachine(JobServiceOptions options) context.Saga.InstanceAddress ??= context.Message.InstanceAddress; context.Saga.ServiceAddress ??= context.Message.ServiceAddress; }) - .SendStartJob(this) .ScheduleJobStatusCheck(this) + .SendStartJob() .TransitionTo(Starting)); During(Starting, @@ -70,14 +75,14 @@ public JobAttemptStateMachine(JobServiceOptions options) .Then(context => { context.Saga.Faulted = context.Message.Timestamp; - context.Saga.InstanceAddress ??= context.GetPayload().SourceAddress; + context.Saga.InstanceAddress ??= context.SourceAddress; }) - .SendJobAttemptFaulted(this) + .SendJobAttemptFaulted() .TransitionTo(Faulted)); During(Starting, When(StatusCheckRequested.Received) - .SendJobAttemptStartTimeout(this) + .SendJobAttemptStartTimeout() .TransitionTo(Faulted)); During(Initial, Starting, Running, @@ -107,28 +112,30 @@ public JobAttemptStateMachine(JobServiceOptions options) When(AttemptCanceled) .Unschedule(StatusCheckRequested) .Finalize(), + When(CancelJobAttempt) + .SendCancelJobAttempt(), When(AttemptFaulted) .Then(context => { context.Saga.Faulted = context.Message.Timestamp; - context.Saga.InstanceAddress ??= context.GetPayload().SourceAddress; + context.Saga.InstanceAddress ??= context.SourceAddress; }) .Unschedule(StatusCheckRequested) .TransitionTo(Faulted)); During(Running, When(StatusCheckRequested.Received) - .SendCheckJobStatus(this) + .ScheduleJobStatusCheck(this) + .SendCheckJobStatus() .TransitionTo(CheckingStatus) - .Catch(eb => eb.TransitionTo(Suspect)) - .ScheduleJobStatusCheck(this)); + ); During(CheckingStatus, When(StatusCheckRequested.Received) - .SendCheckJobStatus(this) + .ScheduleJobStatusCheck(this) + .SendCheckJobStatus() .TransitionTo(Suspect) - .Catch(eb => eb.TransitionTo(Suspect)) - .ScheduleJobStatusCheck(this)); + ); During(Running, CheckingStatus, Suspect, When(AttemptStatus, context => context.Message.Status == JobStatus.Running) @@ -142,13 +149,17 @@ public JobAttemptStateMachine(JobServiceOptions options) During(Suspect, When(StatusCheckRequested.Received) - .SendJobAttemptFaulted(this) + .SendJobAttemptFaulted() .TransitionTo(Faulted)); During(Faulted, Ignore(StatusCheckRequested.Received), Ignore(AttemptFaulted)); + During([Initial, Faulted, CheckingStatus, Suspect], + When(FinalizeJobAttempt) + .Finalize()); + During(Initial, When(AttemptCompleted) .Finalize(), @@ -162,12 +173,8 @@ public JobAttemptStateMachine(JobServiceOptions options) SetCompletedWhenFinalized(); } - public int SuspectJobRetryCount { get; } - public TimeSpan SuspectJobRetryDelay { get; } - - public Uri JobSagaEndpointAddress => _options.JobSagaEndpointAddress; - public Uri JobTypeSagaEndpointAddress => _options.JobTypeSagaEndpointAddress; - public Uri JobAttemptSagaEndpointAddress => _options.JobAttemptSagaEndpointAddress; // ReSharper disable UnassignedGetOnlyAutoProperty + // + // ReSharper disable UnassignedGetOnlyAutoProperty // ReSharper disable MemberCanBePrivate.Global public State Starting { get; } public State Running { get; } @@ -177,6 +184,8 @@ public JobAttemptStateMachine(JobServiceOptions options) public Event StartJobAttempt { get; } public Event> StartJobFaulted { get; } + public Event FinalizeJobAttempt { get; } + public Event CancelJobAttempt { get; } public Event AttemptStarted { get; } public Event AttemptFaulted { get; } @@ -191,80 +200,119 @@ public JobAttemptStateMachine(JobServiceOptions options) static class JobAttemptStateMachineBehaviorExtensions { - public static EventActivityBinder SendStartJob(this EventActivityBinder binder, - JobAttemptStateMachine machine) + public static TimeSpan? GetRetryDelay(this BehaviorContext context) + where T : class + { + var settings = context.GetPayload(); + + return context.Saga.RetryAttempt < settings.SuspectJobRetryCount + ? settings.SuspectJobRetryDelay ?? settings.SlotWaitTime + : default(TimeSpan?); + } + + static Uri GetJobSagaAddress(this SagaConsumeContext context) { - return binder.SendAsync(context => context.Saga.InstanceAddress ?? context.Saga.ServiceAddress, - context => context.Init(new + return context.GetPayload().JobSagaEndpointAddress; + } + + static Uri GetJobAttemptSagaAddress(this SagaConsumeContext context) + { + return context.GetPayload().JobAttemptSagaEndpointAddress; + } + + public static EventActivityBinder SendStartJob(this EventActivityBinder binder) + { + return binder.Send(context => context.Saga.InstanceAddress ?? context.Saga.ServiceAddress, + context => new StartJobCommand { - context.Message.JobId, - context.Message.AttemptId, - context.Message.RetryAttempt, - context.Message.Job, - context.Message.JobTypeId, - }), context => context.ResponseAddress = machine.JobAttemptSagaEndpointAddress); + JobId = context.Message.JobId, + AttemptId = context.Message.AttemptId, + RetryAttempt = context.Message.RetryAttempt, + Job = context.Message.Job, + JobTypeId = context.Message.JobTypeId, + LastProgressValue = context.Message.LastProgressValue, + LastProgressLimit = context.Message.LastProgressLimit, + JobState = context.Message.JobState, + JobProperties = context.Message.JobProperties + }, (behaviorContext, context) => context.FaultAddress = behaviorContext.GetJobAttemptSagaAddress()); } public static EventActivityBinder SendCheckJobStatus(this EventActivityBinder binder, JobAttemptStateMachine machine) + JobStatusCheckRequested> binder) { - return binder.SendAsync(context => context.Saga.InstanceAddress ?? context.Saga.ServiceAddress, - context => context.Init(new - { - context.Saga.JobId, - AttemptId = context.Saga.CorrelationId - }), context => context.ResponseAddress = machine.JobAttemptSagaEndpointAddress) - .Catch(ex => ex.Then(context => LogContext.Error?.Log(context.Exception, "Failed sending GetJobAttemptStatus"))); + return binder.Send( + context => context.Saga.InstanceAddress ?? context.Saga.ServiceAddress, context => new GetJobAttemptStatusRequest + { + JobId = context.Saga.JobId, + AttemptId = context.Saga.CorrelationId + }, (behaviorContext, context) => + { + context.RequestId = behaviorContext.Saga.CorrelationId; + context.ResponseAddress = behaviorContext.GetJobAttemptSagaAddress(); + }); + } + + public static EventActivityBinder SendCancelJobAttempt(this EventActivityBinder binder) + { + return binder.Send(context => context.Saga.InstanceAddress ?? context.Saga.ServiceAddress, + context => context.Message, + (behaviorContext, context) => + { + context.RequestId = behaviorContext.Saga.CorrelationId; + context.ResponseAddress = behaviorContext.GetJobAttemptSagaAddress(); + }); } public static EventActivityBinder ScheduleJobStatusCheck(this EventActivityBinder binder, JobAttemptStateMachine machine) where T : class { - return binder.Schedule(machine.StatusCheckRequested, x => x.Init(new { AttemptId = x.Saga.CorrelationId })); + return binder.Schedule(machine.StatusCheckRequested, x => new JobStatusCheckRequestedEvent { AttemptId = x.Saga.CorrelationId }); } public static EventActivityBinder> SendJobAttemptFaulted( - this EventActivityBinder> binder, JobAttemptStateMachine machine) + this EventActivityBinder> binder) { - return binder.SendAsync(context => machine.JobSagaEndpointAddress, context => context.Init(new - { - context.Saga.JobId, - AttemptId = context.Saga.CorrelationId, - context.Saga.RetryAttempt, - context.Message.Timestamp, - Exceptions = context.Message.Exceptions?.FirstOrDefault() - })); + return binder.Send, JobAttemptFaulted>(context => context.GetJobSagaAddress(), + context => new JobAttemptFaultedEvent + { + JobId = context.Saga.JobId, + AttemptId = context.Saga.CorrelationId, + RetryAttempt = context.Saga.RetryAttempt, + Timestamp = context.Message.Timestamp, + Exceptions = context.Message.Exceptions?.FirstOrDefault() + }); } - public static EventActivityBinder SendJobAttemptFaulted(this EventActivityBinder binder, - JobAttemptStateMachine machine) + public static EventActivityBinder SendJobAttemptFaulted(this EventActivityBinder binder) where T : class { - return binder.SendAsync(context => machine.JobSagaEndpointAddress, context => context.Init(new - { - context.Saga.JobId, - AttemptId = context.Saga.CorrelationId, - context.Saga.RetryAttempt, - InVar.Timestamp, - RetryDelay = context.Saga.RetryAttempt < machine.SuspectJobRetryCount ? machine.SuspectJobRetryDelay : default(TimeSpan?), - Exceptions = new FaultExceptionInfo(new TimeoutException("The job status check timed out.")) - })); + return binder.Send(context => context.GetJobSagaAddress(), + context => new JobAttemptFaultedEvent + { + JobId = context.Saga.JobId, + AttemptId = context.Saga.CorrelationId, + RetryAttempt = context.Saga.RetryAttempt, + Timestamp = DateTime.UtcNow, + RetryDelay = context.GetRetryDelay(), + Exceptions = new FaultExceptionInfo(new TimeoutException("The job status check timed out.")) + }); } - public static EventActivityBinder SendJobAttemptStartTimeout(this EventActivityBinder binder, - JobAttemptStateMachine machine) + public static EventActivityBinder SendJobAttemptStartTimeout(this EventActivityBinder binder) where T : class { - return binder.SendAsync(context => machine.JobSagaEndpointAddress, context => context.Init(new - { - context.Saga.JobId, - AttemptId = context.Saga.CorrelationId, - context.Saga.RetryAttempt, - InVar.Timestamp, - RetryDelay = context.Saga.RetryAttempt < machine.SuspectJobRetryCount ? machine.SuspectJobRetryDelay : default(TimeSpan?), - Exceptions = new FaultExceptionInfo(new TimeoutException($"The job service failed to respond: {context.Saga.InstanceAddress} (Suspect)")) - })); + return binder.Send(context => context.GetJobSagaAddress(), + context => new JobAttemptFaultedEvent + { + JobId = context.Saga.JobId, + AttemptId = context.Saga.CorrelationId, + RetryAttempt = context.Saga.RetryAttempt, + Timestamp = DateTime.UtcNow, + RetryDelay = context.GetRetryDelay(), + Exceptions = new FaultExceptionInfo(new TimeoutException($"The job service failed to respond: {context.Saga.InstanceAddress} (Suspect)")) + }); } } } diff --git a/src/MassTransit/JobService/JobSaga.cs b/src/MassTransit/JobService/JobSaga.cs index 2f4a0075a4e..10a7ef54c4c 100644 --- a/src/MassTransit/JobService/JobSaga.cs +++ b/src/MassTransit/JobService/JobSaga.cs @@ -16,7 +16,7 @@ public class JobSaga : public DateTime? Submitted { get; set; } public Uri ServiceAddress { get; set; } public TimeSpan? JobTimeout { get; set; } - public IDictionary Job { get; set; } + public Dictionary Job { get; set; } public Guid JobTypeId { get; set; } public Guid AttemptId { get; set; } @@ -33,6 +33,61 @@ public class JobSaga : public Guid? JobSlotWaitToken { get; set; } public Guid? JobRetryDelayToken { get; set; } + /// + /// If present, keeps track of any previously faulted attempts so that the faulted job attempt saga instances can be removed when finalized + /// + public List IncompleteAttempts { get; set; } + + /// + /// If present, the last reported progress value + /// + public long? LastProgressValue { get; set; } + + /// + /// If present, the maximum value (can be used to show a percentage) + /// + public long? LastProgressLimit { get; set; } + + /// + /// The last reported sequence number for the current job attempt + /// + public long? LastProgressSequenceNumber { get; set; } + + /// + /// The job state, saved from a previous job attempt + /// + public Dictionary JobState { get; set; } + + /// + /// The job properties, supplied by the submitted job + /// + public Dictionary JobProperties { get; set; } + + /// + /// For recurring jobs, the cron expression used to determine the next start date after the job has completed. + /// + public string CronExpression { get; set; } + + /// + /// The time zone for the cron expression + /// + public string TimeZoneId { get; set; } + + /// + /// If a state date is specified, the job won't start until after the start date. + /// + public DateTimeOffset? StartDate { get; set; } + + /// + /// For recurring jobs, if the is after the end date the job will be completed. + /// + public DateTimeOffset? EndDate { get; set; } + + /// + /// For recurring jobs, the next start date based on the cron expression (and , if specified). + /// + public DateTimeOffset? NextStartDate { get; set; } + public byte[] RowVersion { get; set; } public int Version { get; set; } diff --git a/src/MassTransit/JobService/JobService/ConsumeJobContext.cs b/src/MassTransit/JobService/JobService/ConsumeJobContext.cs index 9487b5447ad..148581444f0 100644 --- a/src/MassTransit/JobService/JobService/ConsumeJobContext.cs +++ b/src/MassTransit/JobService/JobService/ConsumeJobContext.cs @@ -1,135 +1,218 @@ #nullable enable -namespace MassTransit.JobService +namespace MassTransit.JobService; + +using System; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Threading; +using System.Threading.Tasks; +using Context; +using Contracts.JobService; +using Events; +using Messages; +using Serialization; + + +public class ConsumeJobContext : + ConsumeContextProxy, + ConsumeContext, + JobContext, + INotifyJobContext, + IAsyncDisposable + where TJob : class { - using System; - using System.Diagnostics; - using System.Threading; - using System.Threading.Tasks; - using Context; - using Contracts.JobService; - - - public class ConsumeJobContext : - ConsumeContextProxy, - ConsumeContext, - JobContext, - IDisposable - where TJob : class + readonly ConsumeContext _context; + readonly Uri _instanceAddress; + readonly JobOptions _jobOptions; + readonly CancellationTokenSource _source; + readonly Stopwatch _stopwatch; + string? _cancellationReason; + JobProgressBuffer? _updateBuffer; + + public ConsumeJobContext(ConsumeContext context, Uri instanceAddress, TJob job, JobOptions jobOptions) + : base(context) { - readonly ConsumeContext _context; - readonly Uri _instanceAddress; - readonly CancellationTokenSource _source; - readonly Stopwatch _stopwatch; - - public ConsumeJobContext(ConsumeContext context, Uri instanceAddress, Guid jobId, Guid attemptId, int retryAttempt, TJob job, - TimeSpan jobTimeout) - : base(context) - { - _context = context; - _instanceAddress = instanceAddress; + _context = context; + _instanceAddress = instanceAddress; + _jobOptions = jobOptions; - JobId = jobId; - Job = job; - AttemptId = attemptId; - RetryAttempt = retryAttempt; + JobId = context.Message.JobId; + AttemptId = context.Message.AttemptId; + RetryAttempt = context.Message.RetryAttempt; - _source = new CancellationTokenSource(jobTimeout); - _stopwatch = Stopwatch.StartNew(); - } + Job = job; + + LastProgressValue = context.Message.LastProgressValue; + LastProgressLimit = context.Message.LastProgressLimit; + + var jobProperties = new JobPropertyCollection(); + jobProperties.SetMany(context.Message.JobProperties!); + + JobProperties = jobProperties; + + _source = new CancellationTokenSource(jobOptions.JobTimeout); + _stopwatch = Stopwatch.StartNew(); + } + + public override CancellationToken CancellationToken => _source.Token; + + public TJob Message => Job; + + public Task NotifyConsumed(TimeSpan duration, string consumerType) + { + return _context.NotifyConsumed(_context, duration, consumerType); + } + + public Task NotifyFaulted(TimeSpan duration, string consumerType, Exception exception) + { + return _context.NotifyFaulted(_context, duration, consumerType, exception); + } - public override CancellationToken CancellationToken => _source.Token; + public async ValueTask DisposeAsync() + { + if (_updateBuffer != null) + await _updateBuffer.Flush().ConfigureAwait(false); + + _source.Dispose(); + } + + public async Task NotifyCanceled() + { + LogContext.Debug?.Log("Job Canceled: {JobId} {AttemptId} ({RetryAttempt}) {Reason}", JobId, AttemptId, RetryAttempt, _cancellationReason); - public TJob Message => Job; + if (_updateBuffer != null) + await _updateBuffer.Flush().ConfigureAwait(false); - public Task NotifyConsumed(TimeSpan duration, string consumerType) + await Notify(new JobAttemptCanceledEvent { - return _context.NotifyConsumed(_context, duration, consumerType); - } + JobId = JobId, + AttemptId = AttemptId, + Timestamp = DateTime.UtcNow, + Reason = _cancellationReason ?? JobCancellationReasons.ConsumerInitiated + }).ConfigureAwait(false); + } - public Task NotifyFaulted(TimeSpan duration, string consumerType, Exception exception) + public async Task NotifyStarted() + { + LogContext.Debug?.Log("Job Started: {JobId} {AttemptId} ({RetryAttempt})", JobId, AttemptId, RetryAttempt); + + var timestamp = DateTime.UtcNow; + + await Notify(new JobAttemptStartedEvent { - return _context.NotifyFaulted(_context, duration, consumerType, exception); - } + JobId = JobId, + AttemptId = AttemptId, + RetryAttempt = RetryAttempt, + Timestamp = timestamp, + InstanceAddress = _instanceAddress + }).ConfigureAwait(false); + + var endpoint = await _context.ReceiveContext.PublishEndpointProvider.GetPublishSendEndpoint>().ConfigureAwait(false); - public void Dispose() + await endpoint.Send>(new JobStartedEvent { - _source.Dispose(); - } + JobId = JobId, + AttemptId = AttemptId, + RetryAttempt = RetryAttempt, + Timestamp = timestamp + }, CancellationToken.None).ConfigureAwait(false); + } - public Guid JobId { get; } - public Guid AttemptId { get; } - public int RetryAttempt { get; } - public TJob Job { get; } + public async Task NotifyCompleted() + { + LogContext.Debug?.Log("Job Completed: {JobId} {AttemptId} ({RetryAttempt})", JobId, AttemptId, RetryAttempt); - public TimeSpan ElapsedTime => _stopwatch.Elapsed; + if (_updateBuffer != null) + await _updateBuffer.Flush().ConfigureAwait(false); - public Task NotifyCanceled(string? reason = null) + await Notify(new JobAttemptCompletedEvent { - LogContext.Debug?.Log("Job Canceled: {JobId} {AttemptId} ({RetryAttempt})", JobId, AttemptId, RetryAttempt); - - return Notify(new - { - JobId, - AttemptId, - RetryAttempt, - InVar.Timestamp - }); - } + JobId = JobId, + AttemptId = AttemptId, + RetryAttempt = RetryAttempt, + Timestamp = DateTime.UtcNow, + Duration = ElapsedTime + }).ConfigureAwait(false); + } + + public Task NotifyJobProgress(SetJobProgress progress) + { + return Notify(progress); + } + + public async Task NotifyFaulted(Exception exception, TimeSpan? delay) + { + LogContext.Debug?.Log(exception, "Job Faulted: {JobId} {AttemptId} ({RetryAttempt})", JobId, AttemptId, RetryAttempt); + + if (_updateBuffer != null) + await _updateBuffer.Flush().ConfigureAwait(false); - public Task NotifyStarted() + await Notify(new JobAttemptFaultedEvent { - LogContext.Debug?.Log("Job Started: {JobId} {AttemptId} ({RetryAttempt})", JobId, AttemptId, RetryAttempt); - - return Notify(new - { - JobId, - AttemptId, - RetryAttempt, - InVar.Timestamp, - InstanceAddress = _instanceAddress - }); - } + JobId = JobId, + AttemptId = AttemptId, + RetryAttempt = RetryAttempt, + RetryDelay = delay, + Timestamp = DateTime.UtcNow, + Exceptions = new FaultExceptionInfo(exception) + }).ConfigureAwait(false); + } + + public Guid JobId { get; } + public Guid AttemptId { get; } + public int RetryAttempt { get; } + public long? LastProgressValue { get; } + public long? LastProgressLimit { get; } + public TJob Job { get; } + + public TimeSpan ElapsedTime => _stopwatch.Elapsed; + + public Task SetJobProgress(long value, long? limit) + { + _updateBuffer ??= new JobProgressBuffer(this, _jobOptions.ProgressBuffer); + + return _updateBuffer.Update(new JobProgressBuffer.ProgressUpdate(JobId, AttemptId, value, limit), CancellationToken.None); + } - public Task NotifyCompleted() + public Task SaveJobState(T? jobState) + where T : class + { + return Notify(new SaveJobStateCommand { - LogContext.Debug?.Log("Job Completed: {JobId} {AttemptId} ({RetryAttempt})", JobId, AttemptId, RetryAttempt); - - return Notify(new - { - JobId, - AttemptId, - RetryAttempt, - InVar.Timestamp, - Duration = ElapsedTime - }); - } + JobId = JobId, + AttemptId = AttemptId, + JobState = jobState != null ? _context.ToDictionary(jobState) : null + }); + } - public Task NotifyFaulted(Exception exception, TimeSpan? delay) + public bool TryGetJobState([NotNullWhen(true)] out T? jobState) + where T : class + { + if (_context.Message.JobState != null) { - LogContext.Debug?.Log(exception, "Job Faulted: {JobId} {AttemptId} ({RetryAttempt})", JobId, AttemptId, RetryAttempt); - - return Notify(new - { - JobId, - AttemptId, - RetryAttempt, - RetryDelay = delay, - InVar.Timestamp, - Exceptions = exception - }); + jobState = _context.SerializerContext.DeserializeObject(_context.Message.JobState); + return jobState != null; } - async Task Notify(object values) - where T : class - { - var endpoint = await _context.ReceiveContext.PublishEndpointProvider.GetPublishSendEndpoint().ConfigureAwait(false); + jobState = null; + return false; + } - await endpoint.Send(values, CancellationToken.None).ConfigureAwait(false); - } + public IPropertyCollection JobProperties { get; set; } + public IPropertyCollection JobTypeProperties => _jobOptions.JobTypeProperties; + public IPropertyCollection InstanceProperties => _jobOptions.InstanceProperties; - public void Cancel() - { - _source.Cancel(); - } + async Task Notify(T message) + where T : class + { + var endpoint = await _context.ReceiveContext.PublishEndpointProvider.GetPublishSendEndpoint().ConfigureAwait(false); + + await endpoint.Send(message, CancellationToken.None).ConfigureAwait(false); + } + + public void Cancel(string? reason) + { + _cancellationReason = reason; + _source.Cancel(); } } diff --git a/src/MassTransit/JobService/JobService/ConsumerJobHandle.cs b/src/MassTransit/JobService/JobService/ConsumerJobHandle.cs index f6ba27505d3..39de0c0e236 100644 --- a/src/MassTransit/JobService/JobService/ConsumerJobHandle.cs +++ b/src/MassTransit/JobService/JobService/ConsumerJobHandle.cs @@ -1,4 +1,5 @@ -namespace MassTransit.JobService +#nullable enable +namespace MassTransit.JobService { using System; using System.Threading.Tasks; @@ -10,30 +11,37 @@ public class ConsumerJobHandle : where T : class { readonly ConsumeJobContext _context; + readonly TimeSpan _jobCancellationTimeout; - public ConsumerJobHandle(ConsumeJobContext context, Task task) + public ConsumerJobHandle(ConsumeJobContext context, Task task, TimeSpan jobCancellationTimeout) { _context = context; + _jobCancellationTimeout = jobCancellationTimeout; JobTask = task; } public Guid JobId => _context.JobId; public Task JobTask { get; } - public async Task Cancel() + public async Task Cancel(string? reason) { if (_context.CancellationToken.IsCancellationRequested) return; - _context.Cancel(); + _context.Cancel(reason); try { - await JobTask.OrTimeout(TimeSpan.FromSeconds(30)).ConfigureAwait(false); + await JobTask.OrTimeout(_jobCancellationTimeout).ConfigureAwait(false); } catch (OperationCanceledException) { } } + + public ValueTask DisposeAsync() + { + return _context.DisposeAsync(); + } } } diff --git a/src/MassTransit/JobService/JobService/DefaultJobDistributionStrategy.cs b/src/MassTransit/JobService/JobService/DefaultJobDistributionStrategy.cs new file mode 100644 index 00000000000..ff325498b73 --- /dev/null +++ b/src/MassTransit/JobService/JobService/DefaultJobDistributionStrategy.cs @@ -0,0 +1,37 @@ +#nullable enable +namespace MassTransit.JobService; + +using System.Linq; +using System.Threading.Tasks; +using Contracts.JobService; + + +public class DefaultJobDistributionStrategy : + IJobDistributionStrategy +{ + public static readonly IJobDistributionStrategy Instance = new DefaultJobDistributionStrategy(); + + public async Task IsJobSlotAvailable(ConsumeContext context, JobTypeInfo jobTypeInfo) + { + var instances = from i in jobTypeInfo.Instances + join a in jobTypeInfo.ActiveJobs on i.Key equals a.InstanceAddress into ai + where ai.Count() < jobTypeInfo.ConcurrentJobLimit + orderby ai.Count(), i.Value.Used + select new + { + Instance = i.Value, + InstanceAddress = i.Key, + InstanceCount = ai.Count() + }; + + var firstInstance = instances.FirstOrDefault(); + if (firstInstance == null) + return null; + + return new ActiveJob + { + JobId = context.Message.JobId, + InstanceAddress = firstInstance.InstanceAddress + }; + } +} diff --git a/src/MassTransit/JobService/JobService/FinalizeJobConsumer.cs b/src/MassTransit/JobService/JobService/FinalizeJobConsumer.cs index 57d22a4999f..b0e3f77a3b2 100644 --- a/src/MassTransit/JobService/JobService/FinalizeJobConsumer.cs +++ b/src/MassTransit/JobService/JobService/FinalizeJobConsumer.cs @@ -4,6 +4,7 @@ namespace MassTransit.JobService using System.Runtime.Serialization; using System.Threading.Tasks; using Contracts.JobService; + using Messages; public class FinalizeJobConsumer : @@ -25,9 +26,16 @@ public Task Consume(ConsumeContext context) if (context.Message.JobTypeId != _jobTypeId) return Task.CompletedTask; - _ = context.GetJob() ?? throw new SerializationException($"The job could not be deserialized: {TypeCache.ShortName}"); + var job = context.GetJob() ?? throw new SerializationException($"The job could not be deserialized: {TypeCache.ShortName}"); - return context.Publish>(context.Message); + return context.Publish>(new JobCompletedEvent + { + JobId = context.Message.JobId, + Timestamp = context.Message.Timestamp, + Duration = context.Message.Duration, + Job = job, + Result = context.Message.Result, + }); } public Task Consume(ConsumeContext context) diff --git a/src/MassTransit/JobService/JobService/IJobService.cs b/src/MassTransit/JobService/JobService/IJobService.cs index 6e02c6f703c..790d98db03d 100644 --- a/src/MassTransit/JobService/JobService/IJobService.cs +++ b/src/MassTransit/JobService/JobService/IJobService.cs @@ -9,6 +9,8 @@ public interface IJobService { Uri InstanceAddress { get; } + JobServiceSettings Settings { get; } + /// /// Starts a job /// @@ -16,9 +18,9 @@ public interface IJobService /// The context of the message being consumed /// The job command /// The pipe which executes the job - /// The job timeout, after which the job is cancelled + /// The job options /// The newly created job's handle - Task StartJob(ConsumeContext context, T job, IPipe> jobPipe, TimeSpan timeout) + Task StartJob(ConsumeContext context, T job, IPipe> jobPipe, JobOptions jobOptions) where T : class; /// @@ -42,8 +44,9 @@ Task StartJob(ConsumeContext context, T job, IPipe /// /// + /// /// - void RegisterJobType(IReceiveEndpointConfigurator configurator, JobOptions options, Guid jobTypeId) + void RegisterJobType(IReceiveEndpointConfigurator configurator, JobOptions options, Guid jobTypeId, string jobTypeName) where T : class; Task BusStarted(IPublishEndpoint publishEndpoint); @@ -55,5 +58,7 @@ void RegisterJobType(IReceiveEndpointConfigurator configurator, JobOptions /// Guid GetJobTypeId() where T : class; + + void ConfigureSuperviseJobConsumer(IReceiveEndpointConfigurator configurator); } } diff --git a/src/MassTransit/JobService/JobService/JobHandle.cs b/src/MassTransit/JobService/JobService/JobHandle.cs index 227e90737aa..a3db8bdb14c 100644 --- a/src/MassTransit/JobService/JobService/JobHandle.cs +++ b/src/MassTransit/JobService/JobService/JobHandle.cs @@ -1,3 +1,4 @@ +#nullable enable namespace MassTransit.JobService { using System; @@ -7,7 +8,8 @@ namespace MassTransit.JobService /// /// A JobHandle contains the JobContext, Task, and provides access to the job control /// - public interface JobHandle + public interface JobHandle : + IAsyncDisposable { Guid JobId { get; } @@ -17,6 +19,6 @@ public interface JobHandle /// Cancel the job task /// /// - Task Cancel(); + Task Cancel(string? reason); } } diff --git a/src/MassTransit/JobService/JobService/JobMetadataCache.cs b/src/MassTransit/JobService/JobService/JobMetadataCache.cs index 234920ef1f8..91d9f4e8664 100644 --- a/src/MassTransit/JobService/JobService/JobMetadataCache.cs +++ b/src/MassTransit/JobService/JobService/JobMetadataCache.cs @@ -10,11 +10,34 @@ public static class JobMetadataCache where TJob : class { public static Guid GenerateJobTypeId(string queueName) + { + var key = GenerateJobTypeName(queueName); + + using var hasher = MD5.Create(); + + var data = hasher.ComputeHash(Encoding.UTF8.GetBytes(key)); + + return new Guid(data); + } + + public static string GenerateJobTypeName(string queueName) { var consumerTypeName = TypeCache.ShortName; var jobTypeName = TypeCache.ShortName; - var key = $"{consumerTypeName}:{jobTypeName}:{queueName}"; + var name = $"{consumerTypeName}:{jobTypeName}:{queueName}"; + + return name; + } + } + + + public static class JobMetadataCache + where TJob : class + { + public static Guid GenerateRecurringJobId(string jobName) + { + var key = GenerateJobTypeName(jobName); using var hasher = MD5.Create(); @@ -22,5 +45,14 @@ public static Guid GenerateJobTypeId(string queueName) return new Guid(data); } + + public static string GenerateJobTypeName(string jobName) + { + var jobTypeName = TypeCache.ShortName; + + var name = $"{jobTypeName}:{jobName}"; + + return name; + } } } diff --git a/src/MassTransit/JobService/JobService/JobProgressBuffer.cs b/src/MassTransit/JobService/JobService/JobProgressBuffer.cs new file mode 100644 index 00000000000..0918a87443f --- /dev/null +++ b/src/MassTransit/JobService/JobService/JobProgressBuffer.cs @@ -0,0 +1,141 @@ +#nullable enable +namespace MassTransit.JobService; + +using System; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using Messages; + + +public class JobProgressBuffer +{ + readonly Channel _channel; + readonly INotifyJobContext _notifyJobContext; + readonly ProgressBufferSettings _settings; + + readonly Task _updateTask; + long _latestSequenceNumber; + + public JobProgressBuffer(INotifyJobContext notifyJobContext, ProgressBufferSettings? settings = null) + { + _notifyJobContext = notifyJobContext; + _settings = settings ?? new ProgressBufferSettings(); + + var channelOptions = new BoundedChannelOptions(_settings.UpdateLimit) + { + AllowSynchronousContinuations = false, + FullMode = BoundedChannelFullMode.Wait, + SingleReader = true, + SingleWriter = false + }; + + _channel = Channel.CreateBounded(channelOptions); + _updateTask = Task.Run(WaitForUpdate); + } + + public Task Flush() + { + _channel.Writer.TryComplete(); + + return _updateTask; + } + + public async Task Update(ProgressUpdate progress, CancellationToken cancellationToken) + { + await _channel.Writer.WriteAsync(progress, cancellationToken).ConfigureAwait(false); + } + + async Task WaitForUpdate() + { + try + { + while (await _channel.Reader.WaitToReadAsync().ConfigureAwait(false)) + await ReadUpdate().ConfigureAwait(false); + } + catch (ChannelClosedException) + { + } + catch (Exception exception) + { + LogContext.Error?.Log(exception, "WaitForUpdate Faulted"); + } + } + + async Task ReadUpdate() + { + var updateToken = new CancellationTokenSource(_settings.TimeLimit); + + try + { + ProgressUpdate? latestUpdate = null; + try + { + var updateId = 0; + + while (updateId < _settings.UpdateLimit) + { + if (_channel.Reader.TryRead(out var update)) + { + latestUpdate = update; + updateId++; + } + else if (await _channel.Reader.WaitToReadAsync(updateToken.Token).ConfigureAwait(false) == false) + break; + } + } + catch (OperationCanceledException exception) when (exception.CancellationToken == updateToken.Token && latestUpdate != null) + { + } + + if (latestUpdate.HasValue) + { + try + { + await _notifyJobContext.NotifyJobProgress(new SetJobProgressCommand + { + JobId = latestUpdate.Value.JobId, + AttemptId = latestUpdate.Value.AttemptId, + SequenceNumber = ++_latestSequenceNumber, + Value = latestUpdate.Value.Value, + Limit = latestUpdate.Value.Limit + }).ConfigureAwait(false); + } + catch (Exception exception) + { + LogContext.Error?.Log(exception, "Unable to update job progress: {JobId} {AttemptId} {SequenceNumber} {Value} {Limit}", + latestUpdate?.JobId, latestUpdate?.AttemptId, _latestSequenceNumber, latestUpdate?.Value, latestUpdate?.Limit); + } + } + } + catch (OperationCanceledException exception) when (exception.CancellationToken == updateToken.Token) + { + LogContext.Debug?.Log("operation canceled exception"); + } + catch (Exception exception) + { + LogContext.Error?.Log(exception, "ReadUpdate faulted"); + } + finally + { + updateToken.Dispose(); + } + } + + + public readonly struct ProgressUpdate + { + public readonly Guid JobId; + public readonly Guid AttemptId; + public readonly long Value; + public readonly long? Limit; + + public ProgressUpdate(Guid jobId, Guid attemptId, long value, long? limit) + { + JobId = jobId; + AttemptId = attemptId; + Value = value; + Limit = limit; + } + } +} diff --git a/src/MassTransit/JobService/JobService/JobService.cs b/src/MassTransit/JobService/JobService/JobService.cs index dde08423dae..c593311b6b3 100644 --- a/src/MassTransit/JobService/JobService/JobService.cs +++ b/src/MassTransit/JobService/JobService/JobService.cs @@ -1,99 +1,96 @@ -namespace MassTransit.JobService +namespace MassTransit.JobService; + +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Configuration; +using Consumer; +using Contracts.JobService; +using Messages; +using Middleware; + + +public class JobService : + IJobService { - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Configuration; - using Consumer; - using Contracts.JobService; - - - public class JobService : - IJobService + readonly ConcurrentDictionary _jobs; + readonly Dictionary _jobTypes; + Timer _heartbeat; + + public JobService(JobServiceSettings settings) { - readonly ConcurrentDictionary _jobs; - readonly Dictionary _jobTypes; - readonly JobServiceOptions _options; - Timer _heartbeat; + Settings = settings; - public JobService(IServiceInstanceConfigurator configurator, JobServiceOptions options) - { - _options = options; - InstanceAddress = configurator.InstanceAddress; + _jobTypes = new Dictionary(); + _jobs = new ConcurrentDictionary(); + } - _jobTypes = new Dictionary(); - _jobs = new ConcurrentDictionary(); + public JobServiceSettings Settings { get; } - ConfigureSuperviseJobConsumer(configurator.InstanceEndpointConfigurator); - } + public Uri InstanceAddress => Settings.InstanceAddress; - public bool TryGetJob(Guid jobId, out JobHandle jobReference) - { - return _jobs.TryGetValue(jobId, out jobReference); - } + public bool TryGetJob(Guid jobId, out JobHandle jobReference) + { + return _jobs.TryGetValue(jobId, out jobReference); + } - public bool TryRemoveJob(Guid jobId, out JobHandle jobHandle) + public bool TryRemoveJob(Guid jobId, out JobHandle jobHandle) + { + var removed = _jobs.TryRemove(jobId, out jobHandle); + if (removed) { - var removed = _jobs.TryRemove(jobId, out jobHandle); - if (removed) - { - LogContext.Debug?.Log("Removed job: {JobId} ({Status})", jobId, jobHandle.JobTask.Status); + LogContext.Debug?.Log("Removed job: {JobId} ({Status})", jobId, jobHandle.JobTask.Status); - return true; - } - - return false; + return true; } - public Uri InstanceAddress { get; } + return false; + } - public async Task StartJob(ConsumeContext context, T job, IPipe> jobPipe, TimeSpan timeout) - where T : class - { - var startJob = context.Message; + public async Task StartJob(ConsumeContext context, T job, IPipe> jobPipe, JobOptions jobOptions) + where T : class + { + var startJob = context.Message; - if (_jobs.ContainsKey(startJob.JobId)) - throw new JobAlreadyExistsException(startJob.JobId); + if (_jobs.ContainsKey(startJob.JobId)) + throw new JobAlreadyExistsException(startJob.JobId); - var jobContext = new ConsumeJobContext(context, InstanceAddress, startJob.JobId, startJob.AttemptId, startJob.RetryAttempt, job, timeout); + var jobContext = new ConsumeJobContext(context, InstanceAddress, job, jobOptions); - LogContext.Debug?.Log("Executing job: {JobType} {JobId} ({RetryAttempt})", TypeCache.ShortName, startJob.JobId, - startJob.RetryAttempt); + LogContext.Debug?.Log("Executing job: {JobType} {JobId} ({RetryAttempt})", TypeCache.ShortName, startJob.JobId, + startJob.RetryAttempt); - var jobTask = jobPipe.Send(jobContext); + var jobTask = jobPipe.Send(jobContext); - var jobHandle = new ConsumerJobHandle(jobContext, jobTask); + var jobHandle = new ConsumerJobHandle(jobContext, jobTask, jobOptions.JobCancellationTimeout); - Add(jobHandle); + Add(jobHandle); - return jobHandle; - } + return jobHandle; + } - public async Task Stop(IPublishEndpoint publishEndpoint) + public async Task Stop(IPublishEndpoint publishEndpoint) + { + if (_heartbeat != null) { - if (_heartbeat != null) - { - _heartbeat.Dispose(); - _heartbeat = null; - } + _heartbeat.Dispose(); + _heartbeat = null; + } - ICollection pendingJobs = _jobs.Values; + ICollection pendingJobs = _jobs.Values; - foreach (var jobHandle in pendingJobs) + foreach (var jobHandle in pendingJobs) + { + if (!jobHandle.JobTask.IsCompleted) { - if (jobHandle.JobTask.IsCompleted) - continue; - try { LogContext.Debug?.Log("Canceling job: {JobId}", jobHandle.JobId); - await jobHandle.Cancel().ConfigureAwait(false); - - TryRemoveJob(jobHandle.JobId, out _); + await jobHandle.Cancel(JobCancellationReasons.Shutdown).ConfigureAwait(false); } catch (Exception ex) { @@ -101,132 +98,138 @@ public async Task Stop(IPublishEndpoint publishEndpoint) } } - await Task.WhenAll(_jobTypes.Values.Select(x => x.PublishJobInstanceStopped(publishEndpoint, InstanceAddress))).ConfigureAwait(false); + if (TryRemoveJob(jobHandle.JobId, out _)) + await jobHandle.DisposeAsync().ConfigureAwait(false); } - public void RegisterJobType(IReceiveEndpointConfigurator configurator, JobOptions options, Guid jobTypeId) - where T : class - { - if (_jobTypes.ContainsKey(typeof(T))) - throw new ConfigurationException($"A job type can only be registered once per service instance: {TypeCache.ShortName}"); + await Task.WhenAll(_jobTypes.Values.Select(x => x.PublishJobInstanceStopped(publishEndpoint))).ConfigureAwait(false); + } - _jobTypes.Add(typeof(T), new JobTypeRegistration(configurator, options, jobTypeId)); - } + public void RegisterJobType(IReceiveEndpointConfigurator configurator, JobOptions options, Guid jobTypeId, string jobTypeName) + where T : class + { + if (_jobTypes.ContainsKey(typeof(T))) + throw new ConfigurationException($"A job type can only be registered once per service instance: {TypeCache.ShortName}"); - public async Task BusStarted(IPublishEndpoint publishEndpoint) - { - await Task.WhenAll(_jobTypes.Values.Select(x => x.PublishConcurrentJobLimit(publishEndpoint, InstanceAddress))).ConfigureAwait(false); + _jobTypes.Add(typeof(T), new JobTypeRegistration(options, InstanceAddress, jobTypeId, jobTypeName)); + } - void PublishHeartbeats(object state) + public async Task BusStarted(IPublishEndpoint publishEndpoint) + { + await Task.WhenAll(_jobTypes.Values.Select(x => x.PublishConcurrentJobLimit(publishEndpoint))).ConfigureAwait(false); + + void PublishHeartbeats(object state) + { + Task.Run(async () => { - Task.Run(async () => + try { - try - { - await Task.WhenAll(_jobTypes.Values.Select(x => x.PublishHeartbeat(publishEndpoint, InstanceAddress))).ConfigureAwait(false); - } - catch (Exception exception) - { - LogContext.Debug?.Log(exception, "Failed to publish heartbeat"); - } - }); - } - - _heartbeat = new Timer(PublishHeartbeats, null, _options.HeartbeatInterval, _options.HeartbeatInterval); + await Task.WhenAll(_jobTypes.Values.Select(x => x.PublishHeartbeat(publishEndpoint))).ConfigureAwait(false); + } + catch (Exception exception) + { + LogContext.Debug?.Log(exception, "Failed to publish heartbeat"); + } + }); } - public Guid GetJobTypeId() - where T : class - { - if (_jobTypes.TryGetValue(typeof(T), out var registration)) - return registration.JobTypeId; + _heartbeat = new Timer(PublishHeartbeats, null, Settings.HeartbeatInterval, Settings.HeartbeatInterval); + } - throw new ConfigurationException($"The job type was not registered: {TypeCache.ShortName}"); - } + public Guid GetJobTypeId() + where T : class + { + if (_jobTypes.TryGetValue(typeof(T), out var registration)) + return registration.JobTypeId; - void Add(JobHandle jobHandle) - { - if (!_jobs.TryAdd(jobHandle.JobId, jobHandle)) - throw new JobAlreadyExistsException(jobHandle.JobId); + throw new ConfigurationException($"The job type was not registered: {TypeCache.ShortName}"); + } - jobHandle.JobTask.ContinueWith(innerTask => - { - TryRemoveJob(jobHandle.JobId, out _); - }); - } + public void ConfigureSuperviseJobConsumer(IReceiveEndpointConfigurator configurator) + { + var partition = new Middleware.Partitioner(16, new Murmur3UnsafeHashGenerator()); + + configurator.UsePartitioner(partition, p => p.Message.JobId); + configurator.UsePartitioner(partition, p => p.Message.JobId); + + var consumerFactory = new DelegateConsumerFactory(() => new SuperviseJobConsumer(this)); - void ConfigureSuperviseJobConsumer(IReceiveEndpointConfigurator configurator) + var consumerConfigurator = new ConsumerConfigurator(consumerFactory, configurator); + + configurator.AddEndpointSpecification(consumerConfigurator); + } + + void Add(JobHandle jobHandle) + { + if (!_jobs.TryAdd(jobHandle.JobId, jobHandle)) + throw new JobAlreadyExistsException(jobHandle.JobId); + + jobHandle.JobTask.ContinueWith(async innerTask => { - var consumerFactory = new DelegateConsumerFactory(() => new SuperviseJobConsumer(this)); + if (TryRemoveJob(jobHandle.JobId, out _)) + await jobHandle.DisposeAsync().ConfigureAwait(false); + }); + } - var consumerConfigurator = new ConsumerConfigurator(consumerFactory, configurator); - configurator.AddEndpointSpecification(consumerConfigurator); - } + interface IJobTypeRegistration + { + Guid JobTypeId { get; } + Task PublishConcurrentJobLimit(IPublishEndpoint publishEndpoint); + Task PublishHeartbeat(IPublishEndpoint publishEndpoint); + Task PublishJobInstanceStopped(IPublishEndpoint publishEndpoint); + } - interface IJobTypeRegistration + class JobTypeRegistration : + IJobTypeRegistration + where T : class + { + readonly Uri _instanceAddress; + readonly JobOptions _options; + + public JobTypeRegistration(JobOptions options, Uri instanceAddress, Guid jobTypeId, string jobTypeName) { - Guid JobTypeId { get; } - Task PublishConcurrentJobLimit(IPublishEndpoint publishEndpoint, Uri instanceAddress); - Task PublishHeartbeat(IPublishEndpoint publishEndpoint, Uri instanceAddress); - Task PublishJobInstanceStopped(IPublishEndpoint publishEndpoint, Uri instanceAddress); + _options = options; + _instanceAddress = instanceAddress; + JobTypeId = jobTypeId; + JobTypeName = string.IsNullOrWhiteSpace(options.JobTypeName) ? jobTypeName : options.JobTypeName; } + string JobTypeName { get; } - class JobTypeRegistration : - IJobTypeRegistration - where T : class + public Task PublishConcurrentJobLimit(IPublishEndpoint publishEndpoint) { - readonly IReceiveEndpointConfigurator _configurator; - readonly JobOptions _options; + LogContext.Debug?.Log("Job Service type: {JobType}", TypeCache.ShortName); - public JobTypeRegistration(IReceiveEndpointConfigurator configurator, JobOptions options, Guid jobTypeId) - { - _configurator = configurator; - _options = options; - JobTypeId = jobTypeId; - } + return PublishSetConcurrentJobLimit(publishEndpoint, ConcurrentLimitKind.Configured); + } - public Task PublishConcurrentJobLimit(IPublishEndpoint publishEndpoint, Uri instanceAddress) - { - LogContext.Debug?.Log("Job Service type: {JobType}", TypeCache.ShortName); + public Task PublishHeartbeat(IPublishEndpoint publishEndpoint) + { + return PublishSetConcurrentJobLimit(publishEndpoint, ConcurrentLimitKind.Heartbeat); + } - return publishEndpoint.Publish(new - { - JobTypeId, - instanceAddress, - ServiceAddress = _configurator.InputAddress, - _options.ConcurrentJobLimit, - Kind = ConcurrentLimitKind.Configured - }); - } + public Task PublishJobInstanceStopped(IPublishEndpoint publishEndpoint) + { + return PublishSetConcurrentJobLimit(publishEndpoint, ConcurrentLimitKind.Stopped); + } - public Task PublishHeartbeat(IPublishEndpoint publishEndpoint, Uri instanceAddress) - { - return publishEndpoint.Publish(new - { - JobTypeId, - instanceAddress, - ServiceAddress = _configurator.InputAddress, - _options.ConcurrentJobLimit, - Kind = ConcurrentLimitKind.Heartbeat - }); - } + public Guid JobTypeId { get; } - public Task PublishJobInstanceStopped(IPublishEndpoint publishEndpoint, Uri instanceAddress) + Task PublishSetConcurrentJobLimit(IPublishEndpoint publishEndpoint, ConcurrentLimitKind kind) + { + return publishEndpoint.Publish(new SetConcurrentJobLimitCommand { - return publishEndpoint.Publish(new - { - JobTypeId, - instanceAddress, - ServiceAddress = _configurator.InputAddress, - _options.ConcurrentJobLimit, - Kind = ConcurrentLimitKind.Stopped - }); - } - - public Guid JobTypeId { get; } + JobTypeId = JobTypeId, + JobTypeName = JobTypeName, + InstanceAddress = _instanceAddress, + ConcurrentJobLimit = _options.ConcurrentJobLimit, + Kind = kind, + JobTypeProperties = _options.JobTypeProperties.Properties, + InstanceProperties = _options.InstanceProperties.Properties, + GlobalConcurrentJobLimit = _options.GlobalConcurrentJobLimit + }); } } } diff --git a/src/MassTransit/JobService/JobService/JobServiceBusObserver.cs b/src/MassTransit/JobService/JobService/JobServiceBusObserver.cs index 56037c7da4e..415c8f53f55 100644 --- a/src/MassTransit/JobService/JobService/JobServiceBusObserver.cs +++ b/src/MassTransit/JobService/JobService/JobServiceBusObserver.cs @@ -29,10 +29,10 @@ public Task PreStart(IBus bus) public async Task PostStart(IBus bus, Task busReady) { - LogContext.Debug?.Log("Job Service starting: {InstanceAddress}", _jobService.InstanceAddress); - await busReady.ConfigureAwait(false); + LogContext.Debug?.Log("Job Service starting: {InstanceAddress}", _jobService.InstanceAddress); + await _jobService.BusStarted(bus).ConfigureAwait(false); LogContext.Info?.Log("Job Service started: {InstanceAddress}", _jobService.InstanceAddress); diff --git a/src/MassTransit/JobService/JobService/JobServiceSettings.cs b/src/MassTransit/JobService/JobService/JobServiceSettings.cs new file mode 100644 index 00000000000..833ad1bced3 --- /dev/null +++ b/src/MassTransit/JobService/JobService/JobServiceSettings.cs @@ -0,0 +1,22 @@ +#nullable enable +namespace MassTransit.JobService +{ + using System; + using Configuration; + + + /// + /// Settings relevant to the job consumer endpoints and the service instance + /// + public interface JobServiceSettings : + IOptions + { + IJobService JobService { get; } + + TimeSpan HeartbeatInterval { get; } + + Uri? InstanceAddress { get; } + + IReceiveEndpointConfigurator? InstanceEndpointConfigurator { get; } + } +} diff --git a/src/MassTransit/JobService/JobService/Messages/AllocateJobSlotCommand.cs b/src/MassTransit/JobService/JobService/Messages/AllocateJobSlotCommand.cs new file mode 100644 index 00000000000..659df0c2fab --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/AllocateJobSlotCommand.cs @@ -0,0 +1,16 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class AllocateJobSlotCommand : + AllocateJobSlot +{ + public Guid JobTypeId { get; set; } + public TimeSpan JobTimeout { get; set; } + public Guid JobId { get; set; } + public Dictionary? JobProperties { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/CancelJobAttemptCommand.cs b/src/MassTransit/JobService/JobService/Messages/CancelJobAttemptCommand.cs new file mode 100644 index 00000000000..4fc0544e9e5 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/CancelJobAttemptCommand.cs @@ -0,0 +1,14 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class CancelJobAttemptCommand : + CancelJobAttempt +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public string? Reason { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/CancelJobCommand.cs b/src/MassTransit/JobService/JobService/Messages/CancelJobCommand.cs new file mode 100644 index 00000000000..1b8fbc357f4 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/CancelJobCommand.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class CancelJobCommand : + CancelJob +{ + public Guid JobId { get; set; } + public string? Reason { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/CompleteJobCommand.cs b/src/MassTransit/JobService/JobService/Messages/CompleteJobCommand.cs new file mode 100644 index 00000000000..9bfedc14476 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/CompleteJobCommand.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class CompleteJobCommand : + CompleteJob +{ + public Guid JobId { get; set; } + public DateTime Timestamp { get; set; } + public TimeSpan Duration { get; set; } + public Dictionary Job { get; set; } = null!; + public Dictionary Result { get; set; } = null!; + public Guid JobTypeId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/FaultJobCommand.cs b/src/MassTransit/JobService/JobService/Messages/FaultJobCommand.cs new file mode 100644 index 00000000000..6d98dbf6673 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/FaultJobCommand.cs @@ -0,0 +1,19 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class FaultJobCommand : + FaultJob +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public int RetryAttempt { get; set; } + public TimeSpan? Duration { get; set; } + public ExceptionInfo Exceptions { get; set; } = null!; + public Dictionary Job { get; set; } = null!; + public Guid JobTypeId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/FinalizeJobAttemptCommand.cs b/src/MassTransit/JobService/JobService/Messages/FinalizeJobAttemptCommand.cs new file mode 100644 index 00000000000..d9ecc72431b --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/FinalizeJobAttemptCommand.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class FinalizeJobAttemptCommand : + FinalizeJobAttempt +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/FinalizeJobCommand.cs b/src/MassTransit/JobService/JobService/Messages/FinalizeJobCommand.cs new file mode 100644 index 00000000000..a6782f45b29 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/FinalizeJobCommand.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class FinalizeJobCommand : + FinalizeJob +{ + public Guid JobId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/GetJobAttemptStatusRequest.cs b/src/MassTransit/JobService/JobService/Messages/GetJobAttemptStatusRequest.cs new file mode 100644 index 00000000000..eb2ee35503b --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/GetJobAttemptStatusRequest.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class GetJobAttemptStatusRequest : + GetJobAttemptStatus +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/GetJobStateRequest.cs b/src/MassTransit/JobService/JobService/Messages/GetJobStateRequest.cs new file mode 100644 index 00000000000..c00bd9f259e --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/GetJobStateRequest.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class GetJobStateRequest : + GetJobState +{ + public Guid JobId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobAttemptCanceledEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobAttemptCanceledEvent.cs new file mode 100644 index 00000000000..265b9a68b3f --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobAttemptCanceledEvent.cs @@ -0,0 +1,15 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobAttemptCanceledEvent : + JobAttemptCanceled +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public DateTime Timestamp { get; set; } + public string Reason { get; set; } = null!; +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobAttemptCompletedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobAttemptCompletedEvent.cs new file mode 100644 index 00000000000..e2b72263429 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobAttemptCompletedEvent.cs @@ -0,0 +1,16 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobAttemptCompletedEvent : + JobAttemptCompleted +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public int RetryAttempt { get; set; } + public DateTime Timestamp { get; set; } + public TimeSpan Duration { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobAttemptFaultedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobAttemptFaultedEvent.cs new file mode 100644 index 00000000000..75dfb3d0cfc --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobAttemptFaultedEvent.cs @@ -0,0 +1,17 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobAttemptFaultedEvent : + JobAttemptFaulted +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public int RetryAttempt { get; set; } + public TimeSpan? RetryDelay { get; set; } + public DateTime Timestamp { get; set; } + public ExceptionInfo Exceptions { get; set; } = null!; +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobAttemptStartedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobAttemptStartedEvent.cs new file mode 100644 index 00000000000..e5f12c2f7a0 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobAttemptStartedEvent.cs @@ -0,0 +1,16 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobAttemptStartedEvent : + JobAttemptStarted +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public int RetryAttempt { get; set; } + public DateTime Timestamp { get; set; } + public Uri InstanceAddress { get; set; } = null!; +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobAttemptStatusResponse.cs b/src/MassTransit/JobService/JobService/Messages/JobAttemptStatusResponse.cs new file mode 100644 index 00000000000..7375fe379c8 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobAttemptStatusResponse.cs @@ -0,0 +1,15 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobAttemptStatusResponse : + JobAttemptStatus +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public DateTime Timestamp { get; set; } + public JobStatus Status { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobCanceledEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobCanceledEvent.cs new file mode 100644 index 00000000000..26f0edc781b --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobCanceledEvent.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobCanceledEvent : + JobCanceled +{ + public Guid JobId { get; set; } + public DateTime Timestamp { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobCompletedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobCompletedEvent.cs new file mode 100644 index 00000000000..f2b9a32d8b9 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobCompletedEvent.cs @@ -0,0 +1,29 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class JobCompletedEvent : + JobCompleted +{ + public Guid JobId { get; set; } + public DateTime Timestamp { get; set; } + public TimeSpan Duration { get; set; } + public Dictionary Job { get; set; } = null!; + public Dictionary Result { get; set; } = null!; +} + + +public class JobCompletedEvent : + JobCompleted + where T : class +{ + public Guid JobId { get; set; } + public DateTime Timestamp { get; set; } + public TimeSpan Duration { get; set; } + public T Job { get; set; } = null!; + public Dictionary Result { get; set; } = null!; +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobFaultedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobFaultedEvent.cs new file mode 100644 index 00000000000..b06be32b240 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobFaultedEvent.cs @@ -0,0 +1,17 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class JobFaultedEvent : + JobFaulted +{ + public Guid JobId { get; set; } + public DateTime Timestamp { get; set; } + public TimeSpan? Duration { get; set; } + public Dictionary Job { get; set; } = null!; + public ExceptionInfo Exceptions { get; set; } = null!; +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobRetryDelayElapsedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobRetryDelayElapsedEvent.cs new file mode 100644 index 00000000000..c6e8708a780 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobRetryDelayElapsedEvent.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobRetryDelayElapsedEvent : + JobRetryDelayElapsed +{ + public Guid JobId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobSlotAllocatedResponse.cs b/src/MassTransit/JobService/JobService/Messages/JobSlotAllocatedResponse.cs new file mode 100644 index 00000000000..b108c639706 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobSlotAllocatedResponse.cs @@ -0,0 +1,13 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobSlotAllocatedResponse : + JobSlotAllocated +{ + public Guid JobId { get; set; } + public Uri InstanceAddress { get; set; } = null!; +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobSlotReleasedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobSlotReleasedEvent.cs new file mode 100644 index 00000000000..38669373f80 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobSlotReleasedEvent.cs @@ -0,0 +1,14 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobSlotReleasedEvent : + JobSlotReleased +{ + public Guid JobTypeId { get; set; } + public Guid JobId { get; set; } + public JobSlotDisposition Disposition { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobSlotUnavailableResponse.cs b/src/MassTransit/JobService/JobService/Messages/JobSlotUnavailableResponse.cs new file mode 100644 index 00000000000..f8a7ca4938c --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobSlotUnavailableResponse.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobSlotUnavailableResponse : + JobSlotUnavailable +{ + public Guid JobId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobSlotWaitElapsedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobSlotWaitElapsedEvent.cs new file mode 100644 index 00000000000..eac67a69845 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobSlotWaitElapsedEvent.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobSlotWaitElapsedEvent : + JobSlotWaitElapsed +{ + public Guid JobId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobStartedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobStartedEvent.cs new file mode 100644 index 00000000000..6d550178d41 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobStartedEvent.cs @@ -0,0 +1,26 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobStartedEvent : + JobStarted +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public int RetryAttempt { get; set; } + public DateTime Timestamp { get; set; } +} + + +public class JobStartedEvent : + JobStarted + where T : class +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public int RetryAttempt { get; set; } + public DateTime Timestamp { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobStateResponse.cs b/src/MassTransit/JobService/JobService/Messages/JobStateResponse.cs new file mode 100644 index 00000000000..a7761f268b1 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobStateResponse.cs @@ -0,0 +1,61 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class JobStateResponse : + JobState +{ + public Guid JobId { get; set; } + public DateTime? Submitted { get; set; } + public DateTime? Started { get; set; } + public DateTime? Completed { get; set; } + public TimeSpan? Duration { get; set; } + public DateTime? Faulted { get; set; } + public string? Reason { get; set; } + public int LastRetryAttempt { get; set; } + public string CurrentState { get; set; } = null!; + public long? ProgressValue { get; set; } + public long? ProgressLimit { get; set; } + public Dictionary? JobState { get; set; } + public DateTime? NextStartDate { get; set; } + public bool IsRecurring { get; set; } + public DateTime? StartDate { get; set; } + public DateTime? EndDate { get; set; } +} + + +public class JobStateResponse : + JobState + where T : class +{ + readonly JobState _jobState; + readonly T? _jobStateOfT; + + public JobStateResponse(JobState jobState, T? jobStateOfT = null) + { + _jobState = jobState; + _jobStateOfT = jobStateOfT; + } + + public Guid JobId => _jobState.JobId; + public DateTime? Submitted => _jobState.Submitted; + public DateTime? Started => _jobState.Started; + public DateTime? Completed => _jobState.Completed; + public TimeSpan? Duration => _jobState.Duration; + public DateTime? Faulted => _jobState.Faulted; + public string? Reason => _jobState.Reason; + public int LastRetryAttempt => _jobState.LastRetryAttempt; + public string CurrentState => _jobState.CurrentState; + public long? ProgressValue => _jobState.ProgressValue; + public long? ProgressLimit => _jobState.ProgressLimit; + public Dictionary? JobState => _jobState.JobState; + public DateTime? NextStartDate => _jobState.NextStartDate; + public bool IsRecurring => _jobState.IsRecurring; + public DateTime? StartDate => _jobState.StartDate; + public DateTime? EndDate => _jobState.EndDate; + T? JobState.JobState => _jobStateOfT; +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobStatusCheckRequestedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobStatusCheckRequestedEvent.cs new file mode 100644 index 00000000000..adbcc54fd30 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobStatusCheckRequestedEvent.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobStatusCheckRequestedEvent : + JobStatusCheckRequested +{ + public Guid AttemptId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobSubmissionAcceptedResponse.cs b/src/MassTransit/JobService/JobService/Messages/JobSubmissionAcceptedResponse.cs new file mode 100644 index 00000000000..a8a5443d03c --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobSubmissionAcceptedResponse.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class JobSubmissionAcceptedResponse : + JobSubmissionAccepted +{ + public Guid JobId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/JobSubmittedEvent.cs b/src/MassTransit/JobService/JobService/Messages/JobSubmittedEvent.cs new file mode 100644 index 00000000000..f3069be5ad0 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/JobSubmittedEvent.cs @@ -0,0 +1,19 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class JobSubmittedEvent : + JobSubmitted +{ + public Guid JobId { get; set; } + public Guid JobTypeId { get; set; } + public DateTime Timestamp { get; set; } + public TimeSpan JobTimeout { get; set; } + public Dictionary Job { get; set; } = null!; + public Dictionary? JobProperties { get; set; } + public RecurringJobSchedule? Schedule { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/RecurringJobScheduleInfo.cs b/src/MassTransit/JobService/JobService/Messages/RecurringJobScheduleInfo.cs new file mode 100644 index 00000000000..7582e36b2da --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/RecurringJobScheduleInfo.cs @@ -0,0 +1,46 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; +using Scheduling; + + +public class RecurringJobScheduleInfo : + RecurringJobSchedule, + IRecurringJobScheduleConfigurator, + ISpecification +{ + public IEnumerable Validate() + { + var hasCronExpression = !string.IsNullOrWhiteSpace(CronExpression); + + if (!hasCronExpression && Start.HasValue == false) + yield return this.Failure("CronExpression", "must be specified"); + + if (Start.HasValue && End.HasValue && Start.Value > End.Value) + yield return this.Failure("Start", "must be <= End"); + + if (!hasCronExpression) + yield break; + + ValidationResult? failure = null; + try + { + _ = new CronExpression(CronExpression); + } + catch (FormatException exception) + { + failure = this.Failure("CronExpression", $"Is invalid: {exception.Message}"); + } + + if (failure != null) + yield return failure; + } + + public string? CronExpression { get; set; } + public string? TimeZoneId { get; set; } + public DateTimeOffset? Start { get; set; } + public DateTimeOffset? End { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/RetryJobCommand.cs b/src/MassTransit/JobService/JobService/Messages/RetryJobCommand.cs new file mode 100644 index 00000000000..81830c43e4f --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/RetryJobCommand.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class RetryJobCommand : + RetryJob +{ + public Guid JobId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/RunJobCommand.cs b/src/MassTransit/JobService/JobService/Messages/RunJobCommand.cs new file mode 100644 index 00000000000..410a61cd066 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/RunJobCommand.cs @@ -0,0 +1,12 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class RunJobCommand : + RunJob +{ + public Guid JobId { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/SaveJobStateCommand.cs b/src/MassTransit/JobService/JobService/Messages/SaveJobStateCommand.cs new file mode 100644 index 00000000000..d4b7880610e --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/SaveJobStateCommand.cs @@ -0,0 +1,15 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class SaveJobStateCommand : + SaveJobState +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public Dictionary? JobState { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/SetConcurrentJobLimitCommand.cs b/src/MassTransit/JobService/JobService/Messages/SetConcurrentJobLimitCommand.cs new file mode 100644 index 00000000000..63eb5b7c115 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/SetConcurrentJobLimitCommand.cs @@ -0,0 +1,21 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class SetConcurrentJobLimitCommand : + SetConcurrentJobLimit +{ + public Guid JobTypeId { get; set; } + public Uri InstanceAddress { get; set; } = null!; + public int ConcurrentJobLimit { get; set; } + public ConcurrentLimitKind Kind { get; set; } + public TimeSpan? Duration { get; set; } + public string? JobTypeName { get; set; } + public Dictionary? JobTypeProperties { get; set; } + public Dictionary? InstanceProperties { get; set; } + public int? GlobalConcurrentJobLimit { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/SetJobProgressCommand.cs b/src/MassTransit/JobService/JobService/Messages/SetJobProgressCommand.cs new file mode 100644 index 00000000000..c5873603491 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/SetJobProgressCommand.cs @@ -0,0 +1,16 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using Contracts.JobService; + + +public class SetJobProgressCommand : + SetJobProgress +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public long SequenceNumber { get; set; } + public long Value { get; set; } + public long? Limit { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/StartJobAttemptCommand.cs b/src/MassTransit/JobService/JobService/Messages/StartJobAttemptCommand.cs new file mode 100644 index 00000000000..71e7de19409 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/StartJobAttemptCommand.cs @@ -0,0 +1,23 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class StartJobAttemptCommand : + StartJobAttempt +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public int RetryAttempt { get; set; } + public Uri ServiceAddress { get; set; } = null!; + public Uri InstanceAddress { get; set; } = null!; + public Dictionary Job { get; set; } = null!; + public Guid JobTypeId { get; set; } + public long? LastProgressValue { get; set; } + public long? LastProgressLimit { get; set; } + public Dictionary? JobState { get; set; } = null!; + public Dictionary? JobProperties { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/StartJobCommand.cs b/src/MassTransit/JobService/JobService/Messages/StartJobCommand.cs new file mode 100644 index 00000000000..d88c5fde601 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/StartJobCommand.cs @@ -0,0 +1,21 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class StartJobCommand : + StartJob +{ + public Guid JobId { get; set; } + public Guid AttemptId { get; set; } + public int RetryAttempt { get; set; } + public Dictionary Job { get; set; } = null!; + public Guid JobTypeId { get; set; } + public long? LastProgressValue { get; set; } + public long? LastProgressLimit { get; set; } + public Dictionary? JobState { get; set; } + public Dictionary? JobProperties { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Messages/SubmitJobCommand.cs b/src/MassTransit/JobService/JobService/Messages/SubmitJobCommand.cs new file mode 100644 index 00000000000..1022706099a --- /dev/null +++ b/src/MassTransit/JobService/JobService/Messages/SubmitJobCommand.cs @@ -0,0 +1,17 @@ +#nullable enable +namespace MassTransit.JobService.Messages; + +using System; +using System.Collections.Generic; +using Contracts.JobService; + + +public class SubmitJobCommand : + SubmitJob + where T : class +{ + public Guid JobId { get; set; } + public T Job { get; set; } = null!; + public RecurringJobSchedule? Schedule { get; set; } + public Dictionary? Properties { get; set; } +} diff --git a/src/MassTransit/JobService/JobService/Scheduling/CronExpression.cs b/src/MassTransit/JobService/JobService/Scheduling/CronExpression.cs new file mode 100644 index 00000000000..762272ba807 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Scheduling/CronExpression.cs @@ -0,0 +1,1400 @@ +#nullable enable +namespace MassTransit.JobService.Scheduling; + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Internals; + + +public sealed class CronExpression : + IEquatable +{ + static readonly Regex _regex = new(@"^L(-\d{1,2})?(W(-\d{1,2})?)?$", RegexOptions.Compiled | RegexOptions.ExplicitCapture, TimeSpan.FromSeconds(5)); + static readonly Regex _offsetRegex = new("LW-(?[0-9]+)", RegexOptions.Compiled | RegexOptions.ExplicitCapture, TimeSpan.FromSeconds(5)); + + readonly CronField _daysOfMonth = []; + readonly CronField _daysOfWeek = []; + readonly CronField _hours = []; + readonly CronField _minutes = []; + readonly CronField _months = []; + readonly CronField _seconds = []; + readonly CronField _years = []; + + bool _calendarDayOfMonth; + bool _calendarDayOfWeek; + int _everyNthWeek; + int _lastDayOffset; + bool _lastDayOfMonth; + bool _lastDayOfWeek; + int _lastWeekdayOffset; + bool _nearestWeekday; + int _nthDayOfWeek; + TimeZoneInfo? _timeZone; + + static CronExpression() + { + } + + public CronExpression(string? cronExpression) + { + if (cronExpression is null) + throw new ArgumentNullException(nameof(cronExpression)); + + CronExpressionString = CultureInfo.InvariantCulture.TextInfo.ToUpper(cronExpression).Trim(); + + BuildExpression(CronExpressionString); + } + + public TimeZoneInfo TimeZone + { + set => _timeZone = value; + get => _timeZone ??= TimeZoneInfo.Local; + } + + string CronExpressionString { get; } + + public bool Equals(CronExpression? other) + { + if (other is null) + return false; + if (ReferenceEquals(this, other)) + return true; + return Equals(_timeZone, other._timeZone) && CronExpressionString == other.CronExpressionString; + } + + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || (obj is CronExpression other && Equals(other)); + } + + public override int GetHashCode() + { + unchecked + { + return ((_timeZone != null ? _timeZone.GetHashCode() : 0) * 397) ^ CronExpressionString.GetHashCode(); + } + } + + public static bool operator ==(CronExpression? left, CronExpression? right) + { + return Equals(left, right); + } + + public static bool operator !=(CronExpression? left, CronExpression? right) + { + return !Equals(left, right); + } + + public bool IsSatisfiedBy(DateTimeOffset date) + { + var withoutMilliseconds = new DateTimeOffset(date.Year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Offset); + var test = withoutMilliseconds.AddSeconds(-1); + DateTimeOffset? timeAfter = GetTimeAfter(test); + + return timeAfter.HasValue && timeAfter.Value.Equals(withoutMilliseconds); + } + + public DateTimeOffset? GetNextValidTimeAfter(DateTimeOffset date) + { + return GetTimeAfter(date); + } + + public override string ToString() + { + return CronExpressionString; + } + + public static bool IsValidExpression(string cronExpression) + { + try + { + _ = new CronExpression(cronExpression); + } + catch (FormatException) + { + return false; + } + + return true; + } + + public static void ValidateExpression(string cronExpression) + { + _ = new CronExpression(cronExpression); + } + + void BuildExpression(string expression) + { + try + { + ClearExpressionFields(); + + var index = CronExpressionConstants.Second; + + foreach ((ReadOnlySpan expr, ReadOnlySpan _) in expression.SpanSplit(' ', '\t')) + { + if (index > CronExpressionConstants.Year) + break; + + if (index == CronExpressionConstants.DayOfMonth) + { + if (expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(',') >= 0 && expr.Slice(expr.IndexOf('L') + 1).IndexOf('L') != -1) + throw new FormatException("Support for specifying 'L' with other days of the month is limited to one instance of L"); + } + + if (index == CronExpressionConstants.DayOfWeek && expr.IndexOf('L') != -1 && expr.Length > 1 && expr.IndexOf(',') >= 0) + throw new FormatException("Support for specifying 'L' with other days of the week is not implemented"); + + if (index == CronExpressionConstants.DayOfWeek && expr.IndexOf('#') != -1 && expr.Slice(expr.IndexOf('#') + 1 + 1).IndexOf('#') != -1) + throw new FormatException("Support for specifying multiple \"nth\" days is not implemented."); + + if (expr.IndexOf(',') != -1) + { + foreach (var v in expr.SpanSplit(',')) + StoreExpressionValues(0, v, index); + } + else + StoreExpressionValues(0, expr, index); + + index++; + } + + if (index <= CronExpressionConstants.DayOfWeek) + throw new FormatException("Unexpected end of expression."); + + if (index <= CronExpressionConstants.Year) + StoreExpressionValues(0, "*".AsSpan(), CronExpressionConstants.Year); + } + catch (FormatException) + { + throw; + } + catch (Exception e) + { + throw new FormatException($"Illegal cron expression format ({e.Message})", e); + } + } + + void ClearExpressionFields() + { + _seconds.Clear(); + _minutes.Clear(); + _hours.Clear(); + _daysOfMonth.Clear(); + _months.Clear(); + _daysOfWeek.Clear(); + _years.Clear(); + } + + void StoreExpressionQuestionMark(int type, ReadOnlySpan span, int index) + { + index++; + if (index + 1 <= span.Length && !char.IsWhiteSpace(span[index])) + throw new FormatException("Illegal character after '?': " + span[index]); + + if (type != CronExpressionConstants.DayOfWeek && type != CronExpressionConstants.DayOfMonth) + throw new FormatException("'?' can only be specified for Day-of-Month or Day-of-Week."); + + if (type == CronExpressionConstants.DayOfWeek && !_lastDayOfMonth) + { + var val = _daysOfMonth.LastOrDefault(); + if (val == CronExpressionConstants.NoSpec) + throw new FormatException("'?' can only be specified for Day-of-Month -OR- Day-of-Week."); + } + + AddToSet(CronExpressionConstants.NoSpec, -1, 0, type); + } + + void StoreExpressionStarOrSlash(int type, ReadOnlySpan span, int index) + { + var ch = span[index]; + var incr = 0; + var startsWithAsterisk = ch == '*'; + if (startsWithAsterisk && index + 1 >= span.Length) + { + AddToSet(CronExpressionConstants.AllSpec, -1, incr, type); + return; + } + + if (ch == '/' && (index + 1 >= span.Length || char.IsWhiteSpace(span[index + 1]))) + throw new FormatException("'/' must be followed by an integer."); + + if (startsWithAsterisk) + index++; + + ch = span[index]; + if (ch == '/') + { + index++; + if (index >= span.Length) + throw new FormatException("Unexpected end of string."); + + incr = GetNumericValue(span, index); + CheckIncrementRange(incr, type); + } + else + { + if (startsWithAsterisk) + throw new FormatException("Illegal characters after asterisk: " + span.ToString()); + + incr = 1; + } + + AddToSet(CronExpressionConstants.AllSpec, -1, incr, type); + } + + void StoreExpressionL(int type, ReadOnlySpan span, int index) + { + index++; + switch (type) + { + case CronExpressionConstants.DayOfMonth: + { + _lastDayOfMonth = true; + if (span.Length > index) + { + var ch = span[index]; + if (ch == '-') + { + (_lastDayOffset, index) = GetValue(0, span, index + 1); + if (_lastDayOffset > 30) + throw new FormatException("Offset from last day must be <= 30"); + } + + if (span.Length > index) + { + ch = span[index]; + if (ch == 'W') + _nearestWeekday = true; + + var match = _offsetRegex.Match(span.ToString()); + if (match.Success) + { + var offSetGroup = match.Groups["offset"]; + if (offSetGroup.Success) + _lastWeekdayOffset = int.Parse(offSetGroup.Value); + } + } + } + + break; + } + + case CronExpressionConstants.DayOfWeek: + AddToSet(7, 7, 0, type); + break; + + default: + throw new FormatException($"'L' option is not valid here. (pos={index})"); + } + } + + void StoreExpressionNumeric(int type, ReadOnlySpan span, int index) + { + #if NET6_0_OR_GREATER + if (int.TryParse(span, out var temp)) + #else + if (int.TryParse(span.ToString(), out var temp)) + #endif + { + AddToSet(temp, -1, -1, type); + return; + } + + var ch = span[index]; + var value = ToInt32(ch); + index++; + if (index >= span.Length) + AddToSet(value, -1, -1, type); + else + { + ch = span[index]; + if (char.IsDigit(ch)) + (value, index) = GetValue(value, span, index); + + CheckNext(index, span, value, type); + } + } + + void StoreExpressionGeneralValue(int type, ReadOnlySpan span, int index) + { + var incr = 0; + ReadOnlySpan sub = span.Slice(index, 3); + int sval; + var eval = -1; + if (type == CronExpressionConstants.Month) + { + sval = GetMonthNumber(sub) + 1; + if (sval <= 0) + throw new FormatException($"Invalid Month value: '{sub.ToString()}'"); + + if (span.Length > index + 3) + { + if (span[index + 3] == '-') + { + index += 4; + sub = span.Slice(index, 3); + eval = GetMonthNumber(sub) + 1; + if (eval <= 0) + throw new FormatException($"Invalid Month value: '{sub.ToString()}'"); + } + } + } + else if (type == CronExpressionConstants.DayOfWeek) + { + sval = GetDayOfWeekNumber(sub); + if (sval < 0) + throw new FormatException($"Invalid Day-of-Week value: '{sub.ToString()}'"); + + if (span.Length > index + 3) + { + var c = span[index + 3]; + switch (c) + { + case '-': + index += 4; + sub = span.Slice(index, 3); + eval = GetDayOfWeekNumber(sub); + if (eval < 0) + throw new FormatException($"Invalid Day-of-Week value: '{sub.ToString()}'"); + + break; + case '#': + try + { + index += 4; + _nthDayOfWeek = ToInt32(span.Slice(index)); + if (_nthDayOfWeek is < 1 or > 5) + throw new FormatException("nthDayOfWeek is < 1 or > 5"); + } + catch (Exception) + { + throw new FormatException("A numeric value between 1 and 5 must follow the '#' option"); + } + + break; + case '/': + try + { + index += 4; + _everyNthWeek = ToInt32(span.Slice(index)); + if (_everyNthWeek is < 1 or > 5) + throw new FormatException("everyNthWeek is < 1 or > 5"); + } + catch (Exception) + { + throw new FormatException("A numeric value between 1 and 5 must follow the '/' option"); + } + + break; + case 'L': + _lastDayOfWeek = true; + break; + default: + throw new FormatException($"Illegal characters for this position: '{sub.ToString()}'"); + } + } + } + else + throw new FormatException($"Illegal characters for this position: '{sub.ToString()}'"); + + if (eval != -1) + incr = 1; + + AddToSet(sval, eval, incr, type); + } + + void StoreExpressionValues(int position, ReadOnlySpan span, int type) + { + var index = position; + if (index < span.Length && char.IsWhiteSpace(span[index])) + index = SkipWhiteSpace(position, span); + + if (index >= span.Length) + return; + + switch (span[index]) + { + case >= 'A' and <= 'Z' when !span.SequenceEqual("L".AsSpan()) && !_regex.IsMatch(span.ToString()): + StoreExpressionGeneralValue(type, span, index); + break; + + case '?': + StoreExpressionQuestionMark(type, span, index); + break; + + case '*': + case '/': + StoreExpressionStarOrSlash(type, span, index); + break; + + case 'L': + StoreExpressionL(type, span, index); + break; + + case >= '0' and <= '9': + StoreExpressionNumeric(type, span, index); + break; + default: + throw new FormatException($"Unexpected character: {span[index]}"); + } + } + + static void CheckIncrementRange(int increment, int type) + { + switch (type) + { + case CronExpressionConstants.Second or CronExpressionConstants.Minute when increment > 59: + throw new FormatException($"Increment > 59 : {increment}"); + case CronExpressionConstants.Hour when increment > 23: + throw new FormatException($"Increment > 23 : {increment}"); + case CronExpressionConstants.DayOfMonth when increment > 31: + throw new FormatException($"Increment > 31 : {increment}"); + case CronExpressionConstants.DayOfWeek when increment > 7: + throw new FormatException($"Increment > 7 : {increment}"); + case CronExpressionConstants.Month when increment > 12: + throw new FormatException($"Increment > 12 : {increment}"); + } + } + + void CheckNext(int position, ReadOnlySpan span, int value, int type) + { + if (position >= span.Length) + { + AddToSet(value, -1, -1, type); + return; + } + + switch (span[position]) + { + case 'L': + HandleLOption(value, type, position); + return; + + case 'W': + HandleWOption(value, type, position); + return; + + case '#': + HandleHashOption(span, value, type, position); + return; + + case 'C': + HandleCOption(value, type, position); + return; + + case '-': + HandleDashOption(span, value, type, position); + return; + + case '/': + HandleSlashOption(span, value, type, position, -1); + return; + + default: + AddToSet(value, -1, 0, type); + return; + } + } + + void HandleSlashOption(ReadOnlySpan span, int value, int type, int index, int end) + { + if (index + 1 >= span.Length || char.IsWhiteSpace(span[index + 1])) + throw new FormatException("\'/\' must be followed by an integer."); + + index++; + var ch = span[index]; + var charValue = ToInt32(ch); + index++; + if (index >= span.Length) + { + CheckIncrementRange(charValue, type); + AddToSet(value, end, charValue, type); + return; + } + + ch = span[index]; + if (char.IsDigit(ch)) + { + var (nextValue, _) = GetValue(charValue, span, index); + CheckIncrementRange(nextValue, type); + AddToSet(value, end, nextValue, type); + return; + } + + throw new FormatException($"Unexpected character '{ch}' after '/'"); + } + + void HandleDashOption(ReadOnlySpan span, int value, int type, int index) + { + index++; + var ch = span[index]; + var charValue = ToInt32(ch); + var end = charValue; + index++; + if (index >= span.Length) + { + AddToSet(value, end, 1, type); + return; + } + + ch = span[index]; + if (char.IsDigit(ch)) + (end, index) = GetValue(charValue, span, index); + + if (index < span.Length && span[index] == '/') + { + index++; + ch = span[index]; + var endValue = ToInt32(ch); + index++; + if (index >= span.Length) + { + AddToSet(value, end, endValue, type); + return; + } + + ch = span[index]; + if (char.IsDigit(ch)) + { + var (nextEndValue, _) = GetValue(endValue, span, index); + AddToSet(value, end, nextEndValue, type); + return; + } + + AddToSet(value, end, endValue, type); + return; + } + + AddToSet(value, end, 1, type); + } + + void HandleCOption(int value, int type, int index) + { + switch (type) + { + case CronExpressionConstants.DayOfWeek: + _calendarDayOfWeek = true; + break; + case CronExpressionConstants.DayOfMonth: + _calendarDayOfMonth = true; + break; + default: + throw new FormatException($"'C' option is not valid here. (pos={index})"); + } + + var data = GetSet(type); + data.Add(value); + } + + // ReSharper disable once RedundantAssignment + void HandleHashOption(ReadOnlySpan span, int value, int type, int index) + { + var pos = index; + if (type != CronExpressionConstants.DayOfWeek) + throw new FormatException($"'#' option is not valid here. (pos={index})"); + + index++; + try + { + _nthDayOfWeek = ToInt32(span.Slice(index)); + if (_nthDayOfWeek is < 1 or > 5) + throw new FormatException("nthDayOfWeek is < 1 or > 5"); + + #if NET6_0_OR_GREATER + if (int.TryParse(span.Slice(0, pos), out value)) + #else + if (int.TryParse(span.Slice(0, pos).ToString(), out value)) + #endif + { + if (value is < 1 or > 7) + throw new FormatException("Day-of-Week values must be between 1 and 7"); + } + } + catch (Exception) + { + throw new FormatException("A numeric value between 1 and 5 must follow the '#' option"); + } + + var set = GetSet(type); + set.Add(value); + } + + void HandleWOption(int value, int type, int index) + { + if (type == CronExpressionConstants.DayOfMonth) + _nearestWeekday = true; + else + throw new FormatException($"'W' option is not valid here. (pos={index})"); + + if (value > 31) + throw new FormatException("The 'W' option does not make sense with values larger than 31 (max number of days in a month)"); + + var data = GetSet(type); + data.Add(value); + } + + void HandleLOption(int value, int type, int position) + { + if (type == CronExpressionConstants.DayOfWeek) + { + if (value is < 1 or > 7) + throw new FormatException("Day-of-Week values must be between 1 and 7"); + + _lastDayOfWeek = true; + } + else + throw new FormatException($"'L' option is not valid here. (pos={position})"); + + var data = GetSet(type); + data.Add(value); + } + + public string GetExpressionSummary() + { + return new CronExpressionSummary( + _seconds, + _minutes, + _hours, + _daysOfMonth, + _months, + _daysOfWeek, + _lastDayOfWeek, + _nearestWeekday, + _nthDayOfWeek, + _lastDayOfMonth, + _calendarDayOfWeek, + _calendarDayOfMonth, + _years + ).ToString(); + } + + static int SkipWhiteSpace(int position, ReadOnlySpan span) + { + for (; position < span.Length && char.IsWhiteSpace(span[position]); position++) + { + } + + return position; + } + + static int FindNextWhiteSpace(int position, ReadOnlySpan span) + { + for (; position < span.Length && !char.IsWhiteSpace(span[position]); position++) + { + } + + return position; + } + + static (int min, int max, string errorMessage) GetValidationParameters(int type) + { + return type switch + { + CronExpressionConstants.Second or CronExpressionConstants.Minute + => (0, 59, "Minute and Second values must be between 0 and 59"), + CronExpressionConstants.Hour + => (0, 23, "Hour values must be between 0 and 23"), + CronExpressionConstants.DayOfMonth + => (1, 31, "Day of month values must be between 1 and 31"), + CronExpressionConstants.Month + => (1, 12, "Month values must be between 1 and 12"), + CronExpressionConstants.DayOfWeek + => (1, 7, "Day-of-Week values must be between 1 and 7"), + CronExpressionConstants.Year + => (Defaults.FirstYear, Defaults.LastYear, $"Year values must be between {Defaults.FirstYear} and {Defaults.LastYear}"), + _ => throw new ArgumentOutOfRangeException(nameof(type), "Invalid cron expression type") + }; + } + + static bool IsSpecialValue(int value, int type) + { + return value == CronExpressionConstants.AllSpec || + (type is CronExpressionConstants.DayOfMonth or CronExpressionConstants.DayOfWeek && value == CronExpressionConstants.NoSpec); + } + + static void ValidateSetValues(int value, int end, int type) + { + var (min, max, errorMessage) = GetValidationParameters(type); + + if ((value < min || value > max || end > max) && !IsSpecialValue(value, type)) + throw new FormatException(errorMessage); + } + + static (int startAt, int stopAt) GetRangeForType(int type, int value, int end) + { + return type switch + { + CronExpressionConstants.Second or CronExpressionConstants.Minute => (GetStartAt(value, 0), GetStopAt(end, 59)), + CronExpressionConstants.Hour => (GetStartAt(value, 0), GetStopAt(end, 23)), + CronExpressionConstants.DayOfMonth => (GetStartAt(value, 1), GetStopAt(end, 31)), + CronExpressionConstants.Month => (GetStartAt(value, 1), GetStopAt(end, 12)), + CronExpressionConstants.DayOfWeek => (GetStartAt(value, 1), GetStopAt(end, 7)), + CronExpressionConstants.Year => (GetStartAt(value, Defaults.FirstYear), GetStopAt(end, Defaults.LastYear)), + _ => throw new ArgumentException("Unexpected type encountered") + }; + } + + /// + /// Gets the max value for the cron expression type. + /// + /// The type of the cron expression + /// The start value + /// The stop value + /// Returns -1 if stopAt is less than startAt otherwise returns the max value for the type + static int GetMaxValueForType(int type, int startAt, int stopAt) + { + if (stopAt >= startAt) + return -1; + + return type switch + { + CronExpressionConstants.Second or CronExpressionConstants.Minute => 60, + CronExpressionConstants.Hour => 24, + CronExpressionConstants.Month => 12, + CronExpressionConstants.DayOfWeek => 7, + CronExpressionConstants.DayOfMonth => 31, + CronExpressionConstants.Year => throw new ArgumentException("Start year must be less than stop year"), + _ => throw new ArgumentException("Unexpected type encountered") + }; + } + + static int GetStartAt(int value, int defaultValue) + { + return value is -1 or CronExpressionConstants.AllSpec ? defaultValue : value; + } + + static int GetStopAt(int end, int defaultValue) + { + return end == -1 ? defaultValue : end; + } + + void AddToSet(int value, int end, int increment, int type) + { + ValidateSetValues(value, end, type); + + var data = GetSet(type); + + if (increment is 0 or -1 && value != CronExpressionConstants.AllSpec) + { + data.Add(value != -1 ? value : CronExpressionConstants.NoSpec); + return; + } + + if (value == CronExpressionConstants.AllSpec && increment <= 0) + { + data.Add(CronExpressionConstants.AllSpec); + return; + } + + var (startAt, stopAt) = GetRangeForType(type, value, end); + + var max = GetMaxValueForType(type, startAt, stopAt); + if (max != -1) + stopAt += max; + + for (var i = startAt; i <= stopAt; i += increment) + { + if (max == -1) + data.Add(i); + else + { + var i2 = i % max; + + if (i2 == 0 && type is CronExpressionConstants.Month or CronExpressionConstants.DayOfWeek or CronExpressionConstants.DayOfMonth) + i2 = max; + + data.Add(i2); + } + } + } + + public CronField GetSet(int type) + { + var field = type switch + { + CronExpressionConstants.Second => _seconds, + CronExpressionConstants.Minute => _minutes, + CronExpressionConstants.Hour => _hours, + CronExpressionConstants.DayOfMonth => _daysOfMonth, + CronExpressionConstants.Month => _months, + CronExpressionConstants.DayOfWeek => _daysOfWeek, + CronExpressionConstants.Year => _years, + _ => default + }; + + if (field is null) + throw new ArgumentOutOfRangeException(nameof(type)); + + return field; + } + + static ValueAndPosition GetValue(int value, ReadOnlySpan span, int index) + { + var ch = span[index]; + + var builder = new StringBuilder(span.Length); + builder.Append(value); + + while (char.IsDigit(ch)) + { + builder.Append(ch); + index++; + if (index >= span.Length) + break; + + ch = span[index]; + } + + return new ValueAndPosition(Convert.ToInt32(builder.ToString(), CultureInfo.InvariantCulture), index < span.Length ? index : index + 1); + } + + /// + /// Gets the numeric value from string. + /// + static int GetNumericValue(ReadOnlySpan span, int index) + { + var end = FindNextWhiteSpace(index, span); + + return ToInt32(span.Slice(index, end - index)); + } + + /// + /// Gets the month number. + /// + /// The string to map with. + /// + static int GetMonthNumber(ReadOnlySpan span) + { + return span switch + { + "JAN" => 0, + "FEB" => 1, + "MAR" => 2, + "APR" => 3, + "MAY" => 4, + "JUN" => 5, + "JUL" => 6, + "AUG" => 7, + "SEP" => 8, + "OCT" => 9, + "NOV" => 10, + "DEC" => 11, + _ => -1 + }; + } + + static int GetDayOfWeekNumber(ReadOnlySpan span) + { + return span switch + { + "SUN" => 1, + "MON" => 2, + "TUE" => 3, + "WED" => 4, + "THU" => 5, + "FRI" => 6, + "SAT" => 7, + _ => -1 + }; + } + + /// + /// Progress next fire time seconds + /// + NextFireTimeCursor ProgressNextFireTimeSecond(DateTimeOffset date) + { + var second = date.Second; + if (_seconds.TryGetMinValueStartingFrom(second, out var min)) + second = min; + else + { + second = _seconds.Min; + date = date.AddMinutes(1); + } + + return new NextFireTimeCursor(false, + new DateTimeOffset(date.Year, date.Month, date.Day, date.Hour, date.Minute, second, date.Millisecond, date.Offset)); + } + + /// + /// Progress next Fire time Minutes + /// + /// NextFireTimeCheck + NextFireTimeCursor ProgressNextFireTimeMinute(DateTimeOffset date) + { + var minute = date.Minute; + var hour = date.Hour; + var t = -1; + + if (_minutes.TryGetMinValueStartingFrom(minute, out var min)) + { + t = minute; + minute = min; + } + else + { + minute = _minutes.Min; + hour++; + } + + if (minute != t) + { + date = new DateTimeOffset(date.Year, date.Month, date.Day, date.Hour, minute, 0, date.Millisecond, date.Offset); + date = SetCalendarHour(date, hour); + return new NextFireTimeCursor(true, date); + } + + return new NextFireTimeCursor(false, + new DateTimeOffset(date.Year, date.Month, date.Day, date.Hour, minute, date.Second, date.Millisecond, date.Offset)); + } + + /// + /// Progress next fire time Hour + /// + /// NextFireTimeCheck + NextFireTimeCursor ProgressNextFireTimeHour(DateTimeOffset date) + { + int hour; + var day = date.Day; + var t = -1; + + if (_hours.TryGetMinValueStartingFrom(date.Hour, out var min)) + { + t = date.Hour; + hour = min; + } + else + { + hour = _hours.Min; + day++; + } + + if (hour != t) + { + var daysInMonth = DateTime.DaysInMonth(date.Year, date.Month); + date = day > daysInMonth + ? new DateTimeOffset(date.Year, date.Month, daysInMonth, date.Hour, 0, 0, date.Millisecond, date.Offset).AddDays(day - daysInMonth) + : new DateTimeOffset(date.Year, date.Month, day, date.Hour, 0, 0, date.Millisecond, date.Offset); + + date = SetCalendarHour(date, hour); + return new NextFireTimeCursor(true, date); + } + + return new NextFireTimeCursor(false, + new DateTimeOffset(date.Year, date.Month, date.Day, hour, date.Minute, date.Second, date.Millisecond, date.Offset)); + } + + (SortedSet daysOfMonthSet, bool dayHasNegativeOffset) CalculateDaysOfMonth(DateTimeOffset date) + { + var daysOfMonthSet = new SortedSet(_daysOfMonth); + var dayHasNegativeOffset = false; + + if (_lastDayOfMonth) + { + var lastDayOfMonthValue = GetLastDayOfMonth(date.Month, date.Year); + var lastDayOfMonthWithOffset = lastDayOfMonthValue - _lastDayOffset; + + if (_nearestWeekday) + { + var calculatedLastDay = CalculateNearestWeekdayForLastDay(date, lastDayOfMonthWithOffset); + daysOfMonthSet.Add(calculatedLastDay); + } + else + daysOfMonthSet.Add(lastDayOfMonthWithOffset); + } + else if (_nearestWeekday) + (daysOfMonthSet, dayHasNegativeOffset) = CalculateNearestWeekdayForDaysOfMonth(date, daysOfMonthSet); + + return (daysOfMonthSet, dayHasNegativeOffset); + } + + int CalculateNearestWeekdayForLastDay(DateTimeOffset date, int lastDayOfMonthWithOffset) + { + var checkDay = new DateTimeOffset(date.Year, date.Month, lastDayOfMonthWithOffset, date.Hour, date.Minute, date.Second, date.Millisecond, date.Offset); + var calculatedDay = lastDayOfMonthWithOffset; + + switch (checkDay.DayOfWeek) + { + case DayOfWeek.Saturday: + calculatedDay -= 1; + break; + case DayOfWeek.Sunday: + calculatedDay -= 2; + break; + } + + var calculatedLastDayWithOffset = calculatedDay - _lastWeekdayOffset; + + if (calculatedLastDayWithOffset <= 0) + calculatedLastDayWithOffset = 1; + + return calculatedLastDayWithOffset; + } + + static (SortedSet daysOfMonthSet, bool dayHasNegativeOffset) CalculateNearestWeekdayForDaysOfMonth(DateTimeOffset date, SortedSet daysOfMonthSet) + { + var endDayOfMonth = GetLastDayOfMonth(date.Month, date.Year); + var minDay = daysOfMonthSet.Min > endDayOfMonth ? endDayOfMonth : daysOfMonthSet.Min; + + var firstDayOfMonth = new DateTimeOffset(date.Year, date.Month, minDay, 0, 0, 0, date.Offset); + var dayOfWeek = firstDayOfMonth.DayOfWeek; + + if (dayOfWeek is DayOfWeek.Saturday or DayOfWeek.Sunday) + daysOfMonthSet.Remove(minDay); + + var (adjustedDay, dayHasNegativeOffset) = AdjustDayToNearestWeekday(minDay, dayOfWeek, endDayOfMonth); + daysOfMonthSet.Add(adjustedDay); + + return (daysOfMonthSet, dayHasNegativeOffset); + } + + static (int day, bool dayHasNegativeOffset) AdjustDayToNearestWeekday(int day, DayOfWeek dayOfWeek, int endDayOfMonth) + { + var dayHasNegativeOffset = false; + + switch (dayOfWeek) + { + case DayOfWeek.Saturday when day == 1: + day += 2; + break; + case DayOfWeek.Saturday: + day -= 1; + dayHasNegativeOffset = true; + break; + case DayOfWeek.Sunday when day == endDayOfMonth: + day -= 2; + dayHasNegativeOffset = true; + break; + case DayOfWeek.Sunday: + day += 1; + break; + } + + return (day, dayHasNegativeOffset); + } + + NextFireTimeCursor ProgressNextFireTimeDayOfMonth(DateTimeOffset date) + { + var day = date.Day; + var month = date.Month; + var tDay = -1; + var tMonth = month; + + // get day by day of month rule + (SortedSet? daysOfMonthCalculated, var setIncludesDayBeforeStartDay) = CalculateDaysOfMonth(date); + if (daysOfMonthCalculated.TryGetMinValueStartingFrom(date, setIncludesDayBeforeStartDay, out var min)) + { + tDay = day; + day = min; + + // make sure we don't over-run a short month, such as february + var lastDay = GetLastDayOfMonth(month, date.Year); + if (day > lastDay) + { + day = daysOfMonthCalculated.Min; + month++; + } + } + else + { + day = _lastDayOfMonth ? daysOfMonthCalculated.Min : _daysOfMonth.Min; + + month++; + } + + if (day != tDay || month != tMonth) + { + if (month > 12) + date = new DateTimeOffset(date.Year, 12, day, 0, 0, 0, date.Offset).AddMonths(month - 12); + else + { + var daysInMonth = DateTime.DaysInMonth(date.Year, month); + + date = day <= daysInMonth + ? new DateTimeOffset(date.Year, month, day, 0, 0, 0, date.Offset) + : new DateTimeOffset(date.Year, month, daysInMonth, 0, 0, 0, date.Offset).AddDays(day - daysInMonth); + } + + return new NextFireTimeCursor(true, date); + } + + return new NextFireTimeCursor(false, date); + } + + NextFireTimeCursor ProgressNextFireTimeDayOfWeek(DateTimeOffset date) + { + var day = date.Day; + var month = date.Month; + + if (_lastDayOfWeek) + { + var dayOfWeek = _daysOfWeek.Min; + var currentDayOfWeek = (int)date.DayOfWeek + 1; + var daysToAdd = 0; + if (currentDayOfWeek < dayOfWeek) + daysToAdd = dayOfWeek - currentDayOfWeek; + + if (currentDayOfWeek > dayOfWeek) + daysToAdd = dayOfWeek + (7 - currentDayOfWeek); + + var lastDayOfMonth = GetLastDayOfMonth(month, date.Year); + + if (day + daysToAdd > lastDayOfMonth) + { + if (month == 12) + date = new DateTimeOffset(date.Year, month - 11, 1, 0, 0, 0, date.Offset).AddYears(1); + else + date = new DateTimeOffset(date.Year, month + 1, 1, 0, 0, 0, date.Offset); + + return new NextFireTimeCursor(true, date); + } + + while (day + daysToAdd + 7 <= lastDayOfMonth) + daysToAdd += 7; + + day += daysToAdd; + + if (daysToAdd > 0) + return new NextFireTimeCursor(true, new DateTimeOffset(date.Year, month, day, 0, 0, 0, date.Offset)); + } + else if (_nthDayOfWeek != 0) + { + var dayOfWeek = _daysOfWeek.Min; + var currentDayOfWeek = (int)date.DayOfWeek + 1; + var daysToAdd = 0; + if (currentDayOfWeek < dayOfWeek) + daysToAdd = dayOfWeek - currentDayOfWeek; + else if (currentDayOfWeek > dayOfWeek) + daysToAdd = dayOfWeek + (7 - currentDayOfWeek); + + var dayShifted = daysToAdd > 0; + + day += daysToAdd; + var weekOfMonth = day / 7; + if (day % 7 > 0) + weekOfMonth++; + + daysToAdd = (_nthDayOfWeek - weekOfMonth) * 7; + day += daysToAdd; + if (daysToAdd < 0 || day > GetLastDayOfMonth(month, date.Year)) + { + date = month == 12 + ? new DateTimeOffset(date.Year, month - 11, 1, 0, 0, 0, date.Offset).AddYears(1) + : new DateTimeOffset(date.Year, month + 1, 1, 0, 0, 0, date.Offset); + + return new NextFireTimeCursor(true, date); + } + + if (daysToAdd > 0 || dayShifted) + return new NextFireTimeCursor(true, new DateTimeOffset(date.Year, month, day, 0, 0, 0, date.Offset)); + } + else if (_everyNthWeek != 0) + { + var currentDayOfWeek = (int)date.DayOfWeek + 1; + var dayOfWeek = _daysOfWeek.Min; + if (_daysOfWeek.TryGetMinValueStartingFrom(currentDayOfWeek, out var min)) + dayOfWeek = min; + + var daysToAdd = 0; + if (currentDayOfWeek < dayOfWeek) + daysToAdd = dayOfWeek - currentDayOfWeek + 7 * (_everyNthWeek - 1); + + if (currentDayOfWeek > dayOfWeek) + daysToAdd = dayOfWeek + (7 - currentDayOfWeek) + 7 * (_everyNthWeek - 1); + + if (daysToAdd > 0) + { + date = new DateTimeOffset(date.Year, month, day, 0, 0, 0, date.Offset); + date = date.AddDays(daysToAdd); + return new NextFireTimeCursor(true, date); + } + } + else + { + var currentDayOfWeek = (int)date.DayOfWeek + 1; + var dayOfWeek = _daysOfWeek.Min; + if (_daysOfWeek.TryGetMinValueStartingFrom(currentDayOfWeek, out var min)) + dayOfWeek = min; + + var daysToAdd = 0; + if (currentDayOfWeek < dayOfWeek) + daysToAdd = dayOfWeek - currentDayOfWeek; + + if (currentDayOfWeek > dayOfWeek) + daysToAdd = dayOfWeek + (7 - currentDayOfWeek); + + var lDay = GetLastDayOfMonth(month, date.Year); + + if (day + daysToAdd > lDay) + { + date = month == 12 + ? new DateTimeOffset(date.Year, month - 11, 1, 0, 0, 0, date.Offset).AddYears(1) + : new DateTimeOffset(date.Year, month + 1, 1, 0, 0, 0, date.Offset); + + return new NextFireTimeCursor(true, date); + } + + if (daysToAdd > 0) + return new NextFireTimeCursor(true, new DateTimeOffset(date.Year, month, day + daysToAdd, 0, 0, 0, date.Offset)); + } + + return new NextFireTimeCursor(false, new DateTimeOffset(date.Year, date.Month, day, date.Hour, date.Minute, date.Second, date.Offset)); + } + + NextFireTimeCursor ProgressNextFireTimeDay(DateTimeOffset date) + { + var dayOfMonthSpec = !_daysOfMonth.Contains(CronExpressionConstants.NoSpec); + var dayOfWeekSpec = !_daysOfWeek.Contains(CronExpressionConstants.NoSpec); + if (dayOfMonthSpec && !dayOfWeekSpec) + return ProgressNextFireTimeDayOfMonth(date); + + if (dayOfWeekSpec && !dayOfMonthSpec) + return ProgressNextFireTimeDayOfWeek(date); + + var dayOfMonthProgressResult = ProgressNextFireTimeDayOfMonth(date); + var dayOfWeekProgressResult = ProgressNextFireTimeDayOfWeek(date); + if (dayOfMonthProgressResult.RestartLoop && dayOfWeekProgressResult.RestartLoop) + { + return dayOfWeekProgressResult.Date < dayOfMonthProgressResult.Date + ? dayOfWeekProgressResult + : dayOfMonthProgressResult; + } + + if (dayOfWeekProgressResult is { Date: not null, RestartLoop: false }) + return dayOfWeekProgressResult; + if (dayOfMonthProgressResult is { Date: not null, RestartLoop: false }) + return dayOfMonthProgressResult; + + return dayOfWeekProgressResult.Date!.Value < dayOfMonthProgressResult.Date!.Value + ? dayOfWeekProgressResult + : dayOfMonthProgressResult; + } + + NextFireTimeCursor ProgressNextFireTimeMonth(DateTimeOffset date) + { + var month = date.Month; + var year = date.Year; + var tMonth = -1; + + if (_months.TryGetMinValueStartingFrom(month, out var min)) + { + tMonth = month; + month = min; + } + else + { + month = _months.Min; + year++; + } + + return month != tMonth + ? new NextFireTimeCursor(true, new DateTimeOffset(year, month, 1, 0, 0, 0, date.Offset)) + : new NextFireTimeCursor(false, new DateTimeOffset(date.Year, month, date.Day, date.Hour, date.Minute, date.Second, date.Offset)); + } + + NextFireTimeCursor ProgressNextFireTimeYear(DateTimeOffset date) + { + var year = date.Year; + int tYear; + if (_years.TryGetMinValueStartingFrom(date.Year, out var min)) + { + tYear = year; + year = min; + } + else + return new NextFireTimeCursor(false, null); + + return year != tYear + ? new NextFireTimeCursor(true, new DateTimeOffset(year, 1, 1, 0, 0, 0, date.Offset)) + : new NextFireTimeCursor(false, new DateTimeOffset(year, date.Month, date.Day, date.Hour, date.Minute, date.Second, date.Offset)); + } + + public DateTimeOffset? GetTimeAfter(DateTimeOffset afterTime) + { + afterTime = afterTime.AddSeconds(1); + + var date = StripMilliseconds(afterTime); + + date = TimeZoneUtil.ConvertTime(date, TimeZone); + + Func[] nextFireTimeProgressions = + { + ProgressNextFireTimeSecond, + ProgressNextFireTimeMinute, + ProgressNextFireTimeHour, + ProgressNextFireTimeDay, + ProgressNextFireTimeMonth, + ProgressNextFireTimeYear + }; + + var nextFireTimeCursor = new NextFireTimeCursor(false, date); + var foundNextFireTime = false; + + while (!foundNextFireTime) + { + foreach (Func progression in nextFireTimeProgressions) + { + if (nextFireTimeCursor.Date.HasValue) + nextFireTimeCursor = progression(nextFireTimeCursor.Date.Value); + else + break; + + if (nextFireTimeCursor.RestartLoop) + break; + } + + if (nextFireTimeCursor.Date is null || nextFireTimeCursor.Date.Value.Year > Defaults.LastYear) + return null; + + if (nextFireTimeCursor.RestartLoop) + continue; + + date = new DateTimeOffset(nextFireTimeCursor.Date.Value.DateTime, TimeZoneUtil.GetUtcOffset(nextFireTimeCursor.Date.Value.DateTime, TimeZone)); + foundNextFireTime = true; + } + + return date.ToUniversalTime(); + } + + static DateTimeOffset StripMilliseconds(DateTimeOffset time) + { + return new DateTimeOffset(time.Year, time.Month, time.Day, time.Hour, time.Minute, time.Second, time.Offset); + } + + static DateTimeOffset SetCalendarHour(DateTimeOffset date, int hour) + { + var hourToSet = hour; + if (hourToSet == 24) + hourToSet = 0; + + var d = new DateTimeOffset(date.Year, date.Month, date.Day, hourToSet, date.Minute, date.Second, date.Millisecond, date.Offset); + if (hour == 24) + d = d.AddDays(1); + + return d; + } + + static int GetLastDayOfMonth(int month, int year) + { + return DateTime.DaysInMonth(year, month); + } + + static int ToInt32(char c) + { + return c - '0'; + } + + static int ToInt32(ReadOnlySpan span) + { + #if NET6_0_OR_GREATER + return int.Parse(span); + #else + return int.Parse(span.ToString()); + #endif + } +} diff --git a/src/MassTransit/JobService/JobService/Scheduling/CronExpressionConstants.cs b/src/MassTransit/JobService/JobService/Scheduling/CronExpressionConstants.cs new file mode 100644 index 00000000000..038cffbc0cb --- /dev/null +++ b/src/MassTransit/JobService/JobService/Scheduling/CronExpressionConstants.cs @@ -0,0 +1,49 @@ +namespace MassTransit.JobService.Scheduling; + +static class CronExpressionConstants +{ + /// + /// Field specification for second. + /// + public const int Second = 0; + + /// + /// Field specification for minute. + /// + public const int Minute = 1; + + /// + /// Field specification for hour. + /// + public const int Hour = 2; + + /// + /// Field specification for day of month. + /// + public const int DayOfMonth = 3; + + /// + /// Field specification for month. + /// + public const int Month = 4; + + /// + /// Field specification for day of week. + /// + public const int DayOfWeek = 5; + + /// + /// Field specification for year. + /// + public const int Year = 6; + + /// + /// Field specification for wildcard '*'. + /// + public const int AllSpec = 99; + + /// + /// Field specification for no specification at all '?'. + /// + public const int NoSpec = 98; +} diff --git a/src/MassTransit/JobService/JobService/Scheduling/CronExpressionSummary.cs b/src/MassTransit/JobService/JobService/Scheduling/CronExpressionSummary.cs new file mode 100644 index 00000000000..4f1c0445685 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Scheduling/CronExpressionSummary.cs @@ -0,0 +1,101 @@ +namespace MassTransit.JobService.Scheduling; + +using System.Globalization; +using System.Text; + + +readonly struct CronExpressionSummary +{ + public CronExpressionSummary(CronField seconds, CronField minutes, CronField hours, CronField daysOfMonth, + CronField months, CronField daysOfWeek, bool lastDayOfWeek, bool nearestWeekday, int nthDayOfWeek, + bool lastDayOfMonth, bool calendarDayOfWeek, bool calendarDayOfMonth, CronField years) + { + Seconds = seconds; + Minutes = minutes; + Hours = hours; + DaysOfMonth = daysOfMonth; + Months = months; + DaysOfWeek = daysOfWeek; + LastDayOfWeek = lastDayOfWeek; + NearestWeekday = nearestWeekday; + NthDayOfWeek = nthDayOfWeek; + LastDayOfMonth = lastDayOfMonth; + CalendarDayOfWeek = calendarDayOfWeek; + CalendarDayOfMonth = calendarDayOfMonth; + Years = years; + } + + public CronField Seconds { get; } + public CronField Minutes { get; } + public CronField Hours { get; } + public CronField DaysOfMonth { get; } + public CronField Months { get; } + public CronField DaysOfWeek { get; } + public bool LastDayOfWeek { get; } + public bool NearestWeekday { get; } + public int NthDayOfWeek { get; } + public bool LastDayOfMonth { get; } + public bool CalendarDayOfWeek { get; } + public bool CalendarDayOfMonth { get; } + public CronField Years { get; } + + /// + /// Gets the expression set summary. + /// + static string GetExpressionSetSummary(CronField data) + { + if (data.Contains(CronExpressionConstants.NoSpec)) + return "?"; + + if (data.Contains(CronExpressionConstants.AllSpec)) + return "*"; + + var b = new StringBuilder(); + + var first = true; + foreach (var iVal in data) + { + var val = iVal.ToString(CultureInfo.InvariantCulture); + if (!first) + b.Append(','); + + b.Append(val); + first = false; + } + + return b.ToString(); + } + + public override string ToString() + { + var b = new StringBuilder(); + + b.Append("seconds: "); + b.AppendLine(GetExpressionSetSummary(Seconds)); + b.Append("minutes: "); + b.AppendLine(GetExpressionSetSummary(Minutes)); + b.Append("hours: "); + b.AppendLine(GetExpressionSetSummary(Hours)); + b.Append("daysOfMonth: "); + b.AppendLine(GetExpressionSetSummary(DaysOfMonth)); + b.Append("months: "); + b.AppendLine(GetExpressionSetSummary(Months)); + b.Append("daysOfWeek: "); + b.AppendLine(GetExpressionSetSummary(DaysOfWeek)); + b.Append("lastdayOfWeek: "); + b.AppendLine(LastDayOfWeek.ToString()); + b.Append("nearestWeekday: "); + b.AppendLine(NearestWeekday.ToString()); + b.Append("NthDayOfWeek: "); + b.AppendLine(NthDayOfWeek.ToString()); + b.Append("lastdayOfMonth: "); + b.AppendLine(LastDayOfMonth.ToString()); + b.Append("calendardayOfWeek: "); + b.AppendLine(CalendarDayOfWeek.ToString()); + b.Append("calendardayOfMonth: "); + b.AppendLine(CalendarDayOfMonth.ToString()); + b.Append("years: "); + b.AppendLine(GetExpressionSetSummary(Years)); + return b.ToString(); + } +} diff --git a/src/MassTransit/JobService/JobService/Scheduling/CronField.cs b/src/MassTransit/JobService/JobService/Scheduling/CronField.cs new file mode 100644 index 00000000000..6401cdabf6a --- /dev/null +++ b/src/MassTransit/JobService/JobService/Scheduling/CronField.cs @@ -0,0 +1,154 @@ +#nullable enable +namespace MassTransit.JobService.Scheduling; + +using System.Collections; +using System.Collections.Generic; + + +public sealed class CronField : + IEnumerable +{ + bool _hasAllOrNoSpec; + + int? _singleValue; + SortedSet? _values; + + public CronField() + { + Clear(); + } + + public int Count + { + get + { + if (_singleValue is not null) + return 1; + + return _values?.Count ?? 0; + } + } + + internal int Min + { + get + { + if (_singleValue is not null) + return _hasAllOrNoSpec ? 0 : _singleValue.Value; + + if (_values is not null) + return _hasAllOrNoSpec ? 0 : _values.Min; + + return 0; + } + } + + public IEnumerator GetEnumerator() + { + if (_singleValue is not null) + { + yield return _singleValue.Value; + yield break; + } + + if (_values is not null) + { + foreach (var value in _values) + yield return value; + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + internal void Clear() + { + _singleValue = null; + _values = null; + _hasAllOrNoSpec = false; + } + + internal bool TryGetMinValueStartingFrom(int start, out int min) + { + min = 0; + + if (_singleValue == CronExpressionConstants.AllSpec) + { + min = start; + return true; + } + + if (_singleValue is not null) + { + if (_singleValue >= start) + { + min = _singleValue.Value; + return true; + } + + // didn't match + return false; + } + + SortedSet? set = _values; + + if (set is null) + return false; + + min = set.Min; + + if (set.Contains(start)) + { + min = start; + return true; + } + + if (set.Count == 0 || set.Max < start) + return false; + + if (set.Min >= start) + return true; + + SortedSet view = set.GetViewBetween(start, int.MaxValue); + if (view.Count > 0) + { + min = view.Min; + return true; + } + + return false; + } + + public void Add(int value) + { + _hasAllOrNoSpec = value is CronExpressionConstants.AllSpec or CronExpressionConstants.NoSpec; + + if (_singleValue is null) + { + if (_values is null) + _singleValue = value; + else + _values.Add(value); + } + else if (_singleValue != value) + { + _values = + [ + _singleValue.Value, + value + ]; + _singleValue = null; + } + } + + public bool Contains(int value) + { + if (_singleValue == value + || (value != CronExpressionConstants.AllSpec && value != CronExpressionConstants.NoSpec && _hasAllOrNoSpec)) + return true; + + return _values is not null && _values.Contains(value); + } +} diff --git a/src/MassTransit/JobService/JobService/Scheduling/Defaults.cs b/src/MassTransit/JobService/JobService/Scheduling/Defaults.cs new file mode 100644 index 00000000000..a65463bc6cc --- /dev/null +++ b/src/MassTransit/JobService/JobService/Scheduling/Defaults.cs @@ -0,0 +1,11 @@ +namespace MassTransit.JobService.Scheduling; + +using System; + + +static class Defaults +{ + internal const int FirstYear = 1970; + + internal static readonly int LastYear = DateTime.UtcNow.Year + 100; +} diff --git a/src/MassTransit/JobService/JobService/Scheduling/NextFireTimeCursor.cs b/src/MassTransit/JobService/JobService/Scheduling/NextFireTimeCursor.cs new file mode 100644 index 00000000000..4d6c7ceb352 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Scheduling/NextFireTimeCursor.cs @@ -0,0 +1,23 @@ +namespace MassTransit.JobService.Scheduling; + +using System; + + +readonly struct NextFireTimeCursor +{ + public NextFireTimeCursor(bool restartLoop, DateTimeOffset? date) + { + RestartLoop = restartLoop; + Date = date; + } + + public bool RestartLoop { get; } + + public DateTimeOffset? Date { get; } + + public void Deconstruct(out bool restartLoop, out DateTimeOffset? date) + { + restartLoop = RestartLoop; + date = Date; + } +} diff --git a/src/MassTransit/JobService/JobService/Scheduling/SortedSetExtensions.cs b/src/MassTransit/JobService/JobService/Scheduling/SortedSetExtensions.cs new file mode 100644 index 00000000000..74ec2c59819 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Scheduling/SortedSetExtensions.cs @@ -0,0 +1,38 @@ +namespace MassTransit.JobService.Scheduling; + +using System; +using System.Collections.Generic; + + +static class SortedSetExtensions +{ + internal static bool TryGetMinValueStartingFrom(this SortedSet set, DateTimeOffset start, bool allowValueBeforeStartDay, out int minimumDay) + { + minimumDay = set.Min; + var startDay = start.Day; + + if (set.Contains(CronExpressionConstants.AllSpec) || set.Contains(startDay)) + { + minimumDay = startDay; + return true; + } + + if (allowValueBeforeStartDay && set.Min < startDay) + return true; + + if (set.Count == 0 || set.Max < startDay) + return false; + + if (set.Min >= startDay) + return true; + + SortedSet view = set.GetViewBetween(startDay, int.MaxValue); + if (view.Count > 0) + { + minimumDay = view.Min; + return true; + } + + return false; + } +} diff --git a/src/MassTransit/JobService/JobService/Scheduling/TimeZoneUtil.cs b/src/MassTransit/JobService/JobService/Scheduling/TimeZoneUtil.cs new file mode 100644 index 00000000000..1664e6f40a8 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Scheduling/TimeZoneUtil.cs @@ -0,0 +1,124 @@ +#nullable enable +namespace MassTransit.JobService.Scheduling; + +using System; +using System.Collections.Generic; +using System.Linq; + + +public static class TimeZoneUtil +{ + static readonly Dictionary TimeZoneIdAliases = new Dictionary(); + + public static Func? CustomResolver = id => null; + + static TimeZoneUtil() + { + TimeZoneIdAliases["UTC"] = "Coordinated Universal Time"; + TimeZoneIdAliases["Coordinated Universal Time"] = "UTC"; + + TimeZoneIdAliases["Central European Standard Time"] = "CET"; + TimeZoneIdAliases["CET"] = "Central European Standard Time"; + + TimeZoneIdAliases["Eastern Standard Time"] = "US/Eastern"; + TimeZoneIdAliases["US/Eastern"] = "Eastern Standard Time"; + + TimeZoneIdAliases["Central Standard Time"] = "US/Central"; + TimeZoneIdAliases["US/Central"] = "Central Standard Time"; + + TimeZoneIdAliases["US Central Standard Time"] = "US/Indiana-Stark"; + TimeZoneIdAliases["US/Indiana-Stark"] = "US Central Standard Time"; + + TimeZoneIdAliases["Mountain Standard Time"] = "US/Mountain"; + TimeZoneIdAliases["US/Mountain"] = "Mountain Standard Time"; + + TimeZoneIdAliases["US Mountain Standard Time"] = "US/Arizona"; + TimeZoneIdAliases["US/Arizona"] = "US Mountain Standard Time"; + + TimeZoneIdAliases["Pacific Standard Time"] = "US/Pacific"; + TimeZoneIdAliases["US/Pacific"] = "Pacific Standard Time"; + + TimeZoneIdAliases["Alaskan Standard Time"] = "US/Alaska"; + TimeZoneIdAliases["US/Alaska"] = "Alaskan Standard Time"; + + TimeZoneIdAliases["Hawaiian Standard Time"] = "US/Hawaii"; + TimeZoneIdAliases["US/Hawaii"] = "Hawaiian Standard Time"; + + TimeZoneIdAliases["China Standard Time"] = "Asia/Shanghai"; + TimeZoneIdAliases["Asia/Shanghai"] = "China Standard Time"; + + TimeZoneIdAliases["Pakistan Standard Time"] = "Asia/Karachi"; + TimeZoneIdAliases["Asia/Karachi"] = "Pakistan Standard Time"; + } + + /// + /// TimeZoneInfo.ConvertTime is not supported under mono + /// + /// + /// + /// + public static DateTimeOffset ConvertTime(DateTimeOffset dateTimeOffset, TimeZoneInfo timeZoneInfo) + { + return TimeZoneInfo.ConvertTime(dateTimeOffset, timeZoneInfo); + } + + /// + /// TimeZoneInfo.GetUtcOffset(DateTimeOffset) is not supported under mono + /// + /// + /// + /// + public static TimeSpan GetUtcOffset(DateTimeOffset dateTimeOffset, TimeZoneInfo timeZoneInfo) + { + return timeZoneInfo.GetUtcOffset(dateTimeOffset); + } + + public static TimeSpan GetUtcOffset(DateTime dateTime, TimeZoneInfo timeZoneInfo) + { + // Unlike the default behavior of TimeZoneInfo.GetUtcOffset, it is prefered to choose + // the DAYLIGHT time when the input is ambiguous, because the daylight instance is the + // FIRST instance, and time moves in a forward direction. + + var offset = timeZoneInfo.IsAmbiguousTime(dateTime) + ? timeZoneInfo.GetAmbiguousTimeOffsets(dateTime).Max() + : timeZoneInfo.GetUtcOffset(dateTime); + + return offset; + } + + /// + /// Tries to find time zone with given id, has ability do some fallbacks when necessary. + /// + /// System id of the time zone. + /// + public static TimeZoneInfo FindTimeZoneById(string id) + { + TimeZoneInfo? info = null; + try + { + info = TimeZoneInfo.FindSystemTimeZoneById(id); + } + catch (TimeZoneNotFoundException ex) + { + if (TimeZoneIdAliases.TryGetValue(id, out var aliasedId)) + { + try + { + info = TimeZoneInfo.FindSystemTimeZoneById(aliasedId); + } + catch + { + } + } + + info ??= CustomResolver?.Invoke(id); + if (info is null) + { + throw new TimeZoneNotFoundException( + $"Could not find time zone with id {id}, consider using Quartz.Plugins.TimeZoneConverter for resolving more time zones ids", ex); + } + } + + return info; + } +} diff --git a/src/MassTransit/JobService/JobService/Scheduling/ValueAndPosition.cs b/src/MassTransit/JobService/JobService/Scheduling/ValueAndPosition.cs new file mode 100644 index 00000000000..6465c5af143 --- /dev/null +++ b/src/MassTransit/JobService/JobService/Scheduling/ValueAndPosition.cs @@ -0,0 +1,19 @@ +namespace MassTransit.JobService.Scheduling; + +readonly struct ValueAndPosition +{ + public ValueAndPosition(int value, int position) + { + Value = value; + Position = position; + } + + public int Value { get; } + public int Position { get; } + + public void Deconstruct(out int value, out int position) + { + value = Value; + position = Position; + } +} diff --git a/src/MassTransit/JobService/JobService/StartJobConsumer.cs b/src/MassTransit/JobService/JobService/StartJobConsumer.cs index 51bd92eff3a..9d06fdba8a1 100644 --- a/src/MassTransit/JobService/JobService/StartJobConsumer.cs +++ b/src/MassTransit/JobService/JobService/StartJobConsumer.cs @@ -23,14 +23,14 @@ public StartJobConsumer(IJobService jobService, JobOptions options, Guid j _jobPipe = jobPipe; } - public async Task Consume(ConsumeContext context) + public Task Consume(ConsumeContext context) { if (context.Message.JobTypeId != _jobTypeId) - return; + return Task.CompletedTask; var job = context.GetJob() ?? throw new SerializationException($"The job could not be deserialized: {TypeCache.ShortName}"); - await _jobService.StartJob(context, job, _jobPipe, _options.JobTimeout); + return _jobService.StartJob(context, job, _jobPipe, _options); } } } diff --git a/src/MassTransit/JobService/JobService/SubmitJobConsumer.cs b/src/MassTransit/JobService/JobService/SubmitJobConsumer.cs index b461430510e..dc8a485dfa9 100644 --- a/src/MassTransit/JobService/JobService/SubmitJobConsumer.cs +++ b/src/MassTransit/JobService/JobService/SubmitJobConsumer.cs @@ -1,8 +1,12 @@ +#nullable enable namespace MassTransit.JobService { using System; + using System.Collections.Generic; using System.Threading.Tasks; using Contracts.JobService; + using Messages; + using Scheduling; /// @@ -25,29 +29,42 @@ public SubmitJobConsumer(JobOptions options, Guid jobTypeId) public Task Consume(ConsumeContext> context) { - return PublishJobSubmitted(context, context.Message.JobId, context.Message.Job, context.SentTime ?? DateTime.UtcNow); + if (context.Message.Schedule != null) + { + if (string.IsNullOrWhiteSpace(context.Message.Schedule.CronExpression) && !context.Message.Schedule.Start.HasValue) + throw new RecurringJobException("A valid cron expression or start date is required"); + + if (!string.IsNullOrWhiteSpace(context.Message.Schedule.CronExpression)) + CronExpression.ValidateExpression(context.Message.Schedule.CronExpression!); + } + + return PublishJobSubmitted(context, context.Message.JobId, context.Message.Job, context.SentTime ?? DateTime.UtcNow, context.Message.Schedule, + context.Message.Properties); } public Task Consume(ConsumeContext context) { var jobId = context.RequestId ?? NewId.NextGuid(); - return PublishJobSubmitted(context, jobId, context.Message, context.SentTime ?? DateTime.UtcNow); + return PublishJobSubmitted(context, jobId, context.Message, context.SentTime ?? DateTime.UtcNow, null, null); } - async Task PublishJobSubmitted(ConsumeContext context, Guid jobId, TJob job, DateTime timestamp) + async Task PublishJobSubmitted(ConsumeContext context, Guid jobId, TJob job, DateTime timestamp, RecurringJobSchedule? schedule, + Dictionary? jobProperties) { - await context.Publish(new + await context.Publish(new JobSubmittedEvent { JobId = jobId, JobTypeId = _jobTypeId, Timestamp = timestamp, Job = context.ToDictionary(job), - _options.JobTimeout + JobProperties = jobProperties, + JobTimeout = _options.JobTimeout, + Schedule = schedule }); if (context.RequestId.HasValue && context.ResponseAddress != null) - await context.RespondAsync(new { JobId = jobId }); + await context.RespondAsync(new JobSubmissionAcceptedResponse { JobId = jobId }); } } } diff --git a/src/MassTransit/JobService/JobService/SuperviseJobConsumer.cs b/src/MassTransit/JobService/JobService/SuperviseJobConsumer.cs index bd9e14f68d9..f44ca3c91ba 100644 --- a/src/MassTransit/JobService/JobService/SuperviseJobConsumer.cs +++ b/src/MassTransit/JobService/JobService/SuperviseJobConsumer.cs @@ -1,11 +1,13 @@ namespace MassTransit.JobService { + using System; using System.Threading.Tasks; using Contracts.JobService; + using Messages; public class SuperviseJobConsumer : - IConsumer, + IConsumer, IConsumer { readonly IJobService _jobService; @@ -15,21 +17,21 @@ public SuperviseJobConsumer(IJobService jobService) _jobService = jobService; } - public async Task Consume(ConsumeContext context) + public async Task Consume(ConsumeContext context) { if (_jobService.TryGetJob(context.Message.JobId, out var handle)) - await handle.Cancel(); + await handle.Cancel(JobCancellationReasons.CancellationRequested).ConfigureAwait(false); } public Task Consume(ConsumeContext context) { if (_jobService.TryGetJob(context.Message.JobId, out var jobHandle)) { - return context.RespondAsync(new + return context.RespondAsync(new JobAttemptStatusResponse { - context.Message.JobId, - context.Message.AttemptId, - InVar.Timestamp, + JobId = context.Message.JobId, + AttemptId = context.Message.AttemptId, + Timestamp = DateTime.UtcNow, Status = jobHandle.JobTask.Status switch { TaskStatus.RanToCompletion => JobStatus.Completed, diff --git a/src/MassTransit/JobService/JobServiceExtensions.cs b/src/MassTransit/JobService/JobServiceExtensions.cs index b1d814bd2b7..48e263f7b1a 100644 --- a/src/MassTransit/JobService/JobServiceExtensions.cs +++ b/src/MassTransit/JobService/JobServiceExtensions.cs @@ -1,17 +1,292 @@ -namespace MassTransit +#nullable enable +namespace MassTransit; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Clients; +using Contracts.JobService; +using Initializers; +using JobService.Messages; +using Serialization; + + +public static class JobServiceExtensions { - using System; - using System.Threading.Tasks; - using Contracts.JobService; + /// + /// Requests the job state for the specified using the request client + /// + /// + /// + /// + public static async Task GetJobState(this IRequestClient client, Guid jobId) + { + Response response = await client.GetResponse(new GetJobStateRequest { JobId = jobId }); + + return response.Message; + } + + /// + /// Requests the job state for the specified using the request client + /// + /// + /// + /// + public static async Task> GetJobState(this IRequestClient client, Guid jobId) + where T : class + { + Response response = await client.GetResponse(new GetJobStateRequest { JobId = jobId }); + + if (response is MessageResponse messageResponse) + return new JobStateResponse(response.Message, messageResponse.DeserializeObject(response.Message.JobState)); + + return new JobStateResponse(response.Message); + } + + /// + /// Submits a job, returning the generated jobId + /// + /// + /// + /// + /// + /// + /// + public static Task SubmitJob(this IPublishEndpoint publishEndpoint, T job, Action? setJobProperties = null, + CancellationToken cancellationToken = default) + where T : class + { + return SubmitJob(publishEndpoint, NewId.NextGuid(), job, setJobProperties, cancellationToken); + } + + /// + /// Submits a job, returning the generated jobId + /// + /// + /// A unique job id + /// + /// + /// + /// + /// + public static async Task SubmitJob(this IPublishEndpoint publishEndpoint, Guid jobId, T job, Action? + setJobProperties = null, CancellationToken cancellationToken = default) + where T : class + { + if (jobId == Guid.Empty) + throw new ArgumentException("An empty Guid cannot be used as a JobId", nameof(jobId)); + await publishEndpoint.Publish>(CreateSubmitJobCommand(jobId, job, setJobProperties), cancellationToken).ConfigureAwait(false); - public static class JobServiceExtensions + return jobId; + } + + static SubmitJobCommand CreateSubmitJobCommand(Guid jobId, T job, Action? setJobProperties) + where T : class { - public static async Task GetJobState(this IRequestClient client, Guid jobId) + var command = new SubmitJobCommand + { + JobId = jobId, + Job = job + }; + + if (setJobProperties != null) { - Response response = await client.GetResponse(new { JobId = jobId }); + var properties = new JobPropertyCollection(); + setJobProperties(properties); - return response.Message; + if (properties.Count > 0) + command.Properties = properties; } + + return command; + } + + /// + /// Submits a job, returning the generated jobId + /// + /// + /// + /// + /// + /// + /// + public static Task SubmitJob(this IPublishEndpoint publishEndpoint, object job, Action? setJobProperties = null, + CancellationToken cancellationToken = default) + where T : class + { + return SubmitJob(publishEndpoint, NewId.NextGuid(), job, setJobProperties, cancellationToken); + } + + /// + /// Submits a job, returning the generated jobId + /// + /// + /// A unique job id + /// + /// + /// + /// + /// + public static async Task SubmitJob(this IPublishEndpoint publishEndpoint, Guid jobId, object job, Action? + setJobProperties = null, CancellationToken cancellationToken = default) + where T : class + { + if (jobId == Guid.Empty) + throw new ArgumentException("An empty Guid cannot be used as a JobId", nameof(jobId)); + + InitializeContext context = await MessageInitializerCache.Initialize(job, cancellationToken).ConfigureAwait(false); + + await publishEndpoint.Publish>(CreateSubmitJobCommand(jobId, context.Message, setJobProperties), cancellationToken) + .ConfigureAwait(false); + + return jobId; + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// + /// + /// + /// + /// + public static Task SubmitJob(this IRequestClient> client, T job, Action? setJobProperties = null, + CancellationToken cancellationToken = default) + where T : class + { + return SubmitJob(client, NewId.NextGuid(), job, setJobProperties, cancellationToken); + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// A unique job id + /// + /// + /// + /// + /// + public static async Task SubmitJob(this IRequestClient> client, Guid jobId, T job, + Action? setJobProperties = null, CancellationToken cancellationToken = default) + where T : class + { + if (jobId == Guid.Empty) + throw new ArgumentException("An empty Guid cannot be used as a JobId", nameof(jobId)); + + Response response = await client.GetResponse(CreateSubmitJobCommand(jobId, job, setJobProperties), + cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// + /// + /// + /// + /// + public static Task SubmitJob(this IRequestClient> client, object job, Action? setJobProperties = null, + CancellationToken cancellationToken = default) + where T : class + { + return SubmitJob(client, NewId.NextGuid(), job, setJobProperties, cancellationToken); + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// Specify an explicit jobId for the job + /// + /// + /// + /// + /// + public static async Task SubmitJob(this IRequestClient> client, Guid jobId, object job, + Action? setJobProperties = null, CancellationToken cancellationToken = default) + where T : class + { + InitializeContext context = await MessageInitializerCache.Initialize(job, cancellationToken).ConfigureAwait(false); + + Response response = await client.GetResponse(CreateSubmitJobCommand(jobId, context.Message, + setJobProperties), cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// + /// + /// + /// + public static async Task SubmitJob(this IRequestClient client, T job, CancellationToken cancellationToken = default) + where T : class + { + Response response = await client.GetResponse(job, cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// + /// + /// + /// + public static async Task SubmitJob(this IRequestClient client, object job, CancellationToken cancellationToken = default) + where T : class + { + Response response = await client.GetResponse(job, cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Cancel a job if the job exists and is in a state that can be canceled. + /// + /// + /// + /// + /// + public static Task CancelJob(this IPublishEndpoint publishEndpoint, Guid jobId, string? reason = null) + { + return publishEndpoint.Publish(new CancelJobCommand + { + JobId = jobId, + Reason = reason ?? "Unspecified" + }); + } + + /// + /// Retry a job if the job exists and is in a state that can be retried. + /// + /// + /// + /// + public static Task RetryJob(this IPublishEndpoint publishEndpoint, Guid jobId) + { + return publishEndpoint.Publish(new RetryJobCommand { JobId = jobId }); + } + + /// + /// Finalize a job, removing any faulted job attempts, so that it can be removed from the saga repository + /// + /// + /// + /// + public static Task FinalizeJob(this IPublishEndpoint publishEndpoint, Guid jobId) + { + return publishEndpoint.Publish(new FinalizeJobCommand { JobId = jobId }); } } diff --git a/src/MassTransit/JobService/JobStateMachine.cs b/src/MassTransit/JobService/JobStateMachine.cs index c8fc030ec53..7514c153375 100644 --- a/src/MassTransit/JobService/JobStateMachine.cs +++ b/src/MassTransit/JobService/JobStateMachine.cs @@ -1,61 +1,43 @@ namespace MassTransit { using System; + using System.Collections.Generic; using System.Linq; + using Configuration; using Contracts.JobService; + using Internals; + using JobService.Messages; + using JobService.Scheduling; public sealed class JobStateMachine : MassTransitStateMachine { - readonly JobServiceOptions _options; - - public JobStateMachine(JobServiceOptions options) + public JobStateMachine() { - _options = options; - Event(() => JobSubmitted, x => x.CorrelateById(m => m.Message.JobId)); Event(() => JobSlotAllocated, x => { - x.CorrelateById(m => m.Message.JobId); x.ConfigureConsumeTopology = false; }); Event(() => JobSlotUnavailable, x => { - x.CorrelateById(m => m.Message.JobId); x.ConfigureConsumeTopology = false; }); Event(() => AllocateJobSlotFaulted, x => { - x.CorrelateById(m => m.Message.Message.JobId); x.ConfigureConsumeTopology = false; }); - Event(() => JobAttemptCreated, x => - { - x.CorrelateById(m => m.Message.JobId); - x.ConfigureConsumeTopology = false; - }); Event(() => StartJobAttemptFaulted, x => { - x.CorrelateById(m => m.Message.Message.JobId); x.ConfigureConsumeTopology = false; }); - Event(() => AttemptCanceled, x => x.CorrelateById(m => m.Message.JobId)); - Event(() => AttemptCompleted, x => x.CorrelateById(m => m.Message.JobId)); - Event(() => AttemptFaulted, x => x.CorrelateById(m => m.Message.JobId)); - Event(() => AttemptStarted, x => x.CorrelateById(m => m.Message.JobId)); - - Event(() => JobCompleted, x => x.CorrelateById(m => m.Message.JobId)); - - Event(() => CancelJob, x => x.CorrelateById(m => m.Message.JobId)); - Event(() => RetryJob, x => x.CorrelateById(m => m.Message.JobId)); - Event(() => GetJobState, x => { - x.CorrelateById(m => m.Message.JobId); + x.ReadOnly = true; x.OnMissingInstance(i => i.ExecuteAsync(context => context.RespondAsync(new { context.Message.JobId, @@ -65,7 +47,7 @@ public JobStateMachine(JobServiceOptions options) Schedule(() => JobSlotWaitElapsed, instance => instance.JobSlotWaitToken, x => { - x.Delay = options.SlotWaitTime; + x.DelayProvider = context => context.GetPayload().SlotWaitTime; x.Received = r => { r.CorrelateById(context => context.Message.JobId); @@ -83,12 +65,26 @@ public JobStateMachine(JobServiceOptions options) }); InstanceState(x => x.CurrentState, Submitted, WaitingToStart, WaitingForSlot, Started, Completed, Faulted, Canceled, StartingJobAttempt, - AllocatingJobSlot, WaitingToRetry); + AllocatingJobSlot, WaitingToRetry, CancellationPending); Initially( When(JobSubmitted) .InitializeJob() - .RequestJobSlot(this)); + .IfElse(context => context.IsScheduledJob(), + scheduled => scheduled + .IfElse(context => context.CalculateNextStartDate(), + start => start + .WaitForNextScheduledTime(this), + noStart => noStart + .IfElse(context => context.GetPayload().FinalizeCompleted, + final => final.Finalize(), + complete => complete.TransitionTo(Completed) + ) + ), + immediate => immediate + .RequestJobSlot(this) + ) + ); During(AllocatingJobSlot, When(JobSlotAllocated) @@ -96,58 +92,37 @@ public JobStateMachine(JobServiceOptions options) When(JobSlotUnavailable) .WaitForJobSlot(this), When(AllocateJobSlotFaulted) - .WaitForJobSlot(this), - Ignore(CancelJob), - Ignore(RetryJob) + .WaitForJobSlot(this) ); During(WaitingForSlot, When(JobSlotWaitElapsed.Received) - .RequestJobSlot(this), - When(CancelJob) - .Then(context => - { - context.Saga.Faulted = DateTime.UtcNow; - context.Saga.Reason = "Job Cancellation Requested"; - }) - .PublishJobCanceled() - .TransitionTo(Canceled), - Ignore(RetryJob) + .RequestJobSlot(this) ); During(StartingJobAttempt, When(StartJobAttemptFaulted) .Then(context => { + context.AddIncompleteAttempt(context.Message.Message.AttemptId); + context.Saga.Reason = context.Message.Exceptions.FirstOrDefault()?.Message; }) .NotifyJobFaulted() - .TransitionTo(Faulted), - Ignore(CancelJob), - Ignore(RetryJob) - ); - - During(WaitingToStart, - Ignore(CancelJob), - Ignore(RetryJob) + .TransitionTo(Faulted) ); During(Started, Completed, Faulted, - Ignore(JobAttemptCreated), - Ignore(StartJobAttemptFaulted), - Ignore(CancelJob) + Ignore(StartJobAttemptFaulted) ); - During(Started, Completed, - Ignore(RetryJob)); - - During(StartingJobAttempt, WaitingToStart, Started, + During(StartingJobAttempt, Started, When(AttemptStarted) .Then(context => context.Saga.Started = context.Message.Timestamp) .PublishJobStarted() .TransitionTo(Started)); - During(StartingJobAttempt, WaitingToStart, Started, + During(StartingJobAttempt, Started, When(AttemptCompleted) .Then(context => { @@ -157,21 +132,36 @@ public JobStateMachine(JobServiceOptions options) .NotifyJobCompleted() .TransitionTo(Completed)); - During(StartingJobAttempt, WaitingToStart, Started, + During(StartingJobAttempt, Started, When(AttemptFaulted) .Then(context => { + context.AddIncompleteAttempt(context.Message.AttemptId); + context.Saga.Faulted = context.Message.Timestamp; context.Saga.Reason = context.Message.Exceptions?.Message ?? "Job Attempt Faulted (unknown reason)"; }) .IfElse(context => context.Message.RetryDelay.HasValue, retry => retry - .Schedule(JobRetryDelayElapsed, context => context.Init(new { context.Message.JobId }), + .Schedule(JobRetryDelayElapsed, context => new JobRetryDelayElapsedEvent { JobId = context.Message.JobId }, context => context.Message.RetryDelay.Value) .TransitionTo(WaitingToRetry), fault => fault .NotifyJobFaulted() - .TransitionTo(Faulted))); + .IfElse(context => context.IsScheduledJob(), + scheduled => scheduled + .DetermineNextStartDate() + .IfElse(context => context.Saga.NextStartDate.HasValue, + start => start + .WaitForNextScheduledTime(this), + noStart => noStart + .TransitionTo(Faulted) + ), + notScheduled => notScheduled + .TransitionTo(Faulted) + ) + ) + ); During(Completed, When(AttemptCompleted) @@ -180,7 +170,21 @@ public JobStateMachine(JobServiceOptions options) .Then(context => context.Saga.Started = context.Message.Timestamp) .PublishJobStarted(), When(JobCompleted) - .If(_ => options.FinalizeCompleted, x => x.Finalize())); + .FinalizeJobAttempts() + .IfElse(context => context.IsScheduledJob(), + scheduled => scheduled + .DetermineNextStartDate() + .IfElse(context => context.Saga.NextStartDate.HasValue, + start => start + .WaitForNextScheduledTime(this), + noStart => noStart + .If(context => context.GetPayload().FinalizeCompleted, x => x.Finalize() + ) + ), + notScheduled => notScheduled + .If(context => context.GetPayload().FinalizeCompleted, x => x.Finalize()) + ) + ); During(Faulted, When(AttemptFaulted) @@ -189,81 +193,164 @@ public JobStateMachine(JobServiceOptions options) .Then(context => context.Saga.Started = context.Message.Timestamp) .PublishJobStarted()); - During(WaitingToRetry, - Ignore(AttemptFaulted), - When(JobRetryDelayElapsed.Received) + + During(StartingJobAttempt, Started, + When(AttemptCanceled) + .IfElse(context => string.Equals(context.Message.Reason, JobCancellationReasons.Shutdown, StringComparison.Ordinal), + shutdown => shutdown + .SendJobSlotReleased() + .WaitForJobSlot(this), + other => other + .PublishJobCanceled() + .TransitionTo(Canceled) + ) + ); + + During([StartingJobAttempt, Started, Completed, Faulted, Canceled, WaitingToRetry], + When(SetJobProgress) .Then(context => { - context.Saga.AttemptId = NewId.NextGuid(); - context.Saga.RetryAttempt++; - }) - .RequestJobSlot(this), - When(CancelJob) + if (context.Saga.AttemptId == context.Message.AttemptId + && context.Message.SequenceNumber > (context.Saga.LastProgressSequenceNumber ?? 0)) + { + context.Saga.LastProgressValue = context.Message.Value; + context.Saga.LastProgressLimit = context.Message.Limit; + } + })); + + During([Started, Completed, Faulted, Canceled, WaitingToRetry], + When(SaveJobState) .Then(context => { - context.Saga.Faulted = DateTime.UtcNow; - context.Saga.Reason = "Job Cancellation Requested"; + if (context.Saga.AttemptId == context.Message.AttemptId) + context.Saga.JobState = context.Message.JobState; + })); + + DuringAny( + When(GetJobState) + .RespondAsync(async context => new JobStateResponse + { + JobId = context.Message.JobId, + Submitted = context.Saga.Submitted, + Started = context.Saga.Started, + Completed = context.Saga.Completed, + Duration = context.Saga.Duration, + Faulted = context.Saga.Faulted, + Reason = context.Saga.Reason, + LastRetryAttempt = context.Saga.RetryAttempt, + CurrentState = (await Accessor.Get(context).ConfigureAwait(false)).Name, + ProgressValue = context.Saga.LastProgressValue, + ProgressLimit = context.Saga.LastProgressLimit, + JobState = context.Saga.JobState, + NextStartDate = context.Saga.NextStartDate?.UtcDateTime, + IsRecurring = !string.IsNullOrWhiteSpace(context.Saga.CronExpression), + StartDate = context.Saga.StartDate?.UtcDateTime, + EndDate = context.Saga.EndDate?.UtcDateTime }) + ); + + // Cancel Job + During([WaitingForSlot, WaitingToRetry], + When(CancelJob) + .Unschedule(JobSlotWaitElapsed) .PublishJobCanceled() .TransitionTo(Canceled) ); - During(WaitingToRetry, Faulted, Canceled, + During(Canceled, + Ignore(CancelJob), + Ignore(AttemptCanceled)); + + During([StartingJobAttempt, Started], + When(CancelJob) + .CancelCurrentJobAttempt()); + + During(AllocatingJobSlot, + When(CancelJob) + .TransitionTo(CancellationPending)); + + During(CancellationPending, + When(JobSlotAllocated) + .TransitionTo(Canceled), + When(JobSlotUnavailable) + .TransitionTo(Canceled), + When(AllocateJobSlotFaulted) + .TransitionTo(Canceled) + ); + + // Retry Job + During([AllocatingJobSlot, StartingJobAttempt, Started, Completed, CancellationPending], + Ignore(RetryJob)); + + During(WaitingForSlot, When(RetryJob) - .Unschedule(JobRetryDelayElapsed) - .Then(context => - { - context.Saga.AttemptId = NewId.NextGuid(); - context.Saga.RetryAttempt++; - }) + .Unschedule(JobSlotWaitElapsed)); + + During(WaitingToRetry, + When(RetryJob) + .Unschedule(JobRetryDelayElapsed)); + + During(WaitingForSlot, WaitingToRetry, Faulted, Canceled, + When(RetryJob) + .RequestRetryJobSlot(this)); + + During(WaitingToRetry, + Ignore(AttemptFaulted), + When(JobRetryDelayElapsed.Received) + .RequestRetryJobSlot(this)); + + + // Run Job (only accepted while waiting for the scheduled job event) + During([AllocatingJobSlot, StartingJobAttempt, Started, Completed, Canceled, Faulted, WaitingToRetry, CancellationPending], + Ignore(RunJob)); + + During(WaitingForSlot, + When(RunJob) + .Unschedule(JobSlotWaitElapsed) .RequestJobSlot(this)); - During(StartingJobAttempt, WaitingToStart, Started, - When(AttemptCanceled) - .Then(context => - { - context.Saga.Faulted = context.Message.Timestamp; - context.Saga.Reason = "Job Attempt Canceled"; - }) - .PublishJobCanceled() - .TransitionTo(Canceled)); - During(Canceled, - Ignore(CancelJob), - When(AttemptCanceled) - .PublishJobCanceled()); + // Finalize Job (only accepted while waiting for the scheduled job event) + During([WaitingForSlot, AllocatingJobSlot, StartingJobAttempt, Started, Completed, WaitingToRetry], + Ignore(FinalizeJob)); + During(Canceled, Faulted, + When(FinalizeJob) + .FinalizeJobAttempts() + .Finalize()); + + + // Update recurring jobs, otherwise we're just going to any subsequent duplicate job submissions with a warning DuringAny( - When(GetJobState) - .RespondAsync(x => x.Init(new - { - x.Message.JobId, - x.Saga.Submitted, - x.Saga.Started, - x.Saga.Completed, - x.Saga.Faulted, - x.Saga.Reason, - LastRetryAttempt = x.Saga.RetryAttempt, - CurrentState = Accessor.Get(x).ContinueWith(t => t.Result.Name) - }))); - - WhenEnter(Completed, x => x.SendJobSlotReleased(this, JobSlotDisposition.Completed)); - WhenEnter(Canceled, x => x.SendJobSlotReleased(this, JobSlotDisposition.Canceled)); - WhenEnter(Faulted, x => x.SendJobSlotReleased(this, JobSlotDisposition.Faulted)); - WhenEnter(WaitingToRetry, x => x.SendJobSlotReleased(this, JobSlotDisposition.Faulted)); + When(JobSubmitted) + .IfElse(context => context.IsScheduledJob(), x => x.UpdateRecurringJob(), + x => x.Then(context => LogContext.Warning?.Log("Duplicate Job Submission: {JobTypeId} {JobId}", context.Message.JobTypeId, + context.Message.JobId))) + ); + + // if the job is in a state where it could be waiting or idle, update the next scheduled start date + During([WaitingForSlot, Canceled, Completed, Faulted], + When(JobSubmitted) + .If(context => context.IsScheduledJob() && context.CalculateNextStartDate(), + start => start + .WaitForNextScheduledTime(this) + ) + ); + + + WhenEnter(Completed, x => x.SendJobSlotReleased(JobSlotDisposition.Completed)); + WhenEnter(Canceled, x => x.SendJobSlotReleased(JobSlotDisposition.Canceled)); + WhenEnter(Faulted, x => x.SendJobSlotReleased(JobSlotDisposition.Faulted)); + WhenEnter(WaitingToRetry, x => x.SendJobSlotReleased(JobSlotDisposition.Faulted)); SetCompletedWhenFinalized(); } - public Uri JobTypeSagaEndpointAddress => _options.JobTypeSagaEndpointAddress; - public Uri JobSagaEndpointAddress => _options.JobSagaEndpointAddress; - public Uri JobAttemptSagaEndpointAddress => _options.JobAttemptSagaEndpointAddress; - // // ReSharper disable UnassignedGetOnlyAutoProperty // ReSharper disable MemberCanBePrivate.Global public State Submitted { get; } - public State WaitingToStart { get; } + public State WaitingToStart { get; } // no longer used, but do not remove as it would change the CurrentState int values public State WaitingToRetry { get; } public State WaitingForSlot { get; } public State Started { get; } @@ -272,12 +359,12 @@ public JobStateMachine(JobServiceOptions options) public State Faulted { get; } public State AllocatingJobSlot { get; } public State StartingJobAttempt { get; } + public State CancellationPending { get; } public Event JobSlotAllocated { get; } public Event JobSlotUnavailable { get; } public Event> AllocateJobSlotFaulted { get; } - public Event JobAttemptCreated { get; } public Event> StartJobAttemptFaulted { get; } public Event JobSubmitted { get; } @@ -288,8 +375,14 @@ public JobStateMachine(JobServiceOptions options) public Event AttemptFaulted { get; } public Event JobCompleted { get; } + public Event CancelJob { get; } public Event RetryJob { get; } + public Event RunJob { get; } + public Event FinalizeJob { get; } + + public Event SetJobProgress { get; } + public Event SaveJobState { get; } public Event GetJobState { get; } @@ -301,6 +394,72 @@ public JobStateMachine(JobServiceOptions options) static class JobStateMachineBehaviorExtensions { + static Uri GetJobAttemptSagaAddress(this SagaConsumeContext context) + { + return context.GetPayload().JobAttemptSagaEndpointAddress; + } + + static Uri GetJobTypeSagaAddress(this SagaConsumeContext context) + { + return context.GetPayload().JobTypeSagaEndpointAddress; + } + + internal static bool IsScheduledJob(this SagaConsumeContext context) + { + return !string.IsNullOrWhiteSpace(context.Saga.CronExpression) || context.Saga.StartDate is not null; + } + + internal static void AddIncompleteAttempt(this SagaConsumeContext context, Guid attemptId) + { + context.Saga.IncompleteAttempts ??= []; + + if (!context.Saga.IncompleteAttempts.Contains(attemptId)) + context.Saga.IncompleteAttempts.Add(attemptId); + } + + public static bool CalculateNextStartDate(this SagaConsumeContext context) + { + if (string.IsNullOrWhiteSpace(context.Saga.CronExpression)) + { + if (context.Saga.StartDate is not null) + { + // if the start date hasn't changed, clear it and return false (no schedule change) + if (context.Saga.StartDate == context.Saga.NextStartDate) + { + context.Saga.StartDate = null; + return false; + } + + context.Saga.NextStartDate = context.Saga.StartDate.Value; + context.Saga.StartDate = null; + return true; + } + } + + var cronExpression = new CronExpression(context.Saga.CronExpression) { TimeZone = TimeZoneInfo.Utc }; + + var now = DateTimeOffset.UtcNow; + + DateTimeOffset? nextStartDate = cronExpression.GetTimeAfter(context.Saga.StartDate.HasValue + ? context.Saga.StartDate.Value > now + ? context.Saga.StartDate.Value + : now + : now); + + if (nextStartDate != null) + { + if (nextStartDate.Value > context.Saga.EndDate) + nextStartDate = null; + } + + // the next start date didn't change, so don't bother with it + if (nextStartDate == context.Saga.NextStartDate) + return false; + + context.Saga.NextStartDate = nextStartDate; + return true; + } + public static EventActivityBinder InitializeJob(this EventActivityBinder binder) { return binder.Then(context => @@ -308,156 +467,291 @@ public static EventActivityBinder InitializeJob(this Even context.Saga.Submitted = context.Message.Timestamp; context.Saga.Job = context.Message.Job; - context.Saga.ServiceAddress = context.GetPayload().SourceAddress; + context.Saga.ServiceAddress = context.SourceAddress; context.Saga.JobTimeout = context.Message.JobTimeout; context.Saga.JobTypeId = context.Message.JobTypeId; + SetJobProperties(context); + + if (context.Message.Schedule != null) + { + context.Saga.CronExpression = context.Message.Schedule.CronExpression; + context.Saga.TimeZoneId = context.Message.Schedule.TimeZoneId; + context.Saga.StartDate = context.Message.Schedule.Start; + context.Saga.EndDate = context.Message.Schedule.End; + } + context.Saga.AttemptId = NewId.NextGuid(); }); } - public static EventActivityBinder RequestJobSlot(this EventActivityBinder binder, - JobStateMachine machine) + public static EventActivityBinder UpdateRecurringJob(this EventActivityBinder binder) + { + return binder.Then(context => + { + context.Saga.Job = context.Message.Job; + + if (context.Message.Schedule != null) + { + context.Saga.CronExpression = context.Message.Schedule.CronExpression; + context.Saga.TimeZoneId = context.Message.Schedule.TimeZoneId; + context.Saga.StartDate = context.Message.Schedule.Start; + context.Saga.EndDate = context.Message.Schedule.End; + } + + SetJobProperties(context); + }); + } + + static void SetJobProperties(BehaviorContext context) + { + if (context.Message.JobProperties is { Count: > 0 }) + { + context.Saga.JobProperties ??= new Dictionary(context.Message.JobProperties.Count, StringComparer.OrdinalIgnoreCase); + context.Saga.JobProperties.SetValues(context.Message.JobProperties); + } + } + + public static EventActivityBinder RequestJobSlot(this EventActivityBinder binder, JobStateMachine machine) where T : class { - return binder.SendAsync(context => machine.JobTypeSagaEndpointAddress, - context => context.Init(new + return binder + .Send(context => context.GetJobTypeSagaAddress(), + context => new AllocateJobSlotCommand { JobId = context.Saga.CorrelationId, - context.Saga.JobTypeId, - context.Saga.JobTimeout - }), context => context.ResponseAddress = machine.JobSagaEndpointAddress) + JobTypeId = context.Saga.JobTypeId, + JobTimeout = context.Saga.JobTimeout ?? TimeSpan.Zero, + JobProperties = context.Saga.JobProperties + }, (behaviorContext, context) => context.ResponseAddress = behaviorContext.ReceiveContext.InputAddress) .TransitionTo(machine.AllocatingJobSlot); } + public static EventActivityBinder RequestRetryJobSlot(this EventActivityBinder binder, JobStateMachine machine) + where T : class + { + return binder + .Then(context => + { + context.Saga.AttemptId = NewId.NextGuid(); + context.Saga.RetryAttempt++; + }) + .RequestJobSlot(machine); + ; + } + + public static EventActivityBinder ClearJobState(this EventActivityBinder binder) + where T : class + { + return binder + .Then(context => + { + context.Saga.LastProgressValue = null; + context.Saga.LastProgressLimit = null; + context.Saga.LastProgressSequenceNumber = null; + context.Saga.JobState = null; + }); + } + public static EventActivityBinder RequestStartJob(this EventActivityBinder binder, JobStateMachine machine) { - return binder.SendAsync(context => machine.JobAttemptSagaEndpointAddress, - context => context.Init(new + return binder + .Send(context => context.GetJobAttemptSagaAddress(), + context => new StartJobAttemptCommand { JobId = context.Saga.CorrelationId, - context.Saga.AttemptId, - context.Saga.ServiceAddress, - context.Message.InstanceAddress, - context.Saga.RetryAttempt, - context.Saga.Job, - context.Saga.JobTypeId - }), context => context.ResponseAddress = machine.JobSagaEndpointAddress) + AttemptId = context.Saga.AttemptId, + ServiceAddress = context.Saga.ServiceAddress, + InstanceAddress = context.Message.InstanceAddress, + RetryAttempt = context.Saga.RetryAttempt, + Job = context.Saga.Job, + JobTypeId = context.Saga.JobTypeId, + LastProgressValue = context.Saga.LastProgressValue, + LastProgressLimit = context.Saga.LastProgressLimit, + JobState = context.Saga.JobState, + JobProperties = context.Saga.JobProperties + }, (behaviorContext, context) => context.ResponseAddress = behaviorContext.ReceiveContext.InputAddress) .TransitionTo(machine.StartingJobAttempt); } + public static EventActivityBinder FinalizeJobAttempts(this EventActivityBinder binder) + where T : class + { + return binder.ThenAsync(async context => + { + if (context.Saga.IncompleteAttempts is { Count: > 0 }) + { + var endpoint = await context.GetSendEndpoint(context.GetJobAttemptSagaAddress()); + + foreach (var attemptId in context.Saga.IncompleteAttempts) + { + _ = endpoint.Send(new FinalizeJobAttemptCommand + { + JobId = context.Saga.CorrelationId, + AttemptId = attemptId + }); + } + + context.Saga.IncompleteAttempts = null; + } + }); + } + + public static EventActivityBinder CancelCurrentJobAttempt(this EventActivityBinder binder) + { + return binder.Send(context => context.GetJobAttemptSagaAddress(), + context => new CancelJobAttemptCommand + { + JobId = context.Saga.CorrelationId, + AttemptId = context.Saga.AttemptId, + Reason = context.Message.Reason ?? JobCancellationReasons.CancellationRequested + }); + } + public static EventActivityBinder WaitForJobSlot(this EventActivityBinder binder, JobStateMachine machine) where T : class { - return binder.Schedule(machine.JobSlotWaitElapsed, context => context.Init(new { JobId = context.Saga.CorrelationId })) + return binder.Schedule(machine.JobSlotWaitElapsed, context => new JobSlotWaitElapsedEvent { JobId = context.Saga.CorrelationId }) + .TransitionTo(machine.WaitingForSlot); + } + + public static EventActivityBinder WaitForNextScheduledTime(this EventActivityBinder binder, JobStateMachine machine) + where T : class + { + return binder + .ClearJobState() + .Schedule(machine.JobSlotWaitElapsed, context => new JobSlotWaitElapsedEvent { JobId = context.Saga.CorrelationId }, + context => context.Saga.NextStartDate.Value.DateTime) .TransitionTo(machine.WaitingForSlot); } - public static EventActivityBinder SendJobSlotReleased(this EventActivityBinder binder, JobStateMachine machine, - JobSlotDisposition disposition) + public static EventActivityBinder DetermineNextStartDate(this EventActivityBinder binder) + where T : class + { + return binder.Then(context => + { + context.CalculateNextStartDate(); + }); + } + + public static EventActivityBinder SendJobSlotReleased(this EventActivityBinder binder, JobSlotDisposition disposition) + { + return binder.Send(context => context.GetJobTypeSagaAddress(), context => new JobSlotReleasedEvent + { + JobId = context.Saga.CorrelationId, + JobTypeId = context.Saga.JobTypeId, + Disposition = disposition == JobSlotDisposition.Faulted && context.Saga.Reason.Contains("(Suspect)") + ? JobSlotDisposition.Suspect + : disposition + }); + } + + public static EventActivityBinder SendJobSlotReleased(this EventActivityBinder binder) { - return binder.SendAsync(context => machine.JobTypeSagaEndpointAddress, - context => context.Init(new + return binder.Send(context => context.GetJobTypeSagaAddress(), + context => new JobSlotReleasedEvent { JobId = context.Saga.CorrelationId, - context.Saga.JobTypeId, - Disposition = disposition == JobSlotDisposition.Faulted && context.Saga.Reason.Contains("(Suspect)") - ? JobSlotDisposition.Suspect - : disposition - })); + JobTypeId = context.Saga.JobTypeId, + Disposition = JobSlotDisposition.Canceled + }); } public static EventActivityBinder PublishJobStarted(this EventActivityBinder binder) { - return binder.PublishAsync(context => context.Init(new + return binder.Publish(context => new JobStartedEvent { JobId = context.Saga.CorrelationId, - context.Message.AttemptId, - context.Message.RetryAttempt, - context.Message.Timestamp - })); + AttemptId = context.Message.AttemptId, + RetryAttempt = context.Message.RetryAttempt, + Timestamp = context.Message.Timestamp + }); } public static EventActivityBinder NotifyJobCompleted(this EventActivityBinder binder) { - return binder.SendAsync(context => context.Saga.ServiceAddress, - context => context.Init(new + return binder + .Send(context => context.Saga.ServiceAddress, + context => new CompleteJobCommand + { + JobId = context.Saga.CorrelationId, + Job = context.Saga.Job, + JobTypeId = context.Saga.JobTypeId, + Timestamp = context.Message.Timestamp, + Duration = context.Message.Timestamp - context.Saga.Started ?? TimeSpan.Zero + }) + .Publish(context => new JobCompletedEvent { JobId = context.Saga.CorrelationId, - context.Saga.Job, - context.Saga.JobTypeId, - context.Message.Timestamp, - Duration = context.Message.Timestamp - context.Saga.Started ?? TimeSpan.Zero - })).PublishAsync(context => context.Init(new - { - JobId = context.Saga.CorrelationId, - context.Saga.Job, - context.Message.Timestamp, - context.Message.Duration - })); + Job = context.Saga.Job, + Timestamp = context.Message.Timestamp, + Duration = context.Message.Duration + }); } public static EventActivityBinder NotifyJobFaulted(this EventActivityBinder binder) { - return binder.PublishAsync(context => context.Init(new - { - JobId = context.Saga.CorrelationId, - context.Saga.Job, - context.Message.Exceptions, - context.Message.Timestamp, - Duration = context.Message.Timestamp - context.Saga.Started - })).SendAsync(context => context.Saga.ServiceAddress, - context => context.Init(new + return binder + .Send(context => context.Saga.ServiceAddress, + context => new FaultJobCommand + { + JobId = context.Saga.CorrelationId, + Job = context.Saga.Job, + JobTypeId = context.Saga.JobTypeId, + AttemptId = context.Saga.AttemptId, + RetryAttempt = context.Saga.RetryAttempt, + Exceptions = context.Message.Exceptions, + Duration = context.Message.Timestamp - context.Saga.Started + }) + .Publish(context => new JobFaultedEvent { JobId = context.Saga.CorrelationId, - context.Saga.Job, - context.Saga.JobTypeId, - context.Saga.AttemptId, - context.Saga.RetryAttempt, - context.Message.Exceptions, + Job = context.Saga.Job, + Exceptions = context.Message.Exceptions, + Timestamp = context.Message.Timestamp, Duration = context.Message.Timestamp - context.Saga.Started - })); + }); } - public static EventActivityBinder PublishJobCanceled(this EventActivityBinder binder) - { - return binder.PublishAsync(context => context.Init(new - { - JobId = context.Saga.CorrelationId, - context.Message.Timestamp - })); - } - - public static EventActivityBinder PublishJobCanceled(this EventActivityBinder binder) + public static EventActivityBinder PublishJobCanceled(this EventActivityBinder binder, string reason = null) + where T : class { - return binder.PublishAsync(context => context.Init(new - { - JobId = context.Saga.CorrelationId, - context.Message.Timestamp - })); + return binder + .Then(context => + { + context.Saga.Faulted = DateTime.UtcNow; + context.Saga.Reason = reason ?? JobCancellationReasons.CancellationRequested; + }) + .Publish(context => new JobCanceledEvent + { + JobId = context.Saga.CorrelationId, + Timestamp = context.Saga.Faulted.Value + }); } public static EventActivityBinder> NotifyJobFaulted(this EventActivityBinder> binder) { - return binder.SendAsync(context => context.Saga.ServiceAddress, - context => context.Init(new + return binder + .Send, FaultJob>(context => context.Saga.ServiceAddress, + context => new FaultJobCommand + { + JobId = context.Saga.CorrelationId, + Job = context.Saga.Job, + JobTypeId = context.Saga.JobTypeId, + AttemptId = context.Saga.AttemptId, + RetryAttempt = context.Saga.RetryAttempt, + Exceptions = context.Message.Exceptions?.FirstOrDefault(), + Duration = context.Message.Timestamp - context.Saga.Started + }) + .Publish, JobFaulted>(context => new JobFaultedEvent { JobId = context.Saga.CorrelationId, - context.Saga.Job, - context.Saga.JobTypeId, - context.Saga.AttemptId, - context.Saga.RetryAttempt, - context.Message.Exceptions, + Job = context.Saga.Job, + Exceptions = context.Message.Exceptions?.FirstOrDefault(), + Timestamp = context.Message.Timestamp, Duration = context.Message.Timestamp - context.Saga.Started - })).PublishAsync(context => context.Init(new - { - JobId = context.Saga.CorrelationId, - context.Saga.Job, - context.Message.Exceptions, - context.Message.Timestamp, - Duration = context.Message.Timestamp - context.Saga.Started - })); + }); } } } diff --git a/src/MassTransit/JobService/JobTypeInfo.cs b/src/MassTransit/JobService/JobTypeInfo.cs new file mode 100644 index 00000000000..226dfb7b884 --- /dev/null +++ b/src/MassTransit/JobService/JobTypeInfo.cs @@ -0,0 +1,34 @@ +#nullable enable +namespace MassTransit; + +using System; +using System.Collections.Generic; + + +public interface JobTypeInfo +{ + /// + /// The job type name, supplied by the job consumer + /// + string Name { get; } + + /// + /// Set the concurrent job limit. The limit is applied to each instance if the job consumer is scaled out. + /// + int ConcurrentJobLimit { get; } + + /// + /// Job properties configured by + /// + IReadOnlyDictionary Properties { get; } + + /// + /// Currently active jobs for this job type across all instances + /// + IReadOnlyList ActiveJobs { get; } + + /// + /// Currently active instances for this job type, that aren't suspect/dead + /// + IReadOnlyDictionary Instances { get; } +} diff --git a/src/MassTransit/JobService/JobTypeInstance.cs b/src/MassTransit/JobService/JobTypeInstance.cs index 429a08dd6b8..5c25de2b7be 100644 --- a/src/MassTransit/JobService/JobTypeInstance.cs +++ b/src/MassTransit/JobService/JobTypeInstance.cs @@ -1,11 +1,13 @@ -namespace MassTransit -{ - using System; +#nullable enable +namespace MassTransit; + +using System; +using System.Collections.Generic; - public class JobTypeInstance - { - public DateTime? Updated { get; set; } - public DateTime? Used { get; set; } - } +public class JobTypeInstance +{ + public DateTime? Updated { get; set; } + public DateTime? Used { get; set; } + public Dictionary? Properties { get; set; } } diff --git a/src/MassTransit/JobService/JobTypeSaga.cs b/src/MassTransit/JobService/JobTypeSaga.cs index 5ec9c413421..3c60dfffa4e 100644 --- a/src/MassTransit/JobService/JobTypeSaga.cs +++ b/src/MassTransit/JobService/JobTypeSaga.cs @@ -1,62 +1,79 @@ -namespace MassTransit +namespace MassTransit; + +using System; +using System.Collections.Generic; + + +/// +/// Every job type has one entry in this state machine +/// +public class JobTypeSaga : + SagaStateMachineInstance, + JobTypeInfo, + ISagaVersion { - using System; - using System.Collections.Generic; + public JobTypeSaga() + { + ConcurrentJobLimit = 1; + + Instances = new Dictionary(); + ActiveJobs = []; + } + public int CurrentState { get; set; } + + public int ActiveJobCount { get; set; } /// - /// Every job type has one entry in this state machine + /// The concurrent job limit, which is configured by the job options. Initially, it defaults to one when the state machine + /// is created. Once a service endpoint starts, that endpoint sends a command to set the configure concurrent job limit. /// - public class JobTypeSaga : - SagaStateMachineInstance, - ISagaVersion - { - public JobTypeSaga() - { - ConcurrentJobLimit = 1; + public int ConcurrentJobLimit { get; set; } - Instances = new Dictionary(); - ActiveJobs = new List(); - } + /// + /// The job limit may be overridden temporarily, to either reduce or increase the number of concurrent jobs. Once the + /// override job limit expires, the concurrent job limit returns to the original value. + /// + public int? OverrideJobLimit { get; set; } - public int CurrentState { get; set; } + /// + /// If an is specified, the time when the override job limit expires + /// + public DateTime? OverrideLimitExpiration { get; set; } - public int ActiveJobCount { get; set; } + /// + /// The last known active jobs + /// + public List ActiveJobs { get; set; } - /// - /// The concurrent job limit, which is configured by the job options. Initially, it defaults to one when the state machine - /// is created. Once a service endpoint starts, that endpoint sends a command to set the configure concurrent job limit. - /// - public int ConcurrentJobLimit { get; set; } + /// + /// Tracks the instances, when they were last updated + /// + public Dictionary Instances { get; set; } - /// - /// The job limit may be overridden temporarily, to either reduce or increase the number of concurrent jobs. Once the - /// override job limit expires, the concurrent job limit returns to the original value. - /// - public int? OverrideJobLimit { get; set; } + /// + /// Job properties passed by the configuration + /// + public Dictionary Properties { get; set; } - /// - /// If an is specified, the time when the override job limit expires - /// - public DateTime? OverrideLimitExpiration { get; set; } + public byte[] RowVersion { get; set; } - /// - /// The last known active jobs - /// - public List ActiveJobs { get; set; } + public int? GlobalConcurrentJobLimit { get; set; } - /// - /// Tracks the instances, when they were last updated - /// - public Dictionary Instances { get; set; } + public int Version { get; set; } - public byte[] RowVersion { get; set; } + /// + /// The name of the job type + /// + public string Name { get; set; } - public int Version { get; set; } + int JobTypeInfo.ConcurrentJobLimit => OverrideJobLimit ?? ConcurrentJobLimit; + IReadOnlyList JobTypeInfo.ActiveJobs => ActiveJobs; + IReadOnlyDictionary JobTypeInfo.Instances => Instances; + IReadOnlyDictionary JobTypeInfo.Properties => Properties ?? []; - /// - /// The MD5 hash of the job type - /// - public Guid CorrelationId { get; set; } - } + /// + /// The MD5 hash of the job type + /// + public Guid CorrelationId { get; set; } } diff --git a/src/MassTransit/JobService/JobTypeStateMachine.cs b/src/MassTransit/JobService/JobTypeStateMachine.cs index 868552de8d7..d3f2afff422 100644 --- a/src/MassTransit/JobService/JobTypeStateMachine.cs +++ b/src/MassTransit/JobService/JobTypeStateMachine.cs @@ -3,13 +3,19 @@ namespace MassTransit using System; using System.Collections.Generic; using System.Linq; + using System.Threading.Tasks; + using Configuration; using Contracts.JobService; + using Internals; + using JobService; + using JobService.Messages; + using Microsoft.Extensions.DependencyInjection; public sealed class JobTypeStateMachine : MassTransitStateMachine { - public JobTypeStateMachine(JobServiceOptions options) + public JobTypeStateMachine() { Event(() => JobSlotRequested, x => { @@ -27,11 +33,12 @@ public JobTypeStateMachine(JobServiceOptions options) During(Initial, Active, Idle, When(JobSlotRequested) - .IfElse(context => context.IsSlotAvailable(options.HeartbeatTimeout), + .IfElseAsync(context => context.IsSlotAvailable(context.GetPayload().HeartbeatTimeout), allocate => allocate .TransitionTo(Active), unavailable => unavailable - .RespondAsync(context => context.Init(new { context.Message.JobId })))); + .Respond(context => + new JobSlotUnavailableResponse { JobId = context.Message.JobId }))); During(Active, When(JobSlotReleased) @@ -58,6 +65,8 @@ public JobTypeStateMachine(JobServiceOptions options) .If(context => context.Saga.ActiveJobCount == 0, empty => empty.TransitionTo(Idle))); + During(Idle, + Ignore(JobSlotReleased)); During(Initial, When(SetConcurrentJobLimit) @@ -84,7 +93,7 @@ public JobTypeStateMachine(JobServiceOptions options) static class JobTypeStateMachineBehaviorExtensions { - public static bool IsSlotAvailable(this BehaviorContext context, TimeSpan heartbeatTimeout) + public static async Task IsSlotAvailable(this BehaviorContext context, TimeSpan heartbeatTimeout) { if (context.Saga.OverrideLimitExpiration.HasValue) { @@ -104,47 +113,58 @@ public static bool IsSlotAvailable(this BehaviorContext> expiredInstances = context.Saga.Instances.Where(x => timestamp - x.Value.Updated > heartbeatTimeout).ToList(); + foreach (KeyValuePair instance in expiredInstances) context.Saga.Instances.Remove(instance.Key); - var concurrentJobLimit = context.Saga.OverrideJobLimit ?? context.Saga.ConcurrentJobLimit; + if (context.Saga.GlobalConcurrentJobLimit.HasValue && context.Saga.ActiveJobCount >= context.Saga.GlobalConcurrentJobLimit) + return false; - var instances = from i in context.Saga.Instances - join a in context.Saga.ActiveJobs on i.Key equals a.InstanceAddress into ai - where ai.Count() < concurrentJobLimit - orderby ai.Count(), i.Value.Used - select new - { - InstanceAddress = i.Key, - InstanceCount = ai.Count() - }; + var strategy = context.GetJobDistributionStrategyOrUseDefault(); - var nextInstance = instances.FirstOrDefault(); - if (nextInstance == null) + var activeJob = await strategy.IsJobSlotAvailable(context, context.Saga).ConfigureAwait(false); + if (activeJob == null) return false; - context.Saga.ActiveJobCount++; - context.Saga.ActiveJobs.Add(new ActiveJob + var activeInstance = context.Saga.Instances.TryGetValue(activeJob.InstanceAddress, out var value) ? value : null; + if (activeInstance == null) { - JobId = jobId, - Deadline = timestamp + context.Message.JobTimeout, - InstanceAddress = nextInstance.InstanceAddress - }); + LogContext.Warning?.Log("Job Distribution Strategy returned unknown instance address: {InstanceAddress}", activeJob.InstanceAddress); + return false; + } + + activeInstance.Used = timestamp; - context.Saga.Instances[nextInstance.InstanceAddress].Used = timestamp; + activeJob.Deadline = timestamp + context.Message.JobTimeout; + activeJob.Properties = context.Message.JobProperties; + + context.Saga.ActiveJobCount++; + context.Saga.ActiveJobs.Add(activeJob); LogContext.Debug?.Log("Allocated Job Slot: {JobId} ({JobCount}): {InstanceAddress} ({InstanceCount})", jobId, context.Saga.ActiveJobCount, - nextInstance.InstanceAddress, nextInstance.InstanceCount + 1); + activeJob.InstanceAddress, context.Saga.ActiveJobs.Count(x => x.InstanceAddress == activeJob.InstanceAddress)); - context.RespondAsync(new + await context.RespondAsync(new JobSlotAllocatedResponse { - jobId, - nextInstance.InstanceAddress, + JobId = jobId, + InstanceAddress = activeJob.InstanceAddress, }); return true; } + static IJobDistributionStrategy GetJobDistributionStrategyOrUseDefault(this ConsumeContext context) + { + IJobDistributionStrategy strategy = null; + + if (context.TryGetPayload(out IServiceScope serviceScope)) + strategy = serviceScope.ServiceProvider.GetService(); + else if (context.TryGetPayload(out IServiceProvider serviceProvider)) + strategy = serviceProvider.GetService(); + + return strategy ?? DefaultJobDistributionStrategy.Instance; + } + public static EventActivityBinder SetConcurrentLimit( this EventActivityBinder binder) { @@ -170,18 +190,39 @@ public static EventActivityBinder SetConcurr { if (context.Message.Kind != ConcurrentLimitKind.Stopped) { - context.Saga.Instances.Add(instanceAddress, new JobTypeInstance { Updated = instanceUpdated }); + instance = new JobTypeInstance { Updated = instanceUpdated }; + + context.Saga.Instances.Add(instanceAddress, instance); LogContext.Debug?.Log("Job Service Instance Started: {InstanceAddress}", instanceAddress); } } + + if (context.Message.Kind != ConcurrentLimitKind.Stopped) + { + if (context.Message.InstanceProperties is { Count: > 0 }) + { + instance.Properties ??= new Dictionary(context.Message.JobTypeProperties.Count, StringComparer.OrdinalIgnoreCase); + instance.Properties.SetValues(context.Message.InstanceProperties); + } + } } if (context.Message.Kind == ConcurrentLimitKind.Configured) { context.Saga.ConcurrentJobLimit = context.Message.ConcurrentJobLimit; + context.Saga.GlobalConcurrentJobLimit = context.Message.GlobalConcurrentJobLimit; + context.Saga.Name = context.Message.JobTypeName; + + if (context.Message.JobTypeProperties is { Count: > 0 }) + { + context.Saga.Properties ??= new Dictionary(context.Message.JobTypeProperties.Count, StringComparer.OrdinalIgnoreCase); + + context.Saga.Properties.SetValues(context.Message.JobTypeProperties); + } - LogContext.Debug?.Log("Concurrent Job Limit: {ConcurrencyLimit}", context.Saga.ConcurrentJobLimit); + LogContext.Debug?.Log("Concurrent Job Limit: {ConcurrencyLimit} {JobTypeName}", context.Saga.ConcurrentJobLimit, + context.Message.JobTypeName); } else if (context.Message.Kind == ConcurrentLimitKind.Override) { diff --git a/src/MassTransit/JobService/RecurringJobConsumerExtensions.cs b/src/MassTransit/JobService/RecurringJobConsumerExtensions.cs new file mode 100644 index 00000000000..14eb864c7a4 --- /dev/null +++ b/src/MassTransit/JobService/RecurringJobConsumerExtensions.cs @@ -0,0 +1,293 @@ +namespace MassTransit; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Contracts.JobService; +using Initializers; +using JobService; +using JobService.Messages; + + +public static class RecurringJobConsumerExtensions +{ + /// + /// Add or update a recurring job + /// + /// An existing request client + /// + /// + /// The scheduler cron expression + /// + /// + /// + public static async Task AddOrUpdateRecurringJob(this IRequestClient> client, string jobName, T job, string cronExpression, + CancellationToken cancellationToken = default) + where T : class + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + if (cronExpression == null) + throw new ArgumentNullException(nameof(cronExpression)); + + var jobId = JobMetadataCache.GenerateRecurringJobId(jobName); + + var schedule = new RecurringJobScheduleInfo { CronExpression = cronExpression }; + schedule.Validate().ThrowIfContainsFailure("The schedule configuration is invalid:"); + + Response response = await client.GetResponse(new SubmitJobCommand + { + JobId = jobId, + Job = job, + Schedule = schedule + }, cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Add or update a recurring job + /// + /// An existing request client + /// + /// + /// Configure the optional recurring job schedule parameters + /// + /// + /// + public static async Task AddOrUpdateRecurringJob(this IRequestClient> client, string jobName, T job, + Action configure, CancellationToken cancellationToken = default) + where T : class + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + + var jobId = JobMetadataCache.GenerateRecurringJobId(jobName); + + var schedule = new RecurringJobScheduleInfo(); + configure?.Invoke(schedule); + + schedule.Validate().ThrowIfContainsFailure("The schedule configuration is invalid:"); + + Response response = await client.GetResponse(new SubmitJobCommand + { + JobId = jobId, + Job = job, + Schedule = schedule + }, cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Add or update a recurring job + /// + /// An available publish endpoint instance + /// + /// + /// The scheduler cron expression + /// + /// + /// + public static async Task AddOrUpdateRecurringJob(this IPublishEndpoint publishEndpoint, string jobName, T job, string cronExpression, + CancellationToken cancellationToken = default) + where T : class + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + if (cronExpression == null) + throw new ArgumentNullException(nameof(cronExpression)); + + var jobId = JobMetadataCache.GenerateRecurringJobId(jobName); + + var schedule = new RecurringJobScheduleInfo { CronExpression = cronExpression }; + schedule.Validate().ThrowIfContainsFailure("The schedule configuration is invalid:"); + + await publishEndpoint.Publish>(new SubmitJobCommand + { + JobId = jobId, + Job = job, + Schedule = schedule + }, cancellationToken).ConfigureAwait(false); + + return jobId; + } + + /// + /// Add or update a recurring job + /// + /// An available publish endpoint instance + /// + /// + /// Configure the optional recurring job schedule parameters + /// + /// + /// + public static async Task AddOrUpdateRecurringJob(this IPublishEndpoint publishEndpoint, string jobName, T job, + Action configure, CancellationToken cancellationToken = default) + where T : class + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + + var jobId = JobMetadataCache.GenerateRecurringJobId(jobName); + + var schedule = new RecurringJobScheduleInfo(); + configure?.Invoke(schedule); + + schedule.Validate().ThrowIfContainsFailure("The schedule configuration is invalid:"); + + await publishEndpoint.Publish>(new SubmitJobCommand + { + JobId = jobId, + Job = job, + Schedule = schedule + }, cancellationToken).ConfigureAwait(false); + + return jobId; + } + + /// + /// Submits a job, returning the generated jobId + /// + /// + /// The start time for the job + /// + /// + /// + /// + public static async Task ScheduleJob(this IPublishEndpoint publishEndpoint, DateTimeOffset start, T job, + CancellationToken cancellationToken = default) + where T : class + { + var jobId = NewId.NextGuid(); + + await publishEndpoint.Publish>(new SubmitJobCommand + { + JobId = jobId, + Job = job, + Schedule = new RecurringJobScheduleInfo { Start = start.ToUniversalTime() } + }, cancellationToken).ConfigureAwait(false); + + return jobId; + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// The start time for the job + /// + /// + /// + /// + public static async Task ScheduleJob(this IRequestClient> client, DateTimeOffset start, T job, + CancellationToken cancellationToken = default) + where T : class + { + var jobId = NewId.NextGuid(); + + Response response = await client.GetResponse(new SubmitJobCommand + { + JobId = jobId, + Job = job, + Schedule = new RecurringJobScheduleInfo { Start = start.ToUniversalTime() } + }, cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// The start time for the job + /// + /// + /// + /// + public static async Task ScheduleJob(this IRequestClient> client, DateTimeOffset start, object job, + CancellationToken cancellationToken = default) + where T : class + { + var jobId = NewId.NextGuid(); + + InitializeContext context = await MessageInitializerCache.Initialize(job, cancellationToken).ConfigureAwait(false); + + Response response = await client.GetResponse(new SubmitJobCommand + { + JobId = jobId, + Job = context.Message, + Schedule = new RecurringJobScheduleInfo { Start = start.ToUniversalTime() } + }, cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// Specify an explicit jobId for the job + /// The start time for the job + /// + /// + /// + /// + public static async Task ScheduleJob(this IRequestClient> client, Guid jobId, DateTimeOffset start, T job, + CancellationToken cancellationToken = default) + where T : class + { + Response response = await client.GetResponse(new SubmitJobCommand + { + JobId = jobId, + Job = job, + Schedule = new RecurringJobScheduleInfo { Start = start.ToUniversalTime() } + }, cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Submits a job, returning the accepted jobId + /// + /// + /// Specify an explicit jobId for the job + /// The start time for the job + /// + /// + /// + /// + public static async Task ScheduleJob(this IRequestClient> client, Guid jobId, DateTimeOffset start, object job, + CancellationToken cancellationToken = default) + where T : class + { + InitializeContext context = await MessageInitializerCache.Initialize(job, cancellationToken).ConfigureAwait(false); + + Response response = await client.GetResponse(new SubmitJobCommand + { + JobId = jobId, + Job = context.Message, + Schedule = new RecurringJobScheduleInfo { Start = start.ToUniversalTime() } + }, cancellationToken).ConfigureAwait(false); + + return response.Message.JobId; + } + + /// + /// Run a recurring job if it's currently waiting/scheduled to run + /// + /// + /// + /// + public static Task RunRecurringJob(this IPublishEndpoint publishEndpoint, string jobName) + where T : class + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + + var jobId = JobMetadataCache.GenerateRecurringJobId(jobName); + + return publishEndpoint.Publish(new RunJobCommand { JobId = jobId }); + } +} diff --git a/src/MassTransit/JobService/RecurringJobScheduleConfiguratorExtensions.cs b/src/MassTransit/JobService/RecurringJobScheduleConfiguratorExtensions.cs new file mode 100644 index 00000000000..2bdbaa5359a --- /dev/null +++ b/src/MassTransit/JobService/RecurringJobScheduleConfiguratorExtensions.cs @@ -0,0 +1,200 @@ +#nullable enable +namespace MassTransit; + +using System; + + +public static class RecurringJobScheduleConfiguratorExtensions +{ + /// + /// Sets the cron expression to run daily at the specified hour (and optionally, minute and second) + /// + /// + /// + /// + /// + /// + public static IRecurringJobScheduleConfigurator DailyAt(this IRecurringJobScheduleConfigurator configurator, int hour, int minute = 0, int second = 0) + { + ValidateHour(hour); + ValidateMinute(minute); + ValidateSecond(second); + + configurator.CronExpression = $"0 {minute} {hour} ? * *"; + + return configurator; + } + + /// + /// Sets the cron expression to run on the specified days of the week at the specified hour and minute + /// + /// + /// + /// + /// + /// + public static IRecurringJobScheduleConfigurator At(this IRecurringJobScheduleConfigurator configurator, int hour, int minute, params DayOfWeek[] days) + { + ValidateHour(hour); + ValidateMinute(minute); + + if (days is null || days.Length == 0) + throw new ArgumentException("At least one day of the week must be specified", nameof(days)); + + var cronExpression = $"0 {minute} {hour} ? * {(int)days[0] + 1}"; + + for (var i = 1; i < days.Length; i++) + cronExpression = cronExpression + "," + ((int)days[i] + 1); + + configurator.CronExpression = cronExpression; + + return configurator; + } + + /// + /// Sets the cron expression to run on the specified days of the week at the specified hour and minute + /// + /// + /// + /// If specified, job will run every hours at : + /// + /// + /// + /// + /// + /// + public static IRecurringJobScheduleConfigurator Every(this IRecurringJobScheduleConfigurator configurator, int? hours = default, int? minutes = null, + int? seconds = null, int? hour = null, int? minute = null, int? second = null, DayOfWeek[]? days = null) + { + string? cronExpression; + if (hours.HasValue) + cronExpression = $"{second?.ToString() ?? "0"} {minute?.ToString() ?? "0"} {hour?.ToString() ?? "0"}/{hours} * * "; + else if (minutes.HasValue) + cronExpression = $"{second?.ToString() ?? "0"} {minute?.ToString() ?? "0"}/{minutes} {hour?.ToString() ?? "*"} * * "; + else if (seconds.HasValue) + cronExpression = $"{second?.ToString() ?? "0"}/{seconds} {minute?.ToString() ?? "*"} {hour?.ToString() ?? "*"} * * "; + else + throw new ArgumentException("At least one time interval must be specified"); + + if (days is { Length: > 0 }) + { + cronExpression += $"{(int)days[0] + 1}"; + for (var i = 1; i < days.Length; i++) + cronExpression = cronExpression + "," + ((int)days[i] + 1); + } + else + cronExpression += "*"; + + configurator.CronExpression = cronExpression; + + return configurator; + } + + /// + /// Sets the cron expression to run weekly on the specified day at the specified hour and minute + /// + /// + /// The day of the week to run + /// + /// + /// + public static IRecurringJobScheduleConfigurator Weekly(this IRecurringJobScheduleConfigurator configurator, DayOfWeek dayOfWeek, int hour, int minute = 0) + { + ValidateHour(hour); + ValidateMinute(minute); + + configurator.CronExpression = $"0 {minute} {hour} ? * {(int)dayOfWeek + 1}"; + + return configurator; + } + + /// + /// Sets the cron expression to run monthly on the specified day of the month at the specified hour and minute + /// + /// + /// The day of the month to run + /// + /// + /// + public static IRecurringJobScheduleConfigurator Monthly(this IRecurringJobScheduleConfigurator configurator, int dayOfMonth, int hour, int minute = 0) + { + ValidateHour(hour); + ValidateMinute(minute); + ValidateDayOfMonth(dayOfMonth); + + configurator.CronExpression = $"0 {minute} {hour} {dayOfMonth} * ?"; + + return configurator; + } + + /// + /// Sets the cron expression to run annually on the specified day of the month of the specified month at the specified hour and minute + /// + /// + /// The month of the year to run + /// The day of the month to run + /// + /// + /// + public static IRecurringJobScheduleConfigurator Yearly(this IRecurringJobScheduleConfigurator configurator, int month, int dayOfMonth, int hour, + int minute = 0) + { + ValidateMonth(month); + ValidateHour(hour); + ValidateMinute(minute); + ValidateDayOfMonth(dayOfMonth); + + configurator.CronExpression = $"0 {minute} {hour} {dayOfMonth} {month} ?"; + + return configurator; + } + + /// + /// Specify the time zone for the cron expression evaluation + /// + /// + /// + /// + public static IRecurringJobScheduleConfigurator SetTimeZone(this IRecurringJobScheduleConfigurator configurator, TimeZoneInfo tz) + { + configurator.TimeZoneId = tz.Id; + + return configurator; + } + + static void ValidateHour(int hour) + { + if (hour is < 0 or > 23) + throw new ArgumentOutOfRangeException(nameof(hour), "Invalid hour (must be >= 0 and <= 23)."); + } + + static void ValidateMinute(int minute) + { + if (minute is < 0 or > 59) + throw new ArgumentOutOfRangeException(nameof(minute), "Invalid minute (must be >= 0 and <= 59)."); + } + + static void ValidateSecond(int second) + { + if (second is < 0 or > 59) + throw new ArgumentOutOfRangeException(nameof(second), "Invalid second (must be >= 0 and <= 59)."); + } + + static void ValidateDayOfMonth(int day) + { + if (day is < 1 or > 31) + throw new ArgumentOutOfRangeException(nameof(day), "Invalid day of month."); + } + + static void ValidateMonth(int month) + { + if (month is < 1 or > 12) + throw new ArgumentOutOfRangeException(nameof(month), "Invalid month (must be >= 1 and <= 12)."); + } + + static void ValidateYear(int year) + { + if (year is < 1970 or > 2099) + throw new ArgumentOutOfRangeException(nameof(year), "Invalid year (must be >= 1970 and <= 2099)."); + } +} diff --git a/src/MassTransit/JsonMessageBody.cs b/src/MassTransit/JsonMessageBody.cs new file mode 100644 index 00000000000..a661685e9bd --- /dev/null +++ b/src/MassTransit/JsonMessageBody.cs @@ -0,0 +1,12 @@ +namespace MassTransit; + +using System.Text.Json; + + +/// +/// If the incoming message is in a JSON format, use this to unwrap the JSON document from any transport-specific encapsulation +/// +public interface JsonMessageBody +{ + JsonElement? GetJsonElement(JsonSerializerOptions options); +} diff --git a/src/MassTransit/LegacyObsolete/Activity.cs b/src/MassTransit/LegacyObsolete/Activity.cs index 7981b98de42..33240822565 100644 --- a/src/MassTransit/LegacyObsolete/Activity.cs +++ b/src/MassTransit/LegacyObsolete/Activity.cs @@ -24,7 +24,7 @@ public interface Activity : public interface Activity : IStateMachineActivity, Activity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { } @@ -33,7 +33,7 @@ public interface Activity : public interface Activity : IStateMachineActivity, Activity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { } diff --git a/src/MassTransit/LegacyObsolete/Behavior.cs b/src/MassTransit/LegacyObsolete/Behavior.cs index 3b64b3f3565..5356a17e274 100644 --- a/src/MassTransit/LegacyObsolete/Behavior.cs +++ b/src/MassTransit/LegacyObsolete/Behavior.cs @@ -16,7 +16,7 @@ namespace Automatonymous [Obsolete("Deprecated, use IBehavior instead")] public interface Behavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { } @@ -29,7 +29,7 @@ public interface Behavior : [Obsolete("Deprecated, use IBehavior instead")] public interface Behavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { } diff --git a/src/MassTransit/LegacyObsolete/EventObserver.cs b/src/MassTransit/LegacyObsolete/EventObserver.cs index 0cebd17d2db..2200cc6ee05 100644 --- a/src/MassTransit/LegacyObsolete/EventObserver.cs +++ b/src/MassTransit/LegacyObsolete/EventObserver.cs @@ -12,7 +12,7 @@ namespace Automatonymous [Obsolete("Deprecated, use IEventObserver instead")] public interface EventObserver : IEventObserver - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { } } diff --git a/src/MassTransit/LegacyObsolete/StateObserver.cs b/src/MassTransit/LegacyObsolete/StateObserver.cs index 7df8f1c95f1..a6b6023b6a5 100644 --- a/src/MassTransit/LegacyObsolete/StateObserver.cs +++ b/src/MassTransit/LegacyObsolete/StateObserver.cs @@ -12,7 +12,7 @@ namespace Automatonymous [Obsolete("Deprecated, use IStateObserver instead")] public interface StateObserver : IStateObserver - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { } } diff --git a/src/MassTransit/Licensing/LicenseFile.cs b/src/MassTransit/Licensing/LicenseFile.cs new file mode 100644 index 00000000000..66f9d71014a --- /dev/null +++ b/src/MassTransit/Licensing/LicenseFile.cs @@ -0,0 +1,15 @@ +#nullable enable +namespace MassTransit.Licensing +{ + using System.Collections.Generic; + + + public class LicenseFile + { + public string? Version { get; set; } + public string? Kind { get; set; } + public Dictionary? Meta { get; set; } + public string? Data { get; set; } + public string? Signature { get; set; } + } +} diff --git a/src/MassTransit/Licensing/LicenseReader.cs b/src/MassTransit/Licensing/LicenseReader.cs new file mode 100644 index 00000000000..265252ea646 --- /dev/null +++ b/src/MassTransit/Licensing/LicenseReader.cs @@ -0,0 +1,76 @@ +namespace MassTransit.Licensing +{ + using System; + using System.IO; + using System.Linq; + using System.Security.Cryptography; + using System.Text.Json; + + + public class LicenseReader + { + const string _ = + @"MIGbMBAGByqGSM49AgEGBSuBBAAjA4GGAAQA7sH7I9oVjLeDpPaHZCGBQi3Je/NlYYu96gkRipnrplCuox3pMDR0NljeGA35xjsJg7mm6r67/zZAt9GeAIftOnYAc4oaWRGbpaC6O3j/2i+v96gzk21xDh68OTEHLS4J720x/0pd6yvXlZPvGEeyHQgIKoQE11WmPYAP5nXZTJn6KwM="; + + public static LicenseInfo LoadFromFile(string path) + { + using var stream = File.OpenText(path); + var license = stream.ReadToEnd(); + + return Load(license); + } + + public static LicenseInfo Load(string license) + { + var payload = ExtractPayload(license); + + var file = JsonSerializer.Deserialize(payload, LicenseSettings.SerializerOptions); + if (file == null) + throw new InvalidLicenseFormatException(); + + if (string.IsNullOrWhiteSpace(file.Kind) || !string.Equals(file.Kind, "License")) + throw new InvalidLicenseFormatException(); + if (string.IsNullOrWhiteSpace(file.Version) || !string.Equals(file.Version, "v1")) + throw new InvalidLicenseFormatException(); + if (string.IsNullOrWhiteSpace(file.Data)) + throw new InvalidLicenseFormatException(); + if (string.IsNullOrWhiteSpace(file.Signature)) + throw new InvalidLicenseFormatException(); + + var signature = Convert.FromBase64String(file.Signature); + + var kb = Convert.FromBase64String(_); + + using var verify = ECDsa.Create(new ECParameters + { + Curve = ECCurve.NamedCurves.nistP521, + Q = new ECPoint + { + X = kb.Skip(26).Take(66).ToArray(), + Y = kb.Skip(92).Take(66).ToArray() + } + }); + + file.Signature = null; + var serialize = JsonSerializer.SerializeToUtf8Bytes(file, LicenseSettings.SerializerOptions); + + if (verify.VerifyData(serialize, signature, HashAlgorithmName.SHA256) == false) + throw new InvalidLicenseException("Invalid signature"); + + var bytes = Convert.FromBase64String(file.Data); + + var licenseInfo = JsonSerializer.Deserialize(bytes, LicenseSettings.SerializerOptions); + if (licenseInfo == null) + throw new InvalidLicenseException(); + + return licenseInfo; + } + + static byte[] ExtractPayload(string text) + { + return Convert.FromBase64String(string.Concat(text.Split('\n') + .Select(x => x.Trim().Trim('\r')) + .Where(x => !x.StartsWith("-----")))); + } + } +} diff --git a/src/MassTransit/Licensing/LicenseSettings.cs b/src/MassTransit/Licensing/LicenseSettings.cs new file mode 100644 index 00000000000..cf586406a87 --- /dev/null +++ b/src/MassTransit/Licensing/LicenseSettings.cs @@ -0,0 +1,19 @@ +namespace MassTransit.Licensing +{ + using System.Text.Json; + + + public static class LicenseSettings + { + public static readonly JsonSerializerOptions SerializerOptions; + + static LicenseSettings() + { + SerializerOptions = new JsonSerializerOptions + { + WriteIndented = false, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase + }; + } + } +} diff --git a/src/MassTransit/LogContext.cs b/src/MassTransit/LogContext.cs index 5579b1c559b..4e24c608847 100644 --- a/src/MassTransit/LogContext.cs +++ b/src/MassTransit/LogContext.cs @@ -1,11 +1,11 @@ namespace MassTransit { using System; - using System.Diagnostics; using System.Threading; using Logging; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; public static class LogContext @@ -36,7 +36,7 @@ public static ILogContext Current public static void ConfigureCurrentLogContext(ILoggerFactory loggerFactory = null) { - Current = new BusLogContext(loggerFactory ?? NullLoggerFactory.Instance, Cached.Source.Value); + Current = new BusLogContext(loggerFactory ?? NullLoggerFactory.Instance); } /// @@ -46,7 +46,7 @@ public static void ConfigureCurrentLogContext(ILoggerFactory loggerFactory = nul /// An existing logger public static void ConfigureCurrentLogContext(ILogger logger) { - Current = new BusLogContext(new SingleLoggerFactory(logger), Cached.Source.Value); + Current = new BusLogContext(new SingleLoggerFactory(logger)); } public static ILogContext CreateLogContext(string categoryName) @@ -63,6 +63,8 @@ public static ILogContext CreateLogContext(string categoryName) /// public static void ConfigureCurrentLogContextIfNull(IServiceProvider provider) { + LogContextInstrumentationExtensions.TryConfigure(provider); + if (Current == null || Current.Logger is NullLogger) { var loggerFactory = provider.GetService(); @@ -206,17 +208,9 @@ void Log(T1 arg1, T2 arg2, T3 arg3, T4 arg4, T5 arg5, Exception exception) static ILogContext CreateDefaultLogContext() { - var source = Cached.Source.Value; - var loggerFactory = NullLoggerFactory.Instance; - return new BusLogContext(loggerFactory, source); - } - - - static class Cached - { - internal static readonly Lazy Source = new Lazy(() => new ActivitySource(DiagnosticHeaders.DefaultListenerName)); + return new BusLogContext(loggerFactory); } } } diff --git a/src/MassTransit/Logging/BusLogContext.cs b/src/MassTransit/Logging/BusLogContext.cs index e1579b97875..8599ff986d2 100644 --- a/src/MassTransit/Logging/BusLogContext.cs +++ b/src/MassTransit/Logging/BusLogContext.cs @@ -1,14 +1,7 @@ #nullable enable namespace MassTransit.Logging { - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Diagnostics; - using Courier.Contracts; using Microsoft.Extensions.Logging; - using Middleware; - using Transports; public class BusLogContext : @@ -16,29 +9,24 @@ public class BusLogContext : { readonly ILoggerFactory _loggerFactory; readonly ILogContext _messageLogger; - readonly ActivitySource _source; - public BusLogContext(ILoggerFactory loggerFactory, ActivitySource source) + public BusLogContext(ILoggerFactory loggerFactory) { - _source = source; _loggerFactory = loggerFactory; Logger = loggerFactory.CreateLogger(LogCategoryName.MassTransit); - - _messageLogger = new BusLogContext(source, loggerFactory, loggerFactory.CreateLogger("MassTransit.Messages")); + _messageLogger = new BusLogContext(loggerFactory, loggerFactory.CreateLogger("MassTransit.Messages")); } - BusLogContext(ActivitySource source, ILoggerFactory loggerFactory, ILogContext messageLogger, ILogger logger) + BusLogContext(ILoggerFactory loggerFactory, ILogContext messageLogger, ILogger logger) { - _source = source; _loggerFactory = loggerFactory; _messageLogger = messageLogger; Logger = logger; } - BusLogContext(ActivitySource source, ILoggerFactory loggerFactory, ILogger logger) + BusLogContext(ILoggerFactory loggerFactory, ILogger logger) { - _source = source; _loggerFactory = loggerFactory; Logger = logger; @@ -51,7 +39,7 @@ public ILogContext CreateLogContext(string categoryName) { var logger = _loggerFactory.CreateLogger(categoryName); - return new BusLogContext(_source, _loggerFactory, _messageLogger, logger); + return new BusLogContext(_loggerFactory, _messageLogger, logger); } public ILogger Logger { get; } @@ -67,254 +55,5 @@ public ILogContext CreateLogContext(string categoryName) public EnabledLogger? Trace => Logger.IsEnabled(LogLevel.Trace) ? new EnabledLogger(Logger, LogLevel.Trace) : default(EnabledLogger?); public EnabledLogger? Warning => Logger.IsEnabled(LogLevel.Warning) ? new EnabledLogger(Logger, LogLevel.Warning) : default(EnabledLogger?); - - public StartedActivity? StartSendActivity(SendTransportContext transportContext, SendContext context, params (string Key, object? Value)[] tags) - where T : class - { - var activity = _source.CreateActivity(transportContext.ActivityName, ActivityKind.Producer); - if (activity == null) - return null; - - activity.SetTag(DiagnosticHeaders.Messaging.System, transportContext.ActivitySystem); - activity.SetTag(DiagnosticHeaders.Messaging.DestinationName, transportContext.ActivityDestination); - activity.SetTag(DiagnosticHeaders.Messaging.Operation, "send"); - - return PopulateSendActivity(context, activity, tags); - } - - public StartedActivity? StartOutboxSendActivity(SendContext context) - where T : class - { - var parentActivityContext = System.Diagnostics.Activity.Current?.Context ?? default; - - var activity = _source.CreateActivity("outbox send", ActivityKind.Producer, parentActivityContext); - if (activity == null) - return null; - - activity.SetTag(DiagnosticHeaders.Messaging.Operation, "send"); - - return PopulateSendActivity(context, activity); - } - - public StartedActivity? StartOutboxDeliverActivity(OutboxMessageContext context) - { - var parentActivityContext = GetParentActivityContext(context.Headers); - - var activity = _source.CreateActivity("outbox process", ActivityKind.Client, parentActivityContext); - if (activity == null) - return null; - - activity.Start(); - - return new StartedActivity(activity); - } - - public StartedActivity? StartReceiveActivity(string name, string inputAddress, string endpointName, ReceiveContext context) - { - var parentActivityContext = GetParentActivityContext(context.TransportHeaders); - - var activity = _source.CreateActivity(name, ActivityKind.Consumer, parentActivityContext); - if (activity == null) - return null; - - if (activity.IsAllDataRequested) - { - activity.SetTag(DiagnosticHeaders.Messaging.DestinationName, endpointName); - activity.SetTag(DiagnosticHeaders.Messaging.Operation, "receive"); - activity.SetTag(DiagnosticHeaders.InputAddress, inputAddress); - - if ((context.TransportHeaders.TryGetHeader(MessageHeaders.TransportMessageId, out var messageIdHeader) - || context.TransportHeaders.TryGetHeader(MessageHeaders.MessageId, out messageIdHeader)) - && messageIdHeader is string text) - activity.SetTag(DiagnosticHeaders.Messaging.TransportMessageId, text); - } - - activity.Start(); - - return new StartedActivity(activity); - } - - public StartedActivity? StartConsumerActivity(ConsumeContext context) - where T : class - { - return StartActivity(activity => - { - activity.SetTag(DiagnosticHeaders.ConsumerType, TypeCache.ShortName); - activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); - }); - } - - public StartedActivity? StartHandlerActivity(ConsumeContext context) - where T : class - { - return StartActivity(activity => - { - activity.SetTag(DiagnosticHeaders.ConsumerType, "Handler"); - activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); - }); - } - - public StartedActivity? StartSagaActivity(SagaConsumeContext context) - where TSaga : class, ISaga - where T : class - { - return StartActivity(activity => - { - activity.SetTag(DiagnosticHeaders.SagaId, context.Saga.CorrelationId.ToString("D")); - activity.SetTag(DiagnosticHeaders.ConsumerType, TypeCache.ShortName); - activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); - }); - } - - public StartedActivity? StartSagaStateMachineActivity(BehaviorContext context) - where TSaga : class, ISaga - where T : class - { - return StartActivity(activity => - { - activity.SetTag(DiagnosticHeaders.SagaId, context.Saga.CorrelationId.ToString("D")); - activity.SetTag(DiagnosticHeaders.ConsumerType, context.StateMachine.Name); - activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); - }); - } - - public StartedActivity? StartExecuteActivity(ConsumeContext context) - where TActivity : IExecuteActivity - where TArguments : class - { - return StartActivity(activity => - { - activity.SetTag(DiagnosticHeaders.TrackingNumber, context.Message.TrackingNumber.ToString("D")); - activity.SetTag(DiagnosticHeaders.ConsumerType, TypeCache.ShortName); - activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); - }); - } - - public StartedActivity? StartCompensateActivity(ConsumeContext context) - where TActivity : ICompensateActivity - where TLog : class - { - return StartActivity(activity => - { - activity.SetTag(DiagnosticHeaders.TrackingNumber, context.Message.TrackingNumber.ToString("D")); - activity.SetTag(DiagnosticHeaders.ConsumerType, TypeCache.ShortName); - activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); - }); - } - - public StartedActivity? StartGenericActivity(string operationName) - { - var activity = _source.CreateActivity(operationName, ActivityKind.Client); - if (activity == null) - return null; - - activity.Start(); - - return new StartedActivity(activity); - } - - static StartedActivity? PopulateSendActivity(SendContext context, System.Diagnostics.Activity activity, params (string Key, object? Value)[] tags) - where T : class - { - var conversationId = context.ConversationId?.ToString("D"); - - if (context.CorrelationId.HasValue) - activity.SetBaggage(DiagnosticHeaders.CorrelationId, context.CorrelationId.Value.ToString("D")); - if (conversationId != null) - activity.SetBaggage(DiagnosticHeaders.Messaging.ConversationId, conversationId); - - if (activity.IsAllDataRequested) - { - if (context.MessageId.HasValue) - activity.SetTag(DiagnosticHeaders.MessageId, context.MessageId.Value.ToString("D")); - if (conversationId != null) - activity.SetTag(DiagnosticHeaders.Messaging.ConversationId, conversationId); - if (context.CorrelationId.HasValue) - activity.SetTag(DiagnosticHeaders.CorrelationId, context.CorrelationId.Value.ToString("D")); - if (context.RequestId.HasValue) - activity.SetTag(DiagnosticHeaders.RequestId, context.RequestId.Value.ToString("D")); - if (context.InitiatorId.HasValue) - activity.SetTag(DiagnosticHeaders.InitiatorId, context.InitiatorId.Value.ToString("D")); - if (context.SourceAddress != null) - activity.SetTag(DiagnosticHeaders.SourceAddress, context.SourceAddress.ToString()); - if (context.DestinationAddress != null) - activity.SetTag(DiagnosticHeaders.DestinationAddress, context.DestinationAddress.ToString()); - - activity.SetTag(DiagnosticHeaders.MessageTypes, string.Join(",", MessageTypeCache.MessageTypeNames)); - - for (var i = 0; i < tags.Length; i++) - { - if (tags[i].Value != null) - activity.SetTag(tags[i].Key, tags[i].Value?.ToString()); - } - } - - activity.Start(); - - IList>? baggage = null; - foreach (KeyValuePair pair in activity.Baggage) - { - if (pair.Key.Equals(DiagnosticHeaders.Messaging.ConversationId) || pair.Key.Equals(DiagnosticHeaders.CorrelationId)) - continue; - - if (string.IsNullOrWhiteSpace(pair.Value)) - continue; - - baggage ??= new List>(); - baggage.Add(pair); - } - - if (activity.Id != null) - context.Headers.Set(DiagnosticHeaders.ActivityId, activity.Id); - - if (baggage != null) - context.Headers.Set(DiagnosticHeaders.ActivityCorrelationContext, baggage); - - return new StartedActivity(activity); - } - - StartedActivity? StartActivity(Action started) - { - var currentActivity = System.Diagnostics.Activity.Current; - if (currentActivity == null) - return null; - - var operationName = Cached.OperationNames.GetOrAdd(currentActivity.OperationName, add => - { - if (add.EndsWith(" receive")) - return add.Substring(0, add.Length - 8) + " process"; - if (add.EndsWith(" process")) - return add; - - return currentActivity.OperationName; - }); - - var activity = _source.CreateActivity(operationName, ActivityKind.Consumer); - if (activity == null) - return null; - - activity.SetTag(DiagnosticHeaders.Messaging.Operation, "process"); - - if (activity.IsAllDataRequested) - started(activity); - - activity.Start(); - - return new StartedActivity(activity); - } - - static ActivityContext GetParentActivityContext(Headers headers) - { - return headers.TryGetHeader(DiagnosticHeaders.ActivityId, out var headerValue) && headerValue is string activityId - && ActivityContext.TryParse(activityId, null, out var activityContext) - ? activityContext - : default; - } - - - static class Cached - { - internal static readonly ConcurrentDictionary OperationNames = new ConcurrentDictionary(); - } } } diff --git a/src/MassTransit/Logging/DiagnosticActivityExtensions.cs b/src/MassTransit/Logging/Diagnostics/DiagnosticActivityExtensions.cs similarity index 100% rename from src/MassTransit/Logging/DiagnosticActivityExtensions.cs rename to src/MassTransit/Logging/Diagnostics/DiagnosticActivityExtensions.cs diff --git a/src/MassTransit/Logging/DiagnosticHeaders.cs b/src/MassTransit/Logging/Diagnostics/DiagnosticHeaders.cs similarity index 93% rename from src/MassTransit/Logging/DiagnosticHeaders.cs rename to src/MassTransit/Logging/Diagnostics/DiagnosticHeaders.cs index 8b5df585a5c..e3c207fa557 100644 --- a/src/MassTransit/Logging/DiagnosticHeaders.cs +++ b/src/MassTransit/Logging/Diagnostics/DiagnosticHeaders.cs @@ -8,6 +8,7 @@ public static class DiagnosticHeaders public const string DiagnosticId = "Diagnostic-Id"; public const string ActivityId = "MT-Activity-Id"; public const string ActivityCorrelationContext = "MT-Activity-Correlation-Context"; + public const string ActivityPropagation = "MT-Activity-Propagation"; public const string MessageId = "messaging.masstransit.message_id"; public const string CorrelationId = "messaging.masstransit.correlation_id"; @@ -41,10 +42,9 @@ public class Exceptions public static class Messaging { - public const string BodyLength = "messaging.message_payload_size_bytes"; + public const string BodyLength = "messaging.message.body.size"; public const string ConversationId = "messaging.message.conversation_id"; public const string DestinationName = "messaging.destination.name"; - public const string DestinationKind = "messaging.destination.kind"; public const string TransportMessageId = "messaging.message.id"; public const string Operation = "messaging.operation"; public const string System = "messaging.system"; diff --git a/src/MassTransit/Logging/Diagnostics/LogContextActivityExtensions.cs b/src/MassTransit/Logging/Diagnostics/LogContextActivityExtensions.cs new file mode 100644 index 00000000000..0c258fec0fb --- /dev/null +++ b/src/MassTransit/Logging/Diagnostics/LogContextActivityExtensions.cs @@ -0,0 +1,315 @@ +#nullable enable +// ReSharper disable once CheckNamespace +namespace MassTransit.Logging +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Diagnostics; + using Courier.Contracts; + using Metadata; + using Middleware; + using Transports; + + + public static class LogContextActivityExtensions + { + public static StartedActivity? StartSendActivity(this ILogContext logContext, SendTransportContext transportContext, SendContext context, + params (string Key, object? Value)[] tags) + where T : class + { + var parentActivityContext = System.Diagnostics.Activity.Current == null + ? GetParentActivityContext(context.Headers) + : default; + + var activity = Cached.Source.Value.CreateActivity(transportContext.ActivityName, ActivityKind.Producer, parentActivityContext); + if (activity == null) + return null; + + activity.SetTag(DiagnosticHeaders.Messaging.Operation, "send"); + activity.SetTag(DiagnosticHeaders.Messaging.System, transportContext.ActivitySystem); + activity.SetTag(DiagnosticHeaders.Messaging.DestinationName, transportContext.ActivityDestination); + + return PopulateSendActivity(context, activity, tags); + } + + public static StartedActivity? StartOutboxSendActivity(this ILogContext logContext, SendContext context) + where T : class + { + var parentActivityContext = System.Diagnostics.Activity.Current == null + ? GetParentActivityContext(context.Headers) + : default; + + var activity = Cached.Source.Value.CreateActivity("outbox send", ActivityKind.Producer, parentActivityContext); + if (activity == null) + return null; + + activity.SetTag(DiagnosticHeaders.Messaging.Operation, "send"); + + return PopulateSendActivity(context, activity); + } + + public static StartedActivity? StartOutboxDeliverActivity(this ILogContext logContext, OutboxMessageContext context) + { + var parentActivityContext = GetParentActivityContext(context.Headers); + + var activity = Cached.Source.Value.CreateActivity("outbox process", ActivityKind.Client, parentActivityContext); + if (activity == null) + return null; + + activity.Start(); + + return new StartedActivity(activity); + } + + public static StartedActivity? StartReceiveActivity(this ILogContext logContext, string name, string inputAddress, string endpointName, + ReceiveContext context) + { + var parentActivityContext = GetParentActivityContext(context.TransportHeaders, true); + + var activity = context.TransportHeaders.TryGetHeader(DiagnosticHeaders.ActivityPropagation, out var linkTypeValue) switch + { + true => linkTypeValue switch + { + "Link" => Cached.Source.Value.CreateActivity(name, ActivityKind.Consumer, (ActivityContext)default, + links: [new ActivityLink(parentActivityContext)]), + "New" => Cached.Source.Value.CreateActivity(name, ActivityKind.Consumer, (ActivityContext)default), + _ => Cached.Source.Value.CreateActivity(name, ActivityKind.Consumer, parentActivityContext) + }, + false => Cached.Source.Value.CreateActivity(name, ActivityKind.Consumer, parentActivityContext) + }; + + if (activity == null) + return null; + + activity.SetTag(DiagnosticHeaders.Messaging.Operation, "receive"); + activity.SetTag(DiagnosticHeaders.Messaging.DestinationName, endpointName); + + if (activity.IsAllDataRequested) + { + activity.SetTag(DiagnosticHeaders.InputAddress, inputAddress); + + if ((context.TransportHeaders.TryGetHeader(MessageHeaders.TransportMessageId, out var messageIdHeader) + || context.TransportHeaders.TryGetHeader(MessageHeaders.MessageId, out messageIdHeader)) + && messageIdHeader is string text) + activity.SetTag(DiagnosticHeaders.Messaging.TransportMessageId, text); + } + + activity.Start(); + + return new StartedActivity(activity); + } + + public static StartedActivity? StartConsumerActivity(this ILogContext logContext, ConsumeContext context) + where T : class + { + return StartActivity(context, activity => + { + activity.SetTag(DiagnosticHeaders.ConsumerType, TypeCache.ShortName); + activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); + }); + } + + public static StartedActivity? StartHandlerActivity(this ILogContext logContext, ConsumeContext context) + where T : class + { + return StartActivity(context, activity => + { + activity.SetTag(DiagnosticHeaders.ConsumerType, "Handler"); + activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); + }); + } + + public static StartedActivity? StartSagaActivity(this ILogContext logContext, SagaConsumeContext context) + where TSaga : class, ISaga + where T : class + { + return StartActivity(context, activity => + { + activity.SetTag(DiagnosticHeaders.SagaId, context.Saga.CorrelationId.ToString("D")); + activity.SetTag(DiagnosticHeaders.ConsumerType, TypeCache.ShortName); + activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); + }); + } + + public static StartedActivity? StartSagaStateMachineActivity(this ILogContext logContext, BehaviorContext context) + where TSaga : class, SagaStateMachineInstance + where T : class + { + return StartActivity(context, activity => + { + activity.SetTag(DiagnosticHeaders.SagaId, context.Saga.CorrelationId.ToString("D")); + activity.SetTag(DiagnosticHeaders.ConsumerType, context.StateMachine.Name); + activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); + }); + } + + public static StartedActivity? StartExecuteActivity(this ILogContext logContext, ConsumeContext context) + where TActivity : IExecuteActivity + where TArguments : class + { + return StartActivity(context, activity => + { + activity.SetTag(DiagnosticHeaders.TrackingNumber, context.Message.TrackingNumber.ToString("D")); + activity.SetTag(DiagnosticHeaders.ConsumerType, TypeCache.ShortName); + activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); + }); + } + + public static StartedActivity? StartCompensateActivity(this ILogContext logContext, ConsumeContext context) + where TActivity : ICompensateActivity + where TLog : class + { + return StartActivity(context, activity => + { + activity.SetTag(DiagnosticHeaders.TrackingNumber, context.Message.TrackingNumber.ToString("D")); + activity.SetTag(DiagnosticHeaders.ConsumerType, TypeCache.ShortName); + activity.SetTag(DiagnosticHeaders.PeerAddress, MessageTypeCache.DiagnosticAddress); + }); + } + + public static StartedActivity? StartGenericActivity(this ILogContext logContext, string operationName) + { + var activity = Cached.Source.Value.CreateActivity(operationName, ActivityKind.Client); + if (activity == null) + return null; + + activity.Start(); + + return new StartedActivity(activity); + } + + static StartedActivity? PopulateSendActivity(SendContext context, System.Diagnostics.Activity activity, params (string Key, object? Value)[] tags) + where T : class + { + var conversationId = context.ConversationId?.ToString("D"); + + if (context.CorrelationId.HasValue) + activity.SetBaggage(DiagnosticHeaders.CorrelationId, context.CorrelationId.Value.ToString("D")); + if (conversationId != null) + activity.SetBaggage(DiagnosticHeaders.Messaging.ConversationId, conversationId); + + if (activity.IsAllDataRequested) + { + if (context.MessageId.HasValue) + activity.SetTag(DiagnosticHeaders.MessageId, context.MessageId.Value.ToString("D")); + if (conversationId != null) + activity.SetTag(DiagnosticHeaders.Messaging.ConversationId, conversationId); + if (context.CorrelationId.HasValue) + activity.SetTag(DiagnosticHeaders.CorrelationId, context.CorrelationId.Value.ToString("D")); + if (context.RequestId.HasValue) + activity.SetTag(DiagnosticHeaders.RequestId, context.RequestId.Value.ToString("D")); + if (context.InitiatorId.HasValue) + activity.SetTag(DiagnosticHeaders.InitiatorId, context.InitiatorId.Value.ToString("D")); + if (context.SourceAddress != null) + activity.SetTag(DiagnosticHeaders.SourceAddress, context.SourceAddress.ToString()); + if (context.DestinationAddress != null) + activity.SetTag(DiagnosticHeaders.DestinationAddress, context.DestinationAddress.ToString()); + + activity.SetTag(DiagnosticHeaders.MessageTypes, string.Join(",", context.SupportedMessageTypes)); + + for (var i = 0; i < tags.Length; i++) + { + if (tags[i].Value != null) + activity.SetTag(tags[i].Key, tags[i].Value?.ToString()); + } + } + + activity.Start(); + + IList>? baggage = null; + foreach (KeyValuePair pair in activity.Baggage) + { + if (pair.Key.Equals(DiagnosticHeaders.Messaging.ConversationId) || pair.Key.Equals(DiagnosticHeaders.CorrelationId)) + continue; + + if (string.IsNullOrWhiteSpace(pair.Value)) + continue; + + baggage ??= new List>(); + baggage.Add(pair); + } + + if (activity.Id != null) + context.Headers.Set(DiagnosticHeaders.ActivityId, activity.Id); + + if (baggage != null) + context.Headers.Set(DiagnosticHeaders.ActivityCorrelationContext, baggage); + + return new StartedActivity(activity); + } + + static ActivityContext GetParentActivityContext(Headers headers, bool isRemote = false) + { + if (headers.TryGetHeader(DiagnosticHeaders.ActivityId, out var headerValue) + && headerValue is string activityId + && ActivityContext.TryParse(activityId, null, out var activityContext)) + { + if (isRemote && System.Diagnostics.Activity.Current == null) + return new ActivityContext(activityContext.TraceId, activityContext.SpanId, activityContext.TraceFlags, activityContext.TraceState, true); + + return activityContext; + } + + return default; + } + + static StartedActivity? StartActivity(ConsumeContext context, Action started) + { + var currentActivity = System.Diagnostics.Activity.Current; + if (currentActivity == null) + return null; + + var operationName = Cached.OperationNames.GetOrAdd(currentActivity.OperationName, add => + { + if (add.EndsWith(" receive")) + return add.Substring(0, add.Length - 8) + " process"; + if (add.EndsWith(" process")) + return add; + + return currentActivity.OperationName; + }); + + var activity = Cached.Source.Value.CreateActivity(operationName, ActivityKind.Consumer); + if (activity == null) + return null; + + activity.SetTag(DiagnosticHeaders.Messaging.Operation, "process"); + + if (activity.IsAllDataRequested) + { + if (context.MessageId.HasValue) + activity.SetTag(DiagnosticHeaders.MessageId, context.MessageId.Value.ToString("D")); + if (context.ConversationId.HasValue) + activity.SetTag(DiagnosticHeaders.Messaging.ConversationId, context.ConversationId.Value.ToString("D")); + if (context.CorrelationId.HasValue) + activity.SetTag(DiagnosticHeaders.CorrelationId, context.CorrelationId.Value.ToString("D")); + if (context.RequestId.HasValue) + activity.SetTag(DiagnosticHeaders.RequestId, context.RequestId.Value.ToString("D")); + if (context.InitiatorId.HasValue) + activity.SetTag(DiagnosticHeaders.InitiatorId, context.InitiatorId.Value.ToString("D")); + if (context.SourceAddress != null) + activity.SetTag(DiagnosticHeaders.SourceAddress, context.SourceAddress.ToString()); + if (context.DestinationAddress != null) + activity.SetTag(DiagnosticHeaders.DestinationAddress, context.DestinationAddress.ToString()); + + activity.SetTag(DiagnosticHeaders.MessageTypes, string.Join(",", context.SupportedMessageTypes)); + + started(activity); + } + + activity.Start(); + + return new StartedActivity(activity); + } + + + static class Cached + { + internal static readonly Lazy Source = new Lazy(() => + new ActivitySource(DiagnosticHeaders.DefaultListenerName, HostMetadataCache.Host.MassTransitVersion)); + + internal static readonly ConcurrentDictionary OperationNames = new ConcurrentDictionary(StringComparer.Ordinal); + } + } +} diff --git a/src/MassTransit/Logging/StartedActivity.cs b/src/MassTransit/Logging/Diagnostics/StartedActivity.cs similarity index 98% rename from src/MassTransit/Logging/StartedActivity.cs rename to src/MassTransit/Logging/Diagnostics/StartedActivity.cs index 9c479fb5ca0..d35119ce299 100644 --- a/src/MassTransit/Logging/StartedActivity.cs +++ b/src/MassTransit/Logging/Diagnostics/StartedActivity.cs @@ -55,7 +55,7 @@ public void Stop() if (Activity.Status == ActivityStatusCode.Unset) Activity.SetStatus(ActivityStatusCode.Ok); - Activity.Stop(); + Activity.Dispose(); } } } diff --git a/src/MassTransit/Logging/ILogContext.cs b/src/MassTransit/Logging/ILogContext.cs index fcaba678b0a..f586de37cf4 100644 --- a/src/MassTransit/Logging/ILogContext.cs +++ b/src/MassTransit/Logging/ILogContext.cs @@ -1,10 +1,7 @@ #nullable enable namespace MassTransit.Logging { - using Courier.Contracts; using Microsoft.Extensions.Logging; - using Middleware; - using Transports; /// @@ -12,12 +9,13 @@ namespace MassTransit.Logging /// public interface ILogContext { + ILogger Logger { get; } + /// /// The log context for all message movement, sent, received, etc. /// ILogContext Messages { get; } - ILogger Logger { get; } EnabledLogger? Critical { get; } EnabledLogger? Debug { get; } EnabledLogger? Error { get; } @@ -31,39 +29,5 @@ public interface ILogContext /// The category name for messages produced by the logger. /// The . ILogContext CreateLogContext(string categoryName); - - StartedActivity? StartSendActivity(SendTransportContext transportContext, SendContext context, params (string Key, object? Value)[] tags) - where T : class; - - StartedActivity? StartOutboxSendActivity(SendContext context) - where T : class; - - StartedActivity? StartOutboxDeliverActivity(OutboxMessageContext context); - - StartedActivity? StartReceiveActivity(string name, string inputAddress, string endpointName, ReceiveContext context); - - StartedActivity? StartConsumerActivity(ConsumeContext context) - where T : class; - - StartedActivity? StartHandlerActivity(ConsumeContext context) - where T : class; - - StartedActivity? StartSagaActivity(SagaConsumeContext context) - where TSaga : class, ISaga - where T : class; - - StartedActivity? StartSagaStateMachineActivity(BehaviorContext context) - where TSaga : class, ISaga - where T : class; - - StartedActivity? StartExecuteActivity(ConsumeContext context) - where TActivity : IExecuteActivity - where TArguments : class; - - StartedActivity? StartCompensateActivity(ConsumeContext context) - where TActivity : ICompensateActivity - where TLog : class; - - StartedActivity? StartGenericActivity(string operationName); } } diff --git a/src/MassTransit/Logging/MetricsContext.cs b/src/MassTransit/Logging/MetricsContext.cs new file mode 100644 index 00000000000..834fe7ecd98 --- /dev/null +++ b/src/MassTransit/Logging/MetricsContext.cs @@ -0,0 +1,9 @@ +namespace MassTransit; + +using System.Diagnostics; + + +public interface MetricsContext +{ + void Populate(ref TagList tagList); +} diff --git a/src/MassTransit/Logging/MetricsContextExtensions.cs b/src/MassTransit/Logging/MetricsContextExtensions.cs new file mode 100644 index 00000000000..638c5b5286f --- /dev/null +++ b/src/MassTransit/Logging/MetricsContextExtensions.cs @@ -0,0 +1,58 @@ +namespace MassTransit; + +using System.Collections.Generic; +using System.Diagnostics; + + +public static class MetricsContextExtensions +{ + /// + /// Add custom tag to the metrics emitted by the library + /// + /// + /// + /// + public static void AddMetricTags(this PipeContext pipeContext, string key, object value) + { + pipeContext.AddOrUpdatePayload(() => new TagListMetricsContext(key, value), e => e.AddTag(key, value)); + } + + /// + /// Set and override custom metric tags emitted by the library + /// + /// + /// + public static void SetMetricTags(this PipeContext pipeContext, TagList tagList) + { + pipeContext.AddOrUpdatePayload(() => new TagListMetricsContext(tagList), _ => new TagListMetricsContext(tagList)); + } + + + class TagListMetricsContext : + MetricsContext + { + TagList _tagList; + + public TagListMetricsContext(string key, object value) + : this(new TagList { { key, value } }) + { + } + + public TagListMetricsContext(TagList tagList) + { + _tagList = tagList; + } + + public void Populate(ref TagList tagList) + { + foreach (KeyValuePair tag in _tagList) + tagList.Add(tag); + } + + public TagListMetricsContext AddTag(string key, object value) + { + _tagList.Add(key, value); + return this; + } + } +} diff --git a/src/MassTransit/Logging/Monitoring/LogContextInstrumentationExtensions.cs b/src/MassTransit/Logging/Monitoring/LogContextInstrumentationExtensions.cs new file mode 100644 index 00000000000..79fce144063 --- /dev/null +++ b/src/MassTransit/Logging/Monitoring/LogContextInstrumentationExtensions.cs @@ -0,0 +1,583 @@ +namespace MassTransit.Logging +{ + using System; + using System.Collections.Concurrent; + using System.Diagnostics; + using System.Diagnostics.Metrics; + using System.Linq; + using System.Text; + using Courier.Contracts; + using Metadata; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Middleware; + using Monitoring; + using Transports; + + + public static class LogContextInstrumentationExtensions + { + static readonly ConcurrentDictionary _labelCache = new ConcurrentDictionary(StringComparer.Ordinal); + + static bool _isConfigured; + static Counter _receiveTotal; + static Counter _receiveFaultTotal; + static Counter _receiveInProgress; + static Counter _consumeTotal; + static Counter _consumeFaultTotal; + static Counter _consumeRetryTotal; + static Counter _sagaTotal; + static Counter _sagaFaultTotal; + static Counter _sendTotal; + static Counter _sendFaultTotal; + static Counter _executeTotal; + static Counter _executeFaultTotal; + static Counter _compensateTotal; + static Counter _compensateFaultTotal; + static Counter _consumerInProgress; + static Counter _handlerTotal; + static Counter _handlerFaultTotal; + static Counter _handlerInProgress; + static Counter _sagaInProgress; + static Counter _executeInProgress; + static Counter _compensateInProgress; + static Counter _outboxSendTotal; + static Counter _outboxSendFaultTotal; + static Counter _outboxDeliveryTotal; + static Counter _outboxDeliveryFaultTotal; + static Histogram _receiveDuration; + static Histogram _consumeDuration; + static Histogram _handlerDuration; + static Histogram _sagaDuration; + static Histogram _deliveryDuration; + static Histogram _executeDuration; + static Histogram _compensateDuration; + + static readonly char[] _delimiters = { '<', '>' }; + + static Meter _meter; + static InstrumentationOptions _options; + + public static StartedInstrument? StartReceiveInstrument(this ILogContext logContext, ReceiveContext context) + { + if (!_isConfigured || !_receiveTotal.Enabled) + return null; + + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.InputAddress) } + }; + + AddCustomTags(ref tagList, context); + + _receiveTotal.Add(1, tagList); + _receiveInProgress.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _receiveFaultTotal.Add(1, tagList); + }, () => + { + _receiveInProgress.Add(-1, tagList); + _receiveDuration.Record(context.ElapsedTime.TotalMilliseconds, tagList); + }); + } + + public static StartedInstrument? StartHandlerInstrument(this ILogContext logContext, ConsumeContext context, + Stopwatch stopwatch) + where TMessage : class + { + if (!_isConfigured || !_handlerTotal.Enabled) + return null; + + var messageTypeLabel = GetMessageTypeLabel(); + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.ReceiveContext.InputAddress) }, + { _options.MessageTypeLabel, messageTypeLabel }, + { _options.ConsumerTypeLabel, GetConsumerTypeLabel, TMessage>(messageTypeLabel) } + }; + + AddCustomTags(ref tagList, context); + + _handlerTotal.Add(1, tagList); + _handlerInProgress.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _handlerFaultTotal.Add(1, tagList); + }, () => + { + _handlerInProgress.Add(-1, tagList); + _handlerDuration.Record(stopwatch.ElapsedMilliseconds, tagList); + }); + } + + public static StartedInstrument? StartSagaInstrument(this ILogContext logContext, SagaConsumeContext context) + where T : class + where TSaga : class, ISaga + { + if (!_isConfigured || !_sagaTotal.Enabled) + return null; + + var messageTypeLabel = GetMessageTypeLabel(); + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.ReceiveContext.InputAddress) }, + { _options.MessageTypeLabel, messageTypeLabel }, + { _options.ConsumerTypeLabel, GetConsumerTypeLabel(messageTypeLabel) } + }; + + AddCustomTags(ref tagList, context); + + _sagaTotal.Add(1, tagList); + _sagaInProgress.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _sagaFaultTotal.Add(1, tagList); + }, () => + { + _sagaInProgress.Add(-1, tagList); + _sagaDuration.Record(context.ReceiveContext.ElapsedTime.TotalMilliseconds, tagList); + }); + } + + public static StartedInstrument? StartSagaStateMachineInstrument(this ILogContext logContext, BehaviorContext context) + where T : class + where TSaga : class, SagaStateMachineInstance + { + if (!_isConfigured || !_sagaTotal.Enabled) + return null; + + var messageTypeLabel = GetMessageTypeLabel(); + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.ReceiveContext.InputAddress) }, + { _options.MessageTypeLabel, messageTypeLabel }, + { _options.ConsumerTypeLabel, GetConsumerTypeLabel(messageTypeLabel) } + }; + + AddCustomTags(ref tagList, context); + + _sagaTotal.Add(1, tagList); + _sagaInProgress.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _sagaFaultTotal.Add(1, tagList); + }, () => + { + _sagaInProgress.Add(-1, tagList); + _sagaDuration.Record(context.ReceiveContext.ElapsedTime.TotalMilliseconds, tagList); + }); + } + + public static StartedInstrument? StartConsumeInstrument(this ILogContext logContext, ConsumeContext context, Stopwatch timer) + where T : class + { + if (!_isConfigured || !_consumeTotal.Enabled) + return null; + + var messageTypeLabel = GetMessageTypeLabel(); + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.ReceiveContext.InputAddress) }, + { _options.MessageTypeLabel, messageTypeLabel }, + { _options.ConsumerTypeLabel, GetConsumerTypeLabel(messageTypeLabel) } + }; + + AddCustomTags(ref tagList, context); + + _consumeTotal.Add(1, tagList); + _consumerInProgress.Add(1, tagList); + + var retryAttempt = context.GetRetryAttempt(); + if (retryAttempt > 0) + _consumeRetryTotal.Add(1, tagList); + + if (context.SentTime.HasValue) + { + var deliveryDuration = DateTime.UtcNow - context.SentTime.Value; + if (deliveryDuration < TimeSpan.Zero) + deliveryDuration = TimeSpan.Zero; + + _deliveryDuration.Record(deliveryDuration.TotalMilliseconds, tagList); + } + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _consumeFaultTotal.Add(1, tagList); + }, () => + { + _consumerInProgress.Add(-1, tagList); + _consumeDuration.Record(timer.ElapsedMilliseconds, tagList); + }); + } + + public static StartedInstrument? StartActivityExecuteInstrument(this ILogContext logContext, + ConsumeContext context, Stopwatch timer) + where TActivity : class, IExecuteActivity + where TArguments : class + { + if (!_isConfigured || !_executeTotal.Enabled) + return null; + + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.ReceiveContext.InputAddress) }, + { _options.ActivityNameLabel, GetActivityTypeLabel() }, + { _options.ArgumentTypeLabel, GetArgumentTypeLabel() } + }; + + AddCustomTags(ref tagList, context); + + _executeTotal.Add(1, tagList); + _executeInProgress.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _executeFaultTotal.Add(1, tagList); + }, () => + { + _executeInProgress.Add(-1, tagList); + _executeDuration.Record(timer.ElapsedMilliseconds, tagList); + }); + } + + public static StartedInstrument? StartActivityCompensateInstrument(this ILogContext logContext, + ConsumeContext context, Stopwatch timer) + where TActivity : class, ICompensateActivity + where TLog : class + { + if (!_isConfigured || !_compensateTotal.Enabled) + return null; + + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.ReceiveContext.InputAddress) }, + { _options.ActivityNameLabel, GetActivityTypeLabel() }, + { _options.LogTypeLabel, GetLogTypeLabel() } + }; + + AddCustomTags(ref tagList, context); + + _compensateTotal.Add(1, tagList); + _compensateInProgress.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _compensateFaultTotal.Add(1, tagList); + }, () => + { + _compensateInProgress.Add(-1, tagList); + _compensateDuration.Record(timer.ElapsedMilliseconds, tagList); + }); + } + + public static StartedInstrument? StartSendInstrument(this ILogContext logContext, SendTransportContext transportContext, SendContext context) + where T : class + { + if (!_isConfigured || !_sendTotal.Enabled) + return null; + + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.DestinationAddress) }, + { _options.MessageTypeLabel, GetMessageTypeLabel() } + }; + + AddCustomTags(ref tagList, context); + + _sendTotal.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _sendFaultTotal.Add(1, tagList); + }); + } + + public static StartedInstrument? StartOutboxSendInstrument(this ILogContext logContext, SendContext context) + where T : class + { + if (!_isConfigured || !_outboxSendTotal.Enabled) + return null; + + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.DestinationAddress) }, + { _options.MessageTypeLabel, GetMessageTypeLabel() } + }; + + AddCustomTags(ref tagList, context); + + _outboxSendTotal.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _outboxSendFaultTotal.Add(1, tagList); + }); + } + + public static StartedInstrument? StartOutboxDeliveryInstrument(this ILogContext logContext, OutboxMessageContext context) + { + if (!_isConfigured || !_outboxDeliveryTotal.Enabled) + return null; + + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.DestinationAddress) } + }; + + _outboxDeliveryTotal.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _outboxDeliveryFaultTotal.Add(1, tagList); + }); + } + + public static StartedInstrument? StartOutboxDeliveryInstrument(this ILogContext logContext, + OutboxConsumeContext consumeContext, OutboxMessageContext context) + { + if (!_isConfigured || !_outboxDeliveryTotal.Enabled) + return null; + + var tagList = new TagList + { + { _options.ServiceNameLabel, _options.ServiceName }, + { _options.EndpointLabel, GetEndpointLabel(context.DestinationAddress) } + }; + + AddCustomTags(ref tagList, consumeContext); + + _outboxDeliveryTotal.Add(1, tagList); + + return new StartedInstrument(exception => + { + tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); + _outboxDeliveryFaultTotal.Add(1, tagList); + }); + } + + public static void TryConfigure(IServiceProvider provider) + { + if (_isConfigured) + return; + + var instrumentationOptions = provider.GetRequiredService>().Value; + #if NET8_0_OR_GREATER + var meterFactory = provider.GetService(); + if (meterFactory == null) + { + TryConfigure(instrumentationOptions); + return; + } + + var meter = meterFactory.Create(new MeterOptions(InstrumentationOptions.MeterName) { Version = HostMetadataCache.Host.MassTransitVersion }); + Configure(meter, instrumentationOptions); + #else + TryConfigure(instrumentationOptions); + #endif + } + + public static void TryConfigure(InstrumentationOptions options) + { + if (_isConfigured) + return; + + // We have to dispose manually created meter to flush instruments, some day... + Configure(new Meter(InstrumentationOptions.MeterName, HostMetadataCache.Host.MassTransitVersion), options); + } + + static void Configure(Meter meter, InstrumentationOptions options) + { + _options = options; + _meter = meter; + + // Counters + + _receiveTotal = _meter.CreateCounter(options.ReceiveTotal, "ea", "Number of messages received"); + _receiveFaultTotal = _meter.CreateCounter(options.ReceiveFaultTotal, "ea", "Number of messages receive faults"); + + _consumeTotal = _meter.CreateCounter(options.ConsumeTotal, "ea", "Number of messages consumed"); + _consumeFaultTotal = _meter.CreateCounter(options.ConsumeFaultTotal, "ea", "Number of message consume faults"); + _consumeRetryTotal = _meter.CreateCounter(options.ConsumeRetryTotal, "ea", "Number of message consume retries"); + + _sagaTotal = _meter.CreateCounter(options.SagaTotal, "ea", "Number of sagas executed"); + _sagaFaultTotal = _meter.CreateCounter(options.SagaFaultTotal, "ea", "Number of sagas faults"); + + _handlerTotal = _meter.CreateCounter(options.HandlerTotal, "ea", "Number of messages handled"); + _handlerFaultTotal = _meter.CreateCounter(options.HandlerFaultTotal, "ea", "Number of message handler faults"); + + _sendTotal = _meter.CreateCounter(options.SendTotal, "ea", "Number of messages sent"); + _sendFaultTotal = _meter.CreateCounter(options.SendFaultTotal, "ea", "Number of message send faults"); + + _outboxSendTotal = _meter.CreateCounter(options.OutboxSendTotal, "ea", "Number of messages sent to outbox"); + _outboxSendFaultTotal = _meter.CreateCounter(options.OutboxSendFaultTotal, "ea", "Number of message send to outbox faults"); + + _executeTotal = _meter.CreateCounter(options.ActivityExecuteTotal, "ea", "Number of activities executed"); + _executeFaultTotal = _meter.CreateCounter(options.ActivityExecuteFaultTotal, "ea", "Number of activity execution faults"); + + _compensateTotal = _meter.CreateCounter(options.ActivityCompensateTotal, "ea", "Number of activities compensated"); + _compensateFaultTotal = _meter.CreateCounter(options.ActivityCompensateFailureTotal, "ea", "Number of activity compensation failures"); + + _outboxDeliveryTotal = _meter.CreateCounter(options.OutboxDeliveryTotal, "ea", "Number of outbox delivery messages executed"); + _outboxDeliveryFaultTotal = _meter.CreateCounter(options.OutboxDeliveryFaultTotal, "ea", "Number of outbox delivery message failures"); + + // Gauges + + _receiveInProgress = _meter.CreateCounter(options.ReceiveInProgress, "ea", "Number of messages being received"); + + _handlerInProgress = _meter.CreateCounter(options.HandlerInProgress, "ea", "Number of handlers in progress"); + + _consumerInProgress = _meter.CreateCounter(options.ConsumerInProgress, "ea", "Number of consumers in progress"); + + _sagaInProgress = _meter.CreateCounter(options.SagaInProgress, "ea", "Number of sagas in progress"); + + _executeInProgress = _meter.CreateCounter(options.ExecuteInProgress, "ea", "Number of activity executions in progress"); + + _compensateInProgress = _meter.CreateCounter(options.CompensateInProgress, "ea", "Number of activity compensations in progress"); + + // Histograms + + _receiveDuration = _meter.CreateHistogram(options.ReceiveDuration, "ms", "Elapsed time spent receiving a message, in millis"); + + _consumeDuration = _meter.CreateHistogram(options.ConsumeDuration, "ms", "Elapsed time spent consuming a message, in millis"); + + _sagaDuration = _meter.CreateHistogram(options.SagaDuration, "ms", "Elapsed time spent saga processing a message, in millis"); + + _handlerDuration = _meter.CreateHistogram(options.HandlerDuration, "ms", "Elapsed time spent handler processing a message, in millis"); + + _deliveryDuration = _meter.CreateHistogram(options.DeliveryDuration, "ms", + "Elapsed time between when the message was sent and when it was consumed, in millis."); + + _executeDuration = _meter.CreateHistogram(options.ActivityExecuteDuration, "ms", "Elapsed time spent executing an activity, in millis"); + + _compensateDuration = _meter.CreateHistogram(options.ActivityCompensateDuration, "ms", + "Elapsed time spent compensating an activity, in millis"); + + _isConfigured = true; + } + + static void AddCustomTags(ref TagList tags, PipeContext pipeContext) + { + if (pipeContext.TryGetPayload(out var metricsContext)) + metricsContext.Populate(ref tags); + } + + static string GetConsumerTypeLabel(string messageLabel) + { + return _labelCache.GetOrAdd(TypeCache.ShortName, type => + { + if (type.StartsWith("MassTransit.MessageHandler<")) + return "Handler"; + + var genericMessageType = "<" + TypeCache.ShortName + ">"; + if (type.IndexOf(genericMessageType, StringComparison.Ordinal) >= 0) + type = type.Replace(genericMessageType, "_" + messageLabel); + + return CleanupLabel(type); + }); + } + + static string CleanupLabel(string label) + { + string SimpleClean(string text) + { + return text.Split('.', '+').Last(); + } + + var indexOf = label.IndexOfAny(_delimiters); + if (indexOf >= 0) + { + if (label[indexOf] == '<') + return SimpleClean(label.Substring(0, indexOf)) + "_" + CleanupLabel(label.Substring(indexOf + 1)); + + if (label[indexOf] == '>') + return SimpleClean(label.Substring(0, indexOf)) + CleanupLabel(label.Substring(indexOf + 1)); + + return SimpleClean(label); + } + + return SimpleClean(label); + } + + static string GetArgumentTypeLabel() + { + return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TArguments)) + .Replace("Arguments", "")); + } + + static string GetLogTypeLabel() + { + return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TLog)).Replace("Log", "")); + } + + static string GetActivityTypeLabel() + { + return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TActivity)).Replace("Activity", "")); + } + + static string GetEndpointLabel(Uri inputAddress) + { + return inputAddress?.AbsolutePath.Split('/').LastOrDefault()?.Replace(".", "_").Replace("/", "_"); + } + + static string GetMessageTypeLabel() + { + return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TMessage))); + } + + static string FormatTypeName(StringBuilder sb, Type type) + { + if (type.IsGenericParameter) + return ""; + + if (type.IsGenericType) + { + var name = type.GetGenericTypeDefinition().Name; + + //remove `1 + var index = name.IndexOf('`'); + if (index > 0) + name = name.Remove(index); + + sb.Append(name); + sb.Append('_'); + Type[] arguments = type.GenericTypeArguments; + for (var i = 0; i < arguments.Length; i++) + { + if (i > 0) + sb.Append('_'); + + FormatTypeName(sb, arguments[i]); + } + } + else + sb.Append(type.Name); + + return sb.ToString(); + } + } +} diff --git a/src/MassTransit/Logging/Monitoring/StartedInstrument.cs b/src/MassTransit/Logging/Monitoring/StartedInstrument.cs new file mode 100644 index 00000000000..4e111af95d2 --- /dev/null +++ b/src/MassTransit/Logging/Monitoring/StartedInstrument.cs @@ -0,0 +1,28 @@ +#nullable enable +namespace MassTransit.Logging +{ + using System; + + + public readonly struct StartedInstrument + { + readonly Action _onFault; + readonly Action? _onStop; + + public StartedInstrument(Action onFault, Action? onStop = default) + { + _onFault = onFault; + _onStop = onStop; + } + + public void AddException(Exception exception) + { + _onFault(exception); + } + + public void Stop() + { + _onStop?.Invoke(); + } + } +} diff --git a/src/MassTransit/Logging/NullLogger.cs b/src/MassTransit/Logging/NullLogger.cs deleted file mode 100644 index f2daf2e84c9..00000000000 --- a/src/MassTransit/Logging/NullLogger.cs +++ /dev/null @@ -1,32 +0,0 @@ -#nullable enable -namespace MassTransit.Logging -{ - using System; - using Microsoft.Extensions.Logging; - - - public class NullLogger : - ILogger - { - NullLogger() - { - } - - public static NullLogger Instance { get; } = new NullLogger(); - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, - Func formatter) - { - } - - public bool IsEnabled(LogLevel logLevel) - { - return false; - } - - public IDisposable BeginScope(TState state) - { - return NullScope.Instance; - } - } -} diff --git a/src/MassTransit/Logging/NullLoggerFactory.cs b/src/MassTransit/Logging/NullLoggerFactory.cs deleted file mode 100644 index 08b016a694c..00000000000 --- a/src/MassTransit/Logging/NullLoggerFactory.cs +++ /dev/null @@ -1,25 +0,0 @@ -#nullable enable -namespace MassTransit.Logging -{ - using Microsoft.Extensions.Logging; - - - public class NullLoggerFactory : - ILoggerFactory - { - public static readonly NullLoggerFactory Instance = new NullLoggerFactory(); - - public ILogger CreateLogger(string name) - { - return NullLogger.Instance; - } - - public void AddProvider(ILoggerProvider provider) - { - } - - public void Dispose() - { - } - } -} diff --git a/src/MassTransit/Logging/NullScope.cs b/src/MassTransit/Logging/NullScope.cs deleted file mode 100644 index 2d7fc6e2fac..00000000000 --- a/src/MassTransit/Logging/NullScope.cs +++ /dev/null @@ -1,20 +0,0 @@ -#nullable enable -namespace MassTransit.Logging -{ - using System; - - - class NullScope : - IDisposable - { - NullScope() - { - } - - public static NullScope Instance { get; } = new NullScope(); - - public void Dispose() - { - } - } -} diff --git a/src/MassTransit/Logging/TextWriterLogger.cs b/src/MassTransit/Logging/TextWriterLogger.cs index e719443ac36..75502adad8f 100644 --- a/src/MassTransit/Logging/TextWriterLogger.cs +++ b/src/MassTransit/Logging/TextWriterLogger.cs @@ -16,11 +16,18 @@ public TextWriterLogger(TextWriterLoggerFactory factory, LogLevel logLevel) _factory = factory; _logLevel = logLevel; } - + #if NET8_0_OR_GREATER + public IDisposable BeginScope(TState state) + where TState : notnull + { + return TestDisposable.Instance; + } + #else public IDisposable BeginScope(TState state) { return TestDisposable.Instance; } + #endif public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { diff --git a/src/MassTransit/Logging/TextWriterLoggerFactory.cs b/src/MassTransit/Logging/TextWriterLoggerFactory.cs index 2869fc6a73b..a62dd9c8b7b 100644 --- a/src/MassTransit/Logging/TextWriterLoggerFactory.cs +++ b/src/MassTransit/Logging/TextWriterLoggerFactory.cs @@ -3,6 +3,7 @@ namespace MassTransit.Logging { using System.IO; using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; diff --git a/src/MassTransit/Logging/TextWriterLoggerOptions.cs b/src/MassTransit/Logging/TextWriterLoggerOptions.cs index 0e659233a23..50b455e0716 100644 --- a/src/MassTransit/Logging/TextWriterLoggerOptions.cs +++ b/src/MassTransit/Logging/TextWriterLoggerOptions.cs @@ -15,6 +15,8 @@ public TextWriterLoggerOptions() _disabled = new List(); } + public LogLevel LogLevel { get; set; } + public TextWriterLoggerOptions Disable(string name) { _disabled.Add(name); @@ -22,8 +24,6 @@ public TextWriterLoggerOptions Disable(string name) return this; } - public LogLevel LogLevel { get; set; } - public bool IsEnabled(string name) { return !_disabled.Any(x => name.StartsWith(x, StringComparison.OrdinalIgnoreCase)); diff --git a/src/MassTransit/MassTransit.csproj b/src/MassTransit/MassTransit.csproj index 4dcfbd82246..0311ad5e86d 100644 --- a/src/MassTransit/MassTransit.csproj +++ b/src/MassTransit/MassTransit.csproj @@ -2,41 +2,42 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 - + MassTransit $(Description) - + + - + + - - - - + - + - + + + @@ -45,8 +46,4 @@ - - - - diff --git a/src/MassTransit/MassTransit.csproj.DotSettings b/src/MassTransit/MassTransit.csproj.DotSettings index 6d89d581c48..b4dd70f928a 100644 --- a/src/MassTransit/MassTransit.csproj.DotSettings +++ b/src/MassTransit/MassTransit.csproj.DotSettings @@ -1,59 +1,93 @@ - + True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - True - False - True - True - True - True - True - True - True - True - True - False - True - True - True - True - True - True - True - True - False - True - True - True - True - False - True - True - True - True - True - True - True - True - False - True - False - True - False - True - True - True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + True + False + True + True + True + True + True + True + True + True + True + False + True + True + True + True + True + True + True + True + False + True + True + True + True + False + True + True + True + True + True + True + True + True + False + True + True + True + True + True + True + True + True + False + True + False + True + True + True diff --git a/src/MassTransit/MassTransitBus.cs b/src/MassTransit/MassTransitBus.cs index e58e43d2c1d..7cbd5d699ae 100644 --- a/src/MassTransit/MassTransitBus.cs +++ b/src/MassTransit/MassTransitBus.cs @@ -195,13 +195,15 @@ public async Task StartAsync(CancellationToken cancellationToken) } catch (OperationCanceledException exception) when (exception.CancellationToken == cancellationToken) { + LogContext.Warning?.Log(exception, "Bus start canceled: {HostAddress}", _host.Address); + try { await busHandle.StopAsync(TimeSpan.FromSeconds(30)).ConfigureAwait(false); } - catch (Exception ex) + catch (Exception stopException) { - LogContext.Warning?.Log(ex, "Bus start faulted, and failed to stop host"); + LogContext.Warning?.Log(stopException, "Bus start canceled, bus stop faulted: {HostAddress}", _host.Address); } await busHandle.Ready.ConfigureAwait(false); @@ -224,7 +226,7 @@ public async Task StartAsync(CancellationToken cancellationToken) { if (busHandle != null) { - LogContext.Debug?.Log(ex, "Bus start faulted, stopping host"); + LogContext.Warning?.Log(ex, "Bus start faulted: {HostAddress}", _host.Address); await busHandle.StopAsync(cancellationToken).ConfigureAwait(false); } @@ -234,7 +236,7 @@ public async Task StartAsync(CancellationToken cancellationToken) } catch (Exception stopException) { - LogContext.Warning?.Log(stopException, "Bus start faulted, and failed to stop host"); + LogContext.Warning?.Log(stopException, "Bus start faulted, bus stop faulted: {HostAddress}", _host.Address); } _busState = BusState.Faulted; diff --git a/src/MassTransit/Mediator/Contexts/MediatorSendEndpoint.cs b/src/MassTransit/Mediator/Contexts/MediatorSendEndpoint.cs index 68f1d0b1e6b..f5e438fa8e8 100644 --- a/src/MassTransit/Mediator/Contexts/MediatorSendEndpoint.cs +++ b/src/MassTransit/Mediator/Contexts/MediatorSendEndpoint.cs @@ -219,7 +219,7 @@ async Task SendMessage(T message, IPipe> pipe, CancellationTok if (_sendObservers.Count > 0) await _sendObservers.PreSend(context).ConfigureAwait(false); - await _dispatcher.Dispatch(receiveContext).ConfigureAwait(false); + await _dispatcher.Dispatch(receiveContext, NoLockReceiveContext.Instance).ConfigureAwait(false); if (_sendObservers.Count > 0) await _sendObservers.PostSend(context).ConfigureAwait(false); diff --git a/src/MassTransit/MessageData/Configuration/CourierMessageDataConfigurationObserver.cs b/src/MassTransit/MessageData/Configuration/CourierMessageDataConfigurationObserver.cs index dc0840b0a3e..4ad7e9698b3 100644 --- a/src/MassTransit/MessageData/Configuration/CourierMessageDataConfigurationObserver.cs +++ b/src/MassTransit/MessageData/Configuration/CourierMessageDataConfigurationObserver.cs @@ -56,5 +56,17 @@ public override void CompensateActivityConfigured(ICompensateAc configurator.Log(x => x.AddPipeSpecification(specification)); } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } diff --git a/src/MassTransit/MessageData/Configuration/GetMessageDataTransformSpecification.cs b/src/MassTransit/MessageData/Configuration/GetMessageDataTransformSpecification.cs index 190c5683f49..a14905285ee 100644 --- a/src/MassTransit/MessageData/Configuration/GetMessageDataTransformSpecification.cs +++ b/src/MassTransit/MessageData/Configuration/GetMessageDataTransformSpecification.cs @@ -25,7 +25,7 @@ public GetMessageDataTransformSpecification(IMessageDataRepository repository, I Replace = true; - var types = new HashSet(knownTypes ?? Enumerable.Empty()) {typeof(TMessage)}; + var types = new HashSet(knownTypes ?? Enumerable.Empty()) { typeof(TMessage) }; AddMessageDataProperties(repository, types); } diff --git a/src/MassTransit/MessageData/Conventions/MessageDataConsumeTopologyConvention.cs b/src/MassTransit/MessageData/Conventions/MessageDataConsumeTopologyConvention.cs index 33631e9636b..f0ffdd3e9bf 100644 --- a/src/MassTransit/MessageData/Conventions/MessageDataConsumeTopologyConvention.cs +++ b/src/MassTransit/MessageData/Conventions/MessageDataConsumeTopologyConvention.cs @@ -1,5 +1,6 @@ namespace MassTransit.MessageData.Conventions { + using System.Diagnostics.CodeAnalysis; using MassTransit.Configuration; @@ -14,7 +15,7 @@ public MessageDataConsumeTopologyConvention(IMessageDataRepository repository) new Factory(repository)); } - public bool TryGetMessageConsumeTopologyConvention(out IMessageConsumeTopologyConvention convention) + public bool TryGetMessageConsumeTopologyConvention([NotNullWhen(true)] out IMessageConsumeTopologyConvention convention) where T : class { return _cache.GetOrAdd>().TryGetMessageConsumeTopologyConvention(out convention); diff --git a/src/MassTransit/MessageData/Conventions/MessageDataMessageConsumeTopologyConvention.cs b/src/MassTransit/MessageData/Conventions/MessageDataMessageConsumeTopologyConvention.cs index e4260663315..ec19ad798cf 100644 --- a/src/MassTransit/MessageData/Conventions/MessageDataMessageConsumeTopologyConvention.cs +++ b/src/MassTransit/MessageData/Conventions/MessageDataMessageConsumeTopologyConvention.cs @@ -1,5 +1,7 @@ +#nullable enable namespace MassTransit.MessageData.Conventions { + using System.Diagnostics.CodeAnalysis; using Configuration; using MassTransit.Configuration; @@ -15,14 +17,15 @@ public MessageDataMessageConsumeTopologyConvention(IMessageDataRepository reposi _repository = repository; } - bool IMessageConsumeTopologyConvention.TryGetMessageConsumeTopologyConvention(out IMessageConsumeTopologyConvention convention) + bool IMessageConsumeTopologyConvention.TryGetMessageConsumeTopologyConvention( + [NotNullWhen(true)] out IMessageConsumeTopologyConvention? convention) { convention = this as IMessageConsumeTopologyConvention; return convention != null; } - bool IMessageConsumeTopologyConvention.TryGetMessageConsumeTopology(out IMessageConsumeTopology messageConsumeTopology) + public bool TryGetMessageConsumeTopology([NotNullWhen(true)] out IMessageConsumeTopology? messageConsumeTopology) { var specification = new GetMessageDataTransformSpecification(_repository); if (specification.TryGetConsumeTopology(out messageConsumeTopology)) diff --git a/src/MassTransit/MessageData/FileSystemMessageDataRepository.cs b/src/MassTransit/MessageData/FileSystemMessageDataRepository.cs index 76955c04be1..9ce85a093e3 100644 --- a/src/MassTransit/MessageData/FileSystemMessageDataRepository.cs +++ b/src/MassTransit/MessageData/FileSystemMessageDataRepository.cs @@ -11,7 +11,7 @@ public class FileSystemMessageDataRepository : IMessageDataRepository { const int DefaultBufferSize = 4096; - static readonly char[] _separator = {':'}; + static readonly char[] _separator = { ':' }; readonly DirectoryInfo _dataDirectory; public FileSystemMessageDataRepository(DirectoryInfo dataDirectory) @@ -72,7 +72,7 @@ static string ParseFilePath(Uri address) if (address.Scheme != "urn") throw new ArgumentException("The address must be a urn"); - string[] parts = address.Segments[0].Split(_separator); + var parts = address.Segments[0].Split(_separator); if (parts[0] != "file") throw new ArgumentException("The address must be a urn:file"); diff --git a/src/MassTransit/MessageData/InMemoryMessageDataRepository.cs b/src/MassTransit/MessageData/InMemoryMessageDataRepository.cs index a4fd39b6b6a..fa7a68ea2f2 100644 --- a/src/MassTransit/MessageData/InMemoryMessageDataRepository.cs +++ b/src/MassTransit/MessageData/InMemoryMessageDataRepository.cs @@ -1,44 +1,44 @@ -namespace MassTransit.MessageData -{ - using System; - using System.Collections.Concurrent; - using System.IO; - using System.Threading; - using System.Threading.Tasks; - - - public class InMemoryMessageDataRepository : - IMessageDataRepository - { - readonly ConcurrentDictionary _values; - - public InMemoryMessageDataRepository() - { - _values = new ConcurrentDictionary(); - } - - Task IMessageDataRepository.Get(Uri address, CancellationToken cancellationToken) - { - if (address == null) - throw new ArgumentNullException(nameof(address)); - - if (_values.TryGetValue(address, out byte[] value)) - return Task.FromResult(new MemoryStream(value, false)); - - throw new MessageDataNotFoundException(address); - } - - async Task IMessageDataRepository.Put(Stream stream, TimeSpan? timeToLive, CancellationToken cancellationToken) - { - var address = new InMemoryMessageDataId().Uri; - - using var ms = new MemoryStream(); - - await stream.CopyToAsync(ms).ConfigureAwait(false); - - _values.TryAdd(address, ms.ToArray()); - - return address; - } - } -} +namespace MassTransit.MessageData +{ + using System; + using System.Collections.Concurrent; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + + + public class InMemoryMessageDataRepository : + IMessageDataRepository + { + readonly ConcurrentDictionary _values; + + public InMemoryMessageDataRepository() + { + _values = new ConcurrentDictionary(); + } + + Task IMessageDataRepository.Get(Uri address, CancellationToken cancellationToken) + { + if (address == null) + throw new ArgumentNullException(nameof(address)); + + if (_values.TryGetValue(address, out var value)) + return Task.FromResult(new MemoryStream(value, false)); + + throw new MessageDataNotFoundException(address); + } + + async Task IMessageDataRepository.Put(Stream stream, TimeSpan? timeToLive, CancellationToken cancellationToken) + { + var address = new InMemoryMessageDataId().Uri; + + using var ms = new MemoryStream(); + + await stream.CopyToAsync(ms).ConfigureAwait(false); + + _values.TryAdd(address, ms.ToArray()); + + return address; + } + } +} diff --git a/src/MassTransit/MessageDataExtensions.cs b/src/MassTransit/MessageDataExtensions.cs index f3a80e85da4..7180f0f2ec9 100644 --- a/src/MassTransit/MessageDataExtensions.cs +++ b/src/MassTransit/MessageDataExtensions.cs @@ -3,6 +3,7 @@ using System; using System.IO; using System.Text; + using System.Text.Json; using System.Threading; using System.Threading.Tasks; using MessageData.Values; @@ -83,7 +84,7 @@ public static async Task PutObject(this IMessageDataRepository rep if (value == null) return EmptyMessageData.Instance; - var bytes = System.Text.Json.JsonSerializer.SerializeToUtf8Bytes(value, objectType, SystemTextJsonMessageSerializer.Options); + var bytes = JsonSerializer.SerializeToUtf8Bytes(value, objectType, SystemTextJsonMessageSerializer.Options); if (bytes.Length < MessageDataDefaults.Threshold && !MessageDataDefaults.AlwaysWriteToRepository) return new BytesInlineMessageData(bytes); @@ -125,9 +126,9 @@ public static async Task> GetString(this IMessageDataReposit using var ms = new MemoryStream(); - var stream = await repository.Get(address, cancellationToken).ConfigureAwait(false); + using var stream = await repository.Get(address, cancellationToken).ConfigureAwait(false); - await stream.CopyToAsync(ms).ConfigureAwait(false); + await stream.CopyToAsync(ms, 4096, cancellationToken).ConfigureAwait(false); return new StoredMessageData(address, Encoding.UTF8.GetString(ms.ToArray())); } @@ -140,9 +141,9 @@ public static async Task> GetBytes(this IMessageDataReposito using var ms = new MemoryStream(); - var stream = await repository.Get(address, cancellationToken).ConfigureAwait(false); + using var stream = await repository.Get(address, cancellationToken).ConfigureAwait(false); - await stream.CopyToAsync(ms).ConfigureAwait(false); + await stream.CopyToAsync(ms, 4096, cancellationToken).ConfigureAwait(false); return new StoredMessageData(address, ms.ToArray()); } diff --git a/src/MassTransit/Metadata/RegistrationMetadata.cs b/src/MassTransit/Metadata/RegistrationMetadata.cs index 39ba3692d19..7f6eeec4609 100644 --- a/src/MassTransit/Metadata/RegistrationMetadata.cs +++ b/src/MassTransit/Metadata/RegistrationMetadata.cs @@ -2,7 +2,6 @@ namespace MassTransit.Metadata { using System; using System.Linq; - using System.Reflection; using Internals; @@ -15,11 +14,24 @@ public static class RegistrationMetadata /// public static bool IsConsumerOrDefinition(Type type) { - Type[] interfaces = type.GetTypeInfo().GetInterfaces(); + Type[] interfaces = type.GetInterfaces(); - return interfaces.Any(t => InterfaceExtensions.HasInterface(t, typeof(IConsumer<>)) - || InterfaceExtensions.HasInterface(t, typeof(IJobConsumer<>)) - || InterfaceExtensions.HasInterface(t, typeof(IConsumerDefinition<>))); + return !IsSaga(type) && interfaces.Any(t => t.HasInterface(typeof(IConsumer<>)) + || t.HasInterface(typeof(IJobConsumer<>)) + || t.HasInterface(typeof(IConsumerDefinition<>))); + } + + /// + /// Returns true if the type is a consumer, or a consumer definition + /// + /// + /// + public static bool IsConsumer(Type type) + { + Type[] interfaces = type.GetInterfaces(); + + return interfaces.Any(t => t.HasInterface(typeof(IConsumer<>)) + || t.HasInterface(typeof(IJobConsumer<>))); } /// @@ -29,7 +41,7 @@ public static bool IsConsumerOrDefinition(Type type) /// public static bool IsSagaOrDefinition(Type type) { - Type[] interfaces = type.GetTypeInfo().GetInterfaces(); + Type[] interfaces = type.GetInterfaces(); if (interfaces.Contains(typeof(ISaga))) return true; @@ -41,6 +53,24 @@ public static bool IsSagaOrDefinition(Type type) || t.HasInterface(typeof(ISagaDefinition<>))); } + /// + /// Returns true if the type is a saga + /// + /// + /// + public static bool IsSaga(Type type) + { + Type[] interfaces = type.GetInterfaces(); + + if (interfaces.Contains(typeof(ISaga))) + return true; + + return interfaces.Any(t => t.HasInterface(typeof(InitiatedBy<>)) + || t.HasInterface(typeof(Orchestrates<>)) + || t.HasInterface(typeof(InitiatedByOrOrchestrates<>)) + || t.HasInterface(typeof(Observes<,>))); + } + /// /// Returns true if the type is a state machine or saga definition /// @@ -48,7 +78,7 @@ public static bool IsSagaOrDefinition(Type type) /// public static bool IsSagaStateMachineOrDefinition(Type type) { - Type[] interfaces = type.GetTypeInfo().GetInterfaces(); + Type[] interfaces = type.GetInterfaces(); return interfaces.Any(t => t.HasInterface(typeof(SagaStateMachine<>)) || t.HasInterface(typeof(ISagaDefinition<>))); @@ -61,7 +91,7 @@ public static bool IsSagaStateMachineOrDefinition(Type type) /// public static bool IsActivityOrDefinition(Type type) { - Type[] interfaces = type.GetTypeInfo().GetInterfaces(); + Type[] interfaces = type.GetInterfaces(); return interfaces.Any(t => t.HasInterface(typeof(IExecuteActivity<>)) || t.HasInterface(typeof(ICompensateActivity<>)) @@ -76,7 +106,7 @@ public static bool IsActivityOrDefinition(Type type) /// public static bool IsFutureOrDefinition(Type type) { - Type[] interfaces = type.GetTypeInfo().GetInterfaces(); + Type[] interfaces = type.GetInterfaces(); return interfaces.Any(t => t.HasInterface(typeof(SagaStateMachine)) || t.HasInterface(typeof(IFutureDefinition<>))); diff --git a/src/MassTransit/Metadata/TypeMetadataCache.cs b/src/MassTransit/Metadata/TypeMetadataCache.cs index 646605914a4..57aeec0b723 100644 --- a/src/MassTransit/Metadata/TypeMetadataCache.cs +++ b/src/MassTransit/Metadata/TypeMetadataCache.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; using System.Reflection; - using System.Threading; using Internals; @@ -38,16 +37,6 @@ public static bool IsTemporaryMessageType(Type type) return MessageTypeCache.IsTemporaryMessageType(type); } - public static bool HasConsumerInterfaces(Type type) - { - return MessageTypeCache.HasConsumerInterfaces(type); - } - - public static bool HasSagaInterfaces(Type type) - { - return MessageTypeCache.HasSagaInterfaces(type); - } - public static Type[] GetMessageTypes(Type type) { return MessageTypeCache.GetMessageTypes(type); @@ -58,16 +47,15 @@ public static string[] GetMessageTypeNames(Type type) return MessageTypeCache.GetMessageTypeNames(type); } - - static class Cached + public static bool IsValidMessageDataType(Type type) { - internal static readonly IImplementationBuilder Builder = new DynamicImplementationBuilder(); + return type.IsInterfaceOrConcreteClass() && MessageTypeCache.IsValidMessageType(type) && !type.IsValueTypeOrObject(); } - public static bool IsValidMessageDataType(Type type) + static class Cached { - return type.IsInterfaceOrConcreteClass() && MessageTypeCache.IsValidMessageType(type) && !type.IsValueTypeOrObject(); + internal static readonly IImplementationBuilder Builder = new DynamicImplementationBuilder(); } } @@ -86,8 +74,6 @@ public class TypeMetadataCache : public static string ShortName => TypeCache.ShortName; public static string DiagnosticAddress => MessageTypeCache.DiagnosticAddress; - public static bool HasSagaInterfaces => MessageTypeCache.HasSagaInterfaces; - public static bool HasConsumerInterfaces => MessageTypeCache.HasConsumerInterfaces; public static IEnumerable Properties => MessageTypeCache.Properties; public static bool IsValidMessageType => MessageTypeCache.IsValidMessageType; public static string InvalidMessageTypeReason => MessageTypeCache.InvalidMessageTypeReason; @@ -100,8 +86,7 @@ public class TypeMetadataCache : static class Cached { - internal static readonly Lazy> Metadata = new Lazy>( - () => new TypeMetadataCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy> Metadata = new Lazy>(() => new TypeMetadataCache()); } } } diff --git a/src/MassTransit/Middleware/ConsumeContextOutputMessageTypeFilter.cs b/src/MassTransit/Middleware/ConsumeContextOutputMessageTypeFilter.cs new file mode 100644 index 00000000000..084055a0150 --- /dev/null +++ b/src/MassTransit/Middleware/ConsumeContextOutputMessageTypeFilter.cs @@ -0,0 +1,112 @@ +namespace MassTransit.Middleware +{ + using System; + using System.Threading.Tasks; + using Observables; + + + /// + /// Converts an inbound context type to a pipe context type post-dispatch + /// + /// The subsequent pipe context type + public class ConsumeContextOutputMessageTypeFilter : + IConsumeContextOutputMessageTypeFilter + where TMessage : class + { + readonly ConsumeObservable _consumeObservers; + readonly ConsumeMessageObservable _observers; + readonly IRequestIdTeeFilter _output; + + public ConsumeContextOutputMessageTypeFilter(ConsumeObservable observers, IRequestIdTeeFilter output) + { + _output = output; + + _consumeObservers = observers; + _observers = new ConsumeMessageObservable(); + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateFilterScope("dispatchPipe"); + scope.Add("outputType", TypeCache>.ShortName); + + _output.Probe(scope); + } + + public Task Send(ConsumeContext context, IPipe next) + { + return context.TryGetMessage(out ConsumeContext pipeContext) + ? SendToOutput(next, pipeContext) + : next.Send(context); + } + + public ConnectHandle ConnectConsumeMessageObserver(IConsumeMessageObserver observer) + { + return _observers.Connect(observer); + } + + public ConnectHandle ConnectPipe(IPipe> pipe) + { + return _output.ConnectPipe(pipe); + } + + public ConnectHandle ConnectPipe(Guid key, IPipe> pipe) + { + return _output.ConnectPipe(key, pipe); + } + + async Task SendToOutput(IPipe next, ConsumeContext pipeContext) + { + if (_observers.Count > 0) + { + var preConsumeTask = _observers.PreConsume(pipeContext); + if (preConsumeTask.Status != TaskStatus.RanToCompletion) + await preConsumeTask.ConfigureAwait(false); + } + + if (_consumeObservers.Count > 0) + { + var preConsumeTask = _consumeObservers.PreConsume(pipeContext); + if (preConsumeTask.Status != TaskStatus.RanToCompletion) + await preConsumeTask.ConfigureAwait(false); + } + + try + { + await _output.Send(pipeContext, next).ConfigureAwait(false); + + if (_observers.Count > 0) + { + var postConsumeTask = _observers.PostConsume(pipeContext); + if (postConsumeTask.Status != TaskStatus.RanToCompletion) + await postConsumeTask.ConfigureAwait(false); + } + + if (_consumeObservers.Count > 0) + { + var postConsumeTask = _consumeObservers.PostConsume(pipeContext); + if (postConsumeTask.Status != TaskStatus.RanToCompletion) + await postConsumeTask.ConfigureAwait(false); + } + } + catch (Exception ex) + { + if (_observers.Count > 0) + { + var consumeFaultTask = _observers.ConsumeFault(pipeContext, ex); + if (consumeFaultTask.Status != TaskStatus.RanToCompletion) + await consumeFaultTask.ConfigureAwait(false); + } + + if (_consumeObservers.Count > 0) + { + var consumeFaultTask = _consumeObservers.ConsumeFault(pipeContext, ex); + if (consumeFaultTask.Status != TaskStatus.RanToCompletion) + await consumeFaultTask.ConfigureAwait(false); + } + + throw; + } + } + } +} diff --git a/src/MassTransit/Middleware/ConsumePipe.cs b/src/MassTransit/Middleware/ConsumePipe.cs index e1c2dbbaf21..15a39cd137d 100644 --- a/src/MassTransit/Middleware/ConsumePipe.cs +++ b/src/MassTransit/Middleware/ConsumePipe.cs @@ -4,7 +4,6 @@ namespace MassTransit.Middleware using System.Collections.Concurrent; using System.Threading.Tasks; using Configuration; - using Observables; using Transports; @@ -12,16 +11,15 @@ public class ConsumePipe : IConsumePipe { readonly TaskCompletionSource _connected; - readonly IDynamicFilter _dynamicFilter; + readonly IConsumeContextMessageTypeFilter _filter; readonly ConcurrentDictionary _outputPipes; readonly IPipe _pipe; readonly IConsumePipeSpecification _specification; - public ConsumePipe(IConsumePipeSpecification specification, IDynamicFilter dynamicFilter, IPipe pipe, - bool autoStart) + public ConsumePipe(IConsumePipeSpecification specification, IConsumeContextMessageTypeFilter filter, IPipe pipe, bool autoStart) { _specification = specification; - _dynamicFilter = dynamicFilter ?? throw new ArgumentNullException(nameof(dynamicFilter)); + _filter = filter ?? throw new ArgumentNullException(nameof(filter)); _pipe = pipe ?? throw new ArgumentNullException(nameof(pipe)); _outputPipes = new ConcurrentDictionary(); @@ -33,27 +31,28 @@ public ConsumePipe(IConsumePipeSpecification specification, IDynamicFilter _connected.Task; - void IProbeSite.Probe(ProbeContext context) + public void Probe(ProbeContext context) { var scope = context.CreateScope("consumePipe"); _pipe.Probe(scope); } - Task IPipe.Send(ConsumeContext context) + public Task Send(ConsumeContext context) { return _pipe.Send(context); } - ConnectHandle IConsumeMessageObserverConnector.ConnectConsumeMessageObserver(IConsumeMessageObserver observer) + public ConnectHandle ConnectConsumeMessageObserver(IConsumeMessageObserver observer) + where TMessage : class { - return _dynamicFilter.ConnectObserver(new ConsumeObserverAdapter(observer)); + return _filter.ConnectConsumeMessageObserver(observer); } public ConnectHandle ConnectConsumePipe(IPipe> pipe) where T : class { - var handle = _dynamicFilter.ConnectPipe(BuildMessagePipe(pipe)); + var handle = _filter.ConnectMessagePipe(BuildMessagePipe(pipe)); if (_connected.Task.Status == TaskStatus.WaitingForActivation) _connected.TrySetResult(true); @@ -67,9 +66,10 @@ public ConnectHandle ConnectConsumePipe(IPipe> pipe, Connec return ConnectConsumePipe(pipe); } - ConnectHandle IRequestPipeConnector.ConnectRequestPipe(Guid requestId, IPipe> pipe) + public ConnectHandle ConnectRequestPipe(Guid requestId, IPipe> pipe) + where T : class { - var handle = _dynamicFilter.ConnectPipe(requestId, BuildMessagePipe(pipe)); + var handle = _filter.ConnectMessagePipe(requestId, BuildMessagePipe(pipe)); if (_connected.Task.Status == TaskStatus.WaitingForActivation) _connected.TrySetResult(true); @@ -77,9 +77,9 @@ ConnectHandle IRequestPipeConnector.ConnectRequestPipe(Guid requestId, IPipe< return handle; } - ConnectHandle IConsumeObserverConnector.ConnectConsumeObserver(IConsumeObserver observer) + public ConnectHandle ConnectConsumeObserver(IConsumeObserver observer) { - return _dynamicFilter.ConnectObserver(new ConsumeObserverAdapter(observer)); + return _filter.ConnectConsumeObserver(observer); } IPipe> BuildMessagePipe(IPipe> pipe) diff --git a/src/MassTransit/Middleware/ConsumerMessageFilter.cs b/src/MassTransit/Middleware/ConsumerMessageFilter.cs index b4001e3b644..437a92cac5e 100644 --- a/src/MassTransit/Middleware/ConsumerMessageFilter.cs +++ b/src/MassTransit/Middleware/ConsumerMessageFilter.cs @@ -38,9 +38,11 @@ void IProbeSite.Probe(ProbeContext context) [DebuggerNonUserCode] async Task IFilter>.Send(ConsumeContext context, IPipe> next) { + var timer = Stopwatch.StartNew(); + StartedActivity? activity = LogContext.Current?.StartConsumerActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartConsumeInstrument(context, timer); - var timer = Stopwatch.StartNew(); try { await _consumerFactory.Send(context, _consumerPipe).ConfigureAwait(false); @@ -49,12 +51,14 @@ async Task IFilter>.Send(ConsumeContext conte await next.Send(context).ConfigureAwait(false); } - catch (OperationCanceledException exception) + catch (Exception exception) when ((exception is OperationCanceledException || exception.GetBaseException() is OperationCanceledException) + && !context.CancellationToken.IsCancellationRequested) { await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); - if (exception.CancellationToken == context.CancellationToken) - throw; + activity?.AddExceptionEvent(exception); + + instrument?.AddException(exception); throw new ConsumerCanceledException($"The operation was canceled by the consumer: {TypeCache.ShortName}"); } @@ -64,11 +68,14 @@ async Task IFilter>.Send(ConsumeContext conte activity?.AddExceptionEvent(exception); + instrument?.AddException(exception); + throw; } finally { activity?.Stop(); + instrument?.Stop(); } } } diff --git a/src/MassTransit/Middleware/ConsumerSplitFilter.cs b/src/MassTransit/Middleware/ConsumerSplitFilter.cs index 2e0b6f9a5a6..52e63d15b9c 100644 --- a/src/MassTransit/Middleware/ConsumerSplitFilter.cs +++ b/src/MassTransit/Middleware/ConsumerSplitFilter.cs @@ -25,7 +25,7 @@ public ConsumerSplitFilter(IFilter> next) void IProbeSite.Probe(ProbeContext context) { var scope = context.CreateFilterScope("split"); - scope.Set(new {ConsumerType = TypeCache.ShortName}); + scope.Set(new { ConsumerType = TypeCache.ShortName }); _next.Probe(scope); } diff --git a/src/MassTransit/Middleware/CorrelatedSagaFilter.cs b/src/MassTransit/Middleware/CorrelatedSagaFilter.cs index f0249eccc84..29433499ef4 100644 --- a/src/MassTransit/Middleware/CorrelatedSagaFilter.cs +++ b/src/MassTransit/Middleware/CorrelatedSagaFilter.cs @@ -27,7 +27,7 @@ public CorrelatedSagaFilter(ISagaRepository sagaRepository, ISagaPolicy>.Send(ConsumeContext context, IPipe> next) + public async Task Send(ConsumeContext context, IPipe> next) { var timer = Stopwatch.StartNew(); try { - await Task.Yield(); await _sagaRepository.Send(context, _policy, _messagePipe).ConfigureAwait(false); await next.Send(context).ConfigureAwait(false); await context.NotifyConsumed(timer.Elapsed, TypeCache.ShortName).ConfigureAwait(false); } - catch (OperationCanceledException exception) + catch (Exception exception) when ((exception is OperationCanceledException || exception.GetBaseException() is OperationCanceledException) + && !context.CancellationToken.IsCancellationRequested) { await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); - if (exception.CancellationToken == context.CancellationToken) - throw; - - throw new ConsumerCanceledException($"The operation was canceled by the consumer: {TypeCache.ShortName}"); + throw new ConsumerCanceledException($"The operation was canceled by the saga: {TypeCache.ShortName}"); } - catch (Exception ex) + catch (Exception exception) { - await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, ex).ConfigureAwait(false); + await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); + throw; } } diff --git a/src/MassTransit/Middleware/DeadLetterFilter.cs b/src/MassTransit/Middleware/DeadLetterFilter.cs index 87c87fb8319..39027439bc5 100644 --- a/src/MassTransit/Middleware/DeadLetterFilter.cs +++ b/src/MassTransit/Middleware/DeadLetterFilter.cs @@ -34,9 +34,9 @@ async Task IFilter.Send(ReceiveContext context, IPipe : // TODO this needs a pipe like instance and consumer, to handle things like retry, etc. public HandlerMessageFilter(MessageHandler handler) { - if (handler == null) - throw new ArgumentNullException(nameof(handler)); - - _handler = handler; + _handler = handler ?? throw new ArgumentNullException(nameof(handler)); } void IProbeSite.Probe(ProbeContext context) @@ -38,9 +35,10 @@ void IProbeSite.Probe(ProbeContext context) [DebuggerNonUserCode] async Task IFilter>.Send(ConsumeContext context, IPipe> next) { + var timer = Stopwatch.StartNew(); StartedActivity? activity = LogContext.Current?.StartHandlerActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartHandlerInstrument(context, timer); - var timer = Stopwatch.StartNew(); try { await _handler(context).ConfigureAwait(false); @@ -51,12 +49,14 @@ async Task IFilter>.Send(ConsumeContext conte await next.Send(context).ConfigureAwait(false); } - catch (OperationCanceledException exception) + catch (Exception exception) when ((exception is OperationCanceledException || exception.GetBaseException() is OperationCanceledException) + && !context.CancellationToken.IsCancellationRequested) { await context.NotifyFaulted(timer.Elapsed, TypeCache>.ShortName, exception).ConfigureAwait(false); - if (exception.CancellationToken == context.CancellationToken) - throw; + activity?.AddExceptionEvent(exception); + + instrument?.AddException(exception); throw new ConsumerCanceledException($"The operation was canceled by the consumer: {TypeCache>.ShortName}"); } @@ -64,12 +64,16 @@ async Task IFilter>.Send(ConsumeContext conte { await context.NotifyFaulted(timer.Elapsed, TypeCache>.ShortName, ex).ConfigureAwait(false); + activity?.AddExceptionEvent(ex); + instrument?.AddException(ex); + Interlocked.Increment(ref _faulted); throw; } finally { activity?.Stop(); + instrument?.Stop(); } } } diff --git a/src/MassTransit/Middleware/IConsumeContextMessageTypeFilter.cs b/src/MassTransit/Middleware/IConsumeContextMessageTypeFilter.cs new file mode 100644 index 00000000000..c60b2be3687 --- /dev/null +++ b/src/MassTransit/Middleware/IConsumeContextMessageTypeFilter.cs @@ -0,0 +1,17 @@ +namespace MassTransit.Middleware +{ + using System; + + + public interface IConsumeContextMessageTypeFilter : + IFilter, + IConsumeMessageObserverConnector, + IConsumeObserverConnector + { + ConnectHandle ConnectMessagePipe(IPipe> pipe) + where T : class; + + ConnectHandle ConnectMessagePipe(Guid key, IPipe> pipe) + where T : class; + } +} diff --git a/src/MassTransit/Middleware/IOutputMessageTypePipeFilter.cs b/src/MassTransit/Middleware/IOutputMessageTypePipeFilter.cs new file mode 100644 index 00000000000..dfebd80d222 --- /dev/null +++ b/src/MassTransit/Middleware/IOutputMessageTypePipeFilter.cs @@ -0,0 +1,14 @@ +namespace MassTransit.Middleware +{ + using System; + + + public interface IConsumeContextOutputMessageTypeFilter : + IFilter, + IPipeConnector>, + IConsumeMessageObserverConnector + where TMessage : class + { + ConnectHandle ConnectPipe(Guid key, IPipe> pipe); + } +} diff --git a/src/MassTransit/Middleware/IPipeConnector.cs b/src/MassTransit/Middleware/IPipeConnector.cs index 9fb97f243f0..615677abb22 100644 --- a/src/MassTransit/Middleware/IPipeConnector.cs +++ b/src/MassTransit/Middleware/IPipeConnector.cs @@ -44,4 +44,23 @@ public interface IKeyPipeConnector ConnectHandle ConnectPipe(TKey key, IPipe pipe) where T : class, PipeContext; } + + + /// + /// Supports connecting a pipe using a key, which is a method of dispatching to different pipes + /// based on context. + /// + /// + /// + public interface IKeyPipeConnector + where TMessage : class + { + /// + /// Connect a pipe to the filter using the specified key + /// + /// + /// + /// + ConnectHandle ConnectPipe(TKey key, IPipe> pipe); + } } diff --git a/src/MassTransit/Middleware/ITeeFilter.cs b/src/MassTransit/Middleware/ITeeFilter.cs index b8002989a28..baf9270ae95 100644 --- a/src/MassTransit/Middleware/ITeeFilter.cs +++ b/src/MassTransit/Middleware/ITeeFilter.cs @@ -1,5 +1,8 @@ namespace MassTransit.Middleware { + using System; + + public interface ITeeFilter : IFilter, IPipeConnector @@ -15,4 +18,12 @@ public interface ITeeFilter : where TContext : class, PipeContext { } + + + public interface IRequestIdTeeFilter : + ITeeFilter>, + IKeyPipeConnector + where TMessage : class + { + } } diff --git a/src/MassTransit/Middleware/InMemoryOutbox/InMemoryOutboxConsumeContext.cs b/src/MassTransit/Middleware/InMemoryOutbox/InMemoryOutboxConsumeContext.cs index 632dac728b0..05f32eb4511 100644 --- a/src/MassTransit/Middleware/InMemoryOutbox/InMemoryOutboxConsumeContext.cs +++ b/src/MassTransit/Middleware/InMemoryOutbox/InMemoryOutboxConsumeContext.cs @@ -140,5 +140,17 @@ public virtual Task NotifyFaulted(TimeSpan duration, string consumerType, Except { return NotifyFaulted(this, duration, consumerType, exception); } + + public void Method1() + { + } + + public void Method2() + { + } + + public void Method3() + { + } } } diff --git a/src/MassTransit/Middleware/InMemoryOutbox/InMemoryOutboxMessageSchedulerContext.cs b/src/MassTransit/Middleware/InMemoryOutbox/InMemoryOutboxMessageSchedulerContext.cs index 4fd93a8c289..ce0fd13a547 100644 --- a/src/MassTransit/Middleware/InMemoryOutbox/InMemoryOutboxMessageSchedulerContext.cs +++ b/src/MassTransit/Middleware/InMemoryOutbox/InMemoryOutboxMessageSchedulerContext.cs @@ -365,20 +365,20 @@ public async Task> SchedulePublish(DateTime scheduledTime return scheduledMessage; } - public Task CancelScheduledPublish(Guid tokenId) + public Task CancelScheduledPublish(Guid tokenId, CancellationToken cancellationToken) where T : class { - return AddCancelMessage(() => _scheduler.Value.CancelScheduledPublish(tokenId)); + return AddCancelMessage(() => _scheduler.Value.CancelScheduledPublish(tokenId, cancellationToken)); } - public Task CancelScheduledPublish(Type messageType, Guid tokenId) + public Task CancelScheduledPublish(Type messageType, Guid tokenId, CancellationToken cancellationToken) { - return AddCancelMessage(() => _scheduler.Value.CancelScheduledPublish(messageType, tokenId)); + return AddCancelMessage(() => _scheduler.Value.CancelScheduledPublish(messageType, tokenId, cancellationToken)); } - public Task CancelScheduledSend(Uri destinationAddress, Guid tokenId) + public Task CancelScheduledSend(Uri destinationAddress, Guid tokenId, CancellationToken cancellationToken) { - return AddCancelMessage(() => _scheduler.Value.CancelScheduledSend(destinationAddress, tokenId)); + return AddCancelMessage(() => _scheduler.Value.CancelScheduledSend(destinationAddress, tokenId, cancellationToken)); } void AddScheduledMessage(ScheduledMessage scheduledMessage) diff --git a/src/MassTransit/Middleware/InMemoryOutboxFilter.cs b/src/MassTransit/Middleware/InMemoryOutboxFilter.cs index 4a544a8592b..9e3d6b22afa 100644 --- a/src/MassTransit/Middleware/InMemoryOutboxFilter.cs +++ b/src/MassTransit/Middleware/InMemoryOutboxFilter.cs @@ -2,7 +2,6 @@ namespace MassTransit.Middleware { using System; using System.Threading.Tasks; - using DependencyInjection; using InMemoryOutbox; using Microsoft.Extensions.DependencyInjection; @@ -14,9 +13,11 @@ public class InMemoryOutboxFilter : { readonly bool _concurrentMessageDelivery; readonly Func _contextFactory; + readonly ISetScopedConsumeContext _setter; - public InMemoryOutboxFilter(Func contextFactory, bool concurrentMessageDelivery) + public InMemoryOutboxFilter(ISetScopedConsumeContext setter, Func contextFactory, bool concurrentMessageDelivery) { + _setter = setter; _contextFactory = contextFactory; _concurrentMessageDelivery = concurrentMessageDelivery; } @@ -27,7 +28,7 @@ public async Task Send(TContext context, IPipe next) IDisposable pop = null; if (context.TryGetPayload(out IServiceScope scope)) - pop = scope.SetCurrentConsumeContext(outboxContext); + pop = _setter.PushContext(scope, outboxContext); try { @@ -54,5 +55,17 @@ public void Probe(ProbeContext context) var scope = context.CreateFilterScope("outbox"); scope.Add("type", "in-memory"); } + + public void Method1() + { + } + + public void Method2() + { + } + + public void Method3() + { + } } } diff --git a/src/MassTransit/Middleware/InitiatedByOrOrchestratesSagaMessageFilter.cs b/src/MassTransit/Middleware/InitiatedByOrOrchestratesSagaMessageFilter.cs index 62e8205883b..252241b98eb 100644 --- a/src/MassTransit/Middleware/InitiatedByOrOrchestratesSagaMessageFilter.cs +++ b/src/MassTransit/Middleware/InitiatedByOrOrchestratesSagaMessageFilter.cs @@ -24,6 +24,7 @@ void IProbeSite.Probe(ProbeContext context) public async Task Send(SagaConsumeContext context, IPipe> next) { StartedActivity? activity = LogContext.Current?.StartSagaActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartSagaInstrument(context); try { await context.Saga.Consume(context).ConfigureAwait(false); @@ -33,12 +34,14 @@ public async Task Send(SagaConsumeContext context, IPipe context, IPipe> next) { StartedActivity? activity = LogContext.Current?.StartSagaActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartSagaInstrument(context); try { await context.Saga.Consume(context).ConfigureAwait(false); @@ -33,12 +34,14 @@ public async Task Send(SagaConsumeContext context, IPipe : public InstanceMessageFilter(TConsumer instance, IPipe> instancePipe) { - if (instance == null) - throw new ArgumentNullException(nameof(instance)); - - if (instancePipe == null) - throw new ArgumentNullException(nameof(instancePipe)); - - _instance = instance; - _instancePipe = instancePipe; + _instance = instance ?? throw new ArgumentNullException(nameof(instance)); + _instancePipe = instancePipe ?? throw new ArgumentNullException(nameof(instancePipe)); } void IProbeSite.Probe(ProbeContext context) @@ -43,9 +37,11 @@ void IProbeSite.Probe(ProbeContext context) [DebuggerNonUserCode] async Task IFilter>.Send(ConsumeContext context, IPipe> next) { + var timer = Stopwatch.StartNew(); + StartedActivity? activity = LogContext.Current?.StartConsumerActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartConsumeInstrument(context, timer); - var timer = Stopwatch.StartNew(); try { await _instancePipe.Send(new ConsumerConsumeContextScope(context, _instance)).ConfigureAwait(false); @@ -54,12 +50,13 @@ async Task IFilter>.Send(ConsumeContext conte await next.Send(context).ConfigureAwait(false); } - catch (OperationCanceledException exception) + catch (Exception exception) when ((exception is OperationCanceledException || exception.GetBaseException() is OperationCanceledException) + && !context.CancellationToken.IsCancellationRequested) { await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); - if (exception.CancellationToken == context.CancellationToken) - throw; + activity?.AddExceptionEvent(exception); + instrument?.AddException(exception); throw new ConsumerCanceledException($"The operation was canceled by the consumer: {TypeCache.ShortName}"); } @@ -68,12 +65,14 @@ async Task IFilter>.Send(ConsumeContext conte await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); activity?.AddExceptionEvent(exception); + instrument?.AddException(exception); throw; } finally { activity?.Stop(); + instrument?.Stop(); } } } diff --git a/src/MassTransit/Middleware/InstrumentCompensateActivityFilter.cs b/src/MassTransit/Middleware/InstrumentCompensateActivityFilter.cs deleted file mode 100644 index b3660370b3c..00000000000 --- a/src/MassTransit/Middleware/InstrumentCompensateActivityFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.Middleware -{ - using System.Threading.Tasks; - using Monitoring; - - - public class InstrumentCompensateActivityFilter : - IFilter> - where TActivity : class, ICompensateActivity - where TLog : class - { - public async Task Send(CompensateActivityContext context, IPipe> next) - { - using var inProgress = Instrumentation.TrackCompensateActivityInProgress(context); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("instrument"); - } - } -} diff --git a/src/MassTransit/Middleware/InstrumentConsumerFilter.cs b/src/MassTransit/Middleware/InstrumentConsumerFilter.cs deleted file mode 100644 index f43edbb2196..00000000000 --- a/src/MassTransit/Middleware/InstrumentConsumerFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.Middleware -{ - using System.Threading.Tasks; - using Monitoring; - - - public class InstrumentConsumerFilter : - IFilter> - where TConsumer : class - where TMessage : class - { - public async Task Send(ConsumerConsumeContext context, IPipe> next) - { - using var inProgress = Instrumentation.TrackConsumerInProgress(); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("instrument"); - } - } -} diff --git a/src/MassTransit/Middleware/InstrumentExecuteActivityFilter.cs b/src/MassTransit/Middleware/InstrumentExecuteActivityFilter.cs deleted file mode 100644 index 44fa725a2dd..00000000000 --- a/src/MassTransit/Middleware/InstrumentExecuteActivityFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.Middleware -{ - using System.Threading.Tasks; - using Monitoring; - - - public class InstrumentExecuteActivityFilter : - IFilter> - where TActivity : class, IExecuteActivity - where TArguments : class - { - public async Task Send(ExecuteActivityContext context, IPipe> next) - { - using var inProgress = Instrumentation.TrackExecuteActivityInProgress(context); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("instrument"); - } - } -} diff --git a/src/MassTransit/Middleware/InstrumentHandlerFilter.cs b/src/MassTransit/Middleware/InstrumentHandlerFilter.cs deleted file mode 100644 index 978a2961f7c..00000000000 --- a/src/MassTransit/Middleware/InstrumentHandlerFilter.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MassTransit.Middleware -{ - using System.Threading.Tasks; - using Monitoring; - - - public class InstrumentHandlerFilter : - IFilter> - where TMessage : class - { - public async Task Send(ConsumeContext context, IPipe> next) - { - using var inProgress = Instrumentation.TrackHandlerInProgress(); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("instrument"); - } - } -} diff --git a/src/MassTransit/Middleware/InstrumentReceiveFilter.cs b/src/MassTransit/Middleware/InstrumentReceiveFilter.cs deleted file mode 100644 index c922d40496f..00000000000 --- a/src/MassTransit/Middleware/InstrumentReceiveFilter.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MassTransit.Middleware -{ - using System.Threading.Tasks; - using Monitoring; - - - public class InstrumentReceiveFilter : - IFilter - { - public async Task Send(ReceiveContext context, IPipe next) - { - using var inProgress = Instrumentation.TrackReceiveInProgress(context); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("instrument"); - } - } -} diff --git a/src/MassTransit/Middleware/InstrumentSagaFilter.cs b/src/MassTransit/Middleware/InstrumentSagaFilter.cs deleted file mode 100644 index fc736510fa2..00000000000 --- a/src/MassTransit/Middleware/InstrumentSagaFilter.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.Middleware -{ - using System.Threading.Tasks; - using Monitoring; - - - public class InstrumentSagaFilter : - IFilter> - where TSaga : class, ISaga - where TMessage : class - { - public async Task Send(SagaConsumeContext context, IPipe> next) - { - using var inProgress = Instrumentation.TrackSagaInProgress(); - - await next.Send(context).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("instrument"); - } - } -} diff --git a/src/MassTransit/Middleware/InternalOutboxExtensions.cs b/src/MassTransit/Middleware/InternalOutboxExtensions.cs index ccf81adcc19..8e6d0de23c2 100644 --- a/src/MassTransit/Middleware/InternalOutboxExtensions.cs +++ b/src/MassTransit/Middleware/InternalOutboxExtensions.cs @@ -14,7 +14,7 @@ internal static ISendEndpoint SkipOutbox(this ISendEndpoint endpoint) if (endpoint is OutboxSendEndpoint outboxSendEndpoint) endpoint = outboxSendEndpoint.Endpoint; - if (endpoint is Middleware.Outbox.OutboxSendEndpoint outboxEndpoint) + if (endpoint is Outbox.OutboxSendEndpoint outboxEndpoint) return outboxEndpoint.Endpoint; return endpoint; diff --git a/src/MassTransit/Middleware/JobConsumerMessageFilter.cs b/src/MassTransit/Middleware/JobConsumerMessageFilter.cs index e755549dc11..cedd7786d8f 100644 --- a/src/MassTransit/Middleware/JobConsumerMessageFilter.cs +++ b/src/MassTransit/Middleware/JobConsumerMessageFilter.cs @@ -21,13 +21,13 @@ public JobConsumerMessageFilter(IRetryPolicy retryPolicy) _retryPolicy = retryPolicy; } - void IProbeSite.Probe(ProbeContext context) + public void Probe(ProbeContext context) { var scope = context.CreateScope("consume"); scope.Add("method", $"Consume(ConsumeContext<{TypeCache.ShortName}> context)"); } - Task IFilter>.Send(ConsumerConsumeContext context, + public Task Send(ConsumerConsumeContext context, IPipe> next) { if (context.Consumer is IJobConsumer messageConsumer) @@ -41,21 +41,21 @@ Task IFilter>.Send(ConsumerConsumeContex async Task RunJob(PipeContext context, IJobConsumer jobConsumer) { var jobContext = context.GetPayload>(); + var notifyJobContext = context.GetPayload(); RetryPolicyContext> policyContext = _retryPolicy.CreatePolicyContext(jobContext); try { - await jobContext.NotifyStarted().ConfigureAwait(false); + await notifyJobContext.NotifyStarted().ConfigureAwait(false); await jobConsumer.Run(jobContext).ConfigureAwait(false); - await jobContext.NotifyCompleted().ConfigureAwait(false); + await notifyJobContext.NotifyCompleted().ConfigureAwait(false); } - catch (OperationCanceledException exception) + catch (OperationCanceledException exception) when (jobContext.CancellationToken == exception.CancellationToken) { - if (jobContext.CancellationToken == exception.CancellationToken) - await jobContext.NotifyCanceled("Operation canceled").ConfigureAwait(false); + await notifyJobContext.NotifyCanceled().ConfigureAwait(false); } catch (Exception exception) { @@ -68,7 +68,7 @@ async Task RunJob(PipeContext context, IJobConsumer jobConsumer) await retryContext.RetryFaulted(exception).ConfigureAwait(false); } - await jobContext.NotifyFaulted(exception).ConfigureAwait(false); + await notifyJobContext.NotifyFaulted(exception).ConfigureAwait(false); return; } @@ -84,14 +84,14 @@ async Task RunJob(PipeContext context, IJobConsumer jobConsumer) await retryContext.RetryFaulted(exception).ConfigureAwait(false); } - await jobContext.NotifyFaulted(exception).ConfigureAwait(false); + await notifyJobContext.NotifyFaulted(exception).ConfigureAwait(false); return; } } var delay = retryContext.Delay ?? TimeSpan.Zero; - await jobContext.NotifyFaulted(exception, delay).ConfigureAwait(false); + await notifyJobContext.NotifyFaulted(exception, delay).ConfigureAwait(false); } finally { diff --git a/src/MassTransit/Middleware/KeyFilter.cs b/src/MassTransit/Middleware/KeyFilter.cs index dfabc96e21d..99bb4445113 100644 --- a/src/MassTransit/Middleware/KeyFilter.cs +++ b/src/MassTransit/Middleware/KeyFilter.cs @@ -26,7 +26,7 @@ public KeyFilter(KeyAccessor keyAccessor) _pipes = new ConcurrentDictionary>(); } - void IProbeSite.Probe(ProbeContext context) + public void Probe(ProbeContext context) { var scope = context.CreateScope("key"); diff --git a/src/MassTransit/Middleware/MessageSplitFilter.cs b/src/MassTransit/Middleware/MessageSplitFilter.cs index 6ecbc8d6991..b4a57ba61a9 100644 --- a/src/MassTransit/Middleware/MessageSplitFilter.cs +++ b/src/MassTransit/Middleware/MessageSplitFilter.cs @@ -25,7 +25,7 @@ public MessageSplitFilter(IFilter> next) void IProbeSite.Probe(ProbeContext context) { var scope = context.CreateFilterScope("split"); - scope.Set(new {MessageType = TypeCache.ShortName}); + scope.Set(new { MessageType = TypeCache.ShortName }); _next.Probe(scope); } diff --git a/src/MassTransit/Middleware/MessageTypeFilter.cs b/src/MassTransit/Middleware/MessageTypeFilter.cs new file mode 100644 index 00000000000..98060ee0fe8 --- /dev/null +++ b/src/MassTransit/Middleware/MessageTypeFilter.cs @@ -0,0 +1,147 @@ +namespace MassTransit.Middleware +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Observables; + + + /// + /// Converts ConsumeContext to ConsumeContext<T> for a given message type + /// type. + /// + public class ConsumeContextMessageTypeFilter : + IConsumeContextMessageTypeFilter + { + readonly IPipe _empty; + readonly ConsumeObservable _observers; + readonly Dictionary _outputPipes; + + IOutputFilter[] _outputPipeArray; + + public ConsumeContextMessageTypeFilter() + { + _outputPipes = new Dictionary(); + _outputPipeArray = Array.Empty(); + + _empty = Pipe.Empty(); + + _observers = new ConsumeObservable(); + } + + public void Probe(ProbeContext context) + { + foreach (var pipe in _outputPipes.Values) + pipe.Probe(context); + } + + public ConnectHandle ConnectMessagePipe(IPipe> pipe) + where T : class + { + return GetMessagePipe().Filter.ConnectPipe(pipe); + } + + public ConnectHandle ConnectMessagePipe(Guid key, IPipe> pipe) + where T : class + { + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + return GetMessagePipe().Filter.ConnectPipe(key, pipe); + } + + public Task Send(ConsumeContext context, IPipe next) + { + IOutputFilter[] outputPipes = _outputPipeArray; + + if (outputPipes.Length == 0) + return Task.CompletedTask; + + if (outputPipes.Length == 1) + return outputPipes[0].Send(context, next); + + async Task SendAsync() + { + var outputTasks = new List(outputPipes.Length); + for (var i = 0; i < outputPipes.Length; i++) + { + var outputTask = outputPipes[i].Send(context, _empty); + if (outputTask.Status == TaskStatus.RanToCompletion) + continue; + + outputTasks.Add(outputTask); + } + + await Task.WhenAll(outputTasks).ConfigureAwait(false); + await next.Send(context).ConfigureAwait(false); + } + + return SendAsync(); + } + + public ConnectHandle ConnectConsumeMessageObserver(IConsumeMessageObserver observer) + where T : class + { + return GetMessagePipe().Filter.ConnectConsumeMessageObserver(observer); + } + + public ConnectHandle ConnectConsumeObserver(IConsumeObserver observer) + { + return _observers.Connect(observer); + } + + OutputFilter GetMessagePipe() + where T : class + { + lock (_outputPipes) + { + if (_outputPipes.TryGetValue(typeof(T), out var outputPipe)) + return (OutputFilter)outputPipe; + + OutputFilter newOutputPipe = CreateOutputPipe(); + + _outputPipes.Add(typeof(T), newOutputPipe); + + _outputPipeArray = _outputPipes.Values.ToArray(); + + return newOutputPipe; + } + } + + OutputFilter CreateOutputPipe() + where T : class + { + return new OutputFilter(_observers); + } + + + interface IOutputFilter : + IFilter + { + } + + + class OutputFilter : + IOutputFilter + where TMessage : class + { + public OutputFilter(ConsumeObservable observers) + { + Filter = new ConsumeContextOutputMessageTypeFilter(observers, new RequestIdTeeFilter()); + } + + public virtual ConsumeContextOutputMessageTypeFilter Filter { get; } + + public Task Send(ConsumeContext context, IPipe next) + { + return Filter.Send(context, next); + } + + public void Probe(ProbeContext context) + { + Filter.Probe(context); + } + } + } +} diff --git a/src/MassTransit/Middleware/Murmur3UnsafeHashGenerator.cs b/src/MassTransit/Middleware/Murmur3UnsafeHashGenerator.cs index ef4f1454f56..499352690c0 100644 --- a/src/MassTransit/Middleware/Murmur3UnsafeHashGenerator.cs +++ b/src/MassTransit/Middleware/Murmur3UnsafeHashGenerator.cs @@ -18,7 +18,7 @@ public unsafe uint Hash(byte[] data) public unsafe uint Hash(string s) { - char[] data = s.ToCharArray(); + var data = s.ToCharArray(); fixed (char* input = &data[0]) { return Hash((byte*)input, (uint)data.Length * sizeof(char), Seed); diff --git a/src/MassTransit/Middleware/ObservesSagaMessageFilter.cs b/src/MassTransit/Middleware/ObservesSagaMessageFilter.cs index fdc990a8977..896aeb1547b 100644 --- a/src/MassTransit/Middleware/ObservesSagaMessageFilter.cs +++ b/src/MassTransit/Middleware/ObservesSagaMessageFilter.cs @@ -24,6 +24,7 @@ void IProbeSite.Probe(ProbeContext context) public async Task Send(SagaConsumeContext context, IPipe> next) { StartedActivity? activity = LogContext.Current?.StartSagaActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartSagaInstrument(context); try { await context.Saga.Consume(context).ConfigureAwait(false); @@ -33,12 +34,14 @@ public async Task Send(SagaConsumeContext context, IPipe context, IPipe> next) { StartedActivity? activity = LogContext.Current?.StartSagaActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartSagaInstrument(context); try { await context.Saga.Consume(context).ConfigureAwait(false); @@ -33,12 +34,14 @@ public async Task Send(SagaConsumeContext context, IPipe t, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default) + .ConfigureAwait(false); + + if (delay.IsCanceled) + cancellationToken.ThrowIfCancellationRequested(); } finally { diff --git a/src/MassTransit/Middleware/Outbox/InMemoryOutboxConsumeContext.cs b/src/MassTransit/Middleware/Outbox/InMemoryOutboxConsumeContext.cs index 55f7213882f..67b30db7da3 100644 --- a/src/MassTransit/Middleware/Outbox/InMemoryOutboxConsumeContext.cs +++ b/src/MassTransit/Middleware/Outbox/InMemoryOutboxConsumeContext.cs @@ -1,4 +1,3 @@ -#nullable enable namespace MassTransit.Middleware.Outbox { using System; @@ -94,6 +93,7 @@ public override async Task AddSend(SendContext context) FaultAddress = context.FaultAddress, SentTime = context.SentTime ?? now, ContentType = context.ContentType?.ToString() ?? context.Serialization.DefaultContentType.ToString(), + MessageType = string.Join(";", context.SupportedMessageTypes), Body = body.GetString() }; diff --git a/src/MassTransit/Middleware/Outbox/InMemoryOutboxMessage.cs b/src/MassTransit/Middleware/Outbox/InMemoryOutboxMessage.cs index 5d56517ef71..7b5c59ff799 100644 --- a/src/MassTransit/Middleware/Outbox/InMemoryOutboxMessage.cs +++ b/src/MassTransit/Middleware/Outbox/InMemoryOutboxMessage.cs @@ -32,6 +32,7 @@ public class InMemoryOutboxMessage : public Guid MessageId { get; set; } public string ContentType { get; set; } = null!; + public string MessageType { get; set; } = null!; public string Body { get; set; } = null!; public Guid? ConversationId { get; set; } diff --git a/src/MassTransit/Middleware/Outbox/InMemoryOutboxMessageRepository.cs b/src/MassTransit/Middleware/Outbox/InMemoryOutboxMessageRepository.cs index acf312b0465..b203a56057e 100644 --- a/src/MassTransit/Middleware/Outbox/InMemoryOutboxMessageRepository.cs +++ b/src/MassTransit/Middleware/Outbox/InMemoryOutboxMessageRepository.cs @@ -4,6 +4,7 @@ namespace MassTransit.Middleware.Outbox using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Internals; public class InMemoryOutboxMessageRepository @@ -26,16 +27,11 @@ public async Task Lock(Guid messageId, Guid consumerId, Ca { var key = new InMemoryInboxMessageKey(messageId, consumerId); - if (!_dictionary.TryGetValue(key, out var existing)) + var existing = _dictionary.GetOrAdd(key, _ => new InMemoryInboxMessage(messageId, consumerId) { - existing = new InMemoryInboxMessage(messageId, consumerId) - { - Received = DateTime.UtcNow, - ReceiveCount = 0 - }; - - _dictionary.Add(key, existing); - } + Received = DateTime.UtcNow, + ReceiveCount = 0 + }); await existing.MarkInUse(cancellationToken).ConfigureAwait(false); diff --git a/src/MassTransit/Middleware/Outbox/OutboxSendEndpoint.cs b/src/MassTransit/Middleware/Outbox/OutboxSendEndpoint.cs index 9f205f0edd8..b0ca491526e 100644 --- a/src/MassTransit/Middleware/Outbox/OutboxSendEndpoint.cs +++ b/src/MassTransit/Middleware/Outbox/OutboxSendEndpoint.cs @@ -177,6 +177,7 @@ async Task AddSend(SendContext context) where T : class { StartedActivity? activity = LogContext.Current?.StartOutboxSendActivity(context); + StartedInstrument? instrument = LogContext.Current?.StartOutboxSendInstrument(context); try { await _context.AddSend(context).ConfigureAwait(false); @@ -185,11 +186,13 @@ async Task AddSend(SendContext context) catch (Exception ex) { activity?.AddExceptionEvent(ex); + instrument?.AddException(ex); throw; } finally { activity?.Stop(); + instrument?.Stop(); } } diff --git a/src/MassTransit/Middleware/OutboxMessageContext.cs b/src/MassTransit/Middleware/OutboxMessageContext.cs index d8e1695d37c..7ab89df63a8 100644 --- a/src/MassTransit/Middleware/OutboxMessageContext.cs +++ b/src/MassTransit/Middleware/OutboxMessageContext.cs @@ -14,6 +14,8 @@ public interface OutboxMessageContext : string ContentType { get; } + string MessageType { get; } + string Body { get; } IReadOnlyDictionary Properties { get; } diff --git a/src/MassTransit/Middleware/OutboxMessagePipe.cs b/src/MassTransit/Middleware/OutboxMessagePipe.cs index ea339da0a30..98e98813e6a 100644 --- a/src/MassTransit/Middleware/OutboxMessagePipe.cs +++ b/src/MassTransit/Middleware/OutboxMessagePipe.cs @@ -36,7 +36,19 @@ public async Task Send(OutboxConsumeContext context) { await _next.Send(context).ConfigureAwait(false); - await context.SetConsumed().ConfigureAwait(false); + await context.ConsumeCompleted.ConfigureAwait(false); + + try + { + await context.SetConsumed().ConfigureAwait(false); + } + catch (Exception exception) + { + if (!context.ReceiveContext.IsFaulted) + await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); + + throw; + } return; } @@ -54,7 +66,7 @@ public async Task Send(OutboxConsumeContext context) LogContext.Debug?.Log("Outbox Completed: {MessageId} ({ReceiveCount})", context.MessageId, context.ReceiveCount); - if (!context.ReceiveContext.IsDelivered && !context.ReceiveContext.IsDelivered) + if (context.ReceiveContext is { IsDelivered: false, IsFaulted: false }) await context.NotifyConsumed(context, timer.Elapsed, _options.ConsumerType).ConfigureAwait(false); context.ContinueProcessing = false; @@ -100,6 +112,7 @@ async Task DeliverOutboxMessages(OutboxConsumeContext context) throw new ApplicationException("Simulated Delivery Failure Requested"); StartedActivity? activity = LogContext.Current?.StartOutboxDeliverActivity(message); + StartedInstrument? instrument = LogContext.Current?.StartOutboxDeliveryInstrument(context, message); try { await endpoint.Send(new SerializedMessageBody(), pipe, token.Token).ConfigureAwait(false); @@ -107,12 +120,14 @@ async Task DeliverOutboxMessages(OutboxConsumeContext context) catch (Exception exception) { activity?.AddExceptionEvent(exception); + instrument?.AddException(exception); throw; } finally { activity?.Stop(); + instrument?.Stop(); } LogContext.Debug?.Log("Outbox Sent: {InboxMessageId} {SequenceNumber} {MessageId}", context.MessageId, message.SequenceNumber, diff --git a/src/MassTransit/Middleware/OutboxMessageSendPipe.cs b/src/MassTransit/Middleware/OutboxMessageSendPipe.cs index e41863daeed..51b0147f14a 100644 --- a/src/MassTransit/Middleware/OutboxMessageSendPipe.cs +++ b/src/MassTransit/Middleware/OutboxMessageSendPipe.cs @@ -3,6 +3,8 @@ namespace MassTransit.Middleware { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; using System.Net.Mime; using System.Threading.Tasks; using Context; @@ -34,19 +36,20 @@ public Task Send(SendContext context) var serializerContext = deserializer.Deserialize(body, headers, _destinationAddress); - if (serializerContext.MessageId.HasValue) - context.MessageId = serializerContext.MessageId; - - context.RequestId = serializerContext.RequestId; - context.ConversationId = serializerContext.ConversationId; - context.CorrelationId = serializerContext.CorrelationId; - context.InitiatorId = serializerContext.InitiatorId; - context.SourceAddress = serializerContext.SourceAddress; - context.ResponseAddress = serializerContext.ResponseAddress; - context.FaultAddress = serializerContext.FaultAddress; - - if (serializerContext.ExpirationTime.HasValue) - context.TimeToLive = serializerContext.ExpirationTime.Value.ToUniversalTime() - DateTime.UtcNow; + context.MessageId = _message.MessageId; + context.RequestId = _message.RequestId; + context.ConversationId = _message.ConversationId; + context.CorrelationId = _message.CorrelationId; + context.InitiatorId = _message.InitiatorId; + context.SourceAddress = _message.SourceAddress; + context.ResponseAddress = _message.ResponseAddress; + context.FaultAddress = _message.FaultAddress; + context.SupportedMessageTypes = string.IsNullOrWhiteSpace(_message.MessageType) + ? serializerContext.SupportedMessageTypes + : _message.MessageType.Split(';').ToArray(); + + if (_message.ExpirationTime.HasValue) + context.TimeToLive = _message.ExpirationTime.Value.ToUniversalTime() - DateTime.UtcNow; foreach (KeyValuePair header in serializerContext.Headers.GetAll()) context.Headers.Set(header.Key, header.Value); @@ -80,9 +83,23 @@ public IEnumerable> GetAll() if (!string.IsNullOrWhiteSpace(_message.ContentType)) yield return new KeyValuePair(MessageHeaders.ContentType, _message.ContentType!); + + foreach (KeyValuePair header in _message.Headers.GetAll()) + { + switch (header.Key) + { + case MessageHeaders.MessageId: + case MessageHeaders.ContentType: + continue; + + default: + yield return header; + break; + } + } } - public bool TryGetHeader(string key, out object? value) + public bool TryGetHeader(string key, [NotNullWhen(true)] out object? value) { if (nameof(_message.MessageId).Equals(key, StringComparison.OrdinalIgnoreCase)) { @@ -93,7 +110,13 @@ public bool TryGetHeader(string key, out object? value) if (MessageHeaders.ContentType.Equals(key, StringComparison.OrdinalIgnoreCase)) { value = _message.ContentType; - return value != null; + return true; + } + + if (_message.Headers.TryGetHeader(key, out var headerValue)) + { + value = headerValue; + return true; } value = null; diff --git a/src/MassTransit/Middleware/PayloadFilter.cs b/src/MassTransit/Middleware/PayloadFilter.cs new file mode 100644 index 00000000000..90bca98b415 --- /dev/null +++ b/src/MassTransit/Middleware/PayloadFilter.cs @@ -0,0 +1,33 @@ +namespace MassTransit.Middleware +{ + using System.Diagnostics; + using System.Threading.Tasks; + + + public class PayloadFilter : + IFilter + where TContext : class, PipeContext + where TPayload : class + { + readonly TPayload _payload; + + public PayloadFilter(TPayload payload) + { + _payload = payload; + } + + public void Probe(ProbeContext context) + { + context.CreateFilterScope("inline"); + } + + [DebuggerNonUserCode] + [DebuggerStepThrough] + public Task Send(TContext context, IPipe next) + { + context.GetOrAddPayload(() => _payload); + + return next.Send(context); + } + } +} diff --git a/src/MassTransit/Middleware/QuerySagaFilter.cs b/src/MassTransit/Middleware/QuerySagaFilter.cs index c58ac912671..e3a12c25e66 100644 --- a/src/MassTransit/Middleware/QuerySagaFilter.cs +++ b/src/MassTransit/Middleware/QuerySagaFilter.cs @@ -29,7 +29,7 @@ public QuerySagaFilter(ISagaRepository sagaRepository, ISagaPolicy>.Send(ConsumeContext context, IPipe> next) + public async Task Send(ConsumeContext context, IPipe> next) { var timer = Stopwatch.StartNew(); try { if (_queryFactory.TryCreateQuery(context, out ISagaQuery query)) { - await Task.Yield(); - await _sagaRepository.SendQuery(context, query, _policy, _messagePipe).ConfigureAwait(false); await context.NotifyConsumed(timer.Elapsed, TypeCache.ShortName).ConfigureAwait(false); @@ -56,18 +54,17 @@ async Task IFilter>.Send(ConsumeContext conte await next.Send(context).ConfigureAwait(false); } - catch (OperationCanceledException exception) + catch (Exception exception) when ((exception is OperationCanceledException || exception.GetBaseException() is OperationCanceledException) + && !context.CancellationToken.IsCancellationRequested) { await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); - if (exception.CancellationToken == context.CancellationToken) - throw; - - throw new ConsumerCanceledException($"The operation was canceled by the consumer: {TypeCache.ShortName}"); + throw new ConsumerCanceledException($"The operation was canceled by the saga: {TypeCache.ShortName}"); } - catch (Exception ex) + catch (Exception exception) { - await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, ex).ConfigureAwait(false); + await context.NotifyFaulted(timer.Elapsed, TypeCache.ShortName, exception).ConfigureAwait(false); + throw; } } diff --git a/src/MassTransit/Middleware/ReceiveEndpointDependencyFilter.cs b/src/MassTransit/Middleware/ReceiveEndpointDependencyFilter.cs new file mode 100644 index 00000000000..7cdcd65b35b --- /dev/null +++ b/src/MassTransit/Middleware/ReceiveEndpointDependencyFilter.cs @@ -0,0 +1,32 @@ +namespace MassTransit.Middleware +{ + using System.Threading.Tasks; + using Internals; + using Transports; + + + public class ReceiveEndpointDependencyFilter : + IFilter + where TContext : class, PipeContext + { + readonly ReceiveEndpointContext _context; + + public ReceiveEndpointDependencyFilter(ReceiveEndpointContext context) + { + _context = context; + } + + public async Task Send(TContext context, IPipe next) + { + await _context.DependenciesReady.OrCanceled(context.CancellationToken).ConfigureAwait(false); + + await next.Send(context).ConfigureAwait(false); + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateScope("receiveEndpointDependencies"); + scope.Add("contextType", typeof(TContext).Name); + } + } +} diff --git a/src/MassTransit/Middleware/RequestIdFilter.cs b/src/MassTransit/Middleware/RequestIdFilter.cs new file mode 100644 index 00000000000..ffd994cb9e2 --- /dev/null +++ b/src/MassTransit/Middleware/RequestIdFilter.cs @@ -0,0 +1,85 @@ +namespace MassTransit.Middleware +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Threading.Tasks; + + + /// + /// Handles the registration of requests and connecting them to the consume pipe + /// + public class RequestIdFilter : + IFilter>, + IKeyPipeConnector + where TMessage : class + { + readonly ConcurrentDictionary>> _pipes; + + public RequestIdFilter() + { + _pipes = new ConcurrentDictionary>>(); + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateScope("key"); + + ICollection>> pipes = _pipes.Values; + scope.Add("count", pipes.Count); + + foreach (IPipe> pipe in pipes) + pipe.Probe(scope); + } + + public async Task Send(ConsumeContext context, IPipe> next) + { + Guid? key = context.RequestId; + if (key.HasValue && _pipes.TryGetValue(key.Value, out IPipe> pipe)) + await pipe.Send(context).ConfigureAwait(false); + + await next.Send(context).ConfigureAwait(false); + } + + public ConnectHandle ConnectPipe(Guid key, IPipe> pipe) + { + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var added = _pipes.TryAdd(key, pipe); + if (!added) + throw new DuplicateKeyPipeConfigurationException($"A pipe with the specified key already exists: {key}"); + + return new Handle(key, RemovePipe); + } + + void RemovePipe(Guid key) + { + _pipes.TryRemove(key, out IPipe> _); + } + + + class Handle : + ConnectHandle + { + readonly Guid _key; + readonly Action _removeKey; + + public Handle(Guid key, Action removeKey) + { + _key = key; + _removeKey = removeKey; + } + + public void Disconnect() + { + _removeKey(_key); + } + + public void Dispose() + { + Disconnect(); + } + } + } +} diff --git a/src/MassTransit/Middleware/RequestIdTeeFilter.cs b/src/MassTransit/Middleware/RequestIdTeeFilter.cs new file mode 100644 index 00000000000..8fdb0cd73ed --- /dev/null +++ b/src/MassTransit/Middleware/RequestIdTeeFilter.cs @@ -0,0 +1,32 @@ +namespace MassTransit.Middleware +{ + using System; + + + public class RequestIdTeeFilter : + TeeFilter>, + IRequestIdTeeFilter + where TMessage : class + { + readonly Lazy> _keyConnections; + + public RequestIdTeeFilter() + { + _keyConnections = new Lazy>(ConnectKeyFilter); + } + + public ConnectHandle ConnectPipe(Guid key, IPipe> pipe) + { + return _keyConnections.Value.ConnectPipe(key, pipe); + } + + RequestIdFilter ConnectKeyFilter() + { + var filter = new RequestIdFilter(); + + ConnectPipe(filter.ToPipe()); + + return filter; + } + } +} diff --git a/src/MassTransit/Middleware/RetryFilter.cs b/src/MassTransit/Middleware/RetryFilter.cs index 71b03825114..a8557b2f4d4 100644 --- a/src/MassTransit/Middleware/RetryFilter.cs +++ b/src/MassTransit/Middleware/RetryFilter.cs @@ -59,6 +59,8 @@ async Task IFilter.Send(TContext context, IPipe next) { if (_retryPolicy.IsHandled(exception)) { + context.GetOrAddPayload(() => payloadRetryContext); + var retryFaultedTask = policyContext.RetryFaulted(exception); if (retryFaultedTask.Status != TaskStatus.RanToCompletion) await retryFaultedTask.ConfigureAwait(false); @@ -71,8 +73,6 @@ async Task IFilter.Send(TContext context, IPipe next) } } - context.GetOrAddPayload(() => payloadRetryContext); - throw; } @@ -80,6 +80,8 @@ async Task IFilter.Send(TContext context, IPipe next) { if (_retryPolicy.IsHandled(exception)) { + context.GetOrAddPayload(() => genericRetryContext); + var retryFaultedTask = policyContext.RetryFaulted(exception); if (retryFaultedTask.Status != TaskStatus.RanToCompletion) await retryFaultedTask.ConfigureAwait(false); @@ -92,8 +94,6 @@ async Task IFilter.Send(TContext context, IPipe next) } } - context.GetOrAddPayload(() => genericRetryContext); - throw; } @@ -101,6 +101,8 @@ async Task IFilter.Send(TContext context, IPipe next) { if (_retryPolicy.IsHandled(exception)) { + context.GetOrAddPayload(() => retryContext); + var retryFaultedTask = retryContext.RetryFaulted(exception); if (retryFaultedTask.Status != TaskStatus.RanToCompletion) await retryFaultedTask.ConfigureAwait(false); @@ -111,8 +113,6 @@ async Task IFilter.Send(TContext context, IPipe next) if (retryFaultTask.Status != TaskStatus.RanToCompletion) await retryFaultTask.ConfigureAwait(false); } - - context.GetOrAddPayload(() => retryContext); } throw; @@ -179,6 +179,8 @@ async Task Attempt(TContext context, RetryContext retryContext, IPipe< { if (_retryPolicy.IsHandled(exception)) { + context.GetOrAddPayload(() => payloadRetryContext); + var retryFaultedTask = retryContext.RetryFaulted(exception); if (retryFaultedTask.Status != TaskStatus.RanToCompletion) await retryFaultedTask.ConfigureAwait(false); @@ -191,8 +193,6 @@ async Task Attempt(TContext context, RetryContext retryContext, IPipe< } } - context.GetOrAddPayload(() => payloadRetryContext); - throw; } @@ -200,6 +200,8 @@ async Task Attempt(TContext context, RetryContext retryContext, IPipe< { if (_retryPolicy.IsHandled(exception)) { + context.GetOrAddPayload(() => genericRetryContext); + var retryFaultedTask = retryContext.RetryFaulted(exception); if (retryFaultedTask.Status != TaskStatus.RanToCompletion) await retryFaultedTask.ConfigureAwait(false); @@ -212,8 +214,6 @@ async Task Attempt(TContext context, RetryContext retryContext, IPipe< } } - context.GetOrAddPayload(() => genericRetryContext); - throw; } @@ -221,6 +221,8 @@ async Task Attempt(TContext context, RetryContext retryContext, IPipe< { if (_retryPolicy.IsHandled(exception)) { + context.GetOrAddPayload(() => nextRetryContext); + var retryFaultedTask = nextRetryContext.RetryFaulted(exception); if (retryFaultedTask.Status != TaskStatus.RanToCompletion) await retryFaultedTask.ConfigureAwait(false); @@ -231,8 +233,6 @@ async Task Attempt(TContext context, RetryContext retryContext, IPipe< if (retryFaultTask.Status != TaskStatus.RanToCompletion) await retryFaultTask.ConfigureAwait(false); } - - context.GetOrAddPayload(() => nextRetryContext); } throw; diff --git a/src/MassTransit/Middleware/ScopedCompensateFilter.cs b/src/MassTransit/Middleware/ScopedCompensateFilter.cs index 78416392451..fd4129a7191 100644 --- a/src/MassTransit/Middleware/ScopedCompensateFilter.cs +++ b/src/MassTransit/Middleware/ScopedCompensateFilter.cs @@ -2,6 +2,7 @@ namespace MassTransit.Middleware { using System.Threading.Tasks; using DependencyInjection; + using Metadata; public class ScopedCompensateFilter : @@ -29,6 +30,7 @@ public async Task Send(CompensateContext context, IPipe.ShortName); _scopeProvider.Probe(scope); } diff --git a/src/MassTransit/Middleware/ScopedConsumeFilter.cs b/src/MassTransit/Middleware/ScopedConsumeFilter.cs index 56b0af0f347..3363cc275b4 100644 --- a/src/MassTransit/Middleware/ScopedConsumeFilter.cs +++ b/src/MassTransit/Middleware/ScopedConsumeFilter.cs @@ -2,6 +2,7 @@ namespace MassTransit.Middleware { using System.Threading.Tasks; using DependencyInjection; + using Metadata; public class ScopedConsumeFilter : @@ -28,6 +29,7 @@ public async Task Send(ConsumeContext context, IPipe> next) public void Probe(ProbeContext context) { var scope = context.CreateFilterScope("scopedFilter"); + scope.Add("filter", TypeMetadataCache.ShortName); _scopeProvider.Probe(scope); } diff --git a/src/MassTransit/Middleware/ScopedExecuteFilter.cs b/src/MassTransit/Middleware/ScopedExecuteFilter.cs index 73212845d95..49fe73bb6af 100644 --- a/src/MassTransit/Middleware/ScopedExecuteFilter.cs +++ b/src/MassTransit/Middleware/ScopedExecuteFilter.cs @@ -2,6 +2,7 @@ namespace MassTransit.Middleware { using System.Threading.Tasks; using DependencyInjection; + using Metadata; public class ScopedExecuteFilter : @@ -29,6 +30,7 @@ public async Task Send(ExecuteContext context, IPipe.ShortName); _scopeProvider.Probe(scope); } diff --git a/src/MassTransit/Middleware/SendQuerySagaPipe.cs b/src/MassTransit/Middleware/SendQuerySagaPipe.cs index 6f12fe12897..58bc4c2ca4c 100644 --- a/src/MassTransit/Middleware/SendQuerySagaPipe.cs +++ b/src/MassTransit/Middleware/SendQuerySagaPipe.cs @@ -1,7 +1,6 @@ namespace MassTransit.Middleware { using System; - using System.Linq; using System.Threading.Tasks; using Logging; using Saga; @@ -29,7 +28,7 @@ public async Task Send(SagaRepositoryQueryContext context) { if (context.Count > 0) { - async Task LoadInstance(Guid correlationId) + async Task SendToInstance(Guid correlationId) { SagaConsumeContext sagaConsumeContext = await context.Load(correlationId).ConfigureAwait(false); if (sagaConsumeContext != null) @@ -41,9 +40,7 @@ async Task LoadInstance(Guid correlationId) await _policy.Existing(sagaConsumeContext, _next).ConfigureAwait(false); if (_policy.IsReadOnly) - { await context.Undo(sagaConsumeContext).ConfigureAwait(false); - } else { if (sagaConsumeContext.IsCompleted) @@ -71,7 +68,8 @@ async Task LoadInstance(Guid correlationId) } } - await Task.WhenAll(context.Select(LoadInstance)).ConfigureAwait(false); + foreach (var correlationId in context) + await SendToInstance(correlationId).ConfigureAwait(false); } else { diff --git a/src/MassTransit/Middleware/SetPartitionKeyFilter.cs b/src/MassTransit/Middleware/SetPartitionKeyFilter.cs new file mode 100644 index 00000000000..4fc485c397b --- /dev/null +++ b/src/MassTransit/Middleware/SetPartitionKeyFilter.cs @@ -0,0 +1,33 @@ +namespace MassTransit.Middleware +{ + using System.Threading.Tasks; + using Transports; + + + public class SetPartitionKeyFilter : + IFilter> + where TMessage : class + { + readonly IMessagePartitionKeyFormatter _routingKeyFormatter; + + public SetPartitionKeyFilter(IMessagePartitionKeyFormatter routingKeyFormatter) + { + _routingKeyFormatter = routingKeyFormatter; + } + + public Task Send(SendContext context, IPipe> next) + { + var routingKey = _routingKeyFormatter.FormatPartitionKey(context); + + if (context.TryGetPayload(out PartitionKeySendContext routingKeySendContext)) + routingKeySendContext.PartitionKey = routingKey; + + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + context.CreateFilterScope("setPartitionKey"); + } + } +} diff --git a/src/MassTransit/Middleware/SetRoutingKeyFilter.cs b/src/MassTransit/Middleware/SetRoutingKeyFilter.cs new file mode 100644 index 00000000000..0706e497125 --- /dev/null +++ b/src/MassTransit/Middleware/SetRoutingKeyFilter.cs @@ -0,0 +1,33 @@ +namespace MassTransit.Middleware +{ + using System.Threading.Tasks; + using Transports; + + + public class SetRoutingKeyFilter : + IFilter> + where TMessage : class + { + readonly IMessageRoutingKeyFormatter _routingKeyFormatter; + + public SetRoutingKeyFilter(IMessageRoutingKeyFormatter routingKeyFormatter) + { + _routingKeyFormatter = routingKeyFormatter; + } + + public Task Send(SendContext context, IPipe> next) + { + var routingKey = _routingKeyFormatter.FormatRoutingKey(context); + + if (context.TryGetPayload(out RoutingKeySendContext routingKeySendContext)) + routingKeySendContext.RoutingKey = routingKey; + + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + context.CreateFilterScope("setRoutingKey"); + } + } +} diff --git a/src/MassTransit/Middleware/SetSerializerFilter.cs b/src/MassTransit/Middleware/SetSerializerFilter.cs new file mode 100644 index 00000000000..c8adbc35187 --- /dev/null +++ b/src/MassTransit/Middleware/SetSerializerFilter.cs @@ -0,0 +1,35 @@ +namespace MassTransit.Middleware +{ + using System.Net.Mime; + using System.Threading.Tasks; + + + /// + /// Sets the CorrelationId header uses the supplied implementation. + /// + /// The message type + public class SetSerializerFilter : + IFilter> + where T : class + { + readonly ContentType _contentType; + + public SetSerializerFilter(ContentType contentType) + { + _contentType = contentType; + } + + public Task Send(SendContext context, IPipe> next) + { + if (context.Serialization.TryGetMessageSerializer(_contentType, out var serializer)) + context.Serializer = serializer; + + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + context.CreateFilterScope("SetMessageSerializer"); + } + } +} diff --git a/src/MassTransit/Middleware/StateMachineSagaMessageFilter.cs b/src/MassTransit/Middleware/StateMachineSagaMessageFilter.cs index f618d7d12e8..2a2d5e69fb0 100644 --- a/src/MassTransit/Middleware/StateMachineSagaMessageFilter.cs +++ b/src/MassTransit/Middleware/StateMachineSagaMessageFilter.cs @@ -5,24 +5,23 @@ namespace MassTransit.Middleware using System.Linq; using System.Threading.Tasks; using Logging; - using SagaStateMachine; /// /// Dispatches the ConsumeContext to the consumer method for the specified message type /// - /// The consumer type + /// The consumer type /// The message type - public class StateMachineSagaMessageFilter : - ISagaMessageFilter - where TSaga : class, ISaga, SagaStateMachineInstance + public class StateMachineSagaMessageFilter : + ISagaMessageFilter + where TInstance : class, ISaga, SagaStateMachineInstance where TMessage : class { readonly string _activityName; readonly Event _event; - readonly SagaStateMachine _machine; + readonly SagaStateMachine _machine; - public StateMachineSagaMessageFilter(SagaStateMachine machine, Event @event) + public StateMachineSagaMessageFilter(SagaStateMachine machine, Event @event) { _machine = machine; _event = @event; @@ -37,26 +36,29 @@ void IProbeSite.Probe(ProbeContext context) { Event = _event.Name, DataType = TypeCache.ShortName, - InstanceType = TypeCache.ShortName + InstanceType = TypeCache.ShortName }); - List> states = _machine.States.Cast>().Where(x => x.Events.Contains(_event)).ToList(); + List> states = _machine.States.Cast>().Where(x => x.Events.Contains(_event)).ToList(); if (states.Any()) scope.Add("states", states.Select(x => x.Name).ToArray()); _machine.Probe(context); } - public async Task Send(SagaConsumeContext context, IPipe> next) + public async Task Send(SagaConsumeContext context, IPipe> next) { - BehaviorContext behaviorContext = new BehaviorContextProxy(_machine, context, context, _event); + BehaviorContext behaviorContext = + new MassTransitStateMachine.BehaviorContextProxy(_machine, context, context, _event); StartedActivity? activity = LogContext.Current?.StartSagaStateMachineActivity(behaviorContext); + StartedInstrument? instrument = LogContext.Current?.StartSagaStateMachineInstrument(behaviorContext); + try { - if (activity != null && activity.Value.Activity.IsAllDataRequested) + if (activity is { Activity: { IsAllDataRequested: true } }) { - State beginState = await behaviorContext.StateMachine.Accessor.Get(behaviorContext).ConfigureAwait(false); + State beginState = await behaviorContext.StateMachine.Accessor.Get(behaviorContext).ConfigureAwait(false); if (beginState != null) activity?.SetTag(DiagnosticHeaders.BeginState, beginState.Name); } @@ -68,18 +70,20 @@ public async Task Send(SagaConsumeContext context, IPipe currentState = await _machine.Accessor.Get(behaviorContext).ConfigureAwait(false); + State currentState = await _machine.Accessor.Get(behaviorContext).ConfigureAwait(false); - var stateMachineException = new NotAcceptedStateMachineException(typeof(TSaga), typeof(TMessage), + var stateMachineException = new NotAcceptedStateMachineException(typeof(TInstance), typeof(TMessage), context.CorrelationId ?? Guid.Empty, currentState.Name, ex); activity?.AddExceptionEvent(stateMachineException); + instrument?.AddException(ex); throw stateMachineException; } catch (Exception exception) { activity?.AddExceptionEvent(exception); + instrument?.AddException(exception); throw; } @@ -89,13 +93,15 @@ public async Task Send(SagaConsumeContext context, IPipe endState = await behaviorContext.StateMachine.Accessor.Get(behaviorContext).ConfigureAwait(false); + State endState = await behaviorContext.StateMachine.Accessor.Get(behaviorContext).ConfigureAwait(false); if (endState != null) activity?.SetTag(DiagnosticHeaders.EndState, endState.Name); } activity.Value.Stop(); } + + instrument?.Stop(); } } } diff --git a/src/MassTransit/Middleware/TeeFilter.cs b/src/MassTransit/Middleware/TeeFilter.cs index fb822abe2cd..8ccbffe37f6 100644 --- a/src/MassTransit/Middleware/TeeFilter.cs +++ b/src/MassTransit/Middleware/TeeFilter.cs @@ -23,7 +23,7 @@ public TeeFilter() public int Count => _connections.Count; - void IProbeSite.Probe(ProbeContext context) + public void Probe(ProbeContext context) { _connections.ForEach(pipe => pipe.Probe(context)); } diff --git a/src/MassTransit/Middleware/Timeout/TimeoutConsumeContext.cs b/src/MassTransit/Middleware/Timeout/TimeoutConsumeContext.cs index 9d8abcc77c1..ae6c7c0fade 100644 --- a/src/MassTransit/Middleware/Timeout/TimeoutConsumeContext.cs +++ b/src/MassTransit/Middleware/Timeout/TimeoutConsumeContext.cs @@ -6,14 +6,14 @@ namespace MassTransit.Middleware.Timeout using Context; - public class TimeoutConsumeContext : + public class TimeoutConsumeContext : ConsumeContextProxy, - ConsumeContext - where T : class + ConsumeContext + where TMessage : class { - readonly ConsumeContext _context; + readonly ConsumeContext _context; - public TimeoutConsumeContext(ConsumeContext context, CancellationToken cancellationToken) + public TimeoutConsumeContext(ConsumeContext context, CancellationToken cancellationToken) : base(context) { CancellationToken = cancellationToken; @@ -22,7 +22,23 @@ public TimeoutConsumeContext(ConsumeContext context, CancellationToken cancel public override CancellationToken CancellationToken { get; } - public T Message => _context.Message; + public TMessage Message => _context.Message; + + public override async Task NotifyFaulted(ConsumeContext context, TimeSpan duration, string consumerType, Exception exception) + { + switch (exception) + { + case OperationCanceledException canceledException when canceledException.CancellationToken == _context.CancellationToken: + break; + + default: + if (!_context.CancellationToken.IsCancellationRequested) + await GenerateFault(_context, exception).ConfigureAwait(false); + break; + } + + await ReceiveContext.NotifyFaulted(context, duration, consumerType, exception).ConfigureAwait(false); + } public virtual Task NotifyConsumed(TimeSpan duration, string consumerType) { diff --git a/src/MassTransit/Middleware/TimeoutFilter.cs b/src/MassTransit/Middleware/TimeoutFilter.cs index e95da8e9db9..68b16e16a4b 100644 --- a/src/MassTransit/Middleware/TimeoutFilter.cs +++ b/src/MassTransit/Middleware/TimeoutFilter.cs @@ -32,7 +32,7 @@ public async Task Send(TContext context, IPipe next) await timeoutContext.ConsumeCompleted.ConfigureAwait(false); } - catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token) + catch (OperationCanceledException ex) when (ex.CancellationToken == cts.Token && !context.CancellationToken.IsCancellationRequested) { throw new ConsumerCanceledException("The operation was canceled by the timeout filter"); } diff --git a/src/MassTransit/Monitoring/BusHealthCheck.cs b/src/MassTransit/Monitoring/BusHealthCheck.cs index 76f70ade752..559816d9c3e 100644 --- a/src/MassTransit/Monitoring/BusHealthCheck.cs +++ b/src/MassTransit/Monitoring/BusHealthCheck.cs @@ -30,7 +30,16 @@ public Task CheckHealthAsync(HealthCheckContext context, Canc )) }; - return Task.FromResult(result.Status switch + var minimalHealthcheckLevel = context.Registration.FailureStatus switch + { + HealthStatus.Healthy => BusHealthStatus.Healthy, + HealthStatus.Degraded => BusHealthStatus.Degraded, + _ => BusHealthStatus.Unhealthy + }; + + var usedHealthcheckResult = result.Status < minimalHealthcheckLevel ? minimalHealthcheckLevel : result.Status; + + return Task.FromResult(usedHealthcheckResult switch { BusHealthStatus.Healthy => HealthCheckResult.Healthy(result.Description, data), BusHealthStatus.Degraded => HealthCheckResult.Degraded(result.Description, result.Exception, data), diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentActivityConfigurationObserver.cs b/src/MassTransit/Monitoring/Configuration/InstrumentActivityConfigurationObserver.cs deleted file mode 100644 index 3f6d9deb1ec..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentActivityConfigurationObserver.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - using System; - - - public class InstrumentActivityConfigurationObserver : - IActivityConfigurationObserver - { - readonly IActivityObserver _observer; - - public InstrumentActivityConfigurationObserver() - { - _observer = new InstrumentActivityObserver(); - } - - public void ActivityConfigured(IExecuteActivityConfigurator configurator, Uri compensateAddress) - where TActivity : class, IExecuteActivity - where TArguments : class - { - var specification = new InstrumentExecuteActivitySpecification(); - - configurator.AddPipeSpecification(specification); - - configurator.ConnectActivityObserver(_observer); - } - - public void ExecuteActivityConfigured(IExecuteActivityConfigurator configurator) - where TActivity : class, IExecuteActivity - where TArguments : class - { - var specification = new InstrumentExecuteActivitySpecification(); - - configurator.AddPipeSpecification(specification); - - configurator.ConnectActivityObserver(_observer); - } - - public void CompensateActivityConfigured(ICompensateActivityConfigurator configurator) - where TActivity : class, ICompensateActivity - where TLog : class - { - var specification = new InstrumentCompensateActivitySpecification(); - - configurator.AddPipeSpecification(specification); - - configurator.ConnectActivityObserver(_observer); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentCompensateActivitySpecification.cs b/src/MassTransit/Monitoring/Configuration/InstrumentCompensateActivitySpecification.cs deleted file mode 100644 index 82d17ccefb8..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentCompensateActivitySpecification.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class InstrumentCompensateActivitySpecification : - IPipeSpecification> - where TActivity : class, ICompensateActivity - where TLog : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new InstrumentCompensateActivityFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentConsumerConfigurationObserver.cs b/src/MassTransit/Monitoring/Configuration/InstrumentConsumerConfigurationObserver.cs deleted file mode 100644 index 069f1e0808f..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentConsumerConfigurationObserver.cs +++ /dev/null @@ -1,43 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - using System; - using Internals; - - - public class InstrumentConsumerConfigurationObserver : - IConsumerConfigurationObserver - { - public void ConsumerConfigured(IConsumerConfigurator configurator) - where TConsumer : class - { - } - - public void ConsumerMessageConfigured(IConsumerMessageConfigurator configurator) - where TConsumer : class - where TMessage : class - { - if (typeof(TMessage).ClosesType(typeof(Batch<>), out Type[] types)) - { - typeof(InstrumentConsumerConfigurationObserver) - .GetMethod(nameof(BatchConsumerConfigured)) - .MakeGenericMethod(typeof(TConsumer), types[0]) - .Invoke(this, new object[] { configurator }); - } - else - { - var specification = new InstrumentConsumerSpecification(); - - configurator.AddPipeSpecification(specification); - } - } - - public void BatchConsumerConfigured(IConsumerMessageConfigurator> configurator) - where TConsumer : class, IConsumer> - where TMessage : class - { - var specification = new InstrumentConsumerSpecification>(); - - configurator.AddPipeSpecification(specification); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentConsumerSpecification.cs b/src/MassTransit/Monitoring/Configuration/InstrumentConsumerSpecification.cs deleted file mode 100644 index be304ef6330..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentConsumerSpecification.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class InstrumentConsumerSpecification : - IPipeSpecification> - where TConsumer : class - where TMessage : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new InstrumentConsumerFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentExecuteActivitySpecification.cs b/src/MassTransit/Monitoring/Configuration/InstrumentExecuteActivitySpecification.cs deleted file mode 100644 index cf64471cfa4..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentExecuteActivitySpecification.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class InstrumentExecuteActivitySpecification : - IPipeSpecification> - where TActivity : class, IExecuteActivity - where TArguments : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new InstrumentExecuteActivityFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentHandlerConfigurationObserver.cs b/src/MassTransit/Monitoring/Configuration/InstrumentHandlerConfigurationObserver.cs deleted file mode 100644 index 9cecb6b4dfa..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentHandlerConfigurationObserver.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - public class InstrumentHandlerConfigurationObserver : - IHandlerConfigurationObserver - { - public void HandlerConfigured(IHandlerConfigurator configurator) - where T : class - { - var specification = new InstrumentHandlerSpecification(); - - configurator.AddPipeSpecification(specification); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentHandlerSpecification.cs b/src/MassTransit/Monitoring/Configuration/InstrumentHandlerSpecification.cs deleted file mode 100644 index b4fe3be84b6..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentHandlerSpecification.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class InstrumentHandlerSpecification : - IPipeSpecification> - where TMessage : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new InstrumentHandlerFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentReceiveEndpointConfiguratorObserver.cs b/src/MassTransit/Monitoring/Configuration/InstrumentReceiveEndpointConfiguratorObserver.cs deleted file mode 100644 index 255ef9f4e70..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentReceiveEndpointConfiguratorObserver.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - public class InstrumentReceiveEndpointConfiguratorObserver : - IEndpointConfigurationObserver - { - public void EndpointConfigured(T configurator) - where T : IReceiveEndpointConfigurator - { - var specification = new InstrumentReceiveSpecification(); - - configurator.ConfigureReceive(r => r.AddPipeSpecification(specification)); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentReceiveSpecification.cs b/src/MassTransit/Monitoring/Configuration/InstrumentReceiveSpecification.cs deleted file mode 100644 index 2ba7d6271fe..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentReceiveSpecification.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class InstrumentReceiveSpecification : - IPipeSpecification - { - public void Apply(IPipeBuilder builder) - { - builder.AddFilter(new InstrumentReceiveFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentSagaConfigurationObserver.cs b/src/MassTransit/Monitoring/Configuration/InstrumentSagaConfigurationObserver.cs deleted file mode 100644 index a051c54501a..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentSagaConfigurationObserver.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - public class InstrumentSagaConfigurationObserver : - ISagaConfigurationObserver - { - public void SagaConfigured(ISagaConfigurator configurator) - where T : class, ISaga - { - } - - public void StateMachineSagaConfigured(ISagaConfigurator configurator, SagaStateMachine stateMachine) - where TInstance : class, ISaga, SagaStateMachineInstance - { - } - - public void SagaMessageConfigured(ISagaMessageConfigurator configurator) - where T : class, ISaga - where TMessage : class - { - var specification = new InstrumentSagaSpecification(); - - configurator.AddPipeSpecification(specification); - } - } -} diff --git a/src/MassTransit/Monitoring/Configuration/InstrumentSagaSpecification.cs b/src/MassTransit/Monitoring/Configuration/InstrumentSagaSpecification.cs deleted file mode 100644 index 998d22249f2..00000000000 --- a/src/MassTransit/Monitoring/Configuration/InstrumentSagaSpecification.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit.Monitoring.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using Middleware; - - - public class InstrumentSagaSpecification : - IPipeSpecification> - where TSaga : class, ISaga - where TMessage : class - { - public void Apply(IPipeBuilder> builder) - { - builder.AddFilter(new InstrumentSagaFilter()); - } - - public IEnumerable Validate() - { - return Enumerable.Empty(); - } - } -} diff --git a/src/MassTransit/Monitoring/ConfigureBusHealthCheckServiceOptions.cs b/src/MassTransit/Monitoring/ConfigureBusHealthCheckServiceOptions.cs index 9dfda82d39e..13cee595f2f 100644 --- a/src/MassTransit/Monitoring/ConfigureBusHealthCheckServiceOptions.cs +++ b/src/MassTransit/Monitoring/ConfigureBusHealthCheckServiceOptions.cs @@ -4,7 +4,7 @@ namespace MassTransit.Monitoring using System.Collections.Generic; using System.Linq; using System.Reflection; - using MassTransit.Configuration; + using Configuration; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Options; using Transports; @@ -32,7 +32,7 @@ public void Configure(HealthCheckServiceOptions options) var optionsType = typeof(IOptions<>).MakeGenericType(type); var name = busInstance.Name; - HealthStatus? failureStatus = HealthStatus.Unhealthy; + HealthStatus? minimalFailureStatus = HealthStatus.Unhealthy; var tags = new HashSet(_tags, StringComparer.OrdinalIgnoreCase); var busOptions = _provider.GetService(optionsType); @@ -44,14 +44,14 @@ public void Configure(HealthCheckServiceOptions options) if (!string.IsNullOrWhiteSpace(healthCheckOptions.Name)) name = healthCheckOptions.Name; - if (healthCheckOptions.FailureStatus.HasValue) - failureStatus = healthCheckOptions.FailureStatus.Value; + if (healthCheckOptions.MinimalFailureStatus.HasValue) + minimalFailureStatus = healthCheckOptions.MinimalFailureStatus.Value; if (healthCheckOptions.Tags.Any()) tags = healthCheckOptions.Tags; } - options.Registrations.Add(new HealthCheckRegistration(name, new BusHealthCheck(busInstance), failureStatus, tags)); + options.Registrations.Add(new HealthCheckRegistration(name, new BusHealthCheck(busInstance), minimalFailureStatus, tags)); } } } diff --git a/src/MassTransit/Monitoring/ConfigureDefaultInstrumentationOptions.cs b/src/MassTransit/Monitoring/ConfigureDefaultInstrumentationOptions.cs new file mode 100644 index 00000000000..c6ee5afcea5 --- /dev/null +++ b/src/MassTransit/Monitoring/ConfigureDefaultInstrumentationOptions.cs @@ -0,0 +1,57 @@ +namespace MassTransit.Monitoring +{ + using Metadata; + using Microsoft.Extensions.Options; + + + public class ConfigureDefaultInstrumentationOptions : + IConfigureOptions + { + public void Configure(InstrumentationOptions options) + { + options.ServiceName = HostMetadataCache.Host.ProcessName; + options.EndpointLabel = "messaging.masstransit.destination"; + options.ConsumerTypeLabel = "messaging.masstransit.consumer_type"; + options.ExceptionTypeLabel = "messaging.masstransit.exception_type"; + options.MessageTypeLabel = "messaging.masstransit.message_type"; + options.ActivityNameLabel = "messaging.masstransit.activity_type"; + options.ArgumentTypeLabel = "messaging.masstransit.argument_type"; + options.LogTypeLabel = "messaging.masstransit.log_type"; + options.ServiceNameLabel = "messaging.masstransit.service"; + options.ReceiveTotal = "messaging.masstransit.receive"; + options.ReceiveFaultTotal = "messaging.masstransit.receive.errors"; + options.ReceiveDuration = "messaging.masstransit.receive.duration"; + options.ReceiveInProgress = "messaging.masstransit.receive.active"; + options.ConsumeTotal = "messaging.masstransit.consume"; + options.ConsumeFaultTotal = "messaging.masstransit.consume.errors"; + options.ConsumeRetryTotal = "messaging.masstransit.consume.retries"; + options.ConsumeDuration = "messaging.masstransit.consume.duration"; + options.ConsumerInProgress = "messaging.masstransit.consume.active"; + options.SagaTotal = "messaging.masstransit.saga"; + options.SagaFaultTotal = "messaging.masstransit.saga.errors"; + options.SagaDuration = "messaging.masstransit.saga.duration"; + options.HandlerTotal = "messaging.masstransit.handler"; + options.HandlerFaultTotal = "messaging.masstransit.handler.errors"; + options.HandlerDuration = "messaging.masstransit.handler.duration"; + options.OutboxDeliveryTotal = "messaging.masstransit.outbox.delivery"; + options.OutboxDeliveryFaultTotal = "messaging.masstransit.outbox.delivery.errors"; + options.DeliveryDuration = "messaging.masstransit.delivery.duration"; + options.SendTotal = "messaging.masstransit.send"; + options.SendFaultTotal = "messaging.masstransit.send.errors"; + options.OutboxSendTotal = "messaging.masstransit.outbox.send"; + options.OutboxSendFaultTotal = "messaging.masstransit.outbox.send.errors"; + options.ActivityExecuteTotal = "messaging.masstransit.execute"; + options.ActivityExecuteFaultTotal = "messaging.masstransit.execute.errors"; + options.ActivityExecuteDuration = "messaging.masstransit.execute.duration"; + options.ExecuteInProgress = "messaging.masstransit.execute.active"; + options.ActivityCompensateTotal = "messaging.masstransit.compensate"; + options.ActivityCompensateFailureTotal = "messaging.masstransit.compensate.errors"; + options.ActivityCompensateDuration = "messaging.masstransit.compensate.duration"; + options.CompensateInProgress = "messaging.masstransit.compensate.active"; + options.BusInstances = "messaging.masstransit.bus"; + options.EndpointInstances = "messaging.masstransit.endpoint"; + options.HandlerInProgress = "messaging.masstransit.handler.active"; + options.SagaInProgress = "messaging.masstransit.saga.active"; + } + } +} diff --git a/src/MassTransit/Monitoring/InstrumentActivityObserver.cs b/src/MassTransit/Monitoring/InstrumentActivityObserver.cs deleted file mode 100644 index 6043c9dade3..00000000000 --- a/src/MassTransit/Monitoring/InstrumentActivityObserver.cs +++ /dev/null @@ -1,60 +0,0 @@ -namespace MassTransit.Monitoring -{ - using System; - using System.Threading.Tasks; - - - public class InstrumentActivityObserver : - IActivityObserver - { - public Task PreExecute(ExecuteActivityContext context) - where TActivity : class, IExecuteActivity - where TArguments : class - { - return Task.CompletedTask; - } - - public Task PostExecute(ExecuteActivityContext context) - where TActivity : class, IExecuteActivity - where TArguments : class - { - Instrumentation.MeasureExecute(context); - - return Task.CompletedTask; - } - - public Task ExecuteFault(ExecuteActivityContext context, Exception exception) - where TActivity : class, IExecuteActivity - where TArguments : class - { - Instrumentation.MeasureExecute(context, exception); - - return Task.CompletedTask; - } - - public Task PreCompensate(CompensateActivityContext context) - where TActivity : class, ICompensateActivity - where TLog : class - { - return Task.CompletedTask; - } - - public Task PostCompensate(CompensateActivityContext context) - where TActivity : class, ICompensateActivity - where TLog : class - { - Instrumentation.MeasureCompensate(context); - - return Task.CompletedTask; - } - - public Task CompensateFail(CompensateActivityContext context, Exception exception) - where TActivity : class, ICompensateActivity - where TLog : class - { - Instrumentation.MeasureCompensate(context, exception); - - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit/Monitoring/InstrumentBusObserver.cs b/src/MassTransit/Monitoring/InstrumentBusObserver.cs deleted file mode 100644 index 5a5300c098b..00000000000 --- a/src/MassTransit/Monitoring/InstrumentBusObserver.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace MassTransit.Monitoring -{ - using System; - using System.Threading.Tasks; - - - public class InstrumentBusObserver : - IBusObserver - { - public void PostCreate(IBus bus) - { - bus.ConnectPublishObserver(new InstrumentPublishObserver()); - bus.ConnectSendObserver(new InstrumentSendObserver()); - bus.ConnectReceiveObserver(new InstrumentReceiveObserver()); - } - - public void CreateFaulted(Exception exception) - { - } - - public Task PreStart(IBus bus) - { - return Task.CompletedTask; - } - - public Task PostStart(IBus bus, Task busReady) - { - return Task.CompletedTask; - } - - public Task StartFaulted(IBus bus, Exception exception) - { - return Task.CompletedTask; - } - - public Task PreStop(IBus bus) - { - return Task.CompletedTask; - } - - public Task PostStop(IBus bus) - { - return Task.CompletedTask; - } - - public Task StopFaulted(IBus bus, Exception exception) - { - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit/Monitoring/InstrumentPublishObserver.cs b/src/MassTransit/Monitoring/InstrumentPublishObserver.cs deleted file mode 100644 index 070dc96a414..00000000000 --- a/src/MassTransit/Monitoring/InstrumentPublishObserver.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace MassTransit.Monitoring -{ - using System; - using System.Threading.Tasks; - - - public class InstrumentPublishObserver : - IPublishObserver - { - public Task PrePublish(PublishContext context) - where T : class - { - return Task.CompletedTask; - } - - public Task PostPublish(PublishContext context) - where T : class - { - Instrumentation.MeasurePublish(); - - return Task.CompletedTask; - } - - public Task PublishFault(PublishContext context, Exception exception) - where T : class - { - Instrumentation.MeasurePublish(exception); - - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit/Monitoring/InstrumentReceiveObserver.cs b/src/MassTransit/Monitoring/InstrumentReceiveObserver.cs deleted file mode 100644 index fb0c8325aa9..00000000000 --- a/src/MassTransit/Monitoring/InstrumentReceiveObserver.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace MassTransit.Monitoring -{ - using System; - using System.Threading.Tasks; - - - public class InstrumentReceiveObserver : - IReceiveObserver - { - public Task PreReceive(ReceiveContext context) - { - return Task.CompletedTask; - } - - public Task PostReceive(ReceiveContext context) - { - Instrumentation.MeasureReceived(context); - - return Task.CompletedTask; - } - - public Task PostConsume(ConsumeContext context, TimeSpan duration, string consumerType) - where T : class - { - Instrumentation.MeasureConsume(context, duration, consumerType); - - return Task.CompletedTask; - } - - public Task ConsumeFault(ConsumeContext context, TimeSpan duration, string consumerType, Exception exception) - where T : class - { - Instrumentation.MeasureConsume(context, duration, consumerType, exception); - - return Task.CompletedTask; - } - - public Task ReceiveFault(ReceiveContext context, Exception exception) - { - Instrumentation.MeasureReceived(context, exception); - - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit/Monitoring/InstrumentSendObserver.cs b/src/MassTransit/Monitoring/InstrumentSendObserver.cs deleted file mode 100644 index f46a59913b1..00000000000 --- a/src/MassTransit/Monitoring/InstrumentSendObserver.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace MassTransit.Monitoring -{ - using System; - using System.Threading.Tasks; - - - public class InstrumentSendObserver : - ISendObserver - { - public Task PreSend(SendContext context) - where T : class - { - return Task.CompletedTask; - } - - public Task PostSend(SendContext context) - where T : class - { - Instrumentation.MeasureSend(); - - return Task.CompletedTask; - } - - public Task SendFault(SendContext context, Exception exception) - where T : class - { - Instrumentation.MeasureSend(exception); - - return Task.CompletedTask; - } - } -} diff --git a/src/MassTransit/Monitoring/Instrumentation.cs b/src/MassTransit/Monitoring/Instrumentation.cs deleted file mode 100644 index 359882ea7cc..00000000000 --- a/src/MassTransit/Monitoring/Instrumentation.cs +++ /dev/null @@ -1,474 +0,0 @@ -namespace MassTransit.Monitoring -{ - using System; - using System.Collections.Concurrent; - using System.Diagnostics; - using System.Diagnostics.Metrics; - using System.Linq; - using System.Reflection; - using System.Text; - using Metadata; - - - public static class Instrumentation - { - static readonly ConcurrentDictionary _labelCache = new ConcurrentDictionary(); - - static bool _isConfigured; - static string _serviceName; - static Counter _receiveTotal; - static Counter _receiveFaultTotal; - static Counter _receiveInProgress; - static Histogram _receiveDuration; - static Counter _consumeTotal; - static Counter _consumeFaultTotal; - static Counter _consumeRetryTotal; - static Counter _publishTotal; - static Counter _publishFaultTotal; - static Counter _sendTotal; - static Counter _sendFaultTotal; - static Counter _executeTotal; - static Counter _executeFaultTotal; - static Counter _compensateTotal; - static Counter _compensateFailureTotal; - static Counter _consumerInProgress; - static Counter _handlerInProgress; - static Counter _sagaInProgress; - static Counter _executeInProgress; - static Counter _compensateInProgress; - static Histogram _consumeDuration; - static Histogram _deliveryDuration; - static Histogram _executeDuration; - static Histogram _compensateDuration; - - static readonly char[] _delimiters = { '<', '>' }; - - static Meter _meter; - static InstrumentationOptions _options; - - public static void MeasureReceived(ReceiveContext context, Exception exception = default) - { - if (!_receiveTotal.Enabled) - return; - - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.EndpointLabel, GetEndpointLabel(context.InputAddress) }, - }; - - _receiveTotal.Add(1, tagList); - _receiveDuration.Record(context.ElapsedTime.TotalMilliseconds, tagList); - - if (exception != null) - { - tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); - - _receiveFaultTotal.Add(1, tagList); - } - } - - public static void MeasureConsume(ConsumeContext context, TimeSpan duration, string consumerType, Exception exception = default) - where T : class - { - if (!_consumeTotal.Enabled) - return; - - var messageTypeLabel = GetMessageTypeLabel(); - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.MessageTypeLabel, messageTypeLabel }, - { _options.ConsumerTypeLabel, GetConsumerTypeLabel(consumerType, TypeCache.ShortName, messageTypeLabel) } - }; - - _consumeTotal.Add(1, tagList); - _consumeDuration.Record(duration.TotalMilliseconds, tagList); - - var retryAttempt = context.GetRetryAttempt(); - if (retryAttempt > 0) - _consumeRetryTotal.Add(1, tagList); - - if (context.SentTime.HasValue) - { - var deliveryDuration = DateTime.UtcNow - context.SentTime.Value; - if (deliveryDuration < TimeSpan.Zero) - deliveryDuration = TimeSpan.Zero; - - _deliveryDuration.Record(deliveryDuration.TotalMilliseconds, tagList); - } - - if (exception != null) - { - tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); - - _consumeFaultTotal.Add(1, tagList); - } - } - - public static void MeasureExecute(ExecuteActivityContext context, Exception exception = default) - where TActivity : class, IExecuteActivity - where TArguments : class - { - if (!_executeTotal.Enabled) - return; - - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.ActivityNameLabel, context.ActivityName }, - { _options.ArgumentTypeLabel, GetArgumentTypeLabel() } - }; - - _executeTotal.Add(1, tagList); - _executeDuration.Record(context.Elapsed.TotalMilliseconds, tagList); - - if (exception != null) - { - tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); - - _executeFaultTotal.Add(1, tagList); - } - } - - public static void MeasureCompensate(CompensateActivityContext context, Exception exception = default) - where TActivity : class, ICompensateActivity - where TLog : class - { - if (!_compensateTotal.Enabled) - return; - - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.ActivityNameLabel, context.ActivityName }, - { _options.LogTypeLabel, GetLogTypeLabel() } - }; - - _compensateTotal.Add(1, tagList); - _compensateDuration.Record(context.Elapsed.TotalMilliseconds, tagList); - - if (exception != null) - { - tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); - - _compensateFailureTotal.Add(1, tagList); - } - } - - public static void MeasurePublish(Exception exception = default) - where T : class - { - if (!_publishTotal.Enabled) - return; - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.MessageTypeLabel, GetMessageTypeLabel() }, - }; - - _publishTotal.Add(1, tagList); - - if (exception != null) - { - tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); - _publishFaultTotal.Add(1, tagList); - } - } - - public static void MeasureSend(Exception exception = default) - where T : class - { - if (!_sendTotal.Enabled) - return; - - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.MessageTypeLabel, GetMessageTypeLabel() }, - }; - - _sendTotal.Add(1, tagList); - - if (exception != null) - { - tagList.Add(_options.ExceptionTypeLabel, exception.GetType().Name); - _sendFaultTotal.Add(1, tagList); - } - } - - public static IDisposable TrackReceiveInProgress(ReceiveContext context) - { - if (!_receiveTotal.Enabled) - return null; - - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.EndpointLabel, GetEndpointLabel(context.InputAddress) }, - }; - - return TrackInProgress(_receiveInProgress, tagList); - } - - public static IDisposable TrackConsumerInProgress() - where TConsumer : class - where TMessage : class - { - if (!_consumeTotal.Enabled) - return null; - - var messageTypeLabel = GetMessageTypeLabel(); - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.MessageTypeLabel, messageTypeLabel }, - { _options.ConsumerTypeLabel, GetConsumerTypeLabel(TypeCache.ShortName, TypeCache.ShortName, messageTypeLabel) } - }; - - return TrackInProgress(_consumerInProgress, tagList); - } - - public static IDisposable TrackSagaInProgress() - where TSaga : class, ISaga - where TMessage : class - { - if (!_sagaInProgress.Enabled) - return null; - - var messageTypeLabel = GetMessageTypeLabel(); - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.MessageTypeLabel, messageTypeLabel }, - { _options.ConsumerTypeLabel, GetConsumerTypeLabel(TypeCache.ShortName, TypeCache.ShortName, messageTypeLabel) } - }; - - return TrackInProgress(_sagaInProgress, tagList); - } - - public static IDisposable TrackExecuteActivityInProgress(ExecuteActivityContext context) - where TActivity : class, IExecuteActivity - where TArguments : class - { - if (!_executeTotal.Enabled) - return null; - - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.ActivityNameLabel, context.ActivityName }, - { _options.ArgumentTypeLabel, GetArgumentTypeLabel() } - }; - - return TrackInProgress(_executeInProgress, tagList); - } - - public static IDisposable TrackCompensateActivityInProgress(CompensateActivityContext context) - where TActivity : class, ICompensateActivity - where TLog : class - { - if (!_compensateTotal.Enabled) - return null; - - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.ActivityNameLabel, context.ActivityName }, - { _options.LogTypeLabel, GetLogTypeLabel() } - }; - - return TrackInProgress(_compensateInProgress, tagList); - } - - public static IDisposable TrackHandlerInProgress() - where TMessage : class - { - var tagList = new TagList - { - { _options.ServiceNameLabel, _serviceName }, - { _options.MessageTypeLabel, GetMessageTypeLabel() }, - }; - - return TrackInProgress(_handlerInProgress, tagList); - } - - static IDisposable TrackInProgress(Counter counter, TagList tagList) - { - counter.Add(1, tagList); - - return new InProgressTracker(counter, tagList); - } - - public static void TryConfigure(string serviceName, InstrumentationOptions options) - { - if (_isConfigured) - return; - - _meter = new Meter("MassTransit", HostMetadataCache.Host.MassTransitVersion); - - _serviceName = serviceName; - - _options = options; - - // Counters - - _receiveTotal = _meter.CreateCounter(options.ReceiveTotal, "ea", "Number of messages received"); - _receiveFaultTotal = _meter.CreateCounter(options.ReceiveFaultTotal, "ea", "Number of messages receive faults"); - - _consumeTotal = _meter.CreateCounter(options.ConsumeTotal, "ea", "Number of messages consumed"); - _consumeFaultTotal = _meter.CreateCounter(options.ConsumeFaultTotal, "ea", "Number of message consume faults"); - _consumeRetryTotal = _meter.CreateCounter(options.ConsumeRetryTotal, "ea", "Number of message consume faults"); - - _publishTotal = _meter.CreateCounter(options.PublishTotal, "ea", "Number of messages published"); - _publishFaultTotal = _meter.CreateCounter(options.PublishFaultTotal, "ea", "Number of message publish faults"); - - _sendTotal = _meter.CreateCounter(options.SendTotal, "ea", "Number of messages sent"); - _sendFaultTotal = _meter.CreateCounter(options.SendFaultTotal, "ea", "Number of message send faults"); - - _executeTotal = _meter.CreateCounter(options.ActivityExecuteTotal, "ea", "Number of activities executed"); - _executeFaultTotal = _meter.CreateCounter(options.ActivityExecuteFaultTotal, "ea", "Number of activity execution faults"); - - _compensateTotal = _meter.CreateCounter(options.ActivityCompensateTotal, "ea", "Number of activities compensated"); - _compensateFailureTotal = _meter.CreateCounter(options.ActivityCompensateFailureTotal, "ea", "Number of activity compensation failures"); - - // Gauges - - _receiveInProgress = _meter.CreateCounter(options.ReceiveInProgress, "ea", "Number of messages being received"); - - _handlerInProgress = _meter.CreateCounter(options.HandlerInProgress, "ea", "Number of handlers in progress"); - - _consumerInProgress = _meter.CreateCounter(options.ConsumerInProgress, "ea", "Number of consumers in progress"); - - _sagaInProgress = _meter.CreateCounter(options.SagaInProgress, "ea", "Number of sagas in progress"); - - _executeInProgress = _meter.CreateCounter(options.ExecuteInProgress, "ea", "Number of activity executions in progress"); - - _compensateInProgress = _meter.CreateCounter(options.CompensateInProgress, "ea", "Number of activity compensations in progress"); - - // Histograms - - _receiveDuration = _meter.CreateHistogram(options.ReceiveDuration, "ms", "Elapsed time spent receiving a message, in seconds"); - - _consumeDuration = _meter.CreateHistogram(options.ConsumeDuration, "ms", "Elapsed time spent consuming a message, in seconds"); - - _deliveryDuration = _meter.CreateHistogram(options.DeliveryDuration, "ms", - "Elapsed time between when the message was sent and when it was consumed, in seconds."); - - _executeDuration = _meter.CreateHistogram(options.ActivityExecuteDuration, "ms", "Elapsed time spent executing an activity, in seconds"); - - _compensateDuration = _meter.CreateHistogram(options.ActivityCompensateDuration, "ms", - "Elapsed time spent compensating an activity, in seconds"); - - _isConfigured = true; - } - - static string GetConsumerTypeLabel(string consumerType, string messageType, string messageLabel) - { - return _labelCache.GetOrAdd(consumerType, type => - { - if (type.StartsWith("MassTransit.MessageHandler<")) - return "Handler"; - - var genericMessageType = "<" + messageType + ">"; - if (type.IndexOf(genericMessageType, StringComparison.Ordinal) >= 0) - type = type.Replace(genericMessageType, "_" + messageLabel); - - return CleanupLabel(type); - }); - } - - static string CleanupLabel(string label) - { - string SimpleClean(string text) - { - return text.Split('.', '+').Last(); - } - - var indexOf = label.IndexOfAny(_delimiters); - if (indexOf >= 0) - { - if (label[indexOf] == '<') - return SimpleClean(label.Substring(0, indexOf)) + "_" + CleanupLabel(label.Substring(indexOf + 1)); - - if (label[indexOf] == '>') - return SimpleClean(label.Substring(0, indexOf)) + CleanupLabel(label.Substring(indexOf + 1)); - - return SimpleClean(label); - } - - return SimpleClean(label); - } - - static string GetArgumentTypeLabel() - { - return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TArguments)) - .Replace("Arguments", "")); - } - - static string GetLogTypeLabel() - { - return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TLog)).Replace("Log", "")); - } - - static string GetEndpointLabel(Uri inputAddress) - { - return inputAddress?.AbsolutePath.Split('/').LastOrDefault()?.Replace(".", "_").Replace("/", "_"); - } - - static string GetMessageTypeLabel() - { - return _labelCache.GetOrAdd(TypeCache.ShortName, type => FormatTypeName(new StringBuilder(), typeof(TMessage))); - } - - static string FormatTypeName(StringBuilder sb, Type type) - { - if (type.IsGenericParameter) - return ""; - - if (type.GetTypeInfo().IsGenericType) - { - var name = type.GetGenericTypeDefinition().Name; - - //remove `1 - var index = name.IndexOf('`'); - if (index > 0) - name = name.Remove(index); - - sb.Append(name); - sb.Append('_'); - Type[] arguments = type.GetTypeInfo().GenericTypeArguments; - for (var i = 0; i < arguments.Length; i++) - { - if (i > 0) - sb.Append('_'); - - FormatTypeName(sb, arguments[i]); - } - } - else - sb.Append(type.Name); - - return sb.ToString(); - } - - - class InProgressTracker : - IDisposable - { - readonly Counter _counter; - readonly TagList _tagList; - - public InProgressTracker(Counter counter, TagList tagList) - { - _counter = counter; - _tagList = tagList; - } - - public void Dispose() - { - _counter.Add(-1, _tagList); - } - } - } -} diff --git a/src/MassTransit/Monitoring/InstrumentationOptions.cs b/src/MassTransit/Monitoring/InstrumentationOptions.cs index 5761d410cae..17d489b608e 100644 --- a/src/MassTransit/Monitoring/InstrumentationOptions.cs +++ b/src/MassTransit/Monitoring/InstrumentationOptions.cs @@ -1,7 +1,14 @@ namespace MassTransit.Monitoring { + using System; + + public class InstrumentationOptions { + public const string MeterName = "MassTransit"; + + public string ServiceName { get; set; } + public string EndpointLabel { get; set; } public string ConsumerTypeLabel { get; set; } public string ExceptionTypeLabel { get; set; } @@ -19,8 +26,21 @@ public class InstrumentationOptions public string ConsumeTotal { get; set; } public string ConsumeFaultTotal { get; set; } public string ConsumeRetryTotal { get; set; } + + public string SagaTotal { get; set; } + public string SagaFaultTotal { get; set; } + public string SagaDuration { get; set; } + + public string HandlerTotal { get; set; } + public string HandlerFaultTotal { get; set; } + public string HandlerDuration { get; set; } + + [Obsolete] public string PublishTotal { get; set; } + + [Obsolete] public string PublishFaultTotal { get; set; } + public string SendTotal { get; set; } public string SendFaultTotal { get; set; } public string ActivityExecuteTotal { get; set; } @@ -41,45 +61,10 @@ public class InstrumentationOptions public string ConsumeDuration { get; set; } public string DeliveryDuration { get; set; } - public static InstrumentationOptions CreateDefault() - { - return new InstrumentationOptions - { - EndpointLabel = "messaging.destination", - ConsumerTypeLabel = "messaging.masstransit.consumer_type", - ExceptionTypeLabel = "messaging.masstransit.exception_type", - MessageTypeLabel = "messaging.masstransit.message_type", - ActivityNameLabel = "messaging.masstransit.activity_type", - ArgumentTypeLabel = "messaging.masstransit.argument_type", - LogTypeLabel = "messaging.masstransit.log_type", - ServiceNameLabel = "messaging.service", - ReceiveTotal = "messaging.receive", - ReceiveFaultTotal = "messaging.receive.errors", - ReceiveDuration = "messaging.receive.duration", - ReceiveInProgress = "messaging.receive.active", - ConsumeTotal = "messaging.consume", - ConsumeFaultTotal = "messaging.consume.errors", - ConsumeRetryTotal = "messaging.consume.retries", - ConsumeDuration = "messaging.consume.duration", - ConsumerInProgress = "messaging.consume.active", - DeliveryDuration = "messaging.delivery.duration", - PublishTotal = "messaging.publish", - PublishFaultTotal = "messaging.publish.errors", - SendTotal = "messaging.send", - SendFaultTotal = "messaging.send.errors", - ActivityExecuteTotal = "messaging.masstransit.execute", - ActivityExecuteFaultTotal = "messaging.masstransit.execute.errors", - ActivityExecuteDuration = "messaging.masstransit.execute.duration", - ExecuteInProgress = "messaging.masstransit.execute.active", - ActivityCompensateTotal = "messaging.masstransit.compensate", - ActivityCompensateFailureTotal = "messaging.masstransit.compensate.errors", - ActivityCompensateDuration = "messaging.masstransit.compensate.duration", - CompensateInProgress = "messaging.masstransit.compensate.active", - BusInstances = "messaging.masstransit.bus", - EndpointInstances = "messaging.masstransit.endpoint", - HandlerInProgress = "messaging.masstransit.handler.active", - SagaInProgress = "messaging.masstransit.saga.active", - }; - } + public string OutboxSendTotal { get; set; } + public string OutboxSendFaultTotal { get; set; } + + public string OutboxDeliveryTotal { get; set; } + public string OutboxDeliveryFaultTotal { get; set; } } } diff --git a/src/MassTransit/Monitoring/Performance/StatsD/StatsDPerformanceCounter.cs b/src/MassTransit/Monitoring/Performance/StatsD/StatsDPerformanceCounter.cs index c0206bf31b8..504fdf56b18 100644 --- a/src/MassTransit/Monitoring/Performance/StatsD/StatsDPerformanceCounter.cs +++ b/src/MassTransit/Monitoring/Performance/StatsD/StatsDPerformanceCounter.cs @@ -28,14 +28,14 @@ public void Increment() public void IncrementBy(long val) { var payload = $"{_fullName}:{val}|c"; - byte[] datagram = Encoding.UTF8.GetBytes(payload); + var datagram = Encoding.UTF8.GetBytes(payload); Task t = _client.SendAsync(datagram, datagram.Length); } public void Set(long val) { var payload = $"{_fullName}:{val}|g"; - byte[] datagram = Encoding.UTF8.GetBytes(payload); + var datagram = Encoding.UTF8.GetBytes(payload); Task t = _client.SendAsync(datagram, datagram.Length); } diff --git a/src/MassTransit/NullableAttributes.cs b/src/MassTransit/NullableAttributes.cs index 6a67eb9c43e..1d36960845a 100644 --- a/src/MassTransit/NullableAttributes.cs +++ b/src/MassTransit/NullableAttributes.cs @@ -1,8 +1,8 @@ -namespace MassTransit -{ - using System; - +using System; +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ [AttributeUsage(AttributeTargets.Parameter)] sealed class NotNullWhenAttribute : Attribute @@ -20,3 +20,22 @@ public NotNullWhenAttribute(bool returnValue) public bool ReturnValue { get; } } } + + +[AttributeUsage(AttributeTargets.Parameter)] +sealed class MaybeNullWhenAttribute : + Attribute +{ + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public MaybeNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } +} +#endif diff --git a/src/MassTransit/Serialization/RawSerializerOptions.cs b/src/MassTransit/RawSerializerOptions.cs similarity index 86% rename from src/MassTransit/Serialization/RawSerializerOptions.cs rename to src/MassTransit/RawSerializerOptions.cs index 96213e8e2b0..42b94d56fc7 100644 --- a/src/MassTransit/Serialization/RawSerializerOptions.cs +++ b/src/MassTransit/RawSerializerOptions.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Serialization +namespace MassTransit { using System; @@ -21,7 +21,7 @@ public enum RawSerializerOptions /// CopyHeaders = 4, - Default = AnyMessageType | AddTransportHeaders, + Default = CopyHeaders | AddTransportHeaders, All = AnyMessageType | AddTransportHeaders | CopyHeaders } diff --git a/src/MassTransit/RetryPolicies/ExceptionFilters/FilterExceptionFilter.cs b/src/MassTransit/RetryPolicies/ExceptionFilters/FilterExceptionFilter.cs index 08edaa70efc..624e0aeae1d 100644 --- a/src/MassTransit/RetryPolicies/ExceptionFilters/FilterExceptionFilter.cs +++ b/src/MassTransit/RetryPolicies/ExceptionFilters/FilterExceptionFilter.cs @@ -17,10 +17,7 @@ public FilterExceptionFilter(Func filter) void IProbeSite.Probe(ProbeContext context) { var scope = context.CreateScope("filter"); - scope.Set(new - { - ExceptionType = typeof(T).Name - }); + scope.Set(new { ExceptionType = typeof(T).Name }); } bool IExceptionFilter.Match(Exception exception) diff --git a/src/MassTransit/RetryPolicies/ExceptionFilters/HandleExceptionFilter.cs b/src/MassTransit/RetryPolicies/ExceptionFilters/HandleExceptionFilter.cs index c5fbe38fc1c..4a1bcd2c97a 100644 --- a/src/MassTransit/RetryPolicies/ExceptionFilters/HandleExceptionFilter.cs +++ b/src/MassTransit/RetryPolicies/ExceptionFilters/HandleExceptionFilter.cs @@ -2,7 +2,6 @@ namespace MassTransit.RetryPolicies.ExceptionFilters { using System; using System.Linq; - using System.Reflection; public class HandleExceptionFilter : @@ -18,17 +17,14 @@ public HandleExceptionFilter(params Type[] exceptionTypes) void IProbeSite.Probe(ProbeContext context) { var scope = context.CreateScope("selected"); - scope.Set(new - { - ExceptionTypes = _exceptionTypes.Select(x => x.Name).ToArray() - }); + scope.Set(new { ExceptionTypes = _exceptionTypes.Select(x => x.Name).ToArray() }); } bool IExceptionFilter.Match(Exception exception) { for (var i = 0; i < _exceptionTypes.Length; i++) { - if (_exceptionTypes[i].GetTypeInfo().IsInstanceOfType(exception)) + if (_exceptionTypes[i].IsInstanceOfType(exception)) return true; } diff --git a/src/MassTransit/RetryPolicies/ExceptionFilters/IgnoreExceptionFilter.cs b/src/MassTransit/RetryPolicies/ExceptionFilters/IgnoreExceptionFilter.cs index 27800149ffa..b5a92a757cb 100644 --- a/src/MassTransit/RetryPolicies/ExceptionFilters/IgnoreExceptionFilter.cs +++ b/src/MassTransit/RetryPolicies/ExceptionFilters/IgnoreExceptionFilter.cs @@ -2,7 +2,6 @@ namespace MassTransit.RetryPolicies.ExceptionFilters { using System; using System.Linq; - using System.Reflection; public class IgnoreExceptionFilter : @@ -18,17 +17,14 @@ public IgnoreExceptionFilter(params Type[] exceptionTypes) void IProbeSite.Probe(ProbeContext context) { var scope = context.CreateScope("except"); - scope.Set(new - { - ExceptionTypes = _exceptionTypes.Select(x => x.Name).ToArray() - }); + scope.Set(new { ExceptionTypes = _exceptionTypes.Select(x => x.Name).ToArray() }); } bool IExceptionFilter.Match(Exception exception) { for (var i = 0; i < _exceptionTypes.Length; i++) { - if (_exceptionTypes[i].GetTypeInfo().IsInstanceOfType(exception)) + if (_exceptionTypes[i].IsInstanceOfType(exception)) return false; } diff --git a/src/MassTransit/RetryPolicies/NoRetryPolicy.cs b/src/MassTransit/RetryPolicies/NoRetryPolicy.cs index bf1d627c4cb..b626e9c1820 100644 --- a/src/MassTransit/RetryPolicies/NoRetryPolicy.cs +++ b/src/MassTransit/RetryPolicies/NoRetryPolicy.cs @@ -15,10 +15,7 @@ public NoRetryPolicy(IExceptionFilter filter) void IProbeSite.Probe(ProbeContext context) { - context.Set(new - { - Policy = "None" - }); + context.Set(new { Policy = "None" }); } RetryPolicyContext IRetryPolicy.CreatePolicyContext(T context) diff --git a/src/MassTransit/RetryPolicies/PipeRetryExtensions.cs b/src/MassTransit/RetryPolicies/PipeRetryExtensions.cs index 35983712655..635bd591ba1 100644 --- a/src/MassTransit/RetryPolicies/PipeRetryExtensions.cs +++ b/src/MassTransit/RetryPolicies/PipeRetryExtensions.cs @@ -9,7 +9,12 @@ namespace MassTransit.RetryPolicies public static class PipeRetryExtensions { - public static async Task Retry(this IRetryPolicy retryPolicy, Func retryMethod, CancellationToken cancellationToken = default) + public static Task Retry(this IRetryPolicy retryPolicy, Func retryMethod, CancellationToken cancellationToken = default) + { + return Retry(retryPolicy, retryMethod, true, cancellationToken); + } + + public static async Task Retry(this IRetryPolicy retryPolicy, Func retryMethod, bool log, CancellationToken cancellationToken = default) { var inlinePipeContext = new InlinePipeContext(cancellationToken); @@ -29,7 +34,7 @@ public static async Task Retry(this IRetryPolicy retryPolicy, Func retryMe try { - await Attempt(inlinePipeContext, retryContext, retryMethod).ConfigureAwait(false); + await Attempt(inlinePipeContext, retryContext, retryMethod, log).ConfigureAwait(false); return; } @@ -45,7 +50,13 @@ public static async Task Retry(this IRetryPolicy retryPolicy, Func retryMe } } - public static async Task Retry(this IRetryPolicy retryPolicy, Func> retryMethod, CancellationToken cancellationToken = default) + public static Task Retry(this IRetryPolicy retryPolicy, Func> retryMethod, CancellationToken cancellationToken = default) + { + return Retry(retryPolicy, retryMethod, true, cancellationToken); + } + + public static async Task Retry(this IRetryPolicy retryPolicy, Func> retryMethod, bool log, + CancellationToken cancellationToken = default) { var inlinePipeContext = new InlinePipeContext(cancellationToken); @@ -65,7 +76,7 @@ public static async Task Retry(this IRetryPolicy retryPolicy, Func try { - return await Attempt(inlinePipeContext, retryContext, retryMethod).ConfigureAwait(false); + return await Attempt(inlinePipeContext, retryContext, retryMethod, log).ConfigureAwait(false); } catch (OperationCanceledException ex) when (ex.CancellationToken == cancellationToken) { @@ -79,12 +90,13 @@ public static async Task Retry(this IRetryPolicy retryPolicy, Func } } - static async Task Attempt(T context, RetryContext retryContext, Func retryMethod) + static async Task Attempt(T context, RetryContext retryContext, Func retryMethod, bool log) where T : class, PipeContext { while (context.CancellationToken.IsCancellationRequested == false) { - LogContext.Warning?.Log(retryContext.Exception, "Retrying {Delay}: {Message}", retryContext.Delay, retryContext.Exception.Message); + if (log) + LogContext.Warning?.Log(retryContext.Exception, "Retrying {Delay}: {Message}", retryContext.Delay, retryContext.Exception.Message); try { @@ -115,12 +127,13 @@ static async Task Attempt(T context, RetryContext retryContext, Func throw new OperationCanceledException(context.CancellationToken); } - static async Task Attempt(T context, RetryContext retryContext, Func> retryMethod) + static async Task Attempt(T context, RetryContext retryContext, Func> retryMethod, bool log) where T : class, PipeContext { while (context.CancellationToken.IsCancellationRequested == false) { - LogContext.Warning?.Log(retryContext.Exception, "Retrying {Delay}: {Message}", retryContext.Delay, retryContext.Exception.Message); + if (log) + LogContext.Warning?.Log(retryContext.Exception, "Retrying {Delay}: {Message}", retryContext.Delay, retryContext.Exception.Message); try { diff --git a/src/MassTransit/RoutingSlipBuilder.cs b/src/MassTransit/RoutingSlipBuilder.cs index 1c7e2b6a336..84abb498b72 100644 --- a/src/MassTransit/RoutingSlipBuilder.cs +++ b/src/MassTransit/RoutingSlipBuilder.cs @@ -21,13 +21,13 @@ public class RoutingSlipBuilder : { public static readonly IDictionary NoArguments = new Dictionary(StringComparer.OrdinalIgnoreCase); - readonly IList _activityExceptions; - readonly IList _activityLogs; - readonly IList _compensateLogs; + readonly List _activityExceptions; + readonly List _activityLogs; + readonly List _compensateLogs; readonly DateTime _createTimestamp; - readonly IList _itinerary; + readonly List _itinerary; readonly List _sourceItinerary; - readonly IList _subscriptions; + readonly List _subscriptions; readonly IDictionary _variables; public RoutingSlipBuilder(Guid trackingNumber) @@ -159,7 +159,7 @@ public void AddVariable(string key, string value) if (key == null) throw new ArgumentNullException(nameof(key)); - if (string.IsNullOrEmpty(value)) + if (value == null) _variables.Remove(key); else _variables[key] = value; @@ -175,7 +175,7 @@ public void AddVariable(string key, object value) if (key == null) throw new ArgumentNullException(nameof(key)); - if (value == null || value is string s && string.IsNullOrEmpty(s)) + if (value == null) _variables.Remove(key); else _variables[key] = value; @@ -289,6 +289,16 @@ public Task AddSubscription(Uri address, RoutingSlipEvents events, RoutingSlipEv return callback(new RoutingSlipBuilderSendEndpoint(this, address, events, activityName, contents)); } + /// + /// Builds the routing slip using the current state of the builder + /// + /// The RoutingSlip + public RoutingSlip Build() + { + return new RoutingSlipRoutingSlip(TrackingNumber, _createTimestamp, _itinerary, _activityLogs, _compensateLogs, _activityExceptions, + _variables, _subscriptions); + } + /// /// Adds a custom subscription message to the routing slip which is sent at the specified events /// @@ -303,16 +313,6 @@ void IRoutingSlipSendEndpointTarget.AddSubscription(Uri address, RoutingSlipEven _subscriptions.Add(new RoutingSlipSubscription(address, events, contents, activityName, message)); } - /// - /// Builds the routing slip using the current state of the builder - /// - /// The RoutingSlip - public RoutingSlip Build() - { - return new RoutingSlipRoutingSlip(TrackingNumber, _createTimestamp, _itinerary, _activityLogs, _compensateLogs, _activityExceptions, - _variables, _subscriptions); - } - public void AddActivityLog(HostInfo host, string name, Guid activityTrackingNumber, DateTime timestamp, TimeSpan duration) { _activityLogs.Add(new RoutingSlipActivityLog(host, activityTrackingNumber, name, timestamp, duration)); @@ -387,7 +387,7 @@ void SetVariablesFromDictionary(IEnumerable> values { foreach (KeyValuePair value in values) { - if (value.Value == null || value.Value is string s && string.IsNullOrEmpty(s)) + if (value.Value == null) _variables.Remove(value.Key); else _variables[value.Key] = value.Value; diff --git a/src/MassTransit/SagaStateMachine/Components/RequestState.cs b/src/MassTransit/SagaStateMachine/Components/RequestState.cs index 24615844d13..a2a3b41b702 100644 --- a/src/MassTransit/SagaStateMachine/Components/RequestState.cs +++ b/src/MassTransit/SagaStateMachine/Components/RequestState.cs @@ -4,7 +4,8 @@ namespace MassTransit.Components public class RequestState : - SagaStateMachineInstance + SagaStateMachineInstance, + ISagaVersion { public int CurrentState { get; set; } @@ -23,6 +24,8 @@ public class RequestState : /// public Uri SagaAddress { get; set; } + public int Version { get; set; } + /// /// Same as RequestId from the original request /// diff --git a/src/MassTransit/SagaStateMachine/Configuration/CorrelatedByEventCorrelationBuilder.cs b/src/MassTransit/SagaStateMachine/Configuration/CorrelatedByEventCorrelationBuilder.cs index 5a8c851ca0a..562ea8ec27c 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/CorrelatedByEventCorrelationBuilder.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/CorrelatedByEventCorrelationBuilder.cs @@ -8,11 +8,11 @@ public class CorrelatedByEventCorrelationBuilder : where TData : class, CorrelatedBy where TInstance : class, SagaStateMachineInstance { - readonly MassTransitEventCorrelationConfigurator _configurator; + readonly StateMachineInterfaceType.MassTransitEventCorrelationConfigurator _configurator; public CorrelatedByEventCorrelationBuilder(SagaStateMachine machine, Event @event) { - var configurator = new MassTransitEventCorrelationConfigurator(machine, @event, null); + var configurator = new StateMachineInterfaceType.MassTransitEventCorrelationConfigurator(machine, @event, null); configurator.CorrelateById(x => x.Message.CorrelationId); _configurator = configurator; diff --git a/src/MassTransit/SagaStateMachine/Configuration/CorrelatedByFaultEventCorrelationBuilder.cs b/src/MassTransit/SagaStateMachine/Configuration/CorrelatedByFaultEventCorrelationBuilder.cs index 8aeb1204012..09ce5679826 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/CorrelatedByFaultEventCorrelationBuilder.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/CorrelatedByFaultEventCorrelationBuilder.cs @@ -8,11 +8,11 @@ public class CorrelatedByFaultEventCorrelationBuilder : where TData : class, CorrelatedBy where TInstance : class, SagaStateMachineInstance { - readonly MassTransitEventCorrelationConfigurator> _configurator; + readonly StateMachineInterfaceType>.MassTransitEventCorrelationConfigurator _configurator; public CorrelatedByFaultEventCorrelationBuilder(SagaStateMachine machine, Event> @event) { - var configurator = new MassTransitEventCorrelationConfigurator>(machine, @event, null); + var configurator = new StateMachineInterfaceType>.MassTransitEventCorrelationConfigurator(machine, @event, null); configurator.CorrelateById(x => x.Message.Message.CorrelationId); _configurator = configurator; diff --git a/src/MassTransit/SagaStateMachine/Configuration/MassTransitEventCorrelationConfigurator.cs b/src/MassTransit/SagaStateMachine/Configuration/MassTransitEventCorrelationConfigurator.cs index 717df9b7d8d..6e6f742fa75 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/MassTransitEventCorrelationConfigurator.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/MassTransitEventCorrelationConfigurator.cs @@ -7,165 +7,166 @@ using SagaStateMachine; - public class MassTransitEventCorrelationConfigurator : - IEventCorrelationConfigurator, - IEventCorrelationBuilder - where TInstance : class, SagaStateMachineInstance - where TData : class + public partial class StateMachineInterfaceType { - readonly Event _event; - readonly SagaStateMachine _machine; - IFilter> _messageFilter; - IPipe> _missingPipe; - ISagaFactory _sagaFactory; - SagaFilterFactory _sagaFilterFactory; - - public MassTransitEventCorrelationConfigurator(SagaStateMachine machine, Event @event, EventCorrelation existingCorrelation) + public class MassTransitEventCorrelationConfigurator : + IEventCorrelationConfigurator, + IEventCorrelationBuilder { - _event = @event; - _machine = machine; + readonly Event _event; + readonly SagaStateMachine _machine; + IFilter> _messageFilter; + IPipe> _missingPipe; + ISagaFactory _sagaFactory; + SagaFilterFactory _sagaFilterFactory; + + public MassTransitEventCorrelationConfigurator(SagaStateMachine machine, Event @event, EventCorrelation existingCorrelation) + { + _event = @event; + _machine = machine; - InsertOnInitial = false; - ReadOnly = false; - ConfigureConsumeTopology = true; + InsertOnInitial = false; + ReadOnly = false; + ConfigureConsumeTopology = true; - _sagaFactory = new DefaultSagaFactory(); + _sagaFactory = new DefaultSagaFactory(); - var correlation = existingCorrelation as EventCorrelation; - if (correlation != null) - { - _sagaFilterFactory = correlation.FilterFactory; - _messageFilter = correlation.MessageFilter; + var correlation = existingCorrelation as EventCorrelation; + if (correlation != null) + { + _sagaFilterFactory = correlation.FilterFactory; + _messageFilter = correlation.MessageFilter; + } } - } - - public EventCorrelation Build() - { - return new MessageEventCorrelation(_machine, _event, _sagaFilterFactory, _messageFilter, _missingPipe, _sagaFactory, - InsertOnInitial, ReadOnly, ConfigureConsumeTopology); - } - - public bool InsertOnInitial { get; set; } - public bool ReadOnly { get; set; } + public EventCorrelation Build() + { + return new MessageEventCorrelation(_machine, _event, _sagaFilterFactory, _messageFilter, _missingPipe, _sagaFactory, + InsertOnInitial, ReadOnly, ConfigureConsumeTopology); + } - public bool ConfigureConsumeTopology { get; set; } + public bool InsertOnInitial { get; set; } - public IEventCorrelationConfigurator CorrelateById(Func, Guid> selector) - { - _messageFilter = new CorrelationIdMessageFilter(selector); + public bool ReadOnly { get; set; } - _sagaFilterFactory = (repository, policy, sagaPipe) => new CorrelatedSagaFilter(repository, policy, sagaPipe); + public bool ConfigureConsumeTopology { get; set; } - return this; - } + public IEventCorrelationConfigurator CorrelateById(Func, Guid> selector) + { + _messageFilter = new CorrelationIdMessageFilter(selector); - public IEventCorrelationConfigurator CorrelateById(Expression> propertyExpression, - Func, T> selector) - where T : struct - { - if (propertyExpression == null) - throw new ArgumentNullException(nameof(propertyExpression)); + _sagaFilterFactory = (repository, policy, sagaPipe) => new CorrelatedSagaFilter(repository, policy, sagaPipe); - if (selector == null) - throw new ArgumentNullException(nameof(selector)); + return this; + } - _sagaFilterFactory = (repository, policy, sagaPipe) => + public IEventCorrelationConfigurator CorrelateById(Expression> propertyExpression, + Func, T> selector) + where T : struct { - var propertySelector = new NotDefaultValueTypeSagaQueryPropertySelector(selector); - var queryFactory = new PropertyExpressionSagaQueryFactory(propertyExpression, propertySelector); + if (propertyExpression == null) + throw new ArgumentNullException(nameof(propertyExpression)); - return new QuerySagaFilter(repository, policy, queryFactory, sagaPipe); - }; + if (selector == null) + throw new ArgumentNullException(nameof(selector)); - return this; - } + _sagaFilterFactory = (repository, policy, sagaPipe) => + { + var propertySelector = new NotDefaultValueTypeSagaQueryPropertySelector(selector); + var queryFactory = new PropertyExpressionSagaQueryFactory(propertyExpression, propertySelector); - public IEventCorrelationConfigurator CorrelateBy(Expression> propertyExpression, - Func, T?> selector) - where T : struct - { - if (propertyExpression == null) - throw new ArgumentNullException(nameof(propertyExpression)); + return new QuerySagaFilter(repository, policy, queryFactory, sagaPipe); + }; - if (selector == null) - throw new ArgumentNullException(nameof(selector)); + return this; + } - _sagaFilterFactory = (repository, policy, sagaPipe) => + public IEventCorrelationConfigurator CorrelateBy(Expression> propertyExpression, + Func, T?> selector) + where T : struct { - var propertySelector = new HasValueTypeSagaQueryPropertySelector(selector); - var queryFactory = new PropertyExpressionSagaQueryFactory(propertyExpression, propertySelector); + if (propertyExpression == null) + throw new ArgumentNullException(nameof(propertyExpression)); - return new QuerySagaFilter(repository, policy, queryFactory, sagaPipe); - }; + if (selector == null) + throw new ArgumentNullException(nameof(selector)); - return this; - } + _sagaFilterFactory = (repository, policy, sagaPipe) => + { + var propertySelector = new HasValueTypeSagaQueryPropertySelector(selector); + var queryFactory = new PropertyExpressionSagaQueryFactory(propertyExpression, propertySelector); - public IEventCorrelationConfigurator CorrelateBy(Expression> propertyExpression, - Func, T> selector) - where T : class - { - if (propertyExpression == null) - throw new ArgumentNullException(nameof(propertyExpression)); + return new QuerySagaFilter(repository, policy, queryFactory, sagaPipe); + }; - if (selector == null) - throw new ArgumentNullException(nameof(selector)); + return this; + } - _sagaFilterFactory = (repository, policy, sagaPipe) => + public IEventCorrelationConfigurator CorrelateBy(Expression> propertyExpression, + Func, T> selector) + where T : class { - var propertySelector = new SagaQueryPropertySelector(selector); - var queryFactory = new PropertyExpressionSagaQueryFactory(propertyExpression, propertySelector); + if (propertyExpression == null) + throw new ArgumentNullException(nameof(propertyExpression)); - return new QuerySagaFilter(repository, policy, queryFactory, sagaPipe); - }; + if (selector == null) + throw new ArgumentNullException(nameof(selector)); - return this; - } + _sagaFilterFactory = (repository, policy, sagaPipe) => + { + var propertySelector = new SagaQueryPropertySelector(selector); + var queryFactory = new PropertyExpressionSagaQueryFactory(propertyExpression, propertySelector); - public IEventCorrelationConfigurator SelectId(Func, Guid> selector) - { - if (selector == null) - throw new ArgumentNullException(nameof(selector)); + return new QuerySagaFilter(repository, policy, queryFactory, sagaPipe); + }; - _messageFilter = new CorrelationIdMessageFilter(selector); + return this; + } - return this; - } + public IEventCorrelationConfigurator SelectId(Func, Guid> selector) + { + if (selector == null) + throw new ArgumentNullException(nameof(selector)); - public IEventCorrelationConfigurator CorrelateBy(Expression, bool>> correlationExpression) - { - if (correlationExpression == null) - throw new ArgumentNullException(nameof(correlationExpression)); + _messageFilter = new CorrelationIdMessageFilter(selector); - _sagaFilterFactory = (repository, policy, sagaPipe) => + return this; + } + + public IEventCorrelationConfigurator CorrelateBy(Expression, bool>> correlationExpression) { - var queryFactory = new ExpressionCorrelationSagaQueryFactory(correlationExpression); + if (correlationExpression == null) + throw new ArgumentNullException(nameof(correlationExpression)); - return new QuerySagaFilter(repository, policy, queryFactory, sagaPipe); - }; + _sagaFilterFactory = (repository, policy, sagaPipe) => + { + var queryFactory = new ExpressionCorrelationSagaQueryFactory(correlationExpression); - return this; - } + return new QuerySagaFilter(repository, policy, queryFactory, sagaPipe); + }; - public IEventCorrelationConfigurator SetSagaFactory(SagaFactoryMethod factoryMethod) - { - _sagaFactory = new FactoryMethodSagaFactory(factoryMethod); + return this; + } - return this; - } + public IEventCorrelationConfigurator SetSagaFactory(SagaFactoryMethod factoryMethod) + { + _sagaFactory = new FactoryMethodSagaFactory(factoryMethod); - public IEventCorrelationConfigurator OnMissingInstance( - Func, IPipe>> getMissingPipe) - { - if (getMissingPipe == null) - throw new ArgumentNullException(nameof(getMissingPipe)); + return this; + } + + public IEventCorrelationConfigurator OnMissingInstance( + Func, IPipe>> getMissingPipe) + { + if (getMissingPipe == null) + throw new ArgumentNullException(nameof(getMissingPipe)); - var configurator = new EventMissingInstanceConfigurator(); + var configurator = new EventMissingInstanceConfigurator(); - _missingPipe = getMissingPipe(configurator); + _missingPipe = getMissingPipe(configurator); - return this; + return this; + } } } } diff --git a/src/MassTransit/SagaStateMachine/Configuration/MessageCorrelationIdEventCorrelationBuilder.cs b/src/MassTransit/SagaStateMachine/Configuration/MessageCorrelationIdEventCorrelationBuilder.cs index bf611ddcfd8..b7925869d2a 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/MessageCorrelationIdEventCorrelationBuilder.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/MessageCorrelationIdEventCorrelationBuilder.cs @@ -3,28 +3,29 @@ namespace MassTransit.Configuration using System; - public class MessageCorrelationIdEventCorrelationBuilder : - IEventCorrelationBuilder - where TData : class - where TInstance : class, SagaStateMachineInstance + public partial class StateMachineInterfaceType { - readonly MassTransitEventCorrelationConfigurator _configurator; - - public MessageCorrelationIdEventCorrelationBuilder(SagaStateMachine machine, Event @event, - IMessageCorrelationId messageCorrelationId) + public class MessageCorrelationIdEventCorrelationBuilder : + IEventCorrelationBuilder { - var configurator = new MassTransitEventCorrelationConfigurator(machine, @event, null); + readonly MassTransitEventCorrelationConfigurator _configurator; - configurator.CorrelateById(x => messageCorrelationId.TryGetCorrelationId(x.Message, out var correlationId) - ? correlationId - : throw new ArgumentException($"The message {TypeCache.ShortName} did not have a correlationId")); + public MessageCorrelationIdEventCorrelationBuilder(SagaStateMachine machine, Event @event, + IMessageCorrelationId messageCorrelationId) + { + var configurator = new MassTransitEventCorrelationConfigurator(machine, @event, null); - _configurator = configurator; - } + configurator.CorrelateById(x => messageCorrelationId.TryGetCorrelationId(x.Message, out var correlationId) + ? correlationId + : throw new ArgumentException($"The message {TypeCache.ShortName} did not have a correlationId")); - public EventCorrelation Build() - { - return _configurator.Build(); + _configurator = configurator; + } + + public EventCorrelation Build() + { + return _configurator.Build(); + } } } } diff --git a/src/MassTransit/SagaStateMachine/Configuration/MessageCorrelationIdFaultEventCorrelationBuilder.cs b/src/MassTransit/SagaStateMachine/Configuration/MessageCorrelationIdFaultEventCorrelationBuilder.cs index 71bd46ec1ff..e958754a54b 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/MessageCorrelationIdFaultEventCorrelationBuilder.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/MessageCorrelationIdFaultEventCorrelationBuilder.cs @@ -3,28 +3,29 @@ namespace MassTransit.Configuration using System; - public class MessageCorrelationIdFaultEventCorrelationBuilder : - IEventCorrelationBuilder - where TData : class - where TInstance : class, SagaStateMachineInstance + public partial class StateMachineInterfaceType { - readonly MassTransitEventCorrelationConfigurator> _configurator; - - public MessageCorrelationIdFaultEventCorrelationBuilder(SagaStateMachine machine, Event> @event, - IMessageCorrelationId messageCorrelationId) + public class MessageCorrelationIdFaultEventCorrelationBuilder : + IEventCorrelationBuilder { - var configurator = new MassTransitEventCorrelationConfigurator>(machine, @event, null); + readonly StateMachineInterfaceType>.MassTransitEventCorrelationConfigurator _configurator; - configurator.CorrelateById(x => messageCorrelationId.TryGetCorrelationId(x.Message.Message, out var correlationId) - ? correlationId - : throw new ArgumentException($"The message {TypeCache.ShortName} did not have a correlationId")); + public MessageCorrelationIdFaultEventCorrelationBuilder(SagaStateMachine machine, Event> @event, + IMessageCorrelationId messageCorrelationId) + { + var configurator = new StateMachineInterfaceType>.MassTransitEventCorrelationConfigurator(machine, @event, null); - _configurator = configurator; - } + configurator.CorrelateById(x => messageCorrelationId.TryGetCorrelationId(x.Message.Message, out var correlationId) + ? correlationId + : throw new ArgumentException($"The message {TypeCache.ShortName} did not have a correlationId")); - public EventCorrelation Build() - { - return _configurator.Build(); + _configurator = configurator; + } + + public EventCorrelation Build() + { + return _configurator.Build(); + } } } } diff --git a/src/MassTransit/SagaStateMachine/Configuration/StateMachineConnector.cs b/src/MassTransit/SagaStateMachine/Configuration/StateMachineConnector.cs index 196468e329a..a4fe8066baf 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/StateMachineConnector.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/StateMachineConnector.cs @@ -1,85 +1,101 @@ -namespace MassTransit.Configuration +namespace MassTransit { using System; using System.Collections.Generic; using System.Linq; - using System.Reflection; + using Configuration; + using Metadata; using Util; - public class StateMachineConnector : - ISagaConnector - where TInstance : class, ISaga, SagaStateMachineInstance + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly List> _connectors; - readonly SagaStateMachine _stateMachine; - - public StateMachineConnector(SagaStateMachine stateMachine) + internal class StateMachineConnector : + ISagaConnector { - _stateMachine = stateMachine; + readonly List> _connectors; + readonly SagaStateMachine _stateMachine; - try - { - _connectors = StateMachineEvents().ToList(); - } - catch (Exception ex) + public StateMachineConnector(SagaStateMachine stateMachine) { - throw new ConfigurationException($"Failed to create the state machine connector for {TypeCache.ShortName}", ex); + _stateMachine = stateMachine; + + try + { + _connectors = StateMachineEvents().ToList(); + } + catch (Exception ex) + { + throw new ConfigurationException($"Failed to create the state machine connector for {TypeCache.ShortName}", ex); + } } - } - public ISagaSpecification CreateSagaSpecification() - where T : class, ISaga - { - List> messageSpecifications = - _connectors.Select(x => x.CreateSagaMessageSpecification()) - .ToList(); + public ISagaSpecification CreateSagaSpecification() + where T : class, ISaga + { + List> messageSpecifications = + _connectors.Select(x => x.CreateSagaMessageSpecification()) + .ToList(); - var specification = new StateMachineSagaSpecification(_stateMachine, messageSpecifications); + var specification = new StateMachineSagaSpecification(_stateMachine, messageSpecifications); - return specification as ISagaSpecification ?? throw new ArgumentException("The generic argument did not match the connector type", nameof(T)); - } + return specification as ISagaSpecification ?? + throw new ArgumentException("The generic argument did not match the connector type", nameof(T)); + } - public ConnectHandle ConnectSaga(IConsumePipeConnector consumePipe, ISagaRepository sagaRepository, ISagaSpecification specification) - where T : class, ISaga - { - var handles = new List(); - try + public ConnectHandle ConnectSaga(IConsumePipeConnector consumePipe, ISagaRepository sagaRepository, ISagaSpecification specification) + where T : class, ISaga { - foreach (ISagaMessageConnector connector in _connectors.Cast>()) + var handles = new List(_connectors.Count); + try { - var handle = connector.ConnectSaga(consumePipe, sagaRepository, specification); + foreach (ISagaMessageConnector connector in _connectors.Cast>()) + { + var handle = connector.ConnectSaga(consumePipe, sagaRepository, specification); - handles.Add(handle); + handles.Add(handle); + } + + return new MultipleConnectHandle(handles); } + catch (Exception) + { + foreach (var handle in handles) + handle.Dispose(); - return new MultipleConnectHandle(handles); + throw; + } } - catch (Exception) + + IEnumerable> StateMachineEvents() { - foreach (var handle in handles) - handle.Dispose(); + EventCorrelation[] correlations = _stateMachine.Correlations.ToArray(); - throw; - } - } + correlations.SelectMany(x => x.Validate()).ThrowIfContainsFailure("The state machine was not properly configured:"); - IEnumerable> StateMachineEvents() - { - EventCorrelation[] correlations = _stateMachine.Correlations.ToArray(); + var factory = new Factory(); - correlations.SelectMany(x => x.Validate()).ThrowIfContainsFailure("The state machine was not properly configured:"); + foreach (var correlation in correlations) + { + if (correlation.DataType.IsValueType) + continue; - foreach (var correlation in correlations) - { - if (correlation.DataType.GetTypeInfo().IsValueType) - continue; + var interfaceType = Activation.Activate(correlation.DataType, factory, _stateMachine, correlation); - var genericType = typeof(StateMachineInterfaceType<,>).MakeGenericType(typeof(TInstance), correlation.DataType); + yield return interfaceType.GetConnector(); + } + } + } - var interfaceType = (IStateMachineInterfaceType)Activator.CreateInstance(genericType, _stateMachine, correlation); - yield return interfaceType.GetConnector(); + readonly struct Factory : + IActivationType, EventCorrelation> + { + public IStateMachineInterfaceType ActivateType(SagaStateMachine machine, EventCorrelation correlation) + where T : class + { + return new StateMachineInterfaceType(machine, (EventCorrelation)correlation); } } } diff --git a/src/MassTransit/SagaStateMachine/Configuration/StateMachineEventConnectorFactory.cs b/src/MassTransit/SagaStateMachine/Configuration/StateMachineEventConnectorFactory.cs index 88c6394e296..a77a92900a0 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/StateMachineEventConnectorFactory.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/StateMachineEventConnectorFactory.cs @@ -4,27 +4,29 @@ using Middleware; - public class StateMachineEventConnectorFactory : - ISagaConnectorFactory - where TInstance : class, ISaga, SagaStateMachineInstance - where TMessage : class + public partial class StateMachineInterfaceType { - readonly ISagaMessageConnector _connector; - - public StateMachineEventConnectorFactory(SagaStateMachine stateMachine, EventCorrelation correlation) + public class StateMachineEventConnectorFactory : + ISagaConnectorFactory { - var consumeFilter = new StateMachineSagaMessageFilter(stateMachine, correlation.Event); + readonly ISagaMessageConnector _connector; - _connector = new StateMachineSagaMessageConnector(consumeFilter, correlation.Policy, correlation.FilterFactory, - correlation.MessageFilter, correlation.ConfigureConsumeTopology); - } + public StateMachineEventConnectorFactory(SagaStateMachine stateMachine, EventCorrelation correlation) + { + var consumeFilter = new StateMachineSagaMessageFilter(stateMachine, correlation.Event); - ISagaMessageConnector ISagaConnectorFactory.CreateMessageConnector() - { - if (_connector is ISagaMessageConnector connector) - return connector; + _connector = new StateMachineSagaMessageConnector(consumeFilter, correlation.Policy, + correlation.FilterFactory, + correlation.MessageFilter, correlation.ConfigureConsumeTopology); + } + + ISagaMessageConnector ISagaConnectorFactory.CreateMessageConnector() + { + if (_connector is ISagaMessageConnector connector) + return connector; - throw new ArgumentException("The saga type did not match the connector type"); + throw new ArgumentException("The saga type did not match the connector type"); + } } } } diff --git a/src/MassTransit/SagaStateMachine/Configuration/StateMachineInterfaceType.cs b/src/MassTransit/SagaStateMachine/Configuration/StateMachineInterfaceType.cs index 5e9e1cb2019..bad07329ebb 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/StateMachineInterfaceType.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/StateMachineInterfaceType.cs @@ -1,6 +1,6 @@ namespace MassTransit.Configuration { - public class StateMachineInterfaceType : + public partial class StateMachineInterfaceType : IStateMachineInterfaceType where TInstance : class, ISaga, SagaStateMachineInstance where TData : class @@ -9,7 +9,7 @@ public class StateMachineInterfaceType : public StateMachineInterfaceType(SagaStateMachine machine, EventCorrelation correlation) { - _connectorFactory = new StateMachineEventConnectorFactory(machine, correlation); + _connectorFactory = new StateMachineEventConnectorFactory(machine, correlation); } ISagaMessageConnector IStateMachineInterfaceType.GetConnector() diff --git a/src/MassTransit/SagaStateMachine/Configuration/StateMachineRequestConfigurator.cs b/src/MassTransit/SagaStateMachine/Configuration/StateMachineRequestConfigurator.cs index 77b6be05c32..5aa90c6404e 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/StateMachineRequestConfigurator.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/StateMachineRequestConfigurator.cs @@ -1,19 +1,22 @@ namespace MassTransit.Configuration { using System; + using Contracts; - public class StateMachineRequestConfigurator : - IRequestConfigurator, - RequestSettings + public class StateMachineRequestConfigurator : + IRequestConfigurator, + RequestSettings + where TInstance : class, SagaStateMachineInstance where TRequest : class + where TResponse : class { public StateMachineRequestConfigurator() { Timeout = TimeSpan.FromSeconds(30); } - public RequestSettings Settings + public RequestSettings Settings { get { @@ -26,6 +29,70 @@ public RequestSettings Settings public Uri ServiceAddress { get; set; } public TimeSpan Timeout { get; set; } + public bool ClearRequestIdOnFaulted { get; set; } public TimeSpan? TimeToLive { get; set; } + + public Action> Completed { get; set; } + public Action>> Faulted { get; set; } + public Action>> TimeoutExpired { get; set; } + } + + + public class StateMachineRequestConfigurator : + StateMachineRequestConfigurator, + IRequestConfigurator, + RequestSettings + where TInstance : class, SagaStateMachineInstance + where TRequest : class + where TResponse : class + where TResponse2 : class + { + public StateMachineRequestConfigurator() + { + Timeout = TimeSpan.FromSeconds(30); + } + + public new RequestSettings Settings + { + get + { + if (ServiceAddress == null && EndpointConvention.TryGetDestinationAddress(out var serviceAddress)) + ServiceAddress = serviceAddress; + + return this; + } + } + + public Action> Completed2 { get; set; } + } + + + public class StateMachineRequestConfigurator : + StateMachineRequestConfigurator, + IRequestConfigurator, + RequestSettings + where TInstance : class, SagaStateMachineInstance + where TRequest : class + where TResponse : class + where TResponse2 : class + where TResponse3 : class + { + public StateMachineRequestConfigurator() + { + Timeout = TimeSpan.FromSeconds(30); + } + + public new RequestSettings Settings + { + get + { + if (ServiceAddress == null && EndpointConvention.TryGetDestinationAddress(out var serviceAddress)) + ServiceAddress = serviceAddress; + + return this; + } + } + + public Action> Completed3 { get; set; } } } diff --git a/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaConfigurator.cs b/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaConfigurator.cs index f099ed0c15f..16489d3c588 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaConfigurator.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaConfigurator.cs @@ -1,91 +1,94 @@ #nullable enable -namespace MassTransit.Configuration +namespace MassTransit { using System; using System.Collections.Generic; + using Configuration; - public class StateMachineSagaConfigurator : - ISagaConfigurator, - IReceiveEndpointSpecification + public partial class MassTransitStateMachine where TInstance : class, SagaStateMachineInstance { - readonly StateMachineConnector _connector; - readonly ISagaRepository _repository; - readonly ISagaSpecification _specification; - - public StateMachineSagaConfigurator(SagaStateMachine stateMachine, ISagaRepository repository, - ISagaConfigurationObserver observer) - { - _repository = repository; - - _connector = new StateMachineConnector(stateMachine); - - _specification = _connector.CreateSagaSpecification(); - - _specification.ConnectSagaConfigurationObserver(observer); - } - - public IEnumerable Validate() - { - foreach (var result in _specification.Validate()) - yield return result; - } - - public void Configure(IReceiveEndpointBuilder builder) - { - _connector.ConnectSaga(builder, _repository, _specification); - } - - public int? ConcurrentMessageLimit - { - set => _specification.ConcurrentMessageLimit = value; - } - - public void Message(Action> configure) - where T : class - { - _specification.Message(configure); - } - - public void SagaMessage(Action> configure) - where T : class - { - _specification.SagaMessage(configure); - } - - public void AddPipeSpecification(IPipeSpecification> specification) - { - _specification.AddPipeSpecification(specification); - } - - public ConnectHandle ConnectSagaConfigurationObserver(ISagaConfigurationObserver observer) - { - return _specification.ConnectSagaConfigurationObserver(observer); - } - - public T Options(Action? configure) - where T : IOptions, new() - { - return _specification.Options(configure); - } - - public T Options(T options, Action? configure) - where T : IOptions - { - return _specification.Options(options, configure); - } - - public bool TryGetOptions(out T options) - where T : IOptions - { - return _specification.TryGetOptions(out options); - } - - public IEnumerable SelectOptions() - where T : class + internal class StateMachineSagaConfigurator : + ISagaConfigurator, + IReceiveEndpointSpecification { - return _specification.SelectOptions(); + readonly StateMachineConnector _connector; + readonly ISagaRepository _repository; + readonly ISagaSpecification _specification; + + public StateMachineSagaConfigurator(SagaStateMachine stateMachine, ISagaRepository repository, + ISagaConfigurationObserver observer) + { + _repository = repository; + + _connector = new StateMachineConnector(stateMachine); + + _specification = _connector.CreateSagaSpecification(); + + _specification.ConnectSagaConfigurationObserver(observer); + } + + public IEnumerable Validate() + { + return _specification.Validate(); + } + + public void Configure(IReceiveEndpointBuilder builder) + { + _connector.ConnectSaga(builder, _repository, _specification); + } + + public int? ConcurrentMessageLimit + { + set => _specification.ConcurrentMessageLimit = value; + } + + public void Message(Action> configure) + where T : class + { + _specification.Message(configure); + } + + public void SagaMessage(Action> configure) + where T : class + { + _specification.SagaMessage(configure); + } + + public void AddPipeSpecification(IPipeSpecification> specification) + { + _specification.AddPipeSpecification(specification); + } + + public ConnectHandle ConnectSagaConfigurationObserver(ISagaConfigurationObserver observer) + { + return _specification.ConnectSagaConfigurationObserver(observer); + } + + public T Options(Action? configure) + where T : IOptions, new() + { + return _specification.Options(configure); + } + + public T Options(T options, Action? configure) + where T : IOptions + { + return _specification.Options(options, configure); + } + + public bool TryGetOptions(out T options) + where T : IOptions + { + return _specification.TryGetOptions(out options); + } + + public IEnumerable SelectOptions() + where T : class + { + return _specification.SelectOptions(); + } } } } diff --git a/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaMessageConnector.cs b/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaMessageConnector.cs index 9591d3c78eb..99f8d7ca069 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaMessageConnector.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaMessageConnector.cs @@ -1,33 +1,37 @@ namespace MassTransit.Configuration { - public class StateMachineSagaMessageConnector : - SagaMessageConnector - where TInstance : class, ISaga, SagaStateMachineInstance - where TMessage : class + public partial class StateMachineInterfaceType { - readonly IFilter> _messageFilter; - readonly ISagaPolicy _policy; - readonly SagaFilterFactory _sagaFilterFactory; - - public StateMachineSagaMessageConnector(IFilter> consumeFilter, ISagaPolicy policy, - SagaFilterFactory sagaFilterFactory, IFilter> messageFilter, bool configureConsumeTopology) - : base(consumeFilter) + public class StateMachineSagaMessageConnector : + SagaConnector.SagaMessageConnector { - ConfigureConsumeTopology = configureConsumeTopology; - _policy = policy; - _sagaFilterFactory = sagaFilterFactory; - _messageFilter = messageFilter; - } + readonly IFilter> _messageFilter; + readonly ISagaPolicy _policy; + readonly SagaFilterFactory _sagaFilterFactory; - protected override bool ConfigureConsumeTopology { get; } + public StateMachineSagaMessageConnector(IFilter> consumeFilter, ISagaPolicy policy, + SagaFilterFactory sagaFilterFactory, IFilter> messageFilter, bool configureConsumeTopology) + : base(consumeFilter) + { + ConfigureConsumeTopology = configureConsumeTopology; + _policy = policy; + _sagaFilterFactory = sagaFilterFactory; + _messageFilter = messageFilter; + } - protected override void ConfigureMessagePipe(IPipeConfigurator> configurator, ISagaRepository repository, - IPipe> sagaPipe) - { - if (_messageFilter != null) - configurator.UseFilter(_messageFilter); + protected override bool ConfigureConsumeTopology { get; } + + protected override void ConfigureMessagePipe(IPipeConfigurator> configurator, ISagaRepository repository, + IPipe> sagaPipe) + { + if (_messageFilter != null) + configurator.UseFilter(_messageFilter); + + if (_sagaFilterFactory == null) + throw new ConfigurationException($"The event was not properly correlated: {TypeCache.ShortName} - {TypeCache.ShortName}"); - configurator.UseFilter(_sagaFilterFactory(repository, _policy, sagaPipe)); + configurator.UseFilter(_sagaFilterFactory(repository, _policy, sagaPipe)); + } } } } diff --git a/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaSpecification.cs b/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaSpecification.cs index b90d1e1ec9d..dcb407f2143 100644 --- a/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaSpecification.cs +++ b/src/MassTransit/SagaStateMachine/Configuration/StateMachineSagaSpecification.cs @@ -1,25 +1,30 @@ -namespace MassTransit.Configuration +namespace MassTransit { using System.Collections.Generic; + using Configuration; - public class StateMachineSagaSpecification : - SagaSpecification - where TInstance : class, ISaga, SagaStateMachineInstance + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly SagaStateMachine _stateMachine; - - public StateMachineSagaSpecification(SagaStateMachine stateMachine, IEnumerable> messageSpecifications) - : base(messageSpecifications) + class StateMachineSagaSpecification : + SagaSpecification { - _stateMachine = stateMachine; - } + readonly SagaStateMachine _stateMachine; - public override IEnumerable Validate() - { - Observers.ForEach(observer => observer.StateMachineSagaConfigured(this, _stateMachine)); + public StateMachineSagaSpecification(SagaStateMachine stateMachine, + IEnumerable> messageSpecifications) + : base(messageSpecifications) + { + _stateMachine = stateMachine; + } + + public override IEnumerable Validate() + { + Observers.ForEach(observer => observer.StateMachineSagaConfigured(this, _stateMachine)); - return base.Validate(); + return base.Validate(); + } } } } diff --git a/src/MassTransit/SagaStateMachine/EventActivities.cs b/src/MassTransit/SagaStateMachine/EventActivities.cs index 0948b688995..296e740ae59 100644 --- a/src/MassTransit/SagaStateMachine/EventActivities.cs +++ b/src/MassTransit/SagaStateMachine/EventActivities.cs @@ -5,7 +5,7 @@ namespace MassTransit public interface EventActivities - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { IEnumerable> GetStateActivityBinders(); } diff --git a/src/MassTransit/SagaStateMachine/EventActivityBinder.cs b/src/MassTransit/SagaStateMachine/EventActivityBinder.cs index 998517f2ef5..bcb18d2282c 100644 --- a/src/MassTransit/SagaStateMachine/EventActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/EventActivityBinder.cs @@ -5,7 +5,7 @@ namespace MassTransit public interface EventActivityBinder : EventActivities - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { StateMachine StateMachine { get; } @@ -75,7 +75,7 @@ EventActivityBinder IfElseAsync(StateMachineAsyncCondition conditi public interface EventActivityBinder : EventActivities - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { StateMachine StateMachine { get; } diff --git a/src/MassTransit/SagaStateMachine/ExceptionActivityBinder.cs b/src/MassTransit/SagaStateMachine/ExceptionActivityBinder.cs index a57baec3fba..054e91ce503 100644 --- a/src/MassTransit/SagaStateMachine/ExceptionActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/ExceptionActivityBinder.cs @@ -5,7 +5,7 @@ namespace MassTransit public interface ExceptionActivityBinder : EventActivities - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { StateMachine StateMachine { get; } @@ -67,7 +67,7 @@ ExceptionActivityBinder IfElseAsync(StateMachineAsyncExceptio public interface ExceptionActivityBinder : EventActivities - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception where TMessage : class { diff --git a/src/MassTransit/SagaStateMachine/MassTransitStateMachine.cs b/src/MassTransit/SagaStateMachine/MassTransitStateMachine.cs index 53811a2fc83..f1893c3e708 100644 --- a/src/MassTransit/SagaStateMachine/MassTransitStateMachine.cs +++ b/src/MassTransit/SagaStateMachine/MassTransitStateMachine.cs @@ -19,40 +19,43 @@ /// as retry and policy configuration. /// /// The state instance type - public class MassTransitStateMachine : + public partial class MassTransitStateMachine : SagaStateMachine where TInstance : class, SagaStateMachineInstance { readonly HashSet _compositeEvents; - readonly Dictionary> _eventCache; + readonly Dictionary _eventCache; readonly Dictionary _eventCorrelations; - readonly EventObservable _eventObservers; + readonly EventObservable _eventObservers; readonly State _final; readonly State _initial; - readonly Lazy _registrations; + readonly Lazy _registrations; readonly Dictionary> _stateCache; - readonly StateObservable _stateObservers; + readonly StateObservable _stateObservers; IStateAccessor _accessor; + + List _backingFields; Func, Task> _isCompleted; string _name; + List _stateMachineProperties; UnhandledEventCallback _unhandledEventCallback; protected MassTransitStateMachine() { - _registrations = new Lazy(() => ConfigurationHelpers.GetRegistrations(this)); - _stateCache = new Dictionary>(); - _eventCache = new Dictionary>(); + _registrations = new Lazy(() => GetRegistrations()); + _stateCache = new Dictionary>(16); + _eventCache = new Dictionary(16); _compositeEvents = new HashSet(); - _eventObservers = new EventObservable(); - _stateObservers = new StateObservable(); + _eventObservers = new EventObservable(); + _stateObservers = new StateObservable(); - _initial = new StateMachineState((context, state) => UnhandledEvent(context, state), "Initial", _eventObservers); + _initial = new StateMachineState((context, state) => UnhandledEvent(context, state), "Initial", _eventObservers); _stateCache[_initial.Name] = _initial; - _final = new StateMachineState((context, state) => UnhandledEvent(context, state), "Final", _eventObservers); + _final = new StateMachineState((context, state) => UnhandledEvent(context, state), "Final", _eventObservers); _stateCache[_final.Name] = _final; - _accessor = new DefaultInstanceStateAccessor(this, _stateCache[Initial.Name], _stateObservers); + _accessor = new DefaultInstanceStateAccessor(this, _stateCache[Initial.Name], _stateObservers); _unhandledEventCallback = DefaultUnhandledEventCallback; @@ -144,7 +147,7 @@ public State GetState(string name) Event StateMachine.GetEvent(string name) { - if (_eventCache.TryGetValue(name, out StateMachineEvent result)) + if (_eventCache.TryGetValue(name, out var result)) return result.Event; throw new UnknownEventException(_name, name); @@ -199,7 +202,7 @@ public IDisposable ConnectEventObserver(IEventObserver observer) public IDisposable ConnectEventObserver(Event @event, IEventObserver observer) { - var eventObserver = new SelectedEventObserver(@event, observer); + var eventObserver = new SelectedEventObserver(@event, observer); return _eventObservers.Connect(eventObserver); } @@ -230,9 +233,9 @@ Task DefaultUnhandledEventCallback(UnhandledEventContext context) /// protected internal void InstanceState(Expression> instanceStateProperty) { - var stateAccessor = new RawStateAccessor(this, instanceStateProperty, _stateObservers); + var stateAccessor = new RawStateAccessor(this, instanceStateProperty, _stateObservers); - _accessor = new InitialIfNullStateAccessor(_stateCache[Initial.Name], stateAccessor); + _accessor = new InitialIfNullStateAccessor(_stateCache[Initial.Name], stateAccessor); } /// @@ -241,9 +244,9 @@ protected internal void InstanceState(Expression> instanc /// protected internal void InstanceState(Expression> instanceStateProperty) { - var stateAccessor = new StringStateAccessor(this, instanceStateProperty, _stateObservers); + var stateAccessor = new StringStateAccessor(this, instanceStateProperty, _stateObservers); - _accessor = new InitialIfNullStateAccessor(_stateCache[Initial.Name], stateAccessor); + _accessor = new InitialIfNullStateAccessor(_stateCache[Initial.Name], stateAccessor); } /// @@ -253,11 +256,11 @@ protected internal void InstanceState(Expression> instan /// Specifies the states, in order, to which the int values should be assigned protected internal void InstanceState(Expression> instanceStateProperty, params State[] states) { - var stateIndex = new StateAccessorIndex(this, _initial, _final, states); + var stateIndex = new StateAccessorIndex(this, _initial, _final, states); - var stateAccessor = new IntStateAccessor(instanceStateProperty, stateIndex, _stateObservers); + var stateAccessor = new IntStateAccessor(instanceStateProperty, stateIndex, _stateObservers); - _accessor = new InitialIfNullStateAccessor(_stateCache[Initial.Name], stateAccessor); + _accessor = new InitialIfNullStateAccessor(_stateCache[Initial.Name], stateAccessor); } /// @@ -298,7 +301,7 @@ Event DeclareTriggerEvent(string name) protected void SetCompleted(Func> completed) { _isCompleted = completed != null - ? (Func, Task>)(context => completed(context.Saga)) + ? context => completed(context.Saga) : NotCompletedByDefault; } @@ -328,18 +331,21 @@ Event DeclareDataEvent(string name) return DeclareEvent(_ => new MessageEvent(name), name); } - void DeclarePropertyBasedEvent(Func ctor, PropertyInfo property) + TEvent DeclarePropertyBasedEvent(Func ctor, PropertyInfo property) where TEvent : Event { var @event = ctor(property); - ConfigurationHelpers.InitializeEvent(this, property, @event); + + InitializeEvent(this, property, @event); + + return @event; } TEvent DeclareEvent(Func ctor, string name) where TEvent : Event { var @event = ctor(name); - _eventCache[name] = new StateMachineEvent(@event, false); + _eventCache[name] = new StateMachineEvent(@event, false); return @event; } @@ -361,7 +367,7 @@ protected void Event(Expression>> propertyExpression, Action(this, @event, existingCorrelation); + var configurator = new StateMachineInterfaceType.MassTransitEventCorrelationConfigurator(this, @event, existingCorrelation); configureEventCorrelation(configurator); @@ -393,7 +399,7 @@ protected internal void Event(Expression> property _eventCorrelations.TryGetValue(@event, out var existingCorrelation); - var configurator = new MassTransitEventCorrelationConfigurator(this, @event, existingCorrelation); + var configurator = new StateMachineInterfaceType.MassTransitEventCorrelationConfigurator(this, @event, existingCorrelation); configureEventCorrelation(configurator); @@ -421,9 +427,9 @@ protected internal void Event(Expression> property var @event = new MessageEvent(name); - ConfigurationHelpers.InitializeEventProperty(eventProperty, propertyValue, @event); + InitializeEventProperty(eventProperty, propertyValue, @event); - _eventCache[name] = new StateMachineEvent(@event, false); + _eventCache[name] = new StateMachineEvent(@event, false); } /// @@ -479,7 +485,7 @@ protected internal Event Event(string name, Action(this, @event, existingCorrelation); + var configurator = new StateMachineInterfaceType.MassTransitEventCorrelationConfigurator(this, @event, existingCorrelation); configure?.Invoke(configurator); @@ -618,9 +624,9 @@ Event CreateEvent() var @event = new TriggerEvent(eventProperty.Name); - ConfigurationHelpers.InitializeEvent(this, eventProperty, @event); + InitializeEvent(this, eventProperty, @event); - _eventCache[eventProperty.Name] = new StateMachineEvent(@event, false); + _eventCache[eventProperty.Name] = new StateMachineEvent(@event, false); return @event; } @@ -634,7 +640,7 @@ Event CreateEvent() { var @event = new TriggerEvent(name); - _eventCache[name] = new StateMachineEvent(@event, false); + _eventCache[name] = new StateMachineEvent(@event, false); return @event; } @@ -661,7 +667,7 @@ Event CompositeEvent(Event @event, ICompositeEventStatusAccessor acce { var flag = 1 << i; - var activity = new CompositeEventActivity(accessor, flag, complete, @event); + var activity = new CompositeEventActivity(accessor, flag, complete, @event, options); bool Filter(State state) { @@ -703,7 +709,7 @@ protected internal State State(string name) if (TryGetState(name, out State foundState)) return foundState; - var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers); + var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers); SetState(name, state); return state; @@ -716,13 +722,13 @@ void DeclareState(PropertyInfo property) var propertyValue = property.GetValue(this); // If the state was already defined, don't define it again - var existingState = propertyValue as StateMachineState; + var existingState = propertyValue as StateMachineState; if (name.Equals(existingState?.Name)) return; - var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers); + var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers); - ConfigurationHelpers.InitializeState(this, property, state); + InitializeState(this, property, state); SetState(name, state); } @@ -745,28 +751,28 @@ protected internal void State(Expression> propertyExp var name = $"{property.Name}.{stateProperty.Name}"; - StateMachineState existingState = GetStateProperty(stateProperty, propertyValue); + var existingState = GetStateProperty(stateProperty, propertyValue); if (name.Equals(existingState?.Name)) return; - var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers); + var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers); - ConfigurationHelpers.InitializeStateProperty(stateProperty, propertyValue, state); + InitializeStateProperty(stateProperty, propertyValue, state); SetState(name, state); } - static StateMachineState GetStateProperty(PropertyInfo stateProperty, TProperty propertyValue) + static StateMachineState GetStateProperty(PropertyInfo stateProperty, TProperty propertyValue) where TProperty : class { if (stateProperty.CanRead) - return stateProperty.GetValue(propertyValue) as StateMachineState; + return stateProperty.GetValue(propertyValue) as StateMachineState; var objectProperty = propertyValue.GetType().GetProperty(stateProperty.Name, typeof(State)); if (objectProperty == null || !objectProperty.CanRead) throw new ArgumentException($"The state property is not readable: {stateProperty.Name}"); - return objectProperty.GetValue(propertyValue) as StateMachineState; + return objectProperty.GetValue(propertyValue) as StateMachineState; } /// @@ -789,13 +795,13 @@ protected internal void SubState(Expression> propertyExpression, Sta var propertyValue = property.GetValue(this); // If the state was already defined, don't define it again - var existingState = propertyValue as StateMachineState; + var existingState = propertyValue as StateMachineState; if (name.Equals(existingState?.Name) && superState.Name.Equals(existingState?.SuperState?.Name)) return; - var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers, superStateInstance); + var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers, superStateInstance); - ConfigurationHelpers.InitializeState(this, property, state); + InitializeState(this, property, state); SetState(name, state); } @@ -813,7 +819,7 @@ protected internal State SubState(string name, State superState) superState.Name.Equals(existingState?.SuperState?.Name)) return existingState; - var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers, superStateInstance); + var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers, superStateInstance); SetState(name, state); return state; @@ -843,13 +849,13 @@ protected internal void SubState(Expression> property var name = $"{property.Name}.{stateProperty.Name}"; - StateMachineState existingState = GetStateProperty(stateProperty, propertyValue); + var existingState = GetStateProperty(stateProperty, propertyValue); if (name.Equals(existingState?.Name) && superState.Name.Equals(existingState?.SuperState?.Name)) return; - var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers, superStateInstance); + var state = new StateMachineState((c, s) => UnhandledEvent(c, s), name, _eventObservers, superStateInstance); - ConfigurationHelpers.InitializeStateProperty(stateProperty, propertyValue, state); + InitializeStateProperty(stateProperty, propertyValue, state); SetState(name, state); } @@ -859,14 +865,14 @@ protected internal void SubState(Expression> property /// /// /// - void SetState(string name, StateMachineState state) + void SetState(string name, StateMachineState state) { _stateCache[name] = state; - _eventCache[state.BeforeEnter.Name] = new StateMachineEvent(state.BeforeEnter, true); - _eventCache[state.Enter.Name] = new StateMachineEvent(state.Enter, true); - _eventCache[state.Leave.Name] = new StateMachineEvent(state.Leave, true); - _eventCache[state.AfterLeave.Name] = new StateMachineEvent(state.AfterLeave, true); + _eventCache[state.BeforeEnter.Name] = new StateMachineEvent(state.BeforeEnter, true); + _eventCache[state.Enter.Name] = new StateMachineEvent(state.Enter, true); + _eventCache[state.Leave.Name] = new StateMachineEvent(state.Leave, true); + _eventCache[state.AfterLeave.Name] = new StateMachineEvent(state.AfterLeave, true); } /// @@ -1255,7 +1261,7 @@ protected internal void OnUnhandledEvent(UnhandledEventCallback callb Task UnhandledEvent(BehaviorContext context, State state) { - var unhandledEventContext = new UnhandledEventBehaviorContext(this, context, state); + var unhandledEventContext = new UnhandledEventBehaviorContext(this, context, state); return _unhandledEventCallback(unhandledEventContext); } @@ -1272,11 +1278,11 @@ Task UnhandledEvent(BehaviorContext context, State state) /// Allow the request settings to be specified inline protected void Request(Expression>> propertyExpression, Expression> requestIdExpression, - Action configureRequest = default) + Action> configureRequest = default) where TRequest : class where TResponse : class { - var configurator = new StateMachineRequestConfigurator(); + var configurator = new StateMachineRequestConfigurator(); configureRequest?.Invoke(configurator); @@ -1294,11 +1300,11 @@ protected void Request(ExpressionThe request property on the state machine /// Allow the request settings to be specified inline protected void Request(Expression>> propertyExpression, - Action configureRequest = default) + Action> configureRequest = default) where TRequest : class where TResponse : class { - var configurator = new StateMachineRequestConfigurator(); + var configurator = new StateMachineRequestConfigurator(); configureRequest?.Invoke(configurator); @@ -1316,28 +1322,39 @@ protected void Request(ExpressionThe property where the requestId is stored /// The request settings (which can be read from configuration, etc.) protected void Request(Expression>> propertyExpression, - Expression> requestIdExpression, RequestSettings settings) + Expression> requestIdExpression, RequestSettings settings) where TRequest : class where TResponse : class { var property = propertyExpression.GetPropertyInfo(); - var request = new StateMachineRequest(property.Name, settings, requestIdExpression); + var request = new StateMachineRequest(property.Name, settings, requestIdExpression); InitializeRequest(this, property, request); - Event(propertyExpression, x => x.Completed, x => x.CorrelateBy(requestIdExpression, context => context.RequestId)); - Event(propertyExpression, x => x.Faulted, x => x.CorrelateBy(requestIdExpression, context => context.RequestId)); - Event(propertyExpression, x => x.TimeoutExpired, x => x.CorrelateBy(requestIdExpression, context => context.Message.RequestId)); + Event(propertyExpression, x => x.Completed, x => + { + x.CorrelateBy(requestIdExpression, context => context.RequestId); + settings.Completed?.Invoke(x); + }); + Event(propertyExpression, x => x.Faulted, x => + { + x.CorrelateBy(requestIdExpression, context => context.RequestId); + settings.Faulted?.Invoke(x); + }); + Event(propertyExpression, x => x.TimeoutExpired, x => + { + x.CorrelateBy(requestIdExpression, context => context.Message.RequestId); + settings.TimeoutExpired?.Invoke(x); + }); State(propertyExpression, x => x.Pending); DuringAny( When(request.Completed) - .CancelRequestTimeout(request) - .ClearRequest(request), + .CancelRequestTimeout(request), When(request.Faulted) - .CancelRequestTimeout(request)); + .CancelRequestTimeout(request, false)); } /// @@ -1351,19 +1368,31 @@ protected void Request(ExpressionThe request property on the state machine /// The request settings (which can be read from configuration, etc.) protected internal void Request(Expression>> propertyExpression, - RequestSettings settings) + RequestSettings settings) where TRequest : class where TResponse : class { var property = propertyExpression.GetPropertyInfo(); - var request = new StateMachineRequest(property.Name, settings); + var request = new StateMachineRequest(property.Name, settings); InitializeRequest(this, property, request); - Event(propertyExpression, x => x.Completed, x => x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId"))); - Event(propertyExpression, x => x.Faulted, x => x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId"))); - Event(propertyExpression, x => x.TimeoutExpired, x => x.CorrelateById(context => context.Message.RequestId)); + Event(propertyExpression, x => x.Completed, x => + { + x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId")); + settings.Completed?.Invoke(x); + }); + Event(propertyExpression, x => x.Faulted, x => + { + x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId")); + settings.Faulted?.Invoke(x); + }); + Event(propertyExpression, x => x.TimeoutExpired, x => + { + x.CorrelateById(context => context.Message.RequestId); + settings.TimeoutExpired?.Invoke(x); + }); State(propertyExpression, x => x.Pending); @@ -1371,7 +1400,7 @@ protected internal void Request(Expression @@ -1387,12 +1416,12 @@ protected internal void Request(ExpressionAllow the request settings to be specified inline protected void Request(Expression>> propertyExpression, Expression> requestIdExpression, - Action configureRequest = default) + Action> configureRequest = default) where TRequest : class where TResponse : class where TResponse2 : class { - var configurator = new StateMachineRequestConfigurator(); + var configurator = new StateMachineRequestConfigurator(); configureRequest?.Invoke(configurator); @@ -1411,12 +1440,12 @@ protected void Request(ExpressionThe request property on the state machine /// Allow the request settings to be specified inline protected void Request(Expression>> propertyExpression, - Action configureRequest = default) + Action> configureRequest = default) where TRequest : class where TResponse : class where TResponse2 : class { - var configurator = new StateMachineRequestConfigurator(); + var configurator = new StateMachineRequestConfigurator(); configureRequest?.Invoke(configurator); @@ -1436,33 +1465,47 @@ protected void Request(ExpressionThe request settings (which can be read from configuration, etc.) protected internal void Request( Expression>> propertyExpression, - Expression> requestIdExpression, RequestSettings settings) + Expression> requestIdExpression, RequestSettings settings) where TRequest : class where TResponse : class where TResponse2 : class { var property = propertyExpression.GetPropertyInfo(); - var request = new StateMachineRequest(property.Name, settings, requestIdExpression); + var request = new StateMachineRequest(property.Name, settings, requestIdExpression); InitializeRequest(this, property, request); - Event(propertyExpression, x => x.Completed, x => x.CorrelateBy(requestIdExpression, context => context.RequestId)); - Event(propertyExpression, x => x.Completed2, x => x.CorrelateBy(requestIdExpression, context => context.RequestId)); - Event(propertyExpression, x => x.Faulted, x => x.CorrelateBy(requestIdExpression, context => context.RequestId)); - Event(propertyExpression, x => x.TimeoutExpired, x => x.CorrelateBy(requestIdExpression, context => context.Message.RequestId)); + Event(propertyExpression, x => x.Completed, x => + { + x.CorrelateBy(requestIdExpression, context => context.RequestId); + settings.Completed?.Invoke(x); + }); + Event(propertyExpression, x => x.Completed2, x => + { + x.CorrelateBy(requestIdExpression, context => context.RequestId); + settings.Completed2?.Invoke(x); + }); + Event(propertyExpression, x => x.Faulted, x => + { + x.CorrelateBy(requestIdExpression, context => context.RequestId); + settings.Faulted?.Invoke(x); + }); + Event(propertyExpression, x => x.TimeoutExpired, x => + { + x.CorrelateBy(requestIdExpression, context => context.Message.RequestId); + settings.TimeoutExpired?.Invoke(x); + }); State(propertyExpression, x => x.Pending); DuringAny( When(request.Completed) - .CancelRequestTimeout(request) - .ClearRequest(request), + .CancelRequestTimeout(request), When(request.Completed2) - .CancelRequestTimeout(request) - .ClearRequest(request), + .CancelRequestTimeout(request), When(request.Faulted) - .CancelRequestTimeout(request)); + .CancelRequestTimeout(request, false)); } /// @@ -1478,21 +1521,37 @@ protected internal void Request( /// The request settings (which can be read from configuration, etc.) protected internal void Request( Expression>> propertyExpression, - RequestSettings settings) + RequestSettings settings) where TRequest : class where TResponse : class where TResponse2 : class { var property = propertyExpression.GetPropertyInfo(); - var request = new StateMachineRequest(property.Name, settings); + var request = new StateMachineRequest(property.Name, settings); InitializeRequest(this, property, request); - Event(propertyExpression, x => x.Completed, x => x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId"))); - Event(propertyExpression, x => x.Completed2, x => x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId"))); - Event(propertyExpression, x => x.Faulted, x => x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId"))); - Event(propertyExpression, x => x.TimeoutExpired, x => x.CorrelateById(context => context.Message.RequestId)); + Event(propertyExpression, x => x.Completed, x => + { + x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId")); + settings.Completed?.Invoke(x); + }); + Event(propertyExpression, x => x.Completed2, x => + { + x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId")); + settings.Completed2?.Invoke(x); + }); + Event(propertyExpression, x => x.Faulted, x => + { + x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId")); + settings.Faulted?.Invoke(x); + }); + Event(propertyExpression, x => x.TimeoutExpired, x => + { + x.CorrelateById(context => context.Message.RequestId); + settings.TimeoutExpired?.Invoke(x); + }); State(propertyExpression, x => x.Pending); @@ -1502,7 +1561,7 @@ protected internal void Request( When(request.Completed2) .CancelRequestTimeout(request), When(request.Faulted) - .CancelRequestTimeout(request)); + .CancelRequestTimeout(request, false)); } /// @@ -1520,13 +1579,13 @@ protected internal void Request( protected void Request( Expression>> propertyExpression, Expression> requestIdExpression, - Action configureRequest = default) + Action> configureRequest = default) where TRequest : class where TResponse : class where TResponse2 : class where TResponse3 : class { - var configurator = new StateMachineRequestConfigurator(); + var configurator = new StateMachineRequestConfigurator(); configureRequest?.Invoke(configurator); @@ -1547,13 +1606,13 @@ protected void Request( /// Allow the request settings to be specified inline protected void Request( Expression>> propertyExpression, - Action configureRequest = default) + Action> configureRequest = default) where TRequest : class where TResponse : class where TResponse2 : class where TResponse3 : class { - var configurator = new StateMachineRequestConfigurator(); + var configurator = new StateMachineRequestConfigurator(); configureRequest?.Invoke(configurator); @@ -1574,7 +1633,7 @@ protected void Request( /// The request settings (which can be read from configuration, etc.) protected internal void Request( Expression>> propertyExpression, - Expression> requestIdExpression, RequestSettings settings) + Expression> requestIdExpression, RequestSettings settings) where TRequest : class where TResponse : class where TResponse2 : class @@ -1582,30 +1641,47 @@ protected internal void Request( { var property = propertyExpression.GetPropertyInfo(); - var request = new StateMachineRequest(property.Name, settings, requestIdExpression); + var request = new StateMachineRequest(property.Name, settings, requestIdExpression); InitializeRequest(this, property, request); - Event(propertyExpression, x => x.Completed, x => x.CorrelateBy(requestIdExpression, context => context.RequestId)); - Event(propertyExpression, x => x.Completed2, x => x.CorrelateBy(requestIdExpression, context => context.RequestId)); - Event(propertyExpression, x => x.Completed3, x => x.CorrelateBy(requestIdExpression, context => context.RequestId)); - Event(propertyExpression, x => x.Faulted, x => x.CorrelateBy(requestIdExpression, context => context.RequestId)); - Event(propertyExpression, x => x.TimeoutExpired, x => x.CorrelateBy(requestIdExpression, context => context.Message.RequestId)); + Event(propertyExpression, x => x.Completed, x => + { + x.CorrelateBy(requestIdExpression, context => context.RequestId); + settings.Completed?.Invoke(x); + }); + Event(propertyExpression, x => x.Completed2, x => + { + x.CorrelateBy(requestIdExpression, context => context.RequestId); + settings.Completed2?.Invoke(x); + }); + Event(propertyExpression, x => x.Completed3, x => + { + x.CorrelateBy(requestIdExpression, context => context.RequestId); + settings.Completed3?.Invoke(x); + }); + Event(propertyExpression, x => x.Faulted, x => + { + x.CorrelateBy(requestIdExpression, context => context.RequestId); + settings.Faulted?.Invoke(x); + }); + Event(propertyExpression, x => x.TimeoutExpired, x => + { + x.CorrelateBy(requestIdExpression, context => context.Message.RequestId); + settings.TimeoutExpired?.Invoke(x); + }); State(propertyExpression, x => x.Pending); DuringAny( When(request.Completed) - .CancelRequestTimeout(request) - .ClearRequest(request), + .CancelRequestTimeout(request), When(request.Completed2) - .CancelRequestTimeout(request) - .ClearRequest(request), + .CancelRequestTimeout(request), When(request.Completed3) - .CancelRequestTimeout(request) - .ClearRequest(request), + .CancelRequestTimeout(request), When(request.Faulted) - .CancelRequestTimeout(request)); + .CancelRequestTimeout(request, false)); } /// @@ -1622,7 +1698,7 @@ protected internal void Request( /// The request settings (which can be read from configuration, etc.) protected internal void Request( Expression>> propertyExpression, - RequestSettings settings) + RequestSettings settings) where TRequest : class where TResponse : class where TResponse2 : class @@ -1630,15 +1706,35 @@ protected internal void Request( { var property = propertyExpression.GetPropertyInfo(); - var request = new StateMachineRequest(property.Name, settings); + var request = new StateMachineRequest(property.Name, settings); InitializeRequest(this, property, request); - Event(propertyExpression, x => x.Completed, x => x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId"))); - Event(propertyExpression, x => x.Completed2, x => x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId"))); - Event(propertyExpression, x => x.Completed3, x => x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId"))); - Event(propertyExpression, x => x.Faulted, x => x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId"))); - Event(propertyExpression, x => x.TimeoutExpired, x => x.CorrelateById(context => context.Message.RequestId)); + Event(propertyExpression, x => x.Completed, x => + { + x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId")); + settings.Completed?.Invoke(x); + }); + Event(propertyExpression, x => x.Completed2, x => + { + x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId")); + settings.Completed2?.Invoke(x); + }); + Event(propertyExpression, x => x.Completed3, x => + { + x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId")); + settings.Completed3?.Invoke(x); + }); + Event(propertyExpression, x => x.Faulted, x => + { + x.CorrelateById(context => context.RequestId ?? throw new RequestException("Missing RequestId")); + settings.Faulted?.Invoke(x); + }); + Event(propertyExpression, x => x.TimeoutExpired, x => + { + x.CorrelateById(context => context.Message.RequestId); + settings.TimeoutExpired?.Invoke(x); + }); State(propertyExpression, x => x.Pending); @@ -1650,7 +1746,7 @@ protected internal void Request( When(request.Completed3) .CancelRequestTimeout(request), When(request.Faulted) - .CancelRequestTimeout(request)); + .CancelRequestTimeout(request, false)); } /// @@ -1688,21 +1784,16 @@ protected internal void Schedule(Expression(name, tokenIdExpression, settings); + var schedule = new StateMachineSchedule(name, tokenIdExpression, settings); InitializeSchedule(this, property, schedule); Event(propertyExpression, x => x.Received); - if (settings.Received == null) - Event(propertyExpression, x => x.AnyReceived); - else + Event(propertyExpression, x => x.AnyReceived, x => { - Event(propertyExpression, x => x.AnyReceived, x => - { - settings.Received(x); - }); - } + settings.Received?.Invoke(x); + }); DuringAny( When(schedule.AnyReceived) @@ -1739,25 +1830,25 @@ static Task NotCompletedByDefault(BehaviorContext instance) return TaskUtil.False; } - static void InitializeSchedule(MassTransitStateMachine stateMachine, PropertyInfo property, Schedule schedule) + void InitializeSchedule(MassTransitStateMachine stateMachine, PropertyInfo property, Schedule schedule) where T : class { if (property.CanWrite) property.SetValue(stateMachine, schedule); - else if (ConfigurationHelpers.TryGetBackingField(stateMachine.GetType().GetTypeInfo(), property, out var backingField)) + else if (TryGetBackingField(property, out var backingField)) backingField.SetValue(stateMachine, schedule); else throw new ArgumentException($"The schedule property is not writable: {property.Name}"); } - static void InitializeRequest(MassTransitStateMachine stateMachine, PropertyInfo property, + void InitializeRequest(MassTransitStateMachine stateMachine, PropertyInfo property, Request request) where TRequest : class where TResponse : class { if (property.CanWrite) property.SetValue(stateMachine, request); - else if (ConfigurationHelpers.TryGetBackingField(stateMachine.GetType().GetTypeInfo(), property, out var backingField)) + else if (TryGetBackingField(property, out var backingField)) backingField.SetValue(stateMachine, request); else throw new ArgumentException($"The request property is not writable: {property.Name}"); @@ -1770,27 +1861,6 @@ void RegisterImplicit() { foreach (var declaration in _registrations.Value) declaration.Declare(this); - - var machineType = GetType().GetTypeInfo(); - - IEnumerable properties = ConfigurationHelpers.GetStateMachineProperties(machineType); - - foreach (var propertyInfo in properties) - { - var propertyType = propertyInfo.PropertyType.GetTypeInfo(); - if (!propertyType.IsGenericType) - continue; - - if (!propertyType.ClosesType(typeof(Event<>), out Type[] arguments)) - continue; - - var @event = (Event)propertyInfo.GetValue(this); - if (@event == null) - continue; - - var registration = GetEventRegistration(@event, arguments[0]); - registration.RegisterCorrelation(this); - } } static EventRegistration GetEventRegistration(Event @event, Type messageType) @@ -1811,7 +1881,23 @@ static EventRegistration GetEventRegistration(Event @event, Type messageType) : typeof(UncorrelatedEventRegistration<>).MakeGenericType(typeof(TInstance), messageType); } - return (EventRegistration)Activator.CreateInstance(registrationType, @event); + // return (EventRegistration)Activator.CreateInstance(registrationType, @event); + return CreateRegistration(registrationType, @event, messageType); + } + + static EventRegistration CreateRegistration(Type registrationType, Event @event, Type messageType) + { + var constructorInfo = registrationType.GetConstructors().FirstOrDefault(x => x.GetParameters().Length == 1); + if (constructorInfo == null) + throw new ArgumentException("The event correlation could not be created: " + TypeCache.GetShortName(registrationType)); + + var eventParameter = Expression.Parameter(typeof(Event), "event"); + var convertExpression = Expression.Convert(eventParameter, typeof(Event<>).MakeGenericType(messageType)); + var @new = Expression.New(constructorInfo, convertExpression); + + Func factoryMethod = Expression.Lambda>(@new, eventParameter).CompileFast(); + + return factoryMethod(@event); } StateMachine Modify(Action> modifier) @@ -1835,212 +1921,216 @@ public static MassTransitStateMachine New(Action stateMachine) - { - var events = new List(); + var events = new List(); - var machineType = stateMachine.GetType().GetTypeInfo(); + var machineType = GetType(); - IEnumerable properties = GetStateMachineProperties(machineType); + IEnumerable properties = GetStateMachineProperties(); - foreach (var propertyInfo in properties) + foreach (var propertyInfo in properties) + { + if (propertyInfo.PropertyType.IsGenericType) { - if (propertyInfo.PropertyType.GetTypeInfo().IsGenericType) + if (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Event<>)) { - if (propertyInfo.PropertyType.GetGenericTypeDefinition() == typeof(Event<>)) - { - var declarationType = typeof(DataEventRegistration<,>).MakeGenericType(typeof(TInstance), machineType, - propertyInfo.PropertyType.GetGenericArguments().First()); - var declaration = Activator.CreateInstance(declarationType, propertyInfo); - events.Add((StateMachineRegistration)declaration); - } + var declarationType = typeof(DataEventRegistration<,>).MakeGenericType(typeof(TInstance), machineType, + propertyInfo.PropertyType.GetGenericArguments().First()); + var declaration = Activator.CreateInstance(declarationType, propertyInfo); + events.Add((StateMachineRegistration)declaration); } - else + } + else + { + if (propertyInfo.PropertyType == typeof(Event)) { - if (propertyInfo.PropertyType == typeof(Event)) - { - var declarationType = typeof(TriggerEventRegistration<>).MakeGenericType(typeof(TInstance), machineType); - var declaration = Activator.CreateInstance(declarationType, propertyInfo); - events.Add((StateMachineRegistration)declaration); - } - else if (propertyInfo.PropertyType == typeof(State)) - { - var declarationType = typeof(StateRegistration<>).MakeGenericType(typeof(TInstance), machineType); - var declaration = Activator.CreateInstance(declarationType, propertyInfo); - events.Add((StateMachineRegistration)declaration); - } + var declarationType = typeof(TriggerEventRegistration<>).MakeGenericType(typeof(TInstance), machineType); + var declaration = Activator.CreateInstance(declarationType, propertyInfo); + events.Add((StateMachineRegistration)declaration); + } + else if (propertyInfo.PropertyType == typeof(State)) + { + var declarationType = typeof(StateRegistration<>).MakeGenericType(typeof(TInstance), machineType); + var declaration = Activator.CreateInstance(declarationType, propertyInfo); + events.Add((StateMachineRegistration)declaration); } } - - return events.ToArray(); } - public static IEnumerable GetStateMachineProperties(TypeInfo typeInfo) - { - if (typeInfo.IsInterface) - yield break; + return events.ToArray(); + } - if (typeInfo.BaseType != null) - { - foreach (var propertyInfo in GetStateMachineProperties(typeInfo.BaseType.GetTypeInfo())) - yield return propertyInfo; - } + IEnumerable GetStateMachineProperties() + { + return _stateMachineProperties ??= GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public) + .Where(x => x.CanRead && (x.CanWrite || TryGetBackingField(x, out _))).ToList(); + } - IEnumerable properties = typeInfo.DeclaredMethods - .Where(x => x.IsSpecialName && x.Name.StartsWith("get_") && !x.IsStatic) - .Select(x => typeInfo.GetDeclaredProperty(x.Name.Substring("get_".Length))) - .Where(x => x.CanRead && (x.CanWrite || TryGetBackingField(typeInfo, x, out _))); + bool TryGetBackingField(PropertyInfo property, out FieldInfo backingField) + { + _backingFields ??= GetBackingFields(GetType()) + .Where(field => + field.Attributes.HasFlag(FieldAttributes.Private) && + field.Attributes.HasFlag(FieldAttributes.InitOnly) && + field.CustomAttributes.Any(attr => attr.AttributeType == typeof(CompilerGeneratedAttribute)) && + field.Name.StartsWith("<") + ).ToList(); - foreach (var propertyInfo in properties) - yield return propertyInfo; - } + backingField = _backingFields + .FirstOrDefault(field => + field.DeclaringType == property.DeclaringType && + field.FieldType.IsAssignableFrom(property.PropertyType) && + field.Name.StartsWith("<" + property.Name + ">") + ); - public static bool TryGetBackingField(TypeInfo typeInfo, PropertyInfo property, out FieldInfo backingField) - { - backingField = typeInfo - .GetFields(BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static) - .FirstOrDefault(field => - field.Attributes.HasFlag(FieldAttributes.Private) && - field.Attributes.HasFlag(FieldAttributes.InitOnly) && - field.CustomAttributes.Any(attr => attr.AttributeType == typeof(CompilerGeneratedAttribute)) && - field.DeclaringType == property.DeclaringType && - field.FieldType.IsAssignableFrom(property.PropertyType) && - field.Name.StartsWith("<" + property.Name + ">") - ); - - return backingField != null; - } + return backingField != null; + } - public static void InitializeState(MassTransitStateMachine stateMachine, PropertyInfo property, - StateMachineState state) + static IEnumerable GetBackingFields(Type type) + { + while (true) { - if (property.CanWrite) - property.SetValue(stateMachine, state); - else if (TryGetBackingField(stateMachine.GetType().GetTypeInfo(), property, out var backingField)) - backingField.SetValue(stateMachine, state); - else - throw new ArgumentException($"The state property is not writable: {property.Name}"); - } + foreach (var fieldInfo in type.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)) + yield return fieldInfo; - public static void InitializeStateProperty(PropertyInfo stateProperty, TProperty propertyValue, - StateMachineState state) - where TProperty : class - { - if (stateProperty.CanWrite) - stateProperty.SetValue(propertyValue, state); - else - { - var objectProperty = propertyValue.GetType().GetProperty(stateProperty.Name, typeof(State)); - if (objectProperty == null || !objectProperty.CanWrite) - throw new ArgumentException($"The state property is not writable: {stateProperty.Name}"); + if (type.BaseType == null) + break; - objectProperty.SetValue(propertyValue, state); - } - } + if (type.BaseType.IsGenericType && type.BaseType.GetGenericTypeDefinition() == typeof(MassTransitStateMachine<>)) + break; - public static void InitializeEvent(MassTransitStateMachine stateMachine, PropertyInfo property, Event @event) - { - if (property.CanWrite) - property.SetValue(stateMachine, @event); - else if (TryGetBackingField(stateMachine.GetType().GetTypeInfo(), property, out var backingField)) - backingField.SetValue(stateMachine, @event); - else - throw new ArgumentException($"The event property is not writable: {property.Name}"); + type = type.BaseType; } + } - public static void InitializeEventProperty(PropertyInfo eventProperty, TProperty propertyValue, Event @event) - where TProperty : class - where T : class + void InitializeState(MassTransitStateMachine stateMachine, PropertyInfo property, StateMachineState state) + { + if (property.CanWrite) + property.SetValue(stateMachine, state); + else if (TryGetBackingField(property, out var backingField)) + backingField.SetValue(stateMachine, state); + else + throw new ArgumentException($"The state property is not writable: {property.Name}"); + } + + void InitializeStateProperty(PropertyInfo stateProperty, TProperty propertyValue, StateMachineState state) + where TProperty : class + { + if (stateProperty.CanWrite) + stateProperty.SetValue(propertyValue, state); + else { - if (eventProperty.CanWrite) - eventProperty.SetValue(propertyValue, @event); - else - { - var objectProperty = propertyValue.GetType().GetProperty(eventProperty.Name, typeof(Event)); - if (objectProperty == null || !objectProperty.CanWrite) - throw new ArgumentException($"The event property is not writable: {eventProperty.Name}"); + var objectProperty = propertyValue.GetType().GetProperty(stateProperty.Name, typeof(State)); + if (objectProperty == null || !objectProperty.CanWrite) + throw new ArgumentException($"The state property is not writable: {stateProperty.Name}"); - objectProperty.SetValue(propertyValue, @event); - } + objectProperty.SetValue(propertyValue, state); } + } + void InitializeEvent(MassTransitStateMachine stateMachine, PropertyInfo property, Event @event) + { + if (property.CanWrite) + property.SetValue(stateMachine, @event); + else if (TryGetBackingField(property, out var backingField)) + backingField.SetValue(stateMachine, @event); + else + throw new ArgumentException($"The event property is not writable: {property.Name}"); + } - public interface StateMachineRegistration + void InitializeEventProperty(PropertyInfo eventProperty, TProperty propertyValue, Event @event) + where TProperty : class + where T : class + { + if (eventProperty.CanWrite) + eventProperty.SetValue(propertyValue, @event); + else { - void Declare(object stateMachine); + var objectProperty = propertyValue.GetType().GetProperty(eventProperty.Name, typeof(Event)); + if (objectProperty == null || !objectProperty.CanWrite) + throw new ArgumentException($"The event property is not writable: {eventProperty.Name}"); + + objectProperty.SetValue(propertyValue, @event); } + } - class StateRegistration : - StateMachineRegistration - where TStateMachine : MassTransitStateMachine - { - readonly PropertyInfo _propertyInfo; + interface StateMachineRegistration + { + void Declare(object stateMachine); + } - public StateRegistration(PropertyInfo propertyInfo) - { - _propertyInfo = propertyInfo; - } - public void Declare(object stateMachine) - { - var machine = (TStateMachine)stateMachine; - var existing = _propertyInfo.GetValue(machine); - if (existing != null) - return; + class StateRegistration : + StateMachineRegistration + where TStateMachine : MassTransitStateMachine + { + readonly PropertyInfo _propertyInfo; - machine.DeclareState(_propertyInfo); - } + public StateRegistration(PropertyInfo propertyInfo) + { + _propertyInfo = propertyInfo; } - - class TriggerEventRegistration : - StateMachineRegistration - where TStateMachine : MassTransitStateMachine + public void Declare(object stateMachine) { - readonly PropertyInfo _propertyInfo; + var machine = (TStateMachine)stateMachine; + var existing = _propertyInfo.GetValue(machine); + if (existing != null) + return; - public TriggerEventRegistration(PropertyInfo propertyInfo) - { - _propertyInfo = propertyInfo; - } + machine.DeclareState(_propertyInfo); + } + } - public void Declare(object stateMachine) - { - var machine = (TStateMachine)stateMachine; - var existing = _propertyInfo.GetValue(machine); - if (existing != null) - return; - machine.DeclarePropertyBasedEvent(prop => machine.DeclareTriggerEvent(prop.Name), _propertyInfo); - } + class TriggerEventRegistration : + StateMachineRegistration + where TStateMachine : MassTransitStateMachine + { + readonly PropertyInfo _propertyInfo; + + public TriggerEventRegistration(PropertyInfo propertyInfo) + { + _propertyInfo = propertyInfo; } + public void Declare(object stateMachine) + { + var machine = (TStateMachine)stateMachine; + var existing = _propertyInfo.GetValue(machine); + if (existing != null) + return; + + machine.DeclarePropertyBasedEvent(prop => machine.DeclareTriggerEvent(prop.Name), _propertyInfo); + } + } + - class DataEventRegistration : - StateMachineRegistration - where TStateMachine : MassTransitStateMachine - where TData : class + class DataEventRegistration : + StateMachineRegistration + where TStateMachine : MassTransitStateMachine + where TData : class + { + readonly PropertyInfo _propertyInfo; + + public DataEventRegistration(PropertyInfo propertyInfo) { - readonly PropertyInfo _propertyInfo; + _propertyInfo = propertyInfo; + } - public DataEventRegistration(PropertyInfo propertyInfo) - { - _propertyInfo = propertyInfo; - } + public void Declare(object stateMachine) + { + var machine = (TStateMachine)stateMachine; + var existing = _propertyInfo.GetValue(machine); + if (existing != null) + return; - public void Declare(object stateMachine) - { - var machine = (TStateMachine)stateMachine; - var existing = _propertyInfo.GetValue(machine); - if (existing != null) - return; + Event @event = machine.DeclarePropertyBasedEvent(prop => machine.DeclareDataEvent(prop.Name), _propertyInfo); - machine.DeclarePropertyBasedEvent(prop => machine.DeclareDataEvent(prop.Name), _propertyInfo); - } + var eventRegistration = GetEventRegistration(@event, typeof(TData)); + eventRegistration.RegisterCorrelation(machine); } } @@ -2101,14 +2191,13 @@ public void RegisterCorrelation(MassTransitStateMachine machine) if (GlobalTopology.Send.GetMessageTopology().TryGetConvention(out ICorrelationIdMessageSendTopologyConvention convention) && convention.TryGetMessageCorrelationId(out IMessageCorrelationId messageCorrelationId)) { - var builder = new MessageCorrelationIdEventCorrelationBuilder(machine, _event, messageCorrelationId); + var builder = new StateMachineInterfaceType.MessageCorrelationIdEventCorrelationBuilder(machine, _event, + messageCorrelationId); machine._eventCorrelations[_event] = builder.Build(); } else - { - machine._eventCorrelations[_event] = new UncorrelatedEventCorrelation(_event); - } + machine._eventCorrelations[_event] = new UncorrelatedEventCorrelation(_event); } } @@ -2129,14 +2218,13 @@ public void RegisterCorrelation(MassTransitStateMachine machine) if (GlobalTopology.Send.GetMessageTopology().TryGetConvention(out ICorrelationIdMessageSendTopologyConvention convention) && convention.TryGetMessageCorrelationId(out IMessageCorrelationId messageCorrelationId)) { - var builder = new MessageCorrelationIdFaultEventCorrelationBuilder(machine, _event, messageCorrelationId); + var builder = new StateMachineInterfaceType.MessageCorrelationIdFaultEventCorrelationBuilder(machine, _event, + messageCorrelationId); machine._eventCorrelations[_event] = builder.Build(); } else - { - machine._eventCorrelations[_event] = new UncorrelatedEventCorrelation>(_event); - } + machine._eventCorrelations[_event] = new UncorrelatedEventCorrelation>(_event); } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/DefaultInstanceStateAccessor.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/DefaultInstanceStateAccessor.cs index 2fe43b520ad..af391020b53 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/DefaultInstanceStateAccessor.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/DefaultInstanceStateAccessor.cs @@ -1,4 +1,4 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Collections.Generic; @@ -8,76 +8,79 @@ namespace MassTransit.SagaStateMachine using System.Threading.Tasks; - /// - /// The default state accessor will attempt to find and use a single State property on the - /// instance type. If no State property is found, or more than one is found, an exception - /// will be thrown - /// - public class DefaultInstanceStateAccessor : - IStateAccessor - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly Lazy> _accessor; - readonly State _initialState; - readonly StateMachine _machine; - readonly IStateObserver _observer; - - public DefaultInstanceStateAccessor(StateMachine machine, State initialState, IStateObserver observer) - { - _machine = machine; - _initialState = initialState; - _observer = observer; - _accessor = new Lazy>(CreateDefaultAccessor); - } - - Task> IStateAccessor.Get(BehaviorContext context) - { - return _accessor.Value.Get(context); - } - - Task IStateAccessor.Set(BehaviorContext context, State state) + /// + /// The default state accessor will attempt to find and use a single State property on the + /// instance type. If no State property is found, or more than one is found, an exception + /// will be thrown + /// + class DefaultInstanceStateAccessor : + IStateAccessor { - return _accessor.Value.Set(context, state); - } + readonly Lazy> _accessor; + readonly State _initialState; + readonly StateMachine _machine; + readonly IStateObserver _observer; - public Expression> GetStateExpression(params State[] states) - { - return _accessor.Value.GetStateExpression(states); - } + public DefaultInstanceStateAccessor(StateMachine machine, State initialState, IStateObserver observer) + { + _machine = machine; + _initialState = initialState; + _observer = observer; + _accessor = new Lazy>(CreateDefaultAccessor); + } - public void Probe(ProbeContext context) - { - _accessor.Value.Probe(context); - } + Task> IStateAccessor.Get(BehaviorContext context) + { + return _accessor.Value.Get(context); + } - IStateAccessor CreateDefaultAccessor() - { - List states = typeof(TSaga) - .GetProperties(BindingFlags.Public | BindingFlags.Instance) - .Where(x => x.PropertyType == typeof(State)) - .Where(x => x.GetGetMethod(true) != null) - .Where(x => x.GetSetMethod(true) != null) - .ToList(); + Task IStateAccessor.Set(BehaviorContext context, State state) + { + return _accessor.Value.Set(context, state); + } - if (states.Count > 1) + public Expression> GetStateExpression(params State[] states) { - throw new SagaStateMachineException( - "The InstanceState was not configured, and could not be automatically identified as multiple State properties exist."); + return _accessor.Value.GetStateExpression(states); } - if (states.Count == 0) + public void Probe(ProbeContext context) { - throw new SagaStateMachineException( - "The InstanceState was not configured, and no public State property exists."); + _accessor.Value.Probe(context); } - var instance = Expression.Parameter(typeof(TSaga), "instance"); - var memberExpression = Expression.Property(instance, states[0]); + IStateAccessor CreateDefaultAccessor() + { + List states = typeof(TInstance) + .GetProperties(BindingFlags.Public | BindingFlags.Instance) + .Where(x => x.PropertyType == typeof(State)) + .Where(x => x.GetGetMethod(true) != null) + .Where(x => x.GetSetMethod(true) != null) + .ToList(); + + if (states.Count > 1) + { + throw new SagaStateMachineException( + "The InstanceState was not configured, and could not be automatically identified as multiple State properties exist."); + } - Expression> expression = Expression.Lambda>(memberExpression, - instance); + if (states.Count == 0) + { + throw new SagaStateMachineException( + "The InstanceState was not configured, and no public State property exists."); + } - return new InitialIfNullStateAccessor(_initialState, new RawStateAccessor(_machine, expression, _observer)); + var instance = Expression.Parameter(typeof(TInstance), "instance"); + var memberExpression = Expression.Property(instance, states[0]); + + Expression> expression = Expression.Lambda>(memberExpression, + instance); + + return new InitialIfNullStateAccessor(_initialState, new RawStateAccessor(_machine, expression, _observer)); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/InitialIfNullStateAccessor.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/InitialIfNullStateAccessor.cs index 7f5f1e8977e..a1e57960d78 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/InitialIfNullStateAccessor.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/InitialIfNullStateAccessor.cs @@ -1,51 +1,55 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Linq.Expressions; using System.Threading.Tasks; + using SagaStateMachine; - public class InitialIfNullStateAccessor : - IStateAccessor - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly IBehavior _initialBehavior; - readonly IStateAccessor _stateAccessor; - - public InitialIfNullStateAccessor(State initialState, IStateAccessor stateAccessor) + class InitialIfNullStateAccessor : + IStateAccessor { - _stateAccessor = stateAccessor; - - IStateMachineActivity initialActivity = new TransitionActivity(initialState, _stateAccessor); - _initialBehavior = new LastBehavior(initialActivity); - } + readonly IBehavior _initialBehavior; + readonly IStateAccessor _stateAccessor; - async Task> IStateAccessor.Get(BehaviorContext context) - { - State state = await _stateAccessor.Get(context).ConfigureAwait(false); - if (state == null) + public InitialIfNullStateAccessor(State initialState, IStateAccessor stateAccessor) { - await _initialBehavior.Execute(context).ConfigureAwait(false); + _stateAccessor = stateAccessor; - state = await _stateAccessor.Get(context).ConfigureAwait(false); + IStateMachineActivity initialActivity = new TransitionActivity(initialState, _stateAccessor); + _initialBehavior = new LastBehavior(initialActivity); } - return state; - } + async Task> IStateAccessor.Get(BehaviorContext context) + { + State state = await _stateAccessor.Get(context).ConfigureAwait(false); + if (state == null) + { + await _initialBehavior.Execute(context).ConfigureAwait(false); - Task IStateAccessor.Set(BehaviorContext context, State state) - { - return _stateAccessor.Set(context, state); - } + state = await _stateAccessor.Get(context).ConfigureAwait(false); + } - public Expression> GetStateExpression(params State[] states) - { - return _stateAccessor.GetStateExpression(states); - } + return state; + } - public void Probe(ProbeContext context) - { - _stateAccessor.Probe(context); + Task IStateAccessor.Set(BehaviorContext context, State state) + { + return _stateAccessor.Set(context, state); + } + + public Expression> GetStateExpression(params State[] states) + { + return _stateAccessor.GetStateExpression(states); + } + + public void Probe(ProbeContext context) + { + _stateAccessor.Probe(context); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/IntStateAccessor.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/IntStateAccessor.cs index 73a64575574..b642c220f9c 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/IntStateAccessor.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/IntStateAccessor.cs @@ -1,4 +1,4 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Linq; @@ -8,75 +8,77 @@ namespace MassTransit.SagaStateMachine using Internals; - /// - /// Accesses the current state as a string property - /// - /// The instance type - public class IntStateAccessor : - IStateAccessor - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly StateAccessorIndex _index; - readonly IStateObserver _observer; - readonly PropertyInfo _propertyInfo; - readonly IReadProperty _read; - readonly IWriteProperty _write; - - public IntStateAccessor(Expression> currentStateExpression, StateAccessorIndex index, IStateObserver observer) + /// + /// Accesses the current state as a string property + /// + class IntStateAccessor : + IStateAccessor { - _index = index; - _observer = observer; + readonly StateAccessorIndex _index; + readonly IStateObserver _observer; + readonly PropertyInfo _propertyInfo; + readonly IReadProperty _read; + readonly IWriteProperty _write; - _propertyInfo = currentStateExpression.GetPropertyInfo(); + public IntStateAccessor(Expression> currentStateExpression, StateAccessorIndex index, IStateObserver observer) + { + _index = index; + _observer = observer; - _read = ReadPropertyCache.GetProperty(_propertyInfo); - _write = WritePropertyCache.GetProperty(_propertyInfo); - } + _propertyInfo = currentStateExpression.GetPropertyInfo(); - Task> IStateAccessor.Get(BehaviorContext context) - { - var stateIndex = _read.Get(context.Saga); + _read = ReadPropertyCache.GetProperty(_propertyInfo); + _write = WritePropertyCache.GetProperty(_propertyInfo); + } - return Task.FromResult(_index[stateIndex]); - } + Task> IStateAccessor.Get(BehaviorContext context) + { + var stateIndex = _read.Get(context.Saga); - Task IStateAccessor.Set(BehaviorContext context, State state) - { - if (state == null) - throw new ArgumentNullException(nameof(state)); + return Task.FromResult(_index[stateIndex]); + } - var stateIndex = _index[state.Name]; + Task IStateAccessor.Set(BehaviorContext context, State state) + { + if (state == null) + throw new ArgumentNullException(nameof(state)); - var previousIndex = _read.Get(context.Saga); + var stateIndex = _index[state.Name]; - if (stateIndex == previousIndex) - return Task.CompletedTask; + var previousIndex = _read.Get(context.Saga); - _write.Set(context.Saga, stateIndex); + if (stateIndex == previousIndex) + return Task.CompletedTask; - State previousState = _index[previousIndex]; + _write.Set(context.Saga, stateIndex); - return _observer.StateChanged(context, state, previousState); - } + State previousState = _index[previousIndex]; - public Expression> GetStateExpression(params State[] states) - { - if (states == null || states.Length == 0) - throw new ArgumentOutOfRangeException(nameof(states), "One or more states must be specified"); + return _observer.StateChanged(context, state, previousState); + } - var parameterExpression = Expression.Parameter(typeof(TSaga), "instance"); + public Expression> GetStateExpression(params State[] states) + { + if (states == null || states.Length == 0) + throw new ArgumentOutOfRangeException(nameof(states), "One or more states must be specified"); - var statePropertyExpression = Expression.Property(parameterExpression, _propertyInfo.GetMethod); + var parameterExpression = Expression.Parameter(typeof(TInstance), "instance"); - var stateExpression = states.Select(state => Expression.Equal(statePropertyExpression, Expression.Constant(_index[(string)state.Name]))) - .Aggregate((left, right) => Expression.Or(left, right)); + var statePropertyExpression = Expression.Property(parameterExpression, _propertyInfo.GetMethod); - return Expression.Lambda>(stateExpression, parameterExpression); - } + var stateExpression = states.Select(state => Expression.Equal(statePropertyExpression, Expression.Constant(_index[state.Name]))) + .Aggregate((left, right) => Expression.Or(left, right)); - public void Probe(ProbeContext context) - { - context.Add("currentStateProperty", _propertyInfo.Name); + return Expression.Lambda>(stateExpression, parameterExpression); + } + + public void Probe(ProbeContext context) + { + context.Add("currentStateProperty", _propertyInfo.Name); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/RawStateAccessor.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/RawStateAccessor.cs index 618773ae1ab..bd3cfebab1c 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/RawStateAccessor.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/RawStateAccessor.cs @@ -1,4 +1,4 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Linq; @@ -8,72 +8,76 @@ namespace MassTransit.SagaStateMachine using Internals; - public class RawStateAccessor : - IStateAccessor - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly StateMachine _machine; - readonly IStateObserver _observer; - readonly PropertyInfo _propertyInfo; - readonly IReadProperty _read; - readonly IWriteProperty _write; - - public RawStateAccessor(StateMachine machine, Expression> currentStateExpression, IStateObserver observer) + class RawStateAccessor : + IStateAccessor { - _machine = machine; - _observer = observer; + readonly StateMachine _machine; + readonly IStateObserver _observer; + readonly PropertyInfo _propertyInfo; + readonly IReadProperty _read; + readonly IWriteProperty _write; - _propertyInfo = currentStateExpression.GetPropertyInfo(); + public RawStateAccessor(StateMachine machine, Expression> currentStateExpression, + IStateObserver observer) + { + _machine = machine; + _observer = observer; - _read = ReadPropertyCache.GetProperty(_propertyInfo); - _write = WritePropertyCache.GetProperty(_propertyInfo); - } + _propertyInfo = currentStateExpression.GetPropertyInfo(); - Task> IStateAccessor.Get(BehaviorContext context) - { - var state = _read.Get(context.Saga); - if (state == null) - return Task.FromResult>(null); + _read = ReadPropertyCache.GetProperty(_propertyInfo); + _write = WritePropertyCache.GetProperty(_propertyInfo); + } - return Task.FromResult(_machine.GetState(state.Name)); - } + Task> IStateAccessor.Get(BehaviorContext context) + { + var state = _read.Get(context.Saga); + if (state == null) + return Task.FromResult>(null); - Task IStateAccessor.Set(BehaviorContext context, State state) - { - if (state == null) - throw new ArgumentNullException(nameof(state)); + return Task.FromResult(_machine.GetState(state.Name)); + } - var previous = _read.Get(context.Saga); - if (state.Equals(previous)) - return Task.CompletedTask; + Task IStateAccessor.Set(BehaviorContext context, State state) + { + if (state == null) + throw new ArgumentNullException(nameof(state)); - _write.Set(context.Saga, state); + var previous = _read.Get(context.Saga); + if (state.Equals(previous)) + return Task.CompletedTask; - State previousState = null; - if (previous != null) - previousState = _machine.GetState(previous.Name); + _write.Set(context.Saga, state); - return _observer.StateChanged(context, state, previousState); - } + State previousState = null; + if (previous != null) + previousState = _machine.GetState(previous.Name); - public Expression> GetStateExpression(params State[] states) - { - if (states == null || states.Length == 0) - throw new ArgumentOutOfRangeException(nameof(states), "One or more states must be specified"); + return _observer.StateChanged(context, state, previousState); + } - var parameterExpression = Expression.Parameter(typeof(TSaga), "instance"); + public Expression> GetStateExpression(params State[] states) + { + if (states == null || states.Length == 0) + throw new ArgumentOutOfRangeException(nameof(states), "One or more states must be specified"); - var statePropertyExpression = Expression.Property(parameterExpression, _propertyInfo.GetMethod); + var parameterExpression = Expression.Parameter(typeof(TInstance), "instance"); - var stateExpression = states.Select(state => Expression.Equal(statePropertyExpression, - Expression.Constant(state, typeof(State)))).Aggregate((left, right) => Expression.Or(left, right)); + var statePropertyExpression = Expression.Property(parameterExpression, _propertyInfo.GetMethod); - return Expression.Lambda>(stateExpression, parameterExpression); - } + var stateExpression = states.Select(state => Expression.Equal(statePropertyExpression, + Expression.Constant(state, typeof(State)))).Aggregate((left, right) => Expression.Or(left, right)); - public void Probe(ProbeContext context) - { - context.Add("currentStateProperty", _propertyInfo.Name); + return Expression.Lambda>(stateExpression, parameterExpression); + } + + public void Probe(ProbeContext context) + { + context.Add("currentStateProperty", _propertyInfo.Name); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/StateAccessorIndex.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/StateAccessorIndex.cs index 31027d7acfd..4159f9cb8e9 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/StateAccessorIndex.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/StateAccessorIndex.cs @@ -1,56 +1,59 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Linq; - public class StateAccessorIndex - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly State[] _assignedStates; - readonly StateMachine _stateMachine; - readonly Lazy[]> _states; - - public StateAccessorIndex(StateMachine stateMachine, State initial, State final, State[] states) + class StateAccessorIndex { - _stateMachine = stateMachine; + readonly State[] _assignedStates; + readonly StateMachine _stateMachine; + readonly Lazy[]> _states; - _assignedStates = new[] { null, initial, final }.Concat(states.Cast>()).ToArray(); + public StateAccessorIndex(StateMachine stateMachine, State initial, State final, State[] states) + { + _stateMachine = stateMachine; - _states = new Lazy[]>(CreateStateArray); - } + _assignedStates = new[] { null, initial, final }.Concat(states.Cast>()).ToArray(); - public int this[string name] - { - get - { - if (string.IsNullOrWhiteSpace(name)) - throw new ArgumentNullException(nameof(name)); + _states = new Lazy[]>(CreateStateArray); + } - for (var i = 1; i < _states.Value.Length; i++) + public int this[string name] + { + get { - if (_states.Value[i].Name.Equals(name)) - return i; - } + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentNullException(nameof(name)); + + for (var i = 1; i < _states.Value.Length; i++) + { + if (_states.Value[i].Name.Equals(name)) + return i; + } - throw new ArgumentException("Unknown state specified: " + name); + throw new ArgumentException("Unknown state specified: " + name); + } } - } - public State this[int index] - { - get + public State this[int index] { - if (index < 0 || index >= _states.Value.Length) - throw new ArgumentOutOfRangeException(nameof(index)); + get + { + if (index < 0 || index >= _states.Value.Length) + throw new ArgumentOutOfRangeException(nameof(index)); - return _states.Value[index]; + return _states.Value[index]; + } } - } - State[] CreateStateArray() - { - return _assignedStates.Concat(_stateMachine.States.Cast>()).Distinct().ToArray(); + State[] CreateStateArray() + { + return _assignedStates.Concat(_stateMachine.States.Cast>()).Distinct().ToArray(); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/StringStateAccessor.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/StringStateAccessor.cs index 7758776db05..f7e6e60f87c 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/StringStateAccessor.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Accessors/StringStateAccessor.cs @@ -1,4 +1,4 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Linq; @@ -8,76 +8,79 @@ namespace MassTransit.SagaStateMachine using Internals; - /// - /// Accesses the current state as a string property - /// - /// The instance type - public class StringStateAccessor : - IStateAccessor - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly StateMachine _machine; - readonly IStateObserver _observer; - readonly PropertyInfo _propertyInfo; - readonly IReadProperty _read; - readonly IWriteProperty _write; - - public StringStateAccessor(StateMachine machine, Expression> currentStateExpression, IStateObserver observer) + /// + /// Accesses the current state as a string property + /// + class StringStateAccessor : + IStateAccessor { - _machine = machine; - _observer = observer; + readonly StateMachine _machine; + readonly IStateObserver _observer; + readonly PropertyInfo _propertyInfo; + readonly IReadProperty _read; + readonly IWriteProperty _write; - _propertyInfo = currentStateExpression.GetPropertyInfo(); + public StringStateAccessor(StateMachine machine, Expression> currentStateExpression, + IStateObserver observer) + { + _machine = machine; + _observer = observer; - _read = ReadPropertyCache.GetProperty(_propertyInfo); - _write = WritePropertyCache.GetProperty(_propertyInfo); - } + _propertyInfo = currentStateExpression.GetPropertyInfo(); - Task> IStateAccessor.Get(BehaviorContext context) - { - var stateName = _read.Get(context.Saga); - if (string.IsNullOrWhiteSpace(stateName)) - return Task.FromResult>(null); + _read = ReadPropertyCache.GetProperty(_propertyInfo); + _write = WritePropertyCache.GetProperty(_propertyInfo); + } - return Task.FromResult(_machine.GetState(stateName)); - } + Task> IStateAccessor.Get(BehaviorContext context) + { + var stateName = _read.Get(context.Saga); + if (string.IsNullOrWhiteSpace(stateName)) + return Task.FromResult>(null); - Task IStateAccessor.Set(BehaviorContext context, State state) - { - if (state == null) - throw new ArgumentNullException(nameof(state)); + return Task.FromResult(_machine.GetState(stateName)); + } - var previous = _read.Get(context.Saga); - if (state.Name.Equals(previous)) - return Task.CompletedTask; + Task IStateAccessor.Set(BehaviorContext context, State state) + { + if (state == null) + throw new ArgumentNullException(nameof(state)); - _write.Set(context.Saga, state.Name); + var previous = _read.Get(context.Saga); + if (state.Name.Equals(previous)) + return Task.CompletedTask; - State previousState = null; - if (previous != null) - previousState = _machine.GetState(previous); + _write.Set(context.Saga, state.Name); - return _observer.StateChanged(context, state, previousState); - } + State previousState = null; + if (!string.IsNullOrWhiteSpace(previous)) + previousState = _machine.GetState(previous); - public Expression> GetStateExpression(params State[] states) - { - if (states == null || states.Length == 0) - throw new ArgumentOutOfRangeException(nameof(states), "One or more states must be specified"); + return _observer.StateChanged(context, state, previousState); + } - var parameterExpression = Expression.Parameter(typeof(TSaga), "instance"); + public Expression> GetStateExpression(params State[] states) + { + if (states == null || states.Length == 0) + throw new ArgumentOutOfRangeException(nameof(states), "One or more states must be specified"); - var statePropertyExpression = Expression.Property(parameterExpression, _propertyInfo.GetMethod); + var parameterExpression = Expression.Parameter(typeof(TInstance), "instance"); - var stateExpression = states.Select(state => Expression.Equal(statePropertyExpression, Expression.Constant(state.Name))) - .Aggregate((left, right) => Expression.Or(left, right)); + var statePropertyExpression = Expression.Property(parameterExpression, _propertyInfo.GetMethod); - return Expression.Lambda>(stateExpression, parameterExpression); - } + var stateExpression = states.Select(state => Expression.Equal(statePropertyExpression, Expression.Constant(state.Name))) + .Aggregate((left, right) => Expression.Or(left, right)); - public void Probe(ProbeContext context) - { - context.Add("currentStateProperty", _propertyInfo.Name); + return Expression.Lambda>(stateExpression, parameterExpression); + } + + public void Probe(ProbeContext context) + { + context.Add("currentStateProperty", _propertyInfo.Name); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ActionActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ActionActivity.cs index 4213f5f711a..d2445d9414d 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ActionActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ActionActivity.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class ActionActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly Action> _action; @@ -58,7 +58,7 @@ public Task Faulted(BehaviorExceptionContext : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly Action> _action; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncActivity.cs index 7fa9a8352b2..30c0ee5ecba 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncActivity.cs @@ -6,7 +6,7 @@ public class AsyncActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly Func, Task> _asyncAction; @@ -57,7 +57,7 @@ public Task Faulted(BehaviorExceptionContext : IStateMachineActivity - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TData : class { readonly Func, Task> _asyncAction; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncFactoryActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncFactoryActivity.cs index 21061ea365f..82b416cfb43 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncFactoryActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncFactoryActivity.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class AsyncFactoryActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly Func, Task>> _activityFactory; @@ -58,7 +58,7 @@ async Task IStateMachineActivity.Faulted(BehaviorException public class AsyncFactoryActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly Func, Task>> _activityFactory; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncFaultedActionActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncFaultedActionActivity.cs index b5622d1db7e..0d643d61a33 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncFaultedActionActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/AsyncFaultedActionActivity.cs @@ -7,7 +7,7 @@ public class AsyncFaultedActionActivity : IStateMachineActivity where TException : Exception - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly Func, Task> _asyncAction; @@ -62,7 +62,7 @@ public async Task Faulted(BehaviorExceptionContext co public class AsyncFaultedActionActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception where TMessage : class { diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/CancelRequestTimeoutActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/CancelRequestTimeoutActivity.cs index 0e26c61243d..d70ecbe09f8 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/CancelRequestTimeoutActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/CancelRequestTimeoutActivity.cs @@ -12,10 +12,12 @@ public class CancelRequestTimeoutActivity where TMessage : class { readonly Request _request; + readonly bool _completed; - public CancelRequestTimeoutActivity(Request request) + public CancelRequestTimeoutActivity(Request request, bool completed) { _request = request; + _completed = completed; } public void Accept(StateMachineVisitor visitor) @@ -39,6 +41,9 @@ public async Task Execute(BehaviorContext context, IBehavior public class CatchFaultActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { readonly IBehavior _behavior; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ClearRequestActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ClearRequestActivity.cs deleted file mode 100644 index e684172b537..00000000000 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ClearRequestActivity.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace MassTransit.SagaStateMachine -{ - using System; - using System.Threading.Tasks; - - - public class ClearRequestActivity : - IStateMachineActivity - where TSaga : class, SagaStateMachineInstance - where TRequest : class - where TResponse : class - where TMessage : class - { - readonly Request _request; - - public ClearRequestActivity(Request request) - { - _request = request; - } - - public void Accept(StateMachineVisitor visitor) - { - visitor.Visit(this); - } - - public Task Execute(BehaviorContext context, IBehavior next) - { - _request.SetRequestId(context.Saga, default); - - return next.Execute(context); - } - - public Task Faulted(BehaviorExceptionContext context, IBehavior next) - where TException : Exception - { - return next.Faulted(context); - } - - public void Probe(ProbeContext context) - { - context.CreateScope("clearRequest"); - } - } -} diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/CompositeEventActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/CompositeEventActivity.cs index def9dc35f0d..f94e25fecd8 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/CompositeEventActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/CompositeEventActivity.cs @@ -6,17 +6,20 @@ namespace MassTransit.SagaStateMachine public class CompositeEventActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly ICompositeEventStatusAccessor _accessor; readonly CompositeEventStatus _complete; readonly int _flag; + readonly CompositeEventOptions _options; - public CompositeEventActivity(ICompositeEventStatusAccessor accessor, int flag, CompositeEventStatus complete, Event @event) + public CompositeEventActivity(ICompositeEventStatusAccessor accessor, int flag, CompositeEventStatus complete, Event @event, + CompositeEventOptions options) { _accessor = accessor; _flag = flag; _complete = complete; + _options = options; Event = @event; } @@ -66,6 +69,10 @@ public Task Faulted(BehaviorExceptionContext context) { var value = _accessor.Get(context.Saga); + + if (value.IsSet(_flag) && _options.HasFlag(CompositeEventOptions.RaiseOnce)) + return Task.CompletedTask; + value.Set(_flag); _accessor.Set(context.Saga, value); diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ConditionActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ConditionActivity.cs index 280687710d9..69ccf7d5b03 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ConditionActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ConditionActivity.cs @@ -6,7 +6,7 @@ public class ConditionActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly StateMachineAsyncCondition _condition; readonly IBehavior _elseBehavior; @@ -71,7 +71,7 @@ public Task Faulted(BehaviorExceptionContext : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly StateMachineAsyncCondition _condition; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ConditionExceptionActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ConditionExceptionActivity.cs index 538d41fb7d8..36950837cfe 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ConditionExceptionActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ConditionExceptionActivity.cs @@ -6,7 +6,7 @@ public class ConditionExceptionActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TConditionException : Exception { readonly StateMachineAsyncExceptionCondition _condition; @@ -81,7 +81,7 @@ public async Task Faulted(BehaviorExceptionContext : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TConditionException : Exception { diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ContainerFactoryActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ContainerFactoryActivity.cs index 84b79708d3c..ca6a51ec9ea 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ContainerFactoryActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ContainerFactoryActivity.cs @@ -7,7 +7,7 @@ public class ContainerFactoryActivity : IStateMachineActivity where TActivity : class, IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { public void Accept(StateMachineVisitor visitor) { @@ -54,7 +54,7 @@ public void Probe(ProbeContext context) public class ContainerFactoryActivity : IStateMachineActivity where TActivity : class, IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { void IProbeSite.Probe(ProbeContext context) diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/DataConverterActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/DataConverterActivity.cs index 18fd1e1a1b6..25ead8cd539 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/DataConverterActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/DataConverterActivity.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class DataConverterActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly IStateMachineActivity _activity; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ExecuteOnFaultedActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ExecuteOnFaultedActivity.cs index da49dbb16ce..7f3117e0b57 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ExecuteOnFaultedActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/ExecuteOnFaultedActivity.cs @@ -6,7 +6,7 @@ public class ExecuteOnFaultedActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly IStateMachineActivity _activity; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FactoryActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FactoryActivity.cs index 269aad61e69..d046e47c11f 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FactoryActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FactoryActivity.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class FactoryActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly Func, IStateMachineActivity> _activityFactory; @@ -58,7 +58,7 @@ Task IStateMachineActivity.Faulted(BehaviorExceptionContex public class FactoryActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly Func, IStateMachineActivity> _activityFactory; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FaultedActionActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FaultedActionActivity.cs index 664e463359a..77e4702a356 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FaultedActionActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FaultedActionActivity.cs @@ -7,7 +7,7 @@ public class FaultedActionActivity : IStateMachineActivity where TException : Exception - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly Action> _action; @@ -60,7 +60,7 @@ public Task Faulted(BehaviorExceptionContext context, public class FaultedActionActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception where TMessage : class { diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FaultedContainerFactoryActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FaultedContainerFactoryActivity.cs index 8d5a2b19255..32d03f68479 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FaultedContainerFactoryActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/FaultedContainerFactoryActivity.cs @@ -7,7 +7,7 @@ namespace MassTransit.SagaStateMachine public class FaultedContainerFactoryActivity : IStateMachineActivity where TActivity : class, IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { public void Accept(StateMachineVisitor visitor) @@ -65,7 +65,7 @@ public void Probe(ProbeContext context) public class FaultedContainerFactoryActivity : IStateMachineActivity where TActivity : class, IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception where TMessage : class { diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/RetryActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/RetryActivity.cs index e54173a8bbe..764e3b1adeb 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/RetryActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/RetryActivity.cs @@ -7,7 +7,7 @@ namespace MassTransit.SagaStateMachine public class RetryActivity : IStateMachineActivity - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { readonly IBehavior _retryBehavior; readonly IRetryPolicy _retryPolicy; @@ -62,7 +62,7 @@ public Task Faulted(BehaviorExceptionContext : IStateMachineActivity - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TMessage : class { readonly IBehavior _retryBehavior; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/SlimActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/SlimActivity.cs index 0c1bf2b2c10..4504ee8d618 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/SlimActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/SlimActivity.cs @@ -10,7 +10,7 @@ namespace MassTransit.SagaStateMachine /// public class SlimActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly IStateMachineActivity _activity; @@ -35,7 +35,8 @@ Task IStateMachineActivity.Execute(BehaviorContext(behavior, context)); } - Task IStateMachineActivity.Faulted(BehaviorExceptionContext context, IBehavior next) + Task IStateMachineActivity.Faulted(BehaviorExceptionContext context, + IBehavior next) { return _activity.Faulted(context, next); } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/TransitionActivity.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/TransitionActivity.cs index faefae692c8..9c3bf4ba0d4 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/TransitionActivity.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Activities/TransitionActivity.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class TransitionActivity : IStateMachineActivity - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly IStateAccessor _currentStateAccessor; readonly State _toState; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/ActivityBehaviorBuilder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/ActivityBehaviorBuilder.cs index c8dc72f9419..43f0b607069 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/ActivityBehaviorBuilder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/ActivityBehaviorBuilder.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class ActivityBehaviorBuilder : IBehaviorBuilder - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly List> _activities; readonly Lazy> _behavior; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/AllStateEventFilter.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/AllStateEventFilter.cs index 60b2cc3c2df..934e7df9b4a 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/AllStateEventFilter.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/AllStateEventFilter.cs @@ -2,7 +2,7 @@ namespace MassTransit.SagaStateMachine { public class AllStateEventFilter : IStateEventFilter - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { public bool Filter(BehaviorContext context) { diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behavior.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behavior.cs index eb730a399fb..87c43b17511 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behavior.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behavior.cs @@ -12,26 +12,26 @@ public static class Behavior /// The context type /// public static IBehavior Empty() - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return Cached.EmptyBehavior; } public static IBehavior Empty() - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { return Cached.EmptyBehavior; } public static IBehavior Faulted() - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return Cached.FaultedBehavior; } public static IBehavior Faulted() - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { return Cached.FaultedBehavior; @@ -39,7 +39,7 @@ public static IBehavior Faulted() static class Cached - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { internal static readonly IBehavior EmptyBehavior = new EmptyBehavior(); internal static readonly IBehavior FaultedBehavior = new FaultedBehavior(); @@ -47,7 +47,7 @@ static class Cached static class Cached - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { internal static readonly IBehavior EmptyBehavior = new EmptyBehavior(); @@ -55,6 +55,3 @@ static class Cached } } } - - - diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/BehaviorContextProxy.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/BehaviorContextProxy.cs index 16ceba8c948..af2c1c7926a 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/BehaviorContextProxy.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/BehaviorContextProxy.cs @@ -1,4 +1,4 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Threading.Tasks; @@ -6,135 +6,137 @@ namespace MassTransit.SagaStateMachine using Initializers; - public class BehaviorContextProxy : - ConsumeContextProxy, - BehaviorContext - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly SagaConsumeContext _context; - readonly Event _event; - - public BehaviorContextProxy(StateMachine machine, SagaConsumeContext context, Event @event) - : base(context) - { - StateMachine = machine; - _context = context; - _event = @event; - } - - public StateMachine StateMachine { get; } - - public override Guid? CorrelationId => Saga.CorrelationId; - - public TSaga Saga => _context.Saga; - - public Task SetCompleted() - { - return _context.SetCompleted(); - } - - public bool IsCompleted => _context.IsCompleted; - - public Task Raise(Event @event) - { - return StateMachine.RaiseEvent(CreateProxy(@event)); - } - - public Task Raise(Event @event, T data) - where T : class - { - return StateMachine.RaiseEvent(CreateProxy(@event, data)); - } - - Task> BehaviorContext.Init(object values) - { - return MessageInitializerCache.InitializeMessage(this, values); - } - - Event BehaviorContext.Event => _event; - - public TSaga Instance => _context.Saga; - - public BehaviorContext CreateProxy(Event @event) + public class BehaviorContextProxy : + ConsumeContextProxy, + BehaviorContext { - return new BehaviorContextProxy(StateMachine, _context, @event); - } + readonly SagaConsumeContext _context; + readonly Event _event; - public BehaviorContext CreateProxy(Event @event, T data) - where T : class - { - return new BehaviorContextProxy(StateMachine, _context, new MessageConsumeContext(_context, data), @event); - } - } + public BehaviorContextProxy(StateMachine machine, SagaConsumeContext context, Event @event) + : base(context) + { + StateMachine = machine; + _context = context; + _event = @event; + } + public StateMachine StateMachine { get; } - public class BehaviorContextProxy : - ConsumeContextProxy, - BehaviorContext - where TSaga : class, ISaga - where TMessage : class - { - readonly SagaConsumeContext _context; - readonly Event _event; + public override Guid? CorrelationId => Saga.CorrelationId; - public BehaviorContextProxy(StateMachine machine, SagaConsumeContext context, ConsumeContext consumeContext, - Event @event) - : base(consumeContext) - { - StateMachine = machine; - _context = context; - _event = @event; - } + public TInstance Saga => _context.Saga; - public StateMachine StateMachine { get; } + public Task SetCompleted() + { + return _context.SetCompleted(); + } - public override Guid? CorrelationId => Saga.CorrelationId; + public bool IsCompleted => _context.IsCompleted; - public TSaga Saga => _context.Saga; + public Task Raise(Event @event) + { + return StateMachine.RaiseEvent(CreateProxy(@event)); + } - public Task SetCompleted() - { - return _context.SetCompleted(); - } + public Task Raise(Event @event, T data) + where T : class + { + return StateMachine.RaiseEvent(CreateProxy(@event, data)); + } - public bool IsCompleted => _context.IsCompleted; + Task> BehaviorContext.Init(object values) + { + return MessageInitializerCache.InitializeMessage(this, values); + } - public Task Raise(Event @event) - { - return StateMachine.RaiseEvent(CreateProxy(@event)); - } + Event BehaviorContext.Event => _event; - public Task Raise(Event @event, T data) - where T : class - { - return StateMachine.RaiseEvent(CreateProxy(@event, data)); - } + public TInstance Instance => _context.Saga; - Task> BehaviorContext.Init(object values) - { - return MessageInitializerCache.InitializeMessage(this, values); - } + public BehaviorContext CreateProxy(Event @event) + { + return new BehaviorContextProxy(StateMachine, _context, @event); + } - Task> BehaviorContext.Init(object values) - { - return MessageInitializerCache.InitializeMessage(this, values); + public BehaviorContext CreateProxy(Event @event, T data) + where T : class + { + return new BehaviorContextProxy(StateMachine, _context, new MessageConsumeContext(_context, data), @event); + } } - public TMessage Data => Message; - Event BehaviorContext.Event => _event; - Event BehaviorContext.Event => _event; - - public TSaga Instance => _context.Saga; - - public BehaviorContext CreateProxy(Event @event) - { - return new BehaviorContextProxy(StateMachine, _context, @event); - } - public BehaviorContext CreateProxy(Event @event, T data) - where T : class + public class BehaviorContextProxy : + ConsumeContextProxy, + BehaviorContext + where TMessage : class { - return new BehaviorContextProxy(StateMachine, _context, new MessageConsumeContext(_context, data), @event); + readonly SagaConsumeContext _context; + readonly Event _event; + + public BehaviorContextProxy(StateMachine machine, SagaConsumeContext context, ConsumeContext consumeContext, + Event @event) + : base(consumeContext) + { + StateMachine = machine; + _context = context; + _event = @event; + } + + public StateMachine StateMachine { get; } + + public override Guid? CorrelationId => Saga.CorrelationId; + + public TInstance Saga => _context.Saga; + + public Task SetCompleted() + { + return _context.SetCompleted(); + } + + public bool IsCompleted => _context.IsCompleted; + + public Task Raise(Event @event) + { + return StateMachine.RaiseEvent(CreateProxy(@event)); + } + + public Task Raise(Event @event, T data) + where T : class + { + return StateMachine.RaiseEvent(CreateProxy(@event, data)); + } + + Task> BehaviorContext.Init(object values) + { + return MessageInitializerCache.InitializeMessage(this, values); + } + + Task> BehaviorContext.Init(object values) + { + return MessageInitializerCache.InitializeMessage(this, values); + } + + public TMessage Data => Message; + Event BehaviorContext.Event => _event; + Event BehaviorContext.Event => _event; + + public TInstance Instance => _context.Saga; + + public BehaviorContext CreateProxy(Event @event) + { + return new BehaviorContextProxy(StateMachine, _context, @event); + } + + public BehaviorContext CreateProxy(Event @event, T data) + where T : class + { + return new BehaviorContextProxy(StateMachine, _context, new MessageConsumeContext(_context, data), @event); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/BehaviorExceptionContextProxy.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/BehaviorExceptionContextProxy.cs index 3b39e3d950e..9dc1bfdbf80 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/BehaviorExceptionContextProxy.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/BehaviorExceptionContextProxy.cs @@ -1,55 +1,57 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; - public class BehaviorExceptionContextProxy : - BehaviorContextProxy, - BehaviorExceptionContext - where TSaga : class, ISaga - where TException : Exception + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly BehaviorContext _context; - - public BehaviorExceptionContextProxy(BehaviorContext context, TException exception) - : base(context.StateMachine, context, context.Event) - { - _context = context; - Exception = exception; - } - - public TException Exception { get; } - - public new BehaviorExceptionContext CreateProxy(Event @event, T data) - where T : class - { - return new BehaviorExceptionContextProxy(_context.CreateProxy(@event, data), Exception); - } - } - - - public class BehaviorExceptionContextProxy : - BehaviorContextProxy, - BehaviorExceptionContext - where TSaga : class, ISaga - where TData : class - where TException : Exception - { - readonly BehaviorContext _context; - - public BehaviorExceptionContextProxy(BehaviorContext context, TException exception) - : base(context.StateMachine, context, context, context.Event) + public class BehaviorExceptionContextProxy : + BehaviorContextProxy, + BehaviorExceptionContext + where TException : Exception { - _context = context; - Exception = exception; + readonly BehaviorContext _context; + + public BehaviorExceptionContextProxy(BehaviorContext context, TException exception) + : base(context.StateMachine, context, context.Event) + { + _context = context; + Exception = exception; + } + + public TException Exception { get; } + + public new BehaviorExceptionContext CreateProxy(Event @event, T data) + where T : class + { + return new BehaviorExceptionContextProxy(_context.CreateProxy(@event, data), Exception); + } } - public TException Exception { get; } - public new BehaviorExceptionContext CreateProxy(Event @event, T data) - where T : class + public class BehaviorExceptionContextProxy : + BehaviorContextProxy, + BehaviorExceptionContext + where TData : class + where TException : Exception { - return new BehaviorExceptionContextProxy(_context.CreateProxy(@event, data), Exception); + readonly BehaviorContext _context; + + public BehaviorExceptionContextProxy(BehaviorContext context, TException exception) + : base(context.StateMachine, context, context, context.Event) + { + _context = context; + Exception = exception; + } + + public TException Exception { get; } + + public new BehaviorExceptionContext CreateProxy(Event @event, T data) + where T : class + { + return new BehaviorExceptionContextProxy(_context.CreateProxy(@event, data), Exception); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ActivityBehavior.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ActivityBehavior.cs index 5ca8c6faf2f..30e8b9bf123 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ActivityBehavior.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ActivityBehavior.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class ActivityBehavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly IStateMachineActivity _activity; readonly IBehavior _next; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/CatchBehaviorBuilder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/CatchBehaviorBuilder.cs index b2e546bda3e..45c007baafb 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/CatchBehaviorBuilder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/CatchBehaviorBuilder.cs @@ -2,11 +2,12 @@ { using System; using System.Collections.Generic; + using System.Threading.Tasks; public class CatchBehaviorBuilder : IBehaviorBuilder - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly List> _activities; readonly Lazy> _behavior; @@ -32,12 +33,58 @@ IBehavior CreateBehavior() if (_activities.Count == 0) return SagaStateMachine.Behavior.Empty(); - IBehavior current = new LastCatchBehavior(_activities[_activities.Count - 1]); + IBehavior current = new LastCatchBehavior(_activities[_activities.Count - 1]); for (var i = _activities.Count - 2; i >= 0; i--) current = new ActivityBehavior(_activities[i], current); return current; } + + + class LastCatchBehavior : + IBehavior + { + readonly IStateMachineActivity _activity; + + public LastCatchBehavior(IStateMachineActivity activity) + { + _activity = activity; + } + + public void Accept(StateMachineVisitor visitor) + { + _activity.Accept(visitor); + } + + public void Probe(ProbeContext context) + { + _activity.Probe(context); + } + + public Task Execute(BehaviorContext context) + { + return _activity.Execute(context, SagaStateMachine.Behavior.Empty()); + } + + public Task Execute(BehaviorContext context) + where T : class + { + return _activity.Execute(context, SagaStateMachine.Behavior.Empty()); + } + + public Task Faulted(BehaviorExceptionContext context) + where T : class + where TException : Exception + { + return _activity.Faulted(context, SagaStateMachine.Behavior.Empty()); + } + + public Task Faulted(BehaviorExceptionContext context) + where TException : Exception + { + return _activity.Faulted(context, SagaStateMachine.Behavior.Empty()); + } + } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/DataBehavior.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/DataBehavior.cs index 54f93b46bbc..dd32bd32a56 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/DataBehavior.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/DataBehavior.cs @@ -10,7 +10,7 @@ namespace MassTransit.SagaStateMachine /// The event data type public class DataBehavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly IBehavior _behavior; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/EmptyBehavior.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/EmptyBehavior.cs index a02e596afed..d5979558c82 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/EmptyBehavior.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/EmptyBehavior.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class EmptyBehavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { public void Accept(StateMachineVisitor visitor) { @@ -45,7 +45,7 @@ public Task Faulted(BehaviorExceptionContext cont public class EmptyBehavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { public void Accept(StateMachineVisitor visitor) diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ExceptionTypeCache.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ExceptionTypeCache.cs index e4654a0eece..019d6d35378 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ExceptionTypeCache.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ExceptionTypeCache.cs @@ -13,7 +13,7 @@ static CachedConfigurator GetOrAdd(Type type) } public static Task Faulted(IBehavior behavior, BehaviorContext context, Exception exception) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { if (exception == null) throw new ArgumentNullException(nameof(exception)); @@ -22,7 +22,7 @@ public static Task Faulted(IBehavior behavior, BehaviorContext(IBehavior behavior, BehaviorContext context, Exception exception) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { if (exception == null) @@ -41,10 +41,10 @@ static class Cached interface CachedConfigurator { Task Faulted(IBehavior behavior, BehaviorContext context, Exception exception) - where TSaga : class, ISaga; + where TSaga : class, SagaStateMachineInstance; Task Faulted(IBehavior behavior, BehaviorContext context, Exception exception) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class; } @@ -57,7 +57,7 @@ Task CachedConfigurator.Faulted(IBehavior behavior, Behavi { if (exception is TException typedException) { - var exceptionContext = new BehaviorExceptionContextProxy(context, typedException); + var exceptionContext = new MassTransitStateMachine.BehaviorExceptionContextProxy(context, typedException); return behavior.Faulted(exceptionContext); } @@ -70,7 +70,7 @@ Task CachedConfigurator.Faulted(IBehavior be { if (exception is TException typedException) { - var exceptionContext = new BehaviorExceptionContextProxy(context, typedException); + var exceptionContext = new MassTransitStateMachine.BehaviorExceptionContextProxy(context, typedException); return behavior.Faulted(exceptionContext); } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ExecuteOnFaultedBehavior.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ExecuteOnFaultedBehavior.cs index 385a700be5f..7263f3d0f50 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ExecuteOnFaultedBehavior.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/ExecuteOnFaultedBehavior.cs @@ -7,7 +7,7 @@ namespace MassTransit.SagaStateMachine public class ExecuteOnFaultedBehavior : IBehavior where TException : Exception - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly BehaviorExceptionContext _context; readonly IBehavior _next; @@ -53,7 +53,7 @@ Task IBehavior.Faulted(BehaviorExceptionContext context) public class ExecuteOnFaultedBehavior : IBehavior where TException : Exception - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly BehaviorExceptionContext _context; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/FaultedBehavior.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/FaultedBehavior.cs index fb9f0e9f470..4e6c5d2ca02 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/FaultedBehavior.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/FaultedBehavior.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class FaultedBehavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { public void Accept(StateMachineVisitor visitor) { @@ -46,7 +46,7 @@ public Task Faulted(BehaviorExceptionContext cont public class FaultedBehavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { public void Accept(StateMachineVisitor visitor) diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/LastBehavior.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/LastBehavior.cs index 20fc161e33a..4279e698332 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/LastBehavior.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/LastBehavior.cs @@ -11,7 +11,7 @@ namespace MassTransit.SagaStateMachine /// public class LastBehavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly IStateMachineActivity _activity; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/LastCatchBehavior.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/LastCatchBehavior.cs index f6c5bb9a8a8..c495df4522a 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/LastCatchBehavior.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/LastCatchBehavior.cs @@ -11,7 +11,7 @@ namespace MassTransit.SagaStateMachine /// public class LastCatchBehavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly IStateMachineActivity _activity; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/WidenBehavior.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/WidenBehavior.cs index 60112c38b13..1d862ea9032 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/WidenBehavior.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Behaviors/WidenBehavior.cs @@ -5,7 +5,7 @@ namespace MassTransit.SagaStateMachine public class WidenBehavior : IBehavior - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly Event _event; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/CatchActivityBinder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/CatchActivityBinder.cs index 325302b8062..740a7c1a337 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/CatchActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/CatchActivityBinder.cs @@ -10,7 +10,7 @@ namespace MassTransit.SagaStateMachine /// public class CatchActivityBinder : IActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TException : Exception { readonly EventActivities _activities; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/CatchExceptionActivityBinder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/CatchExceptionActivityBinder.cs index c2011705979..ad9e4e7167d 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/CatchExceptionActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/CatchExceptionActivityBinder.cs @@ -6,7 +6,7 @@ namespace MassTransit.SagaStateMachine public class CatchExceptionActivityBinder : ExceptionActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TException : Exception { readonly IActivityBinder[] _activities; @@ -107,7 +107,7 @@ ExceptionActivityBinder GetBinder( public class CatchExceptionActivityBinder : ExceptionActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TException : Exception where TData : class { diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/ConditionalActivityBinder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/ConditionalActivityBinder.cs index 38052a46c7c..c32e9afb60d 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/ConditionalActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/ConditionalActivityBinder.cs @@ -5,12 +5,11 @@ namespace MassTransit.SagaStateMachine public class ConditionalActivityBinder : IActivityBinder - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly StateMachineAsyncCondition _condition; readonly EventActivities _elseActivities; readonly EventActivities _thenActivities; - public Event Event { get; } public ConditionalActivityBinder(Event @event, StateMachineCondition condition, EventActivities thenActivities, EventActivities elseActivities) @@ -27,6 +26,8 @@ public ConditionalActivityBinder(Event @event, StateMachineAsyncCondition Event = @event; } + public Event Event { get; } + public bool IsStateTransitionEvent(State state) { return Equals(Event, state.Enter) || Equals(Event, state.BeforeEnter) @@ -67,13 +68,12 @@ static IBehavior GetBehavior(EventActivities activities) public class ConditionalActivityBinder : IActivityBinder - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly StateMachineAsyncCondition _condition; readonly EventActivities _elseActivities; readonly EventActivities _thenActivities; - public Event Event { get; } public ConditionalActivityBinder(Event @event, StateMachineCondition condition, EventActivities thenActivities, EventActivities elseActivities) @@ -90,6 +90,8 @@ public ConditionalActivityBinder(Event @event, StateMachineAsyncCondition : IActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TException : Exception { readonly StateMachineAsyncExceptionCondition _condition; readonly EventActivities _elseActivities; readonly EventActivities _thenActivities; - public Event Event { get; } public ConditionalExceptionActivityBinder(Event @event, StateMachineExceptionCondition condition, EventActivities thenActivities, EventActivities elseActivities) @@ -29,16 +28,18 @@ public ConditionalExceptionActivityBinder(Event @event, StateMachineAsyncExcepti Event = @event; } + public Event Event { get; } + public bool IsStateTransitionEvent(State state) { return Equals(Event, state.Enter) || Equals(Event, state.BeforeEnter) - || Equals(Event, state.AfterLeave) || Equals(Event, state.Leave); + || Equals(Event, state.AfterLeave) || Equals(Event, state.Leave); } public void Bind(State state) { - var thenBehavior = GetBehavior(_thenActivities); - var elseBehavior = GetBehavior(_elseActivities); + IBehavior thenBehavior = GetBehavior(_thenActivities); + IBehavior elseBehavior = GetBehavior(_elseActivities); var conditionActivity = new ConditionExceptionActivity(_condition, thenBehavior, elseBehavior); @@ -47,8 +48,8 @@ public void Bind(State state) public void Bind(IBehaviorBuilder builder) { - var thenBehavior = GetBehavior(_thenActivities); - var elseBehavior = GetBehavior(_elseActivities); + IBehavior thenBehavior = GetBehavior(_thenActivities); + IBehavior elseBehavior = GetBehavior(_elseActivities); var conditionActivity = new ConditionExceptionActivity(_condition, thenBehavior, elseBehavior); @@ -59,7 +60,7 @@ static IBehavior GetBehavior(EventActivities activities) { var builder = new CatchBehaviorBuilder(); - foreach (var activity in activities.GetStateActivityBinders()) + foreach (IActivityBinder activity in activities.GetStateActivityBinders()) activity.Bind(builder); return builder.Behavior; @@ -69,14 +70,13 @@ static IBehavior GetBehavior(EventActivities activities) public class ConditionalExceptionActivityBinder : IActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TException : Exception where TData : class { readonly StateMachineAsyncExceptionCondition _condition; readonly EventActivities _elseActivities; readonly EventActivities _thenActivities; - public Event Event { get; } public ConditionalExceptionActivityBinder(Event @event, StateMachineExceptionCondition condition, EventActivities thenActivities, EventActivities elseActivities) @@ -93,16 +93,18 @@ public ConditionalExceptionActivityBinder(Event @event, StateMachineAsyncExcepti Event = @event; } + public Event Event { get; } + public bool IsStateTransitionEvent(State state) { return Equals(Event, state.Enter) || Equals(Event, state.BeforeEnter) - || Equals(Event, state.AfterLeave) || Equals(Event, state.Leave); + || Equals(Event, state.AfterLeave) || Equals(Event, state.Leave); } public void Bind(State state) { - var thenBehavior = GetBehavior(_thenActivities); - var elseBehavior = GetBehavior(_elseActivities); + IBehavior thenBehavior = GetBehavior(_thenActivities); + IBehavior elseBehavior = GetBehavior(_elseActivities); var conditionActivity = new ConditionExceptionActivity(_condition, thenBehavior, elseBehavior); @@ -111,8 +113,8 @@ public void Bind(State state) public void Bind(IBehaviorBuilder builder) { - var thenBehavior = GetBehavior(_thenActivities); - var elseBehavior = GetBehavior(_elseActivities); + IBehavior thenBehavior = GetBehavior(_thenActivities); + IBehavior elseBehavior = GetBehavior(_elseActivities); var conditionActivity = new ConditionExceptionActivity(_condition, thenBehavior, elseBehavior); @@ -123,7 +125,7 @@ static IBehavior GetBehavior(EventActivities activities) { var builder = new CatchBehaviorBuilder(); - foreach (var activity in activities.GetStateActivityBinders()) + foreach (IActivityBinder activity in activities.GetStateActivityBinders()) activity.Bind(builder); return builder.Behavior; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/DataEventActivityBinder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/DataEventActivityBinder.cs index e546551bf2c..f537c220b00 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/DataEventActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/DataEventActivityBinder.cs @@ -8,7 +8,7 @@ namespace MassTransit.SagaStateMachine public class DataEventActivityBinder : EventActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TData : class { readonly IActivityBinder[] _activities; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/ExecuteActivityBinder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/ExecuteActivityBinder.cs index 6fcfe70738a..28346115c0c 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/ExecuteActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/ExecuteActivityBinder.cs @@ -6,10 +6,9 @@ namespace MassTransit.SagaStateMachine /// public class ExecuteActivityBinder : IActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { readonly IStateMachineActivity _activity; - public Event Event { get; } public ExecuteActivityBinder(Event @event, IStateMachineActivity activity) { @@ -17,6 +16,8 @@ public ExecuteActivityBinder(Event @event, IStateMachineActivity acti _activity = activity; } + public Event Event { get; } + public bool IsStateTransitionEvent(State state) { return Equals(Event, state.Enter) || Equals(Event, state.BeforeEnter) diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/IActivityBinder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/IActivityBinder.cs index 8eeb383af69..ed9066fd3ca 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/IActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/IActivityBinder.cs @@ -1,7 +1,7 @@ namespace MassTransit.SagaStateMachine { public interface IActivityBinder - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { Event Event { get; } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/IgnoreEventActivityBinder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/IgnoreEventActivityBinder.cs index dd2a3237d27..10e6d0b88aa 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/IgnoreEventActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/IgnoreEventActivityBinder.cs @@ -2,19 +2,19 @@ namespace MassTransit.SagaStateMachine { public class IgnoreEventActivityBinder : IActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { - public Event Event { get; } - public IgnoreEventActivityBinder(Event @event) { Event = @event; } + public Event Event { get; } + public bool IsStateTransitionEvent(State state) { return Equals(Event, state.Enter) || Equals(Event, state.BeforeEnter) - || Equals(Event, state.AfterLeave) || Equals(Event, state.Leave); + || Equals(Event, state.AfterLeave) || Equals(Event, state.Leave); } public void Bind(State state) @@ -30,12 +30,11 @@ public void Bind(IBehaviorBuilder builder) public class IgnoreEventActivityBinder : IActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TData : class { readonly Event _event; readonly StateMachineCondition _filter; - public Event Event => _event; public IgnoreEventActivityBinder(Event @event, StateMachineCondition filter) { @@ -43,10 +42,12 @@ public IgnoreEventActivityBinder(Event @event, StateMachineCondition _event; + public bool IsStateTransitionEvent(State state) { return Equals(_event, state.Enter) || Equals(_event, state.BeforeEnter) - || Equals(_event, state.AfterLeave) || Equals(_event, state.Leave); + || Equals(_event, state.AfterLeave) || Equals(_event, state.Leave); } public void Bind(State state) diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/RetryActivityBinder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/RetryActivityBinder.cs index d9f9b270b92..3125e29a2e4 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/RetryActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/RetryActivityBinder.cs @@ -2,7 +2,7 @@ namespace MassTransit.SagaStateMachine { public class RetryActivityBinder : IActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { readonly IStateMachineActivity _activity; @@ -42,7 +42,7 @@ public void Bind(IBehaviorBuilder builder) public class RetryActivityBinder : IActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance where TMessage : class { readonly IStateMachineActivity _activity; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/TriggerEventActivityBinder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/TriggerEventActivityBinder.cs index 46803bd00e8..809fb0b9486 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/TriggerEventActivityBinder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Binders/TriggerEventActivityBinder.cs @@ -8,7 +8,7 @@ namespace MassTransit.SagaStateMachine public class TriggerEventActivityBinder : EventActivityBinder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { readonly IActivityBinder[] _activities; readonly StateMachineCondition _filter; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/Correlation/UncorrelatedEventCorrelation.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/Correlation/UncorrelatedEventCorrelation.cs index baf8b467a10..adf9bac89a8 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/Correlation/UncorrelatedEventCorrelation.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/Correlation/UncorrelatedEventCorrelation.cs @@ -1,34 +1,37 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Collections.Generic; - public class UncorrelatedEventCorrelation : - EventCorrelation - where TSaga : class, SagaStateMachineInstance - where TMessage : class + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - public UncorrelatedEventCorrelation(Event @event) + public class UncorrelatedEventCorrelation : + EventCorrelation + where TData : class { - Event = @event; - } + public UncorrelatedEventCorrelation(Event @event) + { + Event = @event; + } - public SagaFilterFactory FilterFactory => null; + public SagaFilterFactory FilterFactory => null; - public Event Event { get; } + public Event Event { get; } - Type EventCorrelation.DataType => typeof(TMessage); + Type EventCorrelation.DataType => typeof(TData); - public bool ConfigureConsumeTopology => false; + public bool ConfigureConsumeTopology => false; - public IFilter> MessageFilter => null; + public IFilter> MessageFilter => null; - public ISagaPolicy Policy => null; + public ISagaPolicy Policy => null; - public IEnumerable Validate() - { - yield return this.Failure(Event.Name, "Correlation", "was not specified"); + public IEnumerable Validate() + { + yield return this.Failure(Event.Name, "Correlation", "was not specified"); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/EventObservable.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/EventObservable.cs index 476f547b961..0d4c2f87220 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/EventObservable.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/EventObservable.cs @@ -1,46 +1,61 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Threading.Tasks; using Util; - public class EventObservable : - Connectable>, - IEventObserver - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - public Task PreExecute(BehaviorContext context) + public class EventObservable : + Connectable>, + IEventObserver { - return ForEachAsync(x => x.PreExecute(context)); - } + public Task PreExecute(BehaviorContext context) + { + return ForEachAsync(x => x.PreExecute(context)); + } - public Task PreExecute(BehaviorContext context) - where T : class - { - return ForEachAsync(x => x.PreExecute(context)); - } + public Task PreExecute(BehaviorContext context) + where T : class + { + return ForEachAsync(x => x.PreExecute(context)); + } - public Task PostExecute(BehaviorContext context) - { - return ForEachAsync(x => x.PostExecute(context)); - } + public Task PostExecute(BehaviorContext context) + { + return ForEachAsync(x => x.PostExecute(context)); + } - public Task PostExecute(BehaviorContext context) - where T : class - { - return ForEachAsync(x => x.PostExecute(context)); - } + public Task PostExecute(BehaviorContext context) + where T : class + { + return ForEachAsync(x => x.PostExecute(context)); + } - public Task ExecuteFault(BehaviorContext context, Exception exception) - { - return ForEachAsync(x => x.ExecuteFault(context, exception)); - } + public Task ExecuteFault(BehaviorContext context, Exception exception) + { + return ForEachAsync(x => x.ExecuteFault(context, exception)); + } - public Task ExecuteFault(BehaviorContext context, Exception exception) - where T : class - { - return ForEachAsync(x => x.ExecuteFault(context, exception)); + public Task ExecuteFault(BehaviorContext context, Exception exception) + where T : class + { + return ForEachAsync(x => x.ExecuteFault(context, exception)); + } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/GraphStateMachineExtensions.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/GraphStateMachineExtensions.cs index 41b7c1a4afc..7b9745c0ff6 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/GraphStateMachineExtensions.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/GraphStateMachineExtensions.cs @@ -3,7 +3,7 @@ namespace MassTransit.SagaStateMachine public static class GraphStateMachineExtensions { public static StateMachineGraph GetGraph(this StateMachine machine) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { var inspector = new GraphStateMachineVisitor(machine); diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/GraphStateMachineVisitor.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/GraphStateMachineVisitor.cs index f1fc1922397..43b6a118313 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/GraphStateMachineVisitor.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/GraphStateMachineVisitor.cs @@ -3,20 +3,19 @@ using System; using System.Collections.Generic; using System.Linq; - using System.Reflection; public class GraphStateMachineVisitor : StateMachineVisitor - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { readonly HashSet _edges; readonly Dictionary _events; readonly StateMachine _machine; readonly Dictionary _states; + Edge _currentEdge; Vertex _currentEvent; Vertex _currentState; - Edge _currentEdge; public GraphStateMachineVisitor(StateMachine machine) { @@ -70,15 +69,6 @@ public void Visit(Event @event, Action> next) next(@event); } - void AddCurrentEdge() - { - if (_currentEvent.IsComposite || _currentEdge != null) - return; - - _currentEdge = new Edge(_currentState, _currentEvent, _currentEvent.Title); - _edges.Add(_currentEdge); - } - public void Visit(IStateMachineActivity activity) { Visit(activity, x => @@ -87,7 +77,7 @@ public void Visit(IStateMachineActivity activity) } public void Visit(IBehavior behavior) - where T : class, ISaga + where T : class, SagaStateMachineInstance { Visit(behavior, x => { @@ -95,13 +85,13 @@ public void Visit(IBehavior behavior) } public void Visit(IBehavior behavior, Action> next) - where T : class, ISaga + where T : class, SagaStateMachineInstance { next(behavior); } public void Visit(IBehavior behavior) - where T : class, ISaga + where T : class, SagaStateMachineInstance where TData : class { Visit(behavior, x => @@ -110,7 +100,7 @@ public void Visit(IBehavior behavior) } public void Visit(IBehavior behavior, Action> next) - where T : class, ISaga + where T : class, SagaStateMachineInstance where TData : class { next(behavior); @@ -133,7 +123,7 @@ public void Visit(IStateMachineActivity activity, Action } var activityType = activity.GetType(); - var compensateType = activityType.GetTypeInfo().IsGenericType + var compensateType = activityType.IsGenericType && activityType.GetGenericTypeDefinition() == typeof(CatchFaultActivity<,>) ? activityType.GetGenericArguments().Skip(1).First() : null; @@ -159,6 +149,15 @@ public void Visit(IStateMachineActivity activity, Action next(activity); } + void AddCurrentEdge() + { + if (_currentEvent.IsComposite || _currentEdge != null) + return; + + _currentEdge = new Edge(_currentState, _currentEvent, _currentEvent.Title); + _edges.Add(_currentEdge); + } + void InspectTransitionActivity(TransitionActivity transitionActivity) { AddCurrentEdge(); @@ -209,7 +208,7 @@ Vertex CreateEventVertex(Event @event) var targetType = @event .GetType() .GetInterfaces() - .Where(x => x.GetTypeInfo().IsGenericType) + .Where(x => x.IsGenericType) .Where(x => x.GetGenericTypeDefinition() == typeof(Event<>)) .Select(x => x.GetGenericArguments()[0]) .DefaultIfEmpty(typeof(Event)) diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/IBehaviorBuilder.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/IBehaviorBuilder.cs index 8ea4675b27d..34cc62531ea 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/IBehaviorBuilder.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/IBehaviorBuilder.cs @@ -1,7 +1,7 @@ namespace MassTransit.SagaStateMachine { public interface IBehaviorBuilder - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { void Add(IStateMachineActivity activity); } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/IStateEventFilter.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/IStateEventFilter.cs index d2f508ea756..624d2f36966 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/IStateEventFilter.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/IStateEventFilter.cs @@ -1,7 +1,7 @@ namespace MassTransit.SagaStateMachine { public interface IStateEventFilter - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { bool Filter(BehaviorContext context); diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/MessageEventCorrelation.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/MessageEventCorrelation.cs index f2b6dec857c..413d829adca 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/MessageEventCorrelation.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/MessageEventCorrelation.cs @@ -11,6 +11,7 @@ public class MessageEventCorrelation : where TSaga : class, SagaStateMachineInstance where TMessage : class { + readonly Lazy _includesInitial; readonly bool _insertOnInitial; readonly SagaStateMachine _machine; readonly IPipe> _missingPipe; @@ -33,6 +34,7 @@ public MessageEventCorrelation(SagaStateMachine machine, Event _machine = machine; _policy = new Lazy>(GetSagaPolicy); + _includesInitial = new Lazy(() => IncludesInitial()); } public bool ConfigureConsumeTopology { get; } @@ -52,13 +54,13 @@ public IEnumerable Validate() if (_insertOnInitial && _readOnly) yield return this.Failure("ReadOnly", "ReadOnly cannot be set when InsertOnInitial is true"); - if (IncludesInitial() && _readOnly) + if (_includesInitial.Value && _readOnly) yield return this.Failure("ReadOnly", "ReadOnly cannot be used for events in the initial state"); } ISagaPolicy GetSagaPolicy() { - if (IncludesInitial()) + if (_includesInitial.Value) return new NewOrExistingSagaPolicy(_sagaFactory, _insertOnInitial); return new AnyExistingSagaPolicy(_missingPipe, _readOnly); @@ -66,9 +68,7 @@ ISagaPolicy GetSagaPolicy() bool IncludesInitial() { - return _machine.States - .Where(state => _machine.NextEvents(state).Contains(Event)) - .Any(x => x.Name.Equals(_machine.Initial.Name)); + return _machine.NextEvents(_machine.Initial).Contains(Event); } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/MessageFactory.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/MessageFactory.cs index f4832aa3d96..76bcc05239c 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/MessageFactory.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/MessageFactory.cs @@ -80,7 +80,7 @@ async Task> Factory() public static ContextMessageFactory, T> Create(T message, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { return callback == null @@ -90,7 +90,7 @@ public static ContextMessageFactory, T> Create< public static ContextMessageFactory, T> Create(Task factory, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { return callback == null @@ -100,7 +100,7 @@ public static ContextMessageFactory, T> Create< public static ContextMessageFactory, T> Create( Func, Task>> factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { return new ContextMessageFactory, T>(factory); @@ -108,7 +108,7 @@ public static ContextMessageFactory, T> Create< public static ContextMessageFactory, T> Create( Func, Task>> factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { if (callback == null) @@ -131,7 +131,7 @@ async Task> Factory(BehaviorContext context) public static ContextMessageFactory, T> Create( Func, Task>> factory, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { if (callback == null) @@ -153,7 +153,7 @@ async Task> Factory(BehaviorContext context) } public static ContextMessageFactory, T> Create(AsyncEventMessageFactory factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { Task> Factory(BehaviorContext context) @@ -175,7 +175,7 @@ async Task> GetResult() public static ContextMessageFactory, T> Create(AsyncEventMessageFactory factory, IPipe> pipe) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { if (!pipe.IsNotEmpty()) @@ -200,7 +200,7 @@ async Task> GetResult() public static ContextMessageFactory, T> Create(AsyncEventMessageFactory factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { return callback == null ? Create(factory) : Create(factory, Pipe.Execute(callback)); @@ -208,7 +208,7 @@ public static ContextMessageFactory, T> Create< public static ContextMessageFactory, T> Create(AsyncEventMessageFactory factory, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { if (callback == null) @@ -234,7 +234,7 @@ async Task> GetResult() } public static ContextMessageFactory, T> Create(EventMessageFactory factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { Task> Factory(BehaviorContext context) @@ -248,7 +248,7 @@ Task> Factory(BehaviorContext context) public static ContextMessageFactory, T> Create(EventMessageFactory factory, IPipe> pipe) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { if (!pipe.IsNotEmpty()) @@ -265,7 +265,7 @@ Task> Factory(BehaviorContext context) public static ContextMessageFactory, T> Create(EventMessageFactory factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { return callback == null ? Create(factory) : Create(factory, Pipe.Execute(callback)); @@ -273,7 +273,7 @@ public static ContextMessageFactory, T> Create< public static ContextMessageFactory, T> Create(EventMessageFactory factory, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { if (callback == null) @@ -292,7 +292,7 @@ Task> Factory(BehaviorContext context) public static ContextMessageFactory, T> Create(T message, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -303,7 +303,7 @@ public static ContextMessageFactory, T> Create(Task factory, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -314,7 +314,7 @@ public static ContextMessageFactory, T> Create( Func, Task>> factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -323,7 +323,7 @@ public static ContextMessageFactory, T> Create( Func, Task>> factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -348,7 +348,7 @@ async Task> Factory(BehaviorExceptionContext, T> Create( Func, Task>> factory, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -372,7 +372,7 @@ async Task> Factory(BehaviorExceptionContext, T> Create( AsyncEventExceptionMessageFactory factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -395,7 +395,7 @@ async Task> GetResult() public static ContextMessageFactory, T> Create( AsyncEventExceptionMessageFactory factory, IPipe> pipe) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -421,7 +421,7 @@ async Task> GetResult() public static ContextMessageFactory, T> Create( AsyncEventExceptionMessageFactory factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -430,7 +430,7 @@ public static ContextMessageFactory, T> Create( AsyncEventExceptionMessageFactory factory, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -458,7 +458,7 @@ async Task> GetResult() public static ContextMessageFactory, T> Create( EventExceptionMessageFactory factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -473,7 +473,7 @@ Task> Factory(BehaviorExceptionContext public static ContextMessageFactory, T> Create( EventExceptionMessageFactory factory, IPipe> pipe) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -491,7 +491,7 @@ Task> Factory(BehaviorExceptionContext public static ContextMessageFactory, T> Create( EventExceptionMessageFactory factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -500,7 +500,7 @@ public static ContextMessageFactory, T> Create( EventExceptionMessageFactory factory, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class where TException : Exception { @@ -521,7 +521,7 @@ Task> Factory(BehaviorExceptionContext // Saga Only public static ContextMessageFactory, T> Create(T message, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return callback == null ? Create(message) @@ -529,7 +529,7 @@ public static ContextMessageFactory, T> Create(T m } public static ContextMessageFactory, T> Create(Task factory, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return callback == null ? Create(factory) @@ -538,7 +538,7 @@ public static ContextMessageFactory, T> Create(Tas public static ContextMessageFactory, T> Create(T message, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { return callback == null @@ -548,7 +548,7 @@ public static ContextMessageFactory, public static ContextMessageFactory, T> Create(Task factory, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { return callback == null @@ -557,14 +557,14 @@ public static ContextMessageFactory, } public static ContextMessageFactory, T> Create(Func, Task>> factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return new ContextMessageFactory, T>(factory); } public static ContextMessageFactory, T> Create(Func, Task>> factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { if (callback == null) return Create(factory); @@ -586,7 +586,7 @@ async Task> Factory(BehaviorContext context) public static ContextMessageFactory, T> Create(Func, Task>> factory, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { if (callback == null) return Create(factory); @@ -607,7 +607,7 @@ async Task> Factory(BehaviorContext context) } public static ContextMessageFactory, T> Create(AsyncEventMessageFactory factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { Task> Factory(BehaviorContext context) { @@ -628,7 +628,7 @@ async Task> GetResult() public static ContextMessageFactory, T> Create(AsyncEventMessageFactory factory, IPipe> pipe) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { if (!pipe.IsNotEmpty()) return Create(factory); @@ -652,14 +652,14 @@ async Task> GetResult() public static ContextMessageFactory, T> Create(AsyncEventMessageFactory factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return callback == null ? Create(factory) : Create(factory, Pipe.Execute(callback)); } public static ContextMessageFactory, T> Create(AsyncEventMessageFactory factory, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { if (callback == null) return Create(factory); @@ -684,7 +684,7 @@ async Task> GetResult() } public static ContextMessageFactory, T> Create(EventMessageFactory factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { Task> Factory(BehaviorContext context) { @@ -697,7 +697,7 @@ Task> Factory(BehaviorContext context) public static ContextMessageFactory, T> Create(EventMessageFactory factory, IPipe> pipe) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { if (!pipe.IsNotEmpty()) return Create(factory); @@ -713,14 +713,14 @@ Task> Factory(BehaviorContext context) public static ContextMessageFactory, T> Create(EventMessageFactory factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return callback == null ? Create(factory) : Create(factory, Pipe.Execute(callback)); } public static ContextMessageFactory, T> Create(EventMessageFactory factory, SendContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { if (callback == null) return Create(factory); @@ -738,7 +738,7 @@ Task> Factory(BehaviorContext context) public static ContextMessageFactory, T> Create( Func, Task>> factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { return new ContextMessageFactory, T>(factory); @@ -746,7 +746,7 @@ public static ContextMessageFactory, public static ContextMessageFactory, T> Create( Func, Task>> factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { if (callback == null) @@ -769,7 +769,7 @@ async Task> Factory(BehaviorExceptionContext con public static ContextMessageFactory, T> Create( Func, Task>> factory, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { if (callback == null) @@ -792,7 +792,7 @@ async Task> Factory(BehaviorExceptionContext con public static ContextMessageFactory, T> Create( AsyncEventExceptionMessageFactory factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { Task> Factory(BehaviorExceptionContext context) @@ -814,7 +814,7 @@ async Task> GetResult() public static ContextMessageFactory, T> Create( AsyncEventExceptionMessageFactory factory, IPipe> pipe) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { if (!pipe.IsNotEmpty()) @@ -839,7 +839,7 @@ async Task> GetResult() public static ContextMessageFactory, T> Create( AsyncEventExceptionMessageFactory factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { return callback == null ? Create(factory) : Create(factory, Pipe.Execute(callback)); @@ -847,7 +847,7 @@ public static ContextMessageFactory, public static ContextMessageFactory, T> Create( AsyncEventExceptionMessageFactory factory, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { if (callback == null) @@ -874,7 +874,7 @@ async Task> GetResult() public static ContextMessageFactory, T> Create( EventExceptionMessageFactory factory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { Task> Factory(BehaviorExceptionContext context) @@ -888,7 +888,7 @@ Task> Factory(BehaviorExceptionContext context) public static ContextMessageFactory, T> Create( EventExceptionMessageFactory factory, IPipe> pipe) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { if (!pipe.IsNotEmpty()) @@ -905,7 +905,7 @@ Task> Factory(BehaviorExceptionContext context) public static ContextMessageFactory, T> Create( EventExceptionMessageFactory factory, Action> callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { return callback == null ? Create(factory) : Create(factory, Pipe.Execute(callback)); @@ -913,7 +913,7 @@ public static ContextMessageFactory, public static ContextMessageFactory, T> Create( EventExceptionMessageFactory factory, SendExceptionContextCallback callback) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { if (callback == null) diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/NonTransitionEventObserver.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/NonTransitionEventObserver.cs index ea335427530..d7a44df79e8 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/NonTransitionEventObserver.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/NonTransitionEventObserver.cs @@ -1,72 +1,76 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Collections.Generic; using System.Threading.Tasks; - public class NonTransitionEventObserver : - IEventObserver - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly IReadOnlyDictionary> _eventCache; - readonly IEventObserver _observer; - - public NonTransitionEventObserver(IReadOnlyDictionary> eventCache, IEventObserver observer) + class NonTransitionEventObserver : + IEventObserver + where TSaga : class, SagaStateMachineInstance { - _eventCache = eventCache; - _observer = observer; - } + readonly IReadOnlyDictionary _eventCache; + readonly IEventObserver _observer; - public Task PreExecute(BehaviorContext context) - { - if (_eventCache.TryGetValue(context.Event.Name, out StateMachineEvent stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) - return _observer.PreExecute(context); + public NonTransitionEventObserver(IReadOnlyDictionary eventCache, IEventObserver observer) + { + _eventCache = eventCache; + _observer = observer; + } - return Task.CompletedTask; - } + public Task PreExecute(BehaviorContext context) + { + if (_eventCache.TryGetValue(context.Event.Name, out var stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) + return _observer.PreExecute(context); - public Task PreExecute(BehaviorContext context) - where T : class - { - if (_eventCache.TryGetValue(context.Event.Name, out StateMachineEvent stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) - return _observer.PreExecute(context); + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public Task PreExecute(BehaviorContext context) + where T : class + { + if (_eventCache.TryGetValue(context.Event.Name, out var stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) + return _observer.PreExecute(context); - public Task PostExecute(BehaviorContext context) - { - if (_eventCache.TryGetValue(context.Event.Name, out StateMachineEvent stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) - return _observer.PostExecute(context); + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public Task PostExecute(BehaviorContext context) + { + if (_eventCache.TryGetValue(context.Event.Name, out var stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) + return _observer.PostExecute(context); - public Task PostExecute(BehaviorContext context) - where T : class - { - if (_eventCache.TryGetValue(context.Event.Name, out StateMachineEvent stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) - return _observer.PostExecute(context); + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public Task PostExecute(BehaviorContext context) + where T : class + { + if (_eventCache.TryGetValue(context.Event.Name, out var stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) + return _observer.PostExecute(context); - public Task ExecuteFault(BehaviorContext context, Exception exception) - { - if (_eventCache.TryGetValue(context.Event.Name, out StateMachineEvent stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) - return _observer.ExecuteFault(context, exception); + return Task.CompletedTask; + } - return Task.CompletedTask; - } + public Task ExecuteFault(BehaviorContext context, Exception exception) + { + if (_eventCache.TryGetValue(context.Event.Name, out var stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) + return _observer.ExecuteFault(context, exception); - public Task ExecuteFault(BehaviorContext context, Exception exception) - where T : class - { - if (_eventCache.TryGetValue(context.Event.Name, out StateMachineEvent stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) - return _observer.ExecuteFault(context, exception); + return Task.CompletedTask; + } + + public Task ExecuteFault(BehaviorContext context, Exception exception) + where T : class + { + if (_eventCache.TryGetValue(context.Event.Name, out var stateMachineEvent) && !stateMachineEvent.IsTransitionEvent) + return _observer.ExecuteFault(context, exception); - return Task.CompletedTask; + return Task.CompletedTask; + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/SelectedEventObserver.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/SelectedEventObserver.cs index 8a13858d73e..e193de5720d 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/SelectedEventObserver.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/SelectedEventObserver.cs @@ -1,65 +1,68 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Threading.Tasks; - public class SelectedEventObserver : - IEventObserver - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly Event _event; - readonly IEventObserver _observer; - - public SelectedEventObserver(Event @event, IEventObserver observer) + public class SelectedEventObserver : + IEventObserver { - _event = @event; - _observer = observer; - } + readonly Event _event; + readonly IEventObserver _observer; - public Task PreExecute(BehaviorContext context) - { - return _event.Equals(context.Event) - ? _observer.PreExecute(context) - : Task.CompletedTask; - } + public SelectedEventObserver(Event @event, IEventObserver observer) + { + _event = @event; + _observer = observer; + } - public Task PreExecute(BehaviorContext context) - where T : class - { - return _event.Equals(context.Event) - ? _observer.PreExecute(context) - : Task.CompletedTask; - } + public Task PreExecute(BehaviorContext context) + { + return _event.Equals(context.Event) + ? _observer.PreExecute(context) + : Task.CompletedTask; + } - public Task PostExecute(BehaviorContext context) - { - return _event.Equals(context.Event) - ? _observer.PostExecute(context) - : Task.CompletedTask; - } + public Task PreExecute(BehaviorContext context) + where T : class + { + return _event.Equals(context.Event) + ? _observer.PreExecute(context) + : Task.CompletedTask; + } - public Task PostExecute(BehaviorContext context) - where T : class - { - return _event.Equals(context.Event) - ? _observer.PostExecute(context) - : Task.CompletedTask; - } + public Task PostExecute(BehaviorContext context) + { + return _event.Equals(context.Event) + ? _observer.PostExecute(context) + : Task.CompletedTask; + } - public Task ExecuteFault(BehaviorContext context, Exception exception) - { - return _event.Equals(context.Event) - ? _observer.ExecuteFault(context, exception) - : Task.CompletedTask; - } + public Task PostExecute(BehaviorContext context) + where T : class + { + return _event.Equals(context.Event) + ? _observer.PostExecute(context) + : Task.CompletedTask; + } - public Task ExecuteFault(BehaviorContext context, Exception exception) - where T : class - { - return _event.Equals(context.Event) - ? _observer.ExecuteFault(context, exception) - : Task.CompletedTask; + public Task ExecuteFault(BehaviorContext context, Exception exception) + { + return _event.Equals(context.Event) + ? _observer.ExecuteFault(context, exception) + : Task.CompletedTask; + } + + public Task ExecuteFault(BehaviorContext context, Exception exception) + where T : class + { + return _event.Equals(context.Event) + ? _observer.ExecuteFault(context, exception) + : Task.CompletedTask; + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/SelectedStateEventFilter.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/SelectedStateEventFilter.cs index 27d37be8a71..224af728b0f 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/SelectedStateEventFilter.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/SelectedStateEventFilter.cs @@ -2,7 +2,7 @@ namespace MassTransit.SagaStateMachine { public class SelectedStateEventFilter : IStateEventFilter - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { readonly StateMachineCondition _filter; diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineEvent.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineEvent.cs index 64483a6ce46..8eeb74ab3af 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineEvent.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineEvent.cs @@ -1,14 +1,18 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { - public class StateMachineEvent + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - public StateMachineEvent(Event @event, bool isTransitionEvent) + class StateMachineEvent { - Event = @event; - IsTransitionEvent = isTransitionEvent; - } + public StateMachineEvent(Event @event, bool isTransitionEvent) + { + Event = @event; + IsTransitionEvent = isTransitionEvent; + } - public bool IsTransitionEvent { get; } - public Event Event { get; } + public bool IsTransitionEvent { get; } + public Event Event { get; } + } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineRequest.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineRequest.cs index db2cb4779a6..be056ce346d 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineRequest.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineRequest.cs @@ -1,4 +1,4 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Collections.Generic; @@ -7,125 +7,153 @@ namespace MassTransit.SagaStateMachine using Internals; - public class StateMachineRequest : - Request + public partial class MassTransitStateMachine where TInstance : class, SagaStateMachineInstance - where TRequest : class - where TResponse : class { - readonly IList _accept; - readonly IReadProperty _read; - readonly IWriteProperty _write; - - public StateMachineRequest(string name, RequestSettings settings, Expression> requestIdExpression = default) + public class StateMachineRequest : + Request + where TRequest : class + where TResponse : class { - Name = name; - Settings = settings; + readonly List _accept; + readonly IReadProperty _read; + readonly IWriteProperty _write; + + public StateMachineRequest(string name, RequestSettings settings, + Expression> requestIdExpression = default) + { + Name = name; + Settings = settings; + + _accept = new List(); - _accept = new List(); + AcceptResponse(); + + if (requestIdExpression != null) + { + var propertyInfo = requestIdExpression.GetPropertyInfo(); + + _read = ReadPropertyCache.GetProperty(propertyInfo); + _write = WritePropertyCache.GetProperty(propertyInfo); + } + } - AcceptResponse(); + public string Name { get; } + public RequestSettings Settings { get; } + public Event Completed { get; set; } + public Event> Faulted { get; set; } + public Event> TimeoutExpired { get; set; } + public State Pending { get; set; } - if (requestIdExpression != null) + public void SetRequestId(TInstance instance, Guid? requestId) { - var propertyInfo = requestIdExpression.GetPropertyInfo(); + if (instance == null) + throw new ArgumentNullException(nameof(instance)); - _read = ReadPropertyCache.GetProperty(propertyInfo); - _write = WritePropertyCache.GetProperty(propertyInfo); + _write?.Set(instance, requestId); } - } - public string Name { get; } - public RequestSettings Settings { get; } - public Event Completed { get; set; } - public Event> Faulted { get; set; } - public Event> TimeoutExpired { get; set; } - public State Pending { get; set; } + public Guid? GetRequestId(TInstance instance) + { + if (instance == null) + throw new ArgumentNullException(nameof(instance)); - public void SetRequestId(TInstance instance, Guid? requestId) - { - if (instance == null) - throw new ArgumentNullException(nameof(instance)); + return _read != null + ? _read.Get(instance) + : instance.CorrelationId; + } - _write?.Set(instance, requestId); - } + public Guid GenerateRequestId(TInstance instance) + { + return _read != null + ? NewId.NextGuid() + : instance.CorrelationId; + } - public Guid? GetRequestId(TInstance instance) - { - if (instance == null) - throw new ArgumentNullException(nameof(instance)); + public void SetSendContextHeaders(SendContext context) + { + if (Settings.TimeToLive.HasValue && Settings.TimeToLive.Value > TimeSpan.Zero) + context.TimeToLive = Settings.TimeToLive.Value; - return _read != null - ? _read.Get(instance) - : instance.CorrelationId; - } + context.Headers.Set(MessageHeaders.Request.Accept, _accept); + } - public Guid GenerateRequestId(TInstance instance) - { - return _read != null - ? NewId.NextGuid() - : instance.CorrelationId; - } + public bool EventFilter(BehaviorContext> context) + { + if (!context.RequestId.HasValue) + return false; - public void SetSendContextHeaders(SendContext context) - { - if (Settings.TimeToLive.HasValue && Settings.TimeToLive.Value > TimeSpan.Zero) - context.TimeToLive = Settings.TimeToLive.Value; + Guid? requestId = GetRequestId(context.Saga); + + return requestId.HasValue && requestId.Value == context.RequestId.Value; + } - context.Headers.Set(MessageHeaders.Request.Accept, _accept); + protected void AcceptResponse() + where T : class + { + _accept.Add(MessageUrn.ForTypeString()); + } } - public bool EventFilter(BehaviorContext> context) + + public class StateMachineRequest : + StateMachineRequest, + Request + where TRequest : class + where TResponse : class + where TResponse2 : class { - if (!context.RequestId.HasValue) - return false; + public StateMachineRequest(string name, RequestSettings settings, + Expression> requestIdExpression = default) + : base(name, settings, requestIdExpression) + { + Settings = settings; - Guid? requestId = GetRequestId(context.Saga); + AcceptResponse(); + } - return requestId.HasValue && requestId.Value == context.RequestId.Value; - } + public new RequestSettings Settings { get; } - protected void AcceptResponse() - where T : class - { - _accept.Add(MessageUrn.ForTypeString()); + public Event Completed2 { get; set; } + + public void Method1() + { + } + + public void Method2() + { + } } - } - public class StateMachineRequest : - StateMachineRequest, - Request - where TInstance : class, SagaStateMachineInstance - where TRequest : class - where TResponse : class - where TResponse2 : class - { - public StateMachineRequest(string name, RequestSettings settings, Expression> requestIdExpression = default) - : base(name, settings, requestIdExpression) + public class StateMachineRequest : + StateMachineRequest, + Request + where TRequest : class + where TResponse : class + where TResponse2 : class + where TResponse3 : class { - AcceptResponse(); - } + public StateMachineRequest(string name, RequestSettings settings, + Expression> requestIdExpression = default) + : base(name, settings, requestIdExpression) + { + Settings = settings; - public Event Completed2 { get; set; } - } + AcceptResponse(); + } + public new RequestSettings Settings { get; } - public class StateMachineRequest : - StateMachineRequest, - Request - where TInstance : class, SagaStateMachineInstance - where TRequest : class - where TResponse : class - where TResponse2 : class - where TResponse3 : class - { - public StateMachineRequest(string name, RequestSettings settings, Expression> requestIdExpression = default) - : base(name, settings, requestIdExpression) - { - AcceptResponse(); - } + public Event Completed3 { get; set; } - public Event Completed3 { get; set; } + public void Method12() + { + } + + public void Method22() + { + } + } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineSchedule.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineSchedule.cs index 97d87287d5f..01df3681b79 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineSchedule.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineSchedule.cs @@ -1,48 +1,51 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Linq.Expressions; using Internals; - public class StateMachineSchedule : - Schedule - where TSaga : class, SagaStateMachineInstance - where TMessage : class + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly string _name; - readonly IReadProperty _read; - readonly ScheduleSettings _settings; - readonly IWriteProperty _write; - - public StateMachineSchedule(string name, Expression> tokenIdExpression, ScheduleSettings settings) - { - _name = name; - _settings = settings; - - var propertyInfo = tokenIdExpression.GetPropertyInfo(); - - _read = ReadPropertyCache.GetProperty(propertyInfo); - _write = WritePropertyCache.GetProperty(propertyInfo); - } - - string Schedule.Name => _name; - public Event Received { get; set; } - public Event AnyReceived { get; set; } - - public TimeSpan GetDelay(BehaviorContext context) - { - return _settings.DelayProvider(context); - } - - public Guid? GetTokenId(TSaga instance) - { - return _read.Get(instance); - } - - public void SetTokenId(TSaga instance, Guid? tokenId) + public class StateMachineSchedule : + Schedule + where TMessage : class { - _write.Set(instance, tokenId); + readonly string _name; + readonly IReadProperty _read; + readonly ScheduleSettings _settings; + readonly IWriteProperty _write; + + public StateMachineSchedule(string name, Expression> tokenIdExpression, ScheduleSettings settings) + { + _name = name; + _settings = settings; + + var propertyInfo = tokenIdExpression.GetPropertyInfo(); + + _read = ReadPropertyCache.GetProperty(propertyInfo); + _write = WritePropertyCache.GetProperty(propertyInfo); + } + + string Schedule.Name => _name; + public Event Received { get; set; } + public Event AnyReceived { get; set; } + + public TimeSpan GetDelay(BehaviorContext context) + { + return _settings.DelayProvider(context); + } + + public Guid? GetTokenId(TInstance instance) + { + return _read.Get(instance); + } + + public void SetTokenId(TInstance instance, Guid? tokenId) + { + _write.Set(instance, tokenId); + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineState.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineState.cs index be6f2900195..b994b0db514 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineState.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateMachineState.cs @@ -1,298 +1,302 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using SagaStateMachine; - public class StateMachineState : - State, - IEquatable - where TSaga : class, SagaStateMachineInstance + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly Dictionary> _behaviors; - readonly Dictionary> _ignoredEvents; - readonly IEventObserver _observer; - readonly HashSet> _subStates; - readonly StateMachineUnhandledEventCallback _unhandledEventCallback; - - public StateMachineState(StateMachineUnhandledEventCallback unhandledEventCallback, string name, IEventObserver observer, - State superState = null) + public class StateMachineState : + State, + IEquatable { - _unhandledEventCallback = unhandledEventCallback; - Name = name; - _observer = observer; + readonly Dictionary> _behaviors; + readonly Dictionary> _ignoredEvents; + readonly IEventObserver _observer; + readonly HashSet> _subStates; + readonly StateMachineUnhandledEventCallback _unhandledEventCallback; + + public StateMachineState(StateMachineUnhandledEventCallback unhandledEventCallback, string name, IEventObserver observer, + State superState = null) + { + _unhandledEventCallback = unhandledEventCallback; + Name = name; + _observer = observer; - _behaviors = new Dictionary>(); - _ignoredEvents = new Dictionary>(); + _behaviors = new Dictionary>(); + _ignoredEvents = new Dictionary>(); - Enter = new TriggerEvent(name + ".Enter"); - Ignore(Enter); - Leave = new TriggerEvent(name + ".Leave"); - Ignore(Leave); + Enter = new TriggerEvent(name + ".Enter"); + Ignore(Enter); + Leave = new TriggerEvent(name + ".Leave"); + Ignore(Leave); - BeforeEnter = new MessageEvent(name + ".BeforeEnter"); - Ignore(BeforeEnter); - AfterLeave = new MessageEvent(name + ".AfterLeave"); - Ignore(AfterLeave); + BeforeEnter = new MessageEvent(name + ".BeforeEnter"); + Ignore(BeforeEnter); + AfterLeave = new MessageEvent(name + ".AfterLeave"); + Ignore(AfterLeave); - _subStates = new HashSet>(); + _subStates = new HashSet>(); - SuperState = superState; - superState?.AddSubstate(this); - } + SuperState = superState; + superState?.AddSubstate(this); + } - public bool Equals(State other) - { - return string.CompareOrdinal(Name, other?.Name ?? "") == 0; - } + public bool Equals(State other) + { + return string.CompareOrdinal(Name, other?.Name ?? "") == 0; + } - public State SuperState { get; } - public string Name { get; } + public State SuperState { get; } + public string Name { get; } - public Event Enter { get; } - public Event Leave { get; } - public Event BeforeEnter { get; } - public Event AfterLeave { get; } + public Event Enter { get; } + public Event Leave { get; } + public Event BeforeEnter { get; } + public Event AfterLeave { get; } - public void Accept(StateMachineVisitor visitor) - { - visitor.Visit(this, _ => + public void Accept(StateMachineVisitor visitor) { - foreach (KeyValuePair> behavior in _behaviors) + visitor.Visit(this, _ => { - behavior.Key.Accept(visitor); - behavior.Value.Behavior.Accept(visitor); - } - }); - } - - public void Probe(ProbeContext context) - { - var scope = context.CreateScope("state"); - scope.Add("name", Name); - - if (_subStates.Any()) - { - var subStateScope = scope.CreateScope("substates"); - foreach (State subState in _subStates) - subStateScope.Add("name", subState.Name); + foreach (KeyValuePair> behavior in _behaviors) + { + behavior.Key.Accept(visitor); + behavior.Value.Behavior.Accept(visitor); + } + }); } - if (_behaviors.Any()) + public void Probe(ProbeContext context) { - foreach (KeyValuePair> behavior in _behaviors) + var scope = context.CreateScope("state"); + scope.Add("name", Name); + + if (_subStates.Any()) { - var eventScope = scope.CreateScope("event"); - behavior.Key.Probe(eventScope); + var subStateScope = scope.CreateScope("substates"); + foreach (State subState in _subStates) + subStateScope.Add("name", subState.Name); + } - behavior.Value.Behavior.Probe(eventScope.CreateScope("behavior")); + if (_behaviors.Any()) + { + foreach (KeyValuePair> behavior in _behaviors) + { + var eventScope = scope.CreateScope("event"); + behavior.Key.Probe(eventScope); + + behavior.Value.Behavior.Probe(eventScope.CreateScope("behavior")); + } } - } - List>> ignored = _ignoredEvents.Where(x => IsRealEvent(x.Key)).ToList(); - if (ignored.Any()) - { - foreach (KeyValuePair> ignoredEvent in ignored) - ignoredEvent.Key.Probe(scope.CreateScope("event-ignored")); + List>> ignored = _ignoredEvents.Where(x => IsRealEvent(x.Key)).ToList(); + if (ignored.Any()) + { + foreach (KeyValuePair> ignoredEvent in ignored) + ignoredEvent.Key.Probe(scope.CreateScope("event-ignored")); + } } - } - async Task State.Raise(BehaviorContext context) - { - if (!_behaviors.TryGetValue(context.Event, out ActivityBehaviorBuilder activities)) + async Task State.Raise(BehaviorContext context) { - if (_ignoredEvents.TryGetValue(context.Event, out IStateEventFilter filter) && filter.Filter(context)) - return; - - if (SuperState != null) + if (!_behaviors.TryGetValue(context.Event, out ActivityBehaviorBuilder activities)) { - try - { - await SuperState.Raise(context).ConfigureAwait(false); + if (_ignoredEvents.TryGetValue(context.Event, out IStateEventFilter filter) && filter.Filter(context)) return; - } - catch (UnhandledEventException) + + if (SuperState != null) { - // the exception is better if it's from the substate + try + { + await SuperState.Raise(context).ConfigureAwait(false); + return; + } + catch (UnhandledEventException) + { + // the exception is better if it's from the substate + } } - } - await _unhandledEventCallback(context, this).ConfigureAwait(false); - return; - } + await _unhandledEventCallback(context, this).ConfigureAwait(false); + return; + } - try - { - await _observer.PreExecute(context).ConfigureAwait(false); + try + { + await _observer.PreExecute(context).ConfigureAwait(false); - await activities.Behavior.Execute(context).ConfigureAwait(false); + await activities.Behavior.Execute(context).ConfigureAwait(false); - await _observer.PostExecute(context).ConfigureAwait(false); - } - catch (Exception ex) - { - await _observer.ExecuteFault(context, ex).ConfigureAwait(false); + await _observer.PostExecute(context).ConfigureAwait(false); + } + catch (Exception ex) + { + await _observer.ExecuteFault(context, ex).ConfigureAwait(false); - throw; + throw; + } } - } - async Task State.Raise(BehaviorContext context) - { - if (!_behaviors.TryGetValue(context.Event, out ActivityBehaviorBuilder activities)) + async Task State.Raise(BehaviorContext context) { - if (_ignoredEvents.TryGetValue(context.Event, out IStateEventFilter filter) && filter.Filter(context)) - return; - - if (SuperState != null) + if (!_behaviors.TryGetValue(context.Event, out ActivityBehaviorBuilder activities)) { - try - { - await SuperState.Raise(context).ConfigureAwait(false); + if (_ignoredEvents.TryGetValue(context.Event, out IStateEventFilter filter) && filter.Filter(context)) return; - } - catch (UnhandledEventException) + + if (SuperState != null) { - // the exception is better if it's from the substate + try + { + await SuperState.Raise(context).ConfigureAwait(false); + return; + } + catch (UnhandledEventException) + { + // the exception is better if it's from the substate + } } + + await _unhandledEventCallback(context, this).ConfigureAwait(false); + return; } - await _unhandledEventCallback(context, this).ConfigureAwait(false); - return; - } + try + { + await _observer.PreExecute(context).ConfigureAwait(false); - try - { - await _observer.PreExecute(context).ConfigureAwait(false); + await activities.Behavior.Execute(context).ConfigureAwait(false); - await activities.Behavior.Execute(context).ConfigureAwait(false); + await _observer.PostExecute(context).ConfigureAwait(false); + } + catch (Exception ex) + { + await _observer.ExecuteFault(context, ex).ConfigureAwait(false); - await _observer.PostExecute(context).ConfigureAwait(false); + throw; + } } - catch (Exception ex) + + public void Bind(Event @event, IStateMachineActivity activity) { - await _observer.ExecuteFault(context, ex).ConfigureAwait(false); + if (!_behaviors.TryGetValue(@event, out ActivityBehaviorBuilder builder)) + { + builder = new ActivityBehaviorBuilder(); + _behaviors.Add(@event, builder); + } - throw; + builder.Add(activity); } - } - public void Bind(Event @event, IStateMachineActivity activity) - { - if (!_behaviors.TryGetValue(@event, out ActivityBehaviorBuilder builder)) + public void Ignore(Event @event) { - builder = new ActivityBehaviorBuilder(); - _behaviors.Add(@event, builder); + _ignoredEvents[@event] = new AllStateEventFilter(); } - builder.Add(activity); - } - - public void Ignore(Event @event) - { - _ignoredEvents[@event] = new AllStateEventFilter(); - } - - public void Ignore(Event @event, StateMachineCondition filter) - where T : class - { - _ignoredEvents[@event] = new SelectedStateEventFilter(filter); - } + public void Ignore(Event @event, StateMachineCondition filter) + where T : class + { + _ignoredEvents[@event] = new SelectedStateEventFilter(filter); + } - public void AddSubstate(State subState) - { - if (subState == null) - throw new ArgumentNullException(nameof(subState)); + public void AddSubstate(State subState) + { + if (subState == null) + throw new ArgumentNullException(nameof(subState)); - if (Name.Equals(subState.Name)) - throw new ArgumentException("A state cannot be a substate of itself", nameof(subState)); + if (Name.Equals(subState.Name)) + throw new ArgumentException("A state cannot be a substate of itself", nameof(subState)); - _subStates.Add(subState); - } + _subStates.Add(subState); + } - public bool HasState(State state) - { - return Name.Equals(state.Name) || _subStates.Any(s => s.HasState(state)); - } + public bool HasState(State state) + { + return Name.Equals(state.Name) || _subStates.Any(s => s.HasState(state)); + } - public bool IsStateOf(State state) - { - return Name.Equals(state.Name) || SuperState != null && SuperState.IsStateOf(state); - } + public bool IsStateOf(State state) + { + return Name.Equals(state.Name) || (SuperState != null && SuperState.IsStateOf(state)); + } - public IEnumerable Events => SuperState != null ? SuperState.Events.Union(GetStateEvents()).Distinct() : GetStateEvents(); + public IEnumerable Events => SuperState != null ? SuperState.Events.Union(GetStateEvents()).Distinct() : GetStateEvents(); - public int CompareTo(State other) - { - return string.CompareOrdinal(Name, other.Name); - } + public int CompareTo(State other) + { + return string.CompareOrdinal(Name, other.Name); + } - bool IsRealEvent(Event @event) - { - if (Equals(@event, Enter) || Equals(@event, Leave) || Equals(@event, BeforeEnter) || Equals(@event, AfterLeave)) - return false; + bool IsRealEvent(Event @event) + { + if (Equals(@event, Enter) || Equals(@event, Leave) || Equals(@event, BeforeEnter) || Equals(@event, AfterLeave)) + return false; - return true; - } + return true; + } - IEnumerable GetStateEvents() - { - return _behaviors.Keys - .Union(_ignoredEvents.Keys) - .Where(IsRealEvent) - .Distinct(); - } + IEnumerable GetStateEvents() + { + return _behaviors.Keys + .Union(_ignoredEvents.Keys) + .Where(IsRealEvent) + .Distinct(); + } - public override bool Equals(object obj) - { - if (ReferenceEquals(null, obj)) - return false; - if (ReferenceEquals(this, obj)) - return true; - var other = obj as State; - return other != null && Equals(other); - } + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + var other = obj as State; + return other != null && Equals(other); + } - public override int GetHashCode() - { - return Name?.GetHashCode() ?? 0; - } + public override int GetHashCode() + { + return Name?.GetHashCode() ?? 0; + } - public static bool operator ==(State left, StateMachineState right) - { - return Equals(left, right); - } + public static bool operator ==(State left, StateMachineState right) + { + return Equals(left, right); + } - public static bool operator !=(State left, StateMachineState right) - { - return !Equals(left, right); - } + public static bool operator !=(State left, StateMachineState right) + { + return !Equals(left, right); + } - public static bool operator ==(StateMachineState left, State right) - { - return Equals(left, right); - } + public static bool operator ==(StateMachineState left, State right) + { + return Equals(left, right); + } - public static bool operator !=(StateMachineState left, State right) - { - return !Equals(left, right); - } + public static bool operator !=(StateMachineState left, State right) + { + return !Equals(left, right); + } - public static bool operator ==(StateMachineState left, StateMachineState right) - { - return Equals(left, right); - } + public static bool operator ==(StateMachineState left, StateMachineState right) + { + return Equals(left, right); + } - public static bool operator !=(StateMachineState left, StateMachineState right) - { - return !Equals(left, right); - } + public static bool operator !=(StateMachineState left, StateMachineState right) + { + return !Equals(left, right); + } - public override string ToString() - { - return $"{Name} (State)"; + public override string ToString() + { + return $"{Name} (State)"; + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateObservable.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateObservable.cs index f4ff95ccb33..059c5a7618c 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/StateObservable.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/StateObservable.cs @@ -1,17 +1,32 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System.Threading.Tasks; using Util; - public class StateObservable : - Connectable>, - IStateObserver - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - public Task StateChanged(BehaviorContext context, State currentState, State previousState) + public class StateObservable : + Connectable>, + IStateObserver { - return ForEachAsync(x => x.StateChanged(context, currentState, previousState)); + public Task StateChanged(BehaviorContext context, State currentState, State previousState) + { + return ForEachAsync(x => x.StateChanged(context, currentState, previousState)); + } + + public void Method4() + { + } + + public void Method5() + { + } + + public void Method6() + { + } } } } diff --git a/src/MassTransit/SagaStateMachine/SagaStateMachine/UnhandledEventBehaviorContext.cs b/src/MassTransit/SagaStateMachine/SagaStateMachine/UnhandledEventBehaviorContext.cs index 322bcc62930..32039b9544a 100644 --- a/src/MassTransit/SagaStateMachine/SagaStateMachine/UnhandledEventBehaviorContext.cs +++ b/src/MassTransit/SagaStateMachine/SagaStateMachine/UnhandledEventBehaviorContext.cs @@ -1,36 +1,39 @@ -namespace MassTransit.SagaStateMachine +namespace MassTransit { using System.Threading.Tasks; - public class UnhandledEventBehaviorContext : - BehaviorContextProxy, - UnhandledEventContext - where TSaga : class, ISaga + public partial class MassTransitStateMachine + where TInstance : class, SagaStateMachineInstance { - readonly BehaviorContext _context; - readonly StateMachine _machine; - - public UnhandledEventBehaviorContext(StateMachine machine, BehaviorContext context, State state) - : base(machine, context, context.Event) + class UnhandledEventBehaviorContext : + BehaviorContextProxy, + UnhandledEventContext { - _context = context; - CurrentState = state; - _machine = machine; - } + readonly BehaviorContext _context; + readonly StateMachine _machine; - public State CurrentState { get; } + public UnhandledEventBehaviorContext(StateMachine machine, BehaviorContext context, State state) + : base(machine, context, context.Event) + { + _context = context; + CurrentState = state; + _machine = machine; + } - public Event Event => _context.Event; + public State CurrentState { get; } - public Task Ignore() - { - return Task.CompletedTask; - } + public Event Event => _context.Event; - public Task Throw() - { - throw new UnhandledEventException(_machine.Name, _context.Event.Name, CurrentState.Name); + public Task Ignore() + { + return Task.CompletedTask; + } + + public Task Throw() + { + throw new UnhandledEventException(_machine.Name, _context.Event.Name, CurrentState.Name); + } } } } diff --git a/src/MassTransit/SagaStateMachine/ScheduleTimeSpanExtensions.cs b/src/MassTransit/SagaStateMachine/ScheduleTimeSpanExtensions.cs index 7ad8e3e6236..3edc6016bff 100644 --- a/src/MassTransit/SagaStateMachine/ScheduleTimeSpanExtensions.cs +++ b/src/MassTransit/SagaStateMachine/ScheduleTimeSpanExtensions.cs @@ -615,6 +615,7 @@ DateTime TimeProvider(BehaviorExceptionContext context return source.Add(new FaultedScheduleActivity(schedule, TimeProvider, MessageFactory.Create(messageFactory, callback))); } + public static ExceptionActivityBinder Schedule( this ExceptionActivityBinder source, Schedule schedule, Func, Task>> messageFactory, diff --git a/src/MassTransit/SagaStateMachine/StateMachineExtensions.cs b/src/MassTransit/SagaStateMachine/StateMachineExtensions.cs index dbdd715abe0..c783ecfc38f 100644 --- a/src/MassTransit/SagaStateMachine/StateMachineExtensions.cs +++ b/src/MassTransit/SagaStateMachine/StateMachineExtensions.cs @@ -14,7 +14,7 @@ public static class StateMachineExtensions /// /// The target state public static Task TransitionToState(this BehaviorContext context, State state) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { IStateAccessor accessor = context.StateMachine.Accessor; State toState = context.StateMachine.GetState(state.Name); diff --git a/src/MassTransit/SagaStateMachine/StateMachineIntrospectionExtensions.cs b/src/MassTransit/SagaStateMachine/StateMachineIntrospectionExtensions.cs index ad1fbb2ae44..28022f15df3 100644 --- a/src/MassTransit/SagaStateMachine/StateMachineIntrospectionExtensions.cs +++ b/src/MassTransit/SagaStateMachine/StateMachineIntrospectionExtensions.cs @@ -7,7 +7,7 @@ namespace MassTransit public static class StateMachineIntrospectionExtensions { public static async Task> NextEvents(this BehaviorContext context) - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { return context.StateMachine.NextEvents(await context.StateMachine.Accessor.Get(context)); } diff --git a/src/MassTransit/SagaStateMachine/StateMachineRequestExtensions.cs b/src/MassTransit/SagaStateMachine/StateMachineRequestExtensions.cs index 60e966f245b..9a4cfbb1e7d 100644 --- a/src/MassTransit/SagaStateMachine/StateMachineRequestExtensions.cs +++ b/src/MassTransit/SagaStateMachine/StateMachineRequestExtensions.cs @@ -645,37 +645,16 @@ public static EventActivityBinder Request /// /// + /// /// public static EventActivityBinder CancelRequestTimeout( - this EventActivityBinder binder, Request request) + this EventActivityBinder binder, Request request, bool completed = true) where TInstance : class, SagaStateMachineInstance where TRequest : class where TResponse : class where TData : class { - var activity = new CancelRequestTimeoutActivity(request); - - return binder.Add(activity); - } - - /// - /// Clears the requestId on the state - /// - /// - /// - /// - /// - /// - /// - /// - public static EventActivityBinder ClearRequest( - this EventActivityBinder binder, Request request) - where TInstance : class, SagaStateMachineInstance - where TRequest : class - where TResponse : class - where TData : class - { - var activity = new ClearRequestActivity(request); + var activity = new CancelRequestTimeoutActivity(request, completed); return binder.Add(activity); } diff --git a/src/MassTransit/SagaStateMachine/ThenExtensions.cs b/src/MassTransit/SagaStateMachine/ThenExtensions.cs index 24a69c7d1fc..73f816298c3 100644 --- a/src/MassTransit/SagaStateMachine/ThenExtensions.cs +++ b/src/MassTransit/SagaStateMachine/ThenExtensions.cs @@ -14,7 +14,7 @@ public static class ThenExtensions /// The event binder /// The synchronous delegate public static EventActivityBinder Then(this EventActivityBinder binder, Action> action) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return binder.Add(new ActionActivity(action)); } @@ -28,7 +28,7 @@ public static EventActivityBinder Then(this EventActivityBinderThe synchronous delegate public static ExceptionActivityBinder Then(this ExceptionActivityBinder binder, Action> action) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { return binder.Add(new FaultedActionActivity(action)); @@ -43,7 +43,7 @@ public static ExceptionActivityBinder Then /// The asynchronous delegate public static ExceptionActivityBinder ThenAsync(this ExceptionActivityBinder binder, Func, Task> asyncAction) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { return binder.Add(new AsyncFaultedActionActivity(asyncAction)); @@ -56,7 +56,7 @@ public static ExceptionActivityBinder ThenAsyncThe event binder /// The asynchronous delegate public static EventActivityBinder ThenAsync(this EventActivityBinder binder, Func, Task> action) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return binder.Add(new AsyncActivity(action)); } @@ -70,7 +70,7 @@ public static EventActivityBinder ThenAsync(this EventActivityBind /// The synchronous delegate public static EventActivityBinder Then(this EventActivityBinder binder, Action> action) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TData : class { return binder.Add(new ActionActivity(action)); @@ -87,7 +87,7 @@ public static EventActivityBinder Then(this EventAct public static ExceptionActivityBinder Then( this ExceptionActivityBinder binder, Action> action) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception where TData : class { @@ -105,7 +105,7 @@ public static ExceptionActivityBinder Then ThenAsync( this ExceptionActivityBinder binder, Func, Task> asyncAction) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception where TData : class { @@ -121,7 +121,7 @@ public static ExceptionActivityBinder ThenAsyncThe asynchronous delegate public static EventActivityBinder ThenAsync(this EventActivityBinder binder, Func, Task> action) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TData : class { return binder.Add(new AsyncActivity(action)); @@ -135,7 +135,7 @@ public static EventActivityBinder ThenAsync(this Eve /// The factory method which returns the activity to execute public static EventActivityBinder Execute(this EventActivityBinder binder, Func, IStateMachineActivity> activityFactory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { var activity = new FactoryActivity(activityFactory); return binder.Add(activity); @@ -148,7 +148,7 @@ public static EventActivityBinder Execute(this EventActivityBinder /// The event binder /// An existing activity public static EventActivityBinder Execute(this EventActivityBinder binder, IStateMachineActivity activity) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { return binder.Add(activity); } @@ -161,7 +161,7 @@ public static EventActivityBinder Execute(this EventActivityBinder /// The factory method which returns the activity to execute public static EventActivityBinder ExecuteAsync(this EventActivityBinder binder, Func, Task>> activityFactory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { var activity = new AsyncFactoryActivity(activityFactory); return binder.Add(activity); @@ -176,7 +176,7 @@ public static EventActivityBinder ExecuteAsync(this EventActivityB /// The factory method which returns the activity to execute public static EventActivityBinder Execute(this EventActivityBinder binder, Func, IStateMachineActivity> activityFactory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TData : class { var activity = new FactoryActivity(activityFactory); @@ -192,7 +192,7 @@ public static EventActivityBinder Execute(this Event /// The factory method which returns the activity to execute public static EventActivityBinder ExecuteAsync(this EventActivityBinder binder, Func, Task>> activityFactory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TData : class { var activity = new AsyncFactoryActivity(activityFactory); @@ -208,7 +208,7 @@ public static EventActivityBinder ExecuteAsync(this /// The factory method which returns the activity to execute public static EventActivityBinder Execute(this EventActivityBinder binder, Func, IStateMachineActivity> activityFactory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TData : class { var activity = new FactoryActivity(context => @@ -230,7 +230,7 @@ public static EventActivityBinder Execute(this Event /// The factory method which returns the activity to execute public static EventActivityBinder ExecuteAsync(this EventActivityBinder binder, Func, Task>> activityFactory) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TData : class { var activity = new AsyncFactoryActivity(async context => diff --git a/src/MassTransit/SagaStateMachine/TransitionExtensions.cs b/src/MassTransit/SagaStateMachine/TransitionExtensions.cs index 9dbf8fae81c..97c7c9fea25 100644 --- a/src/MassTransit/SagaStateMachine/TransitionExtensions.cs +++ b/src/MassTransit/SagaStateMachine/TransitionExtensions.cs @@ -10,7 +10,7 @@ public static class TransitionExtensions /// Transition the state machine to the specified state /// public static EventActivityBinder TransitionTo(this EventActivityBinder source, State toState) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { State state = source.StateMachine.GetState(toState.Name); @@ -29,7 +29,7 @@ public static EventActivityBinder TransitionTo(this EventActivityB /// public static ExceptionActivityBinder TransitionTo(this ExceptionActivityBinder source, State toState) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { State state = source.StateMachine.GetState(toState.Name); @@ -45,7 +45,7 @@ public static ExceptionActivityBinder TransitionTo public static EventActivityBinder TransitionTo(this EventActivityBinder source, State toState) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { State state = source.StateMachine.GetState(toState.Name); @@ -66,7 +66,7 @@ public static EventActivityBinder TransitionTo /// public static ExceptionActivityBinder TransitionTo( this ExceptionActivityBinder source, State toState) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception where TMessage : class { @@ -83,7 +83,7 @@ public static ExceptionActivityBinder TransitionTo< /// Transition the state machine to the Final state /// public static EventActivityBinder Finalize(this EventActivityBinder source) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TMessage : class { State state = source.StateMachine.GetState(source.StateMachine.Final.Name); @@ -97,7 +97,7 @@ public static EventActivityBinder Finalize(thi /// Transition the state machine to the Final state /// public static EventActivityBinder Finalize(this EventActivityBinder source) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance { State state = source.StateMachine.GetState(source.StateMachine.Final.Name); @@ -111,7 +111,7 @@ public static EventActivityBinder Finalize(this EventActivityBinde /// public static ExceptionActivityBinder Finalize( this ExceptionActivityBinder source) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception where TMessage : class { @@ -128,7 +128,7 @@ public static ExceptionActivityBinder Finalize public static ExceptionActivityBinder Finalize(this ExceptionActivityBinder source) - where TSaga : class, ISaga + where TSaga : class, SagaStateMachineInstance where TException : Exception { State state = source.StateMachine.GetState(source.StateMachine.Final.Name); diff --git a/src/MassTransit/Sagas/Configuration/CorrelatedSagaMessageConnector.cs b/src/MassTransit/Sagas/Configuration/CorrelatedSagaMessageConnector.cs index 995d992b905..45daacffa83 100644 --- a/src/MassTransit/Sagas/Configuration/CorrelatedSagaMessageConnector.cs +++ b/src/MassTransit/Sagas/Configuration/CorrelatedSagaMessageConnector.cs @@ -4,33 +4,34 @@ namespace MassTransit.Configuration using Middleware; - /// - /// Connects a message that has an exact CorrelationId to the saga instance - /// to the saga repository. - /// - /// - /// - public class CorrelatedSagaMessageConnector : - SagaMessageConnector + public partial class SagaConnector where TSaga : class, ISaga where TMessage : class { - readonly Func, Guid> _correlationIdSelector; - readonly ISagaPolicy _policy; - - public CorrelatedSagaMessageConnector(IFilter> consumeFilter, ISagaPolicy policy, - Func, Guid> correlationIdSelector) - : base(consumeFilter) + /// + /// Connects a message that has an exact CorrelationId to the saga instance + /// to the saga repository. + /// + public class CorrelatedSagaMessageConnector : + SagaMessageConnector { - _policy = policy; - _correlationIdSelector = correlationIdSelector; - } + readonly Func, Guid> _correlationIdSelector; + readonly ISagaPolicy _policy; - protected override void ConfigureMessagePipe(IPipeConfigurator> configurator, ISagaRepository repository, - IPipe> sagaPipe) - { - configurator.UseFilter(new CorrelationIdMessageFilter(_correlationIdSelector)); - configurator.UseFilter(new CorrelatedSagaFilter(repository, _policy, sagaPipe)); + public CorrelatedSagaMessageConnector(IFilter> consumeFilter, ISagaPolicy policy, + Func, Guid> correlationIdSelector) + : base(consumeFilter) + { + _policy = policy; + _correlationIdSelector = correlationIdSelector; + } + + protected override void ConfigureMessagePipe(IPipeConfigurator> configurator, ISagaRepository repository, + IPipe> sagaPipe) + { + configurator.UseFilter(new CorrelationIdMessageFilter(_correlationIdSelector)); + configurator.UseFilter(new CorrelatedSagaFilter(repository, _policy, sagaPipe)); + } } } } diff --git a/src/MassTransit/Sagas/Configuration/InitiatedByOrOrchestratesSagaConnectorFactory.cs b/src/MassTransit/Sagas/Configuration/InitiatedByOrOrchestratesSagaConnectorFactory.cs index ffd7f891218..c24f63c65dd 100644 --- a/src/MassTransit/Sagas/Configuration/InitiatedByOrOrchestratesSagaConnectorFactory.cs +++ b/src/MassTransit/Sagas/Configuration/InitiatedByOrOrchestratesSagaConnectorFactory.cs @@ -20,7 +20,7 @@ public InitiatedByOrOrchestratesSagaConnectorFactory() var policy = new NewOrExistingSagaPolicy(sagaFactory, false); - _connector = new CorrelatedSagaMessageConnector(consumeFilter, policy, x => x.Message.CorrelationId); + _connector = new SagaConnector.CorrelatedSagaMessageConnector(consumeFilter, policy, x => x.Message.CorrelationId); } ISagaMessageConnector ISagaConnectorFactory.CreateMessageConnector() diff --git a/src/MassTransit/Sagas/Configuration/InitiatedBySagaConnectorFactory.cs b/src/MassTransit/Sagas/Configuration/InitiatedBySagaConnectorFactory.cs index 02608aed0cd..2467cb763b9 100644 --- a/src/MassTransit/Sagas/Configuration/InitiatedBySagaConnectorFactory.cs +++ b/src/MassTransit/Sagas/Configuration/InitiatedBySagaConnectorFactory.cs @@ -20,7 +20,7 @@ public InitiatedBySagaConnectorFactory() var policy = new NewSagaPolicy(sagaFactory, false); - _connector = new CorrelatedSagaMessageConnector(consumeFilter, policy, x => x.Message.CorrelationId); + _connector = new SagaConnector.CorrelatedSagaMessageConnector(consumeFilter, policy, x => x.Message.CorrelationId); } ISagaMessageConnector ISagaConnectorFactory.CreateMessageConnector() diff --git a/src/MassTransit/Sagas/Configuration/ObservesSagaConnectorFactory.cs b/src/MassTransit/Sagas/Configuration/ObservesSagaConnectorFactory.cs index 66838fd8617..1f4c132678f 100644 --- a/src/MassTransit/Sagas/Configuration/ObservesSagaConnectorFactory.cs +++ b/src/MassTransit/Sagas/Configuration/ObservesSagaConnectorFactory.cs @@ -21,7 +21,7 @@ public ObservesSagaConnectorFactory() var consumeFilter = new ObservesSagaMessageFilter(); - _connector = new QuerySagaMessageConnector(consumeFilter, policy, queryFactory); + _connector = new SagaConnector.QuerySagaMessageConnector(consumeFilter, policy, queryFactory); } ISagaMessageConnector ISagaConnectorFactory.CreateMessageConnector() diff --git a/src/MassTransit/Sagas/Configuration/OrchestratesSagaConnectorFactory.cs b/src/MassTransit/Sagas/Configuration/OrchestratesSagaConnectorFactory.cs index 8b2066eb193..29db7f6d3ef 100644 --- a/src/MassTransit/Sagas/Configuration/OrchestratesSagaConnectorFactory.cs +++ b/src/MassTransit/Sagas/Configuration/OrchestratesSagaConnectorFactory.cs @@ -21,7 +21,9 @@ public OrchestratesSagaConnectorFactory() var consumeFilter = new OrchestratesSagaMessageFilter(); - _connector = new CorrelatedSagaMessageConnector(consumeFilter, policy, x => x.Message.CorrelationId); + _connector = new SagaConnector.CorrelatedSagaMessageConnector(consumeFilter, policy, x + => x.Message + .CorrelationId); } ISagaMessageConnector ISagaConnectorFactory.CreateMessageConnector() diff --git a/src/MassTransit/Sagas/Configuration/QuerySagaMessageConnector.cs b/src/MassTransit/Sagas/Configuration/QuerySagaMessageConnector.cs index 7ae0d7624a4..fedbf0dd103 100644 --- a/src/MassTransit/Sagas/Configuration/QuerySagaMessageConnector.cs +++ b/src/MassTransit/Sagas/Configuration/QuerySagaMessageConnector.cs @@ -3,26 +3,29 @@ namespace MassTransit.Configuration using Middleware; - public class QuerySagaMessageConnector : - SagaMessageConnector + public partial class SagaConnector where TSaga : class, ISaga where TMessage : class { - readonly ISagaPolicy _policy; - readonly ISagaQueryFactory _queryFactory; - - public QuerySagaMessageConnector(IFilter> consumeFilter, ISagaPolicy policy, - ISagaQueryFactory queryFactory) - : base(consumeFilter) + public class QuerySagaMessageConnector : + SagaMessageConnector { - _policy = policy; - _queryFactory = queryFactory; - } + readonly ISagaPolicy _policy; + readonly ISagaQueryFactory _queryFactory; - protected override void ConfigureMessagePipe(IPipeConfigurator> configurator, ISagaRepository repository, - IPipe> sagaPipe) - { - configurator.UseFilter(new QuerySagaFilter(repository, _policy, _queryFactory, sagaPipe)); + public QuerySagaMessageConnector(IFilter> consumeFilter, ISagaPolicy policy, + ISagaQueryFactory queryFactory) + : base(consumeFilter) + { + _policy = policy; + _queryFactory = queryFactory; + } + + protected override void ConfigureMessagePipe(IPipeConfigurator> configurator, ISagaRepository repository, + IPipe> sagaPipe) + { + configurator.UseFilter(new QuerySagaFilter(repository, _policy, _queryFactory, sagaPipe)); + } } } } diff --git a/src/MassTransit/Sagas/Configuration/SagaConnector.cs b/src/MassTransit/Sagas/Configuration/SagaConnector.cs index 35e17819d2e..1bcdfc45dbc 100644 --- a/src/MassTransit/Sagas/Configuration/SagaConnector.cs +++ b/src/MassTransit/Sagas/Configuration/SagaConnector.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Linq; + using Metadata; using Util; @@ -16,7 +17,7 @@ public SagaConnector() { try { - if (!MessageTypeCache.HasSagaInterfaces) + if (!RegistrationMetadata.IsSaga(typeof(TSaga))) throw new ConfigurationException("The specified type is does not support any saga methods: " + TypeCache.ShortName); _connectors = Initiates() @@ -43,7 +44,7 @@ ISagaSpecification ISagaConnector.CreateSagaSpecification() ConnectHandle ISagaConnector.ConnectSaga(IConsumePipeConnector consumePipe, ISagaRepository repository, ISagaSpecification specification) { - var handles = new List(); + var handles = new List(_connectors.Count); try { foreach (ISagaMessageConnector connector in _connectors.Cast>()) diff --git a/src/MassTransit/Sagas/Configuration/SagaConnectorCache.cs b/src/MassTransit/Sagas/Configuration/SagaConnectorCache.cs index fdf85adf843..beb611a38fe 100644 --- a/src/MassTransit/Sagas/Configuration/SagaConnectorCache.cs +++ b/src/MassTransit/Sagas/Configuration/SagaConnectorCache.cs @@ -1,7 +1,6 @@ namespace MassTransit.Configuration { using System; - using System.Threading; /// @@ -16,7 +15,7 @@ public class SagaConnectorCache : SagaConnectorCache() { - _connector = new Lazy>(() => new SagaConnector(), LazyThreadSafetyMode.PublicationOnly); + _connector = new Lazy>(() => new SagaConnector()); } public static ISagaConnector Connector => Cached.Instance.Value.Connector; @@ -26,8 +25,7 @@ public class SagaConnectorCache : static class Cached { - internal static readonly Lazy Instance = new Lazy( - () => new SagaConnectorCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy Instance = new Lazy(() => new SagaConnectorCache()); } } } diff --git a/src/MassTransit/Sagas/Configuration/SagaMessageConnector.cs b/src/MassTransit/Sagas/Configuration/SagaMessageConnector.cs index c84c5f9a05e..dd57a8d70b4 100644 --- a/src/MassTransit/Sagas/Configuration/SagaMessageConnector.cs +++ b/src/MassTransit/Sagas/Configuration/SagaMessageConnector.cs @@ -3,54 +3,56 @@ namespace MassTransit.Configuration using System; - public abstract class SagaMessageConnector : - ISagaMessageConnector + public partial class SagaConnector where TSaga : class, ISaga where TMessage : class { - readonly IFilter> _consumeFilter; - - protected SagaMessageConnector(IFilter> consumeFilter) + public abstract class SagaMessageConnector : + ISagaMessageConnector { - _consumeFilter = consumeFilter; - } - - protected virtual bool ConfigureConsumeTopology { get; } = true; + const ConnectPipeOptions NotConfigureConsumeTopology = ConnectPipeOptions.All & ~ConnectPipeOptions.ConfigureConsumeTopology; + readonly IFilter> _consumeFilter; - public Type MessageType => typeof(TMessage); - - public ISagaMessageSpecification CreateSagaMessageSpecification() - { - return new SagaMessageSpecification(); - } + protected SagaMessageConnector(IFilter> consumeFilter) + { + _consumeFilter = consumeFilter; + } - public ConnectHandle ConnectSaga(IConsumePipeConnector consumePipe, ISagaRepository repository, ISagaSpecification specification) - { - ISagaMessageSpecification messageSpecification = specification.GetMessageSpecification(); + protected virtual bool ConfigureConsumeTopology { get; } = true; - IPipe> consumerPipe = messageSpecification.BuildConsumerPipe(_consumeFilter); + public Type MessageType => typeof(TMessage); - IPipe> messagePipe = messageSpecification.BuildMessagePipe(x => + public ISagaMessageSpecification CreateSagaMessageSpecification() { - specification.ConfigureMessagePipe(x); - - ConfigureMessagePipe(x, repository, consumerPipe); - }); + return new SagaMessageSpecification(); + } - return ConfigureConsumeTopology - ? consumePipe.ConnectConsumePipe(messagePipe) - : consumePipe.ConnectConsumePipe(messagePipe, NotConfigureConsumeTopology); + public ConnectHandle ConnectSaga(IConsumePipeConnector consumePipe, ISagaRepository repository, ISagaSpecification specification) + { + ISagaMessageSpecification messageSpecification = specification.GetMessageSpecification(); + + IPipe> consumerPipe = messageSpecification.BuildConsumerPipe(_consumeFilter); + + IPipe> messagePipe = messageSpecification.BuildMessagePipe(x => + { + specification.ConfigureMessagePipe(x); + + ConfigureMessagePipe(x, repository, consumerPipe); + }); + + return ConfigureConsumeTopology + ? consumePipe.ConnectConsumePipe(messagePipe) + : consumePipe.ConnectConsumePipe(messagePipe, NotConfigureConsumeTopology); + } + + /// + /// Configure the message pipe that is prior to the saga repository + /// + /// The pipe configurator + /// + /// + protected abstract void ConfigureMessagePipe(IPipeConfigurator> configurator, ISagaRepository repository, + IPipe> sagaPipe); } - - const ConnectPipeOptions NotConfigureConsumeTopology = ConnectPipeOptions.All & ~ConnectPipeOptions.ConfigureConsumeTopology; - - /// - /// Configure the message pipe that is prior to the saga repository - /// - /// The pipe configurator - /// - /// - protected abstract void ConfigureMessagePipe(IPipeConfigurator> configurator, ISagaRepository repository, - IPipe> sagaPipe); } } diff --git a/src/MassTransit/Sagas/Configuration/SagaMessageSpecification.cs b/src/MassTransit/Sagas/Configuration/SagaMessageSpecification.cs index 89b1f8d7220..9c4539d4874 100644 --- a/src/MassTransit/Sagas/Configuration/SagaMessageSpecification.cs +++ b/src/MassTransit/Sagas/Configuration/SagaMessageSpecification.cs @@ -4,99 +4,103 @@ using System.Collections.Generic; - /// - /// Configures the pipe for a Saga/message combination within a Saga configuration - /// block. Does not add any handlers to the message pipe standalone, everything is within - /// the Saga pipe segment. - /// - /// - /// - public class SagaMessageSpecification : - ISagaMessageSpecification - where TMessage : class + public partial class SagaConnector where TSaga : class, ISaga + where TMessage : class { - readonly IBuildPipeConfigurator> _configurator; - readonly IBuildPipeConfigurator> _messagePipeConfigurator; - readonly SagaConfigurationObservable _observers; - - public SagaMessageSpecification() - { - _configurator = new PipeConfigurator>(); - _messagePipeConfigurator = new PipeConfigurator>(); - _observers = new SagaConfigurationObservable(); - } - - public IEnumerable Validate() + /// + /// Configures the pipe for a Saga/message combination within a Saga configuration + /// block. Does not add any handlers to the message pipe standalone, everything is within + /// the Saga pipe segment. + /// + public class SagaMessageSpecification : + ISagaMessageSpecification { - return _configurator.Validate(); - } + readonly IBuildPipeConfigurator> _configurator; + readonly IBuildPipeConfigurator> _messagePipeConfigurator; + readonly SagaConfigurationObservable _observers; - public Type MessageType => typeof(TMessage); + public SagaMessageSpecification() + { + _configurator = new PipeConfigurator>(); + _messagePipeConfigurator = new PipeConfigurator>(); + _observers = new SagaConfigurationObservable(); + } - ISagaMessageSpecification ISagaMessageSpecification.GetMessageSpecification() - { - if (this is ISagaMessageSpecification result) - return result; + public IEnumerable Validate() + { + return _configurator.Validate(); + } - throw new ArgumentException($"The message type was invalid: {TypeCache.ShortName}"); - } + public Type MessageType => typeof(TMessage); - public void AddPipeSpecification(IPipeSpecification> specification) - { - _configurator.AddPipeSpecification(specification); - } + ISagaMessageSpecification ISagaMessageSpecification.GetMessageSpecification() + { + if (this is ISagaMessageSpecification result) + return result; - public void AddPipeSpecification(IPipeSpecification> specification) - { - _messagePipeConfigurator.AddPipeSpecification(specification); - } + throw new ArgumentException($"The message type was invalid: {TypeCache.ShortName}"); + } - public IPipe> BuildConsumerPipe(IFilter> consumeFilter) - { - _observers.ForEach(observer => observer.SagaMessageConfigured(this)); + public void AddPipeSpecification(IPipeSpecification> specification) + { + _configurator.AddPipeSpecification(specification); + } - _configurator.UseFilter(consumeFilter); + public void AddPipeSpecification(IPipeSpecification> specification) + { + _messagePipeConfigurator.AddPipeSpecification(specification); + } - return _configurator.Build(); - } + public IPipe> BuildConsumerPipe(IFilter> consumeFilter) + { + _observers.ForEach(observer => observer.SagaMessageConfigured(this)); - public IPipe> BuildMessagePipe(Action>> configure) - { - configure?.Invoke(_messagePipeConfigurator); + if (_configurator == null) + throw new ArgumentNullException(nameof(_configurator)); - return _messagePipeConfigurator.Build(); - } + _configurator.AddPipeSpecification(new FilterPipeSpecification>(consumeFilter)); - public void AddPipeSpecification(IPipeSpecification> specification) - { - _configurator.AddPipeSpecification(new SagaPipeSpecificationProxy(specification)); - } + return _configurator.Build(); + } - public ConnectHandle ConnectSagaConfigurationObserver(ISagaConfigurationObserver observer) - { - return _observers.Connect(observer); - } + public IPipe> BuildMessagePipe(Action>> configure) + { + configure?.Invoke(_messagePipeConfigurator); - public void Message(Action> configure) - { - configure?.Invoke(new SagaMessageConfigurator(_configurator)); - } + return _messagePipeConfigurator.Build(); + } + public void AddPipeSpecification(IPipeSpecification> specification) + { + _configurator.AddPipeSpecification(new SagaPipeSpecificationProxy(specification)); + } - class SagaMessageConfigurator : - ISagaMessageConfigurator - { - readonly IPipeConfigurator> _configurator; + public ConnectHandle ConnectSagaConfigurationObserver(ISagaConfigurationObserver observer) + { + return _observers.Connect(observer); + } - public SagaMessageConfigurator(IPipeConfigurator> configurator) + public void Message(Action> configure) { - _configurator = configurator; + configure?.Invoke(new SagaMessageConfigurator(_configurator)); } - public void AddPipeSpecification(IPipeSpecification> specification) + + class SagaMessageConfigurator : + ISagaMessageConfigurator { - _configurator.AddPipeSpecification(new SagaPipeSpecificationProxy(specification)); + readonly IPipeConfigurator> _configurator; + + public SagaMessageConfigurator(IPipeConfigurator> configurator) + { + _configurator = configurator; + } + + public void AddPipeSpecification(IPipeSpecification> specification) + { + _configurator.AddPipeSpecification(new SagaPipeSpecificationProxy(specification)); + } } } } diff --git a/src/MassTransit/Sagas/Configuration/SagaMessageSplitFilterSpecification.cs b/src/MassTransit/Sagas/Configuration/SagaMessageSplitFilterSpecification.cs index 4395e2f666e..63996679a02 100644 --- a/src/MassTransit/Sagas/Configuration/SagaMessageSplitFilterSpecification.cs +++ b/src/MassTransit/Sagas/Configuration/SagaMessageSplitFilterSpecification.cs @@ -4,43 +4,46 @@ namespace MassTransit.Configuration using Middleware; - public class SagaMessageSplitFilterSpecification : - IPipeSpecification> - where TMessage : class + public partial class SagaConnector where TSaga : class, ISaga + where TMessage : class { - readonly IPipeSpecification> _specification; - - public SagaMessageSplitFilterSpecification(IPipeSpecification> specification) - { - _specification = specification; - } - - public void Apply(IPipeBuilder> builder) - { - _specification.Apply(new BuilderProxy(builder)); - } - - public IEnumerable Validate() + public class SagaMessageSplitFilterSpecification : + IPipeSpecification> { - foreach (var validationResult in _specification.Validate()) - yield return validationResult; - } + readonly IPipeSpecification> _specification; + public SagaMessageSplitFilterSpecification(IPipeSpecification> specification) + { + _specification = specification; + } - class BuilderProxy : - IPipeBuilder> - { - readonly IPipeBuilder> _builder; + public void Apply(IPipeBuilder> builder) + { + _specification.Apply(new BuilderProxy(builder)); + } - public BuilderProxy(IPipeBuilder> builder) + public IEnumerable Validate() { - _builder = builder; + foreach (var validationResult in _specification.Validate()) + yield return validationResult; } - public void AddFilter(IFilter> filter) + + class BuilderProxy : + IPipeBuilder> { - _builder.AddFilter(new SagaMessageSplitFilter(filter)); + readonly IPipeBuilder> _builder; + + public BuilderProxy(IPipeBuilder> builder) + { + _builder = builder; + } + + public void AddFilter(IFilter> filter) + { + _builder.AddFilter(new SagaMessageSplitFilter(filter)); + } } } } diff --git a/src/MassTransit/Sagas/Configuration/SagaMetadataCache.cs b/src/MassTransit/Sagas/Configuration/SagaMetadataCache.cs index a3b38facac4..d7d697841bf 100644 --- a/src/MassTransit/Sagas/Configuration/SagaMetadataCache.cs +++ b/src/MassTransit/Sagas/Configuration/SagaMetadataCache.cs @@ -3,7 +3,6 @@ namespace MassTransit.Configuration using System; using System.Collections.Generic; using System.Linq; - using System.Reflection; using System.Threading; using System.Threading.Tasks; using Saga; @@ -13,19 +12,10 @@ public class SagaMetadataCache : ISagaMetadataCache where TSaga : class, ISaga { - readonly SagaInterfaceType[] _initiatedByOrOrchestratesTypes; - readonly SagaInterfaceType[] _initiatedByTypes; - readonly SagaInterfaceType[] _observesTypes; - readonly SagaInterfaceType[] _orchestratesTypes; SagaInstanceFactoryMethod _factoryMethod; SagaMetadataCache() { - _initiatedByTypes = GetInitiatingTypes().ToArray(); - _orchestratesTypes = GetOrchestratingTypes().ToArray(); - _observesTypes = GetObservingTypes().ToArray(); - _initiatedByOrOrchestratesTypes = GetInitiatingOrOrchestratingTypes().ToArray(); - GetActivatorSagaInstanceFactoryMethod(); } @@ -34,11 +24,12 @@ public class SagaMetadataCache : public static SagaInterfaceType[] ObservesTypes => Cached.Instance.Value.ObservesTypes; public static SagaInterfaceType[] InitiatedByOrOrchestratesTypes => Cached.Instance.Value.InitiatedByOrOrchestratesTypes; public static SagaInstanceFactoryMethod FactoryMethod => Cached.Instance.Value.FactoryMethod; + SagaInstanceFactoryMethod ISagaMetadataCache.FactoryMethod => _factoryMethod; - SagaInterfaceType[] ISagaMetadataCache.InitiatedByTypes => _initiatedByTypes; - SagaInterfaceType[] ISagaMetadataCache.OrchestratesTypes => _orchestratesTypes; - SagaInterfaceType[] ISagaMetadataCache.ObservesTypes => _observesTypes; - SagaInterfaceType[] ISagaMetadataCache.InitiatedByOrOrchestratesTypes => _initiatedByOrOrchestratesTypes; + SagaInterfaceType[] ISagaMetadataCache.InitiatedByTypes => GetInitiatingTypes().ToArray(); + SagaInterfaceType[] ISagaMetadataCache.OrchestratesTypes => GetOrchestratingTypes().ToArray(); + SagaInterfaceType[] ISagaMetadataCache.ObservesTypes => GetObservingTypes().ToArray(); + SagaInterfaceType[] ISagaMetadataCache.InitiatedByOrOrchestratesTypes => GetInitiatingOrOrchestratingTypes().ToArray(); void GetActivatorSagaInstanceFactoryMethod() { @@ -119,7 +110,7 @@ async Task GeneratePropertyFactoryMethodAsynchronously() static IEnumerable GetInitiatingTypes() { return typeof(TSaga).GetInterfaces() - .Where(x => x.GetTypeInfo().IsGenericType) + .Where(x => x.IsGenericType) .Where(x => x.GetGenericTypeDefinition() == typeof(InitiatedBy<>)) .Select(x => new SagaInterfaceType(x, x.GetGenericArguments()[0], typeof(TSaga))) .Where(x => MessageTypeCache.IsValidMessageType(x.MessageType)); @@ -128,7 +119,7 @@ static IEnumerable GetInitiatingTypes() static IEnumerable GetOrchestratingTypes() { return typeof(TSaga).GetInterfaces() - .Where(x => x.GetTypeInfo().IsGenericType) + .Where(x => x.IsGenericType) .Where(x => x.GetGenericTypeDefinition() == typeof(Orchestrates<>)) .Select(x => new SagaInterfaceType(x, x.GetGenericArguments()[0], typeof(TSaga))) .Where(x => MessageTypeCache.IsValidMessageType(x.MessageType)); @@ -137,7 +128,7 @@ static IEnumerable GetOrchestratingTypes() static IEnumerable GetObservingTypes() { return typeof(TSaga).GetInterfaces() - .Where(x => x.GetTypeInfo().IsGenericType) + .Where(x => x.IsGenericType) .Where(x => x.GetGenericTypeDefinition() == typeof(Observes<,>)) .Select(x => new SagaInterfaceType(x, x.GetGenericArguments()[0], typeof(TSaga))) .Where(x => MessageTypeCache.IsValidMessageType(x.MessageType)); @@ -146,7 +137,7 @@ static IEnumerable GetObservingTypes() static IEnumerable GetInitiatingOrOrchestratingTypes() { return typeof(TSaga).GetInterfaces() - .Where(x => x.GetTypeInfo().IsGenericType) + .Where(x => x.IsGenericType) .Where(x => x.GetGenericTypeDefinition() == typeof(InitiatedByOrOrchestrates<>)) .Select(x => new SagaInterfaceType(x, x.GetGenericArguments()[0], typeof(TSaga))) .Where(x => MessageTypeCache.IsValidMessageType(x.MessageType)); @@ -155,8 +146,7 @@ static IEnumerable GetInitiatingOrOrchestratingTypes() static class Cached { - internal static readonly Lazy> Instance = new Lazy>( - () => new SagaMetadataCache(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy> Instance = new Lazy>(() => new SagaMetadataCache()); } } } diff --git a/src/MassTransit/Sagas/Configuration/SagaPipeSpecificationProxy.cs b/src/MassTransit/Sagas/Configuration/SagaPipeSpecificationProxy.cs index 274dde39e54..928f53dde3c 100644 --- a/src/MassTransit/Sagas/Configuration/SagaPipeSpecificationProxy.cs +++ b/src/MassTransit/Sagas/Configuration/SagaPipeSpecificationProxy.cs @@ -4,37 +4,40 @@ namespace MassTransit.Configuration using System.Collections.Generic; - public class SagaPipeSpecificationProxy : - IPipeSpecification> + public partial class SagaConnector where TSaga : class, ISaga where TMessage : class { - readonly IPipeSpecification> _specification; - - public SagaPipeSpecificationProxy(IPipeSpecification> specification) + public class SagaPipeSpecificationProxy : + IPipeSpecification> { - if (specification == null) - throw new ArgumentNullException(nameof(specification)); + readonly IPipeSpecification> _specification; - _specification = new SagaSplitFilterSpecification(specification); - } + public SagaPipeSpecificationProxy(IPipeSpecification> specification) + { + if (specification == null) + throw new ArgumentNullException(nameof(specification)); - public SagaPipeSpecificationProxy(IPipeSpecification> specification) - { - if (specification == null) - throw new ArgumentNullException(nameof(specification)); + _specification = new SagaSplitFilterSpecification(specification); + } - _specification = new SagaMessageSplitFilterSpecification(specification); - } + public SagaPipeSpecificationProxy(IPipeSpecification> specification) + { + if (specification == null) + throw new ArgumentNullException(nameof(specification)); - public void Apply(IPipeBuilder> builder) - { - _specification.Apply(builder); - } + _specification = new SagaMessageSplitFilterSpecification(specification); + } - public IEnumerable Validate() - { - return _specification.Validate(); + public void Apply(IPipeBuilder> builder) + { + _specification.Apply(builder); + } + + public IEnumerable Validate() + { + return _specification.Validate(); + } } } } diff --git a/src/MassTransit/Sagas/Configuration/SagaSplitFilterSpecification.cs b/src/MassTransit/Sagas/Configuration/SagaSplitFilterSpecification.cs index 8e76c3444c6..69f377e74f9 100644 --- a/src/MassTransit/Sagas/Configuration/SagaSplitFilterSpecification.cs +++ b/src/MassTransit/Sagas/Configuration/SagaSplitFilterSpecification.cs @@ -4,42 +4,45 @@ namespace MassTransit.Configuration using Middleware; - public class SagaSplitFilterSpecification : - IPipeSpecification> - where TMessage : class + public partial class SagaConnector where TSaga : class, ISaga + where TMessage : class { - readonly IPipeSpecification> _specification; - - public SagaSplitFilterSpecification(IPipeSpecification> specification) - { - _specification = specification; - } - - public void Apply(IPipeBuilder> builder) - { - _specification.Apply(new BuilderProxy(builder)); - } - - public IEnumerable Validate() + public class SagaSplitFilterSpecification : + IPipeSpecification> { - return _specification.Validate(); - } + readonly IPipeSpecification> _specification; + public SagaSplitFilterSpecification(IPipeSpecification> specification) + { + _specification = specification; + } - class BuilderProxy : - IPipeBuilder> - { - readonly IPipeBuilder> _builder; + public void Apply(IPipeBuilder> builder) + { + _specification.Apply(new BuilderProxy(builder)); + } - public BuilderProxy(IPipeBuilder> builder) + public IEnumerable Validate() { - _builder = builder; + return _specification.Validate(); } - public void AddFilter(IFilter> filter) + + class BuilderProxy : + IPipeBuilder> { - _builder.AddFilter(new SagaSplitFilter(filter)); + readonly IPipeBuilder> _builder; + + public BuilderProxy(IPipeBuilder> builder) + { + _builder = builder; + } + + public void AddFilter(IFilter> filter) + { + _builder.AddFilter(new SagaSplitFilter(filter)); + } } } } diff --git a/src/MassTransit/Sagas/InMemorySagaRepository.cs b/src/MassTransit/Sagas/InMemorySagaRepository.cs index 0872babea55..92e378dcfe4 100644 --- a/src/MassTransit/Sagas/InMemorySagaRepository.cs +++ b/src/MassTransit/Sagas/InMemorySagaRepository.cs @@ -12,8 +12,7 @@ public class InMemorySagaRepository : ILoadSagaRepository where TSaga : class, ISaga { - readonly ISagaRepository _repository; - readonly ISagaRepositoryContextFactory _repositoryContextFactory; + readonly SagaRepository _repository; readonly IndexedSagaDictionary _sagas; public InMemorySagaRepository() @@ -22,9 +21,9 @@ public InMemorySagaRepository() var factory = new InMemorySagaConsumeContextFactory(); - _repositoryContextFactory = new InMemorySagaRepositoryContextFactory(_sagas, factory); + var repositoryContextFactory = new InMemorySagaRepositoryContextFactory(_sagas, factory); - _repository = new SagaRepository(_repositoryContextFactory); + _repository = new SagaRepository(repositoryContextFactory, repositoryContextFactory, repositoryContextFactory); } public SagaInstance this[Guid id] => _sagas[id]; @@ -33,12 +32,12 @@ public InMemorySagaRepository() public Task Load(Guid correlationId) { - return _repositoryContextFactory.Execute(context => context.Load(correlationId)); + return _repository.Load(correlationId); } public Task> Find(ISagaQuery query) { - return _repositoryContextFactory.Execute>(async context => await context.Query(query).ConfigureAwait(false)); + return _repository.Find(query); } void IProbeSite.Probe(ProbeContext context) diff --git a/src/MassTransit/Sagas/Saga/DefaultSagaRepositoryQueryContext.cs b/src/MassTransit/Sagas/Saga/DefaultSagaRepositoryQueryContext.cs index 8a21965aa5c..8b9398cd03d 100644 --- a/src/MassTransit/Sagas/Saga/DefaultSagaRepositoryQueryContext.cs +++ b/src/MassTransit/Sagas/Saga/DefaultSagaRepositoryQueryContext.cs @@ -90,13 +90,13 @@ public class DefaultSagaRepositoryQueryContext : SagaRepositoryQueryContext where TSaga : class, ISaga { - readonly SagaRepositoryContext _context; + readonly QuerySagaRepositoryContext _queryContext; readonly IList _results; - public DefaultSagaRepositoryQueryContext(SagaRepositoryContext context, IList results) - : base(context) + public DefaultSagaRepositoryQueryContext(QuerySagaRepositoryContext queryContext, IList results) + : base(queryContext) { - _context = context; + _queryContext = queryContext; _results = results; } @@ -104,12 +104,7 @@ public DefaultSagaRepositoryQueryContext(SagaRepositoryContext context, I public Task> Query(ISagaQuery query, CancellationToken cancellationToken) { - return _context.Query(query, cancellationToken); - } - - public Task Load(Guid correlationId) - { - return _context.Load(correlationId); + return _queryContext.Query(query, cancellationToken); } public IEnumerator GetEnumerator() diff --git a/src/MassTransit/Sagas/Saga/ILoadSagaRepositoryContextFactory.cs b/src/MassTransit/Sagas/Saga/ILoadSagaRepositoryContextFactory.cs new file mode 100644 index 00000000000..f28ba20237f --- /dev/null +++ b/src/MassTransit/Sagas/Saga/ILoadSagaRepositoryContextFactory.cs @@ -0,0 +1,25 @@ +namespace MassTransit.Saga +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + + /// + /// + /// + public interface ILoadSagaRepositoryContextFactory : + IProbeSite + where TSaga : class, ISaga + { + /// + /// Create a and send it to the next pipe. + /// + /// + /// + /// + /// + Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class; + } +} diff --git a/src/MassTransit/Sagas/Saga/IQuerySagaRepositoryContextFactory.cs b/src/MassTransit/Sagas/Saga/IQuerySagaRepositoryContextFactory.cs new file mode 100644 index 00000000000..ac027add0e8 --- /dev/null +++ b/src/MassTransit/Sagas/Saga/IQuerySagaRepositoryContextFactory.cs @@ -0,0 +1,25 @@ +namespace MassTransit.Saga +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + + /// + /// + /// + public interface IQuerySagaRepositoryContextFactory : + IProbeSite + where TSaga : class, ISaga + { + /// + /// Create a and send it to the next pipe. + /// + /// + /// + /// + /// + Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class; + } +} diff --git a/src/MassTransit/Sagas/Saga/ISagaConsumeContextFactory.cs b/src/MassTransit/Sagas/Saga/ISagaConsumeContextFactory.cs index bf4fc9afe0d..d8c9b9d4676 100644 --- a/src/MassTransit/Sagas/Saga/ISagaConsumeContextFactory.cs +++ b/src/MassTransit/Sagas/Saga/ISagaConsumeContextFactory.cs @@ -4,7 +4,7 @@ namespace MassTransit.Saga /// - /// Creates the as needed by the . + /// Creates the as needed by the . /// /// /// @@ -15,7 +15,7 @@ public interface ISagaConsumeContextFactory /// /// Create a new . /// - /// The + /// The /// The message consume context being delivered to the saga /// The saga instance /// The creation mode of the saga instance @@ -28,7 +28,7 @@ Task> CreateSagaConsumeContext(TContext context, /// - /// Creates the as needed by the . + /// Creates the as needed by the . /// /// public interface ISagaConsumeContextFactory diff --git a/src/MassTransit/Sagas/Saga/ISagaRepositoryContextFactory.cs b/src/MassTransit/Sagas/Saga/ISagaRepositoryContextFactory.cs index 66059d925ea..4d808905d55 100644 --- a/src/MassTransit/Sagas/Saga/ISagaRepositoryContextFactory.cs +++ b/src/MassTransit/Sagas/Saga/ISagaRepositoryContextFactory.cs @@ -1,7 +1,5 @@ namespace MassTransit.Saga { - using System; - using System.Threading; using System.Threading.Tasks; @@ -32,15 +30,5 @@ Task Send(ConsumeContext context, IPipe> n /// Task SendQuery(ConsumeContext context, ISagaQuery query, IPipe> next) where T : class; - - /// - /// Create a and send it to the next pipe. - /// - /// - /// - /// - /// - Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) - where T : class; } } diff --git a/src/MassTransit/Sagas/Saga/InMemoryRepository/InMemorySagaRepositoryContext.cs b/src/MassTransit/Sagas/Saga/InMemoryRepository/InMemorySagaRepositoryContext.cs index 36cbabc9377..405f08f45cb 100644 --- a/src/MassTransit/Sagas/Saga/InMemoryRepository/InMemorySagaRepositoryContext.cs +++ b/src/MassTransit/Sagas/Saga/InMemoryRepository/InMemorySagaRepositoryContext.cs @@ -182,7 +182,8 @@ public Task> CreateSagaConsumeContext(ConsumeCon public class InMemorySagaRepositoryContext : BasePipeContext, - SagaRepositoryContext + QuerySagaRepositoryContext, + LoadSagaRepositoryContext where TSaga : class, ISaga { readonly IndexedSagaDictionary _sagas; diff --git a/src/MassTransit/Sagas/Saga/InMemoryRepository/InMemorySagaRepositoryContextFactory.cs b/src/MassTransit/Sagas/Saga/InMemoryRepository/InMemorySagaRepositoryContextFactory.cs index 344681af60b..5966651155e 100644 --- a/src/MassTransit/Sagas/Saga/InMemoryRepository/InMemorySagaRepositoryContextFactory.cs +++ b/src/MassTransit/Sagas/Saga/InMemoryRepository/InMemorySagaRepositoryContextFactory.cs @@ -12,7 +12,9 @@ namespace MassTransit.Saga /// /// public class InMemorySagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + IQuerySagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISaga { readonly ISagaConsumeContextFactory, TSaga> _factory; @@ -24,6 +26,18 @@ public InMemorySagaRepositoryContextFactory(IndexedSagaDictionary sagas, _factory = factory; } + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + public void Probe(ProbeContext context) { context.Add("count", _sagas.Count); @@ -54,7 +68,7 @@ public async Task SendQuery(ConsumeContext context, ISagaQuery quer await next.Send(queryContext).ConfigureAwait(false); } - public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken) + Task ExecuteAsyncMethod(Func, Task> asyncMethod, CancellationToken cancellationToken) where T : class { var repositoryContext = new InMemorySagaRepositoryContext(_sagas, cancellationToken); diff --git a/src/MassTransit/Sagas/Saga/LoadSagaRepository.cs b/src/MassTransit/Sagas/Saga/LoadSagaRepository.cs new file mode 100644 index 00000000000..63809d12972 --- /dev/null +++ b/src/MassTransit/Sagas/Saga/LoadSagaRepository.cs @@ -0,0 +1,34 @@ +namespace MassTransit.Saga +{ + using System; + using System.Threading.Tasks; + + + /// + /// The modern query saga repository, which can be used with any storage engine. Leverages the new interfaces for query context. + /// + /// + public class LoadSagaRepository : + ILoadSagaRepository + where TSaga : class, ISaga + { + readonly ILoadSagaRepositoryContextFactory _repositoryContextFactory; + + public LoadSagaRepository(ILoadSagaRepositoryContextFactory repositoryContextFactory) + { + _repositoryContextFactory = repositoryContextFactory; + } + + public Task Load(Guid correlationId) + { + return _repositoryContextFactory.Execute(context => context.Load(correlationId)); + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateScope("loadSagaRepository"); + + _repositoryContextFactory.Probe(scope); + } + } +} diff --git a/src/MassTransit/Sagas/Saga/LoadedSagaRepositoryQueryContext.cs b/src/MassTransit/Sagas/Saga/LoadedSagaRepositoryQueryContext.cs index 38abda12275..616411d483f 100644 --- a/src/MassTransit/Sagas/Saga/LoadedSagaRepositoryQueryContext.cs +++ b/src/MassTransit/Sagas/Saga/LoadedSagaRepositoryQueryContext.cs @@ -105,12 +105,12 @@ public class LoadedSagaRepositoryQueryContext : where TSaga : class, ISaga { readonly IDictionary _index; - readonly SagaRepositoryContext _repositoryContext; + readonly QuerySagaRepositoryContext _querySagaRepositoryContext; - public LoadedSagaRepositoryQueryContext(SagaRepositoryContext repositoryContext, IEnumerable instances) - : base(repositoryContext) + public LoadedSagaRepositoryQueryContext(QuerySagaRepositoryContext querySagaRepositoryContext, IEnumerable instances) + : base(querySagaRepositoryContext) { - _repositoryContext = repositoryContext; + _querySagaRepositoryContext = querySagaRepositoryContext; _index = instances.ToDictionary(x => x.CorrelationId); } @@ -119,15 +119,7 @@ public LoadedSagaRepositoryQueryContext(SagaRepositoryContext repositoryC public Task> Query(ISagaQuery query, CancellationToken cancellationToken = default) { - return _repositoryContext.Query(query, cancellationToken); - } - - public Task Load(Guid correlationId) - { - if (_index.TryGetValue(correlationId, out var instance)) - return Task.FromResult(instance); - - return _repositoryContext.Load(correlationId); + return _querySagaRepositoryContext.Query(query, cancellationToken); } public IEnumerator GetEnumerator() diff --git a/src/MassTransit/Sagas/Saga/QuerySagaRepository.cs b/src/MassTransit/Sagas/Saga/QuerySagaRepository.cs new file mode 100644 index 00000000000..bbaa4dbef5f --- /dev/null +++ b/src/MassTransit/Sagas/Saga/QuerySagaRepository.cs @@ -0,0 +1,35 @@ +namespace MassTransit.Saga +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + + /// + /// The modern query saga repository, which can be used with any storage engine. Leverages the new interfaces for query context. + /// + /// + public class QuerySagaRepository : + IQuerySagaRepository + where TSaga : class, ISaga + { + readonly IQuerySagaRepositoryContextFactory _repositoryContextFactory; + + public QuerySagaRepository(IQuerySagaRepositoryContextFactory repositoryContextFactory) + { + _repositoryContextFactory = repositoryContextFactory; + } + + public Task> Find(ISagaQuery query) + { + return _repositoryContextFactory.Execute>(async context => await context.Query(query).ConfigureAwait(false)); + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateScope("querySagaRepository"); + + _repositoryContextFactory.Probe(scope); + } + } +} diff --git a/src/MassTransit/Sagas/Saga/QuerySagaRepositoryContext.cs b/src/MassTransit/Sagas/Saga/QuerySagaRepositoryContext.cs new file mode 100644 index 00000000000..96b72d5cbb1 --- /dev/null +++ b/src/MassTransit/Sagas/Saga/QuerySagaRepositoryContext.cs @@ -0,0 +1,101 @@ +namespace MassTransit.Saga +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + + public interface SagaRepositoryContext : + ISagaConsumeContextFactory, + ConsumeContext + where TSaga : class, ISaga + where TMessage : class + { + /// + /// Add the saga instance, using the specified + /// + /// + /// + Task> Add(TSaga instance); + + /// + /// Insert the saga instance, if it does not already exist. + /// + /// + /// + /// A valid if the instance inserted successfully, otherwise default + /// + Task> Insert(TSaga instance); + + /// + /// Load an existing saga instance + /// + /// + /// + /// A valid if the instance loaded successfully, otherwise default + /// + Task> Load(Guid correlationId); + + /// + /// Save the saga, called after an Add, without an insert + /// + /// + /// + Task Save(SagaConsumeContext context); + + /// + /// Update the saga, called after a load or insert where the saga has not completed + /// + /// + /// + Task Update(SagaConsumeContext context); + + /// + /// Delete the saga, called after a Load when the saga is completed + /// + /// + /// + Task Delete(SagaConsumeContext context); + + /// + /// Discard the saga, called after an Add when the saga is completed within the same transaction + /// + /// + /// + Task Discard(SagaConsumeContext context); + + /// + /// Undo the changes for the saga + /// + /// + /// + Task Undo(SagaConsumeContext context); + } + + + public interface QuerySagaRepositoryContext : + PipeContext + where TSaga : class, ISaga + { + /// + /// Query saga instances + /// + /// + /// + /// + Task> Query(ISagaQuery query, CancellationToken cancellationToken = default); + } + + + public interface LoadSagaRepositoryContext : + PipeContext + where TSaga : class, ISaga + { + /// + /// Load an existing saga instance + /// + /// + /// The saga, if found, or null + Task Load(Guid correlationId); + } +} diff --git a/src/MassTransit/Sagas/Saga/SagaRepository.cs b/src/MassTransit/Sagas/Saga/SagaRepository.cs index 90ea07820bd..15e034fcf6d 100644 --- a/src/MassTransit/Sagas/Saga/SagaRepository.cs +++ b/src/MassTransit/Sagas/Saga/SagaRepository.cs @@ -2,6 +2,7 @@ namespace MassTransit.Saga { using System; using System.Collections.Generic; + using System.Threading; using System.Threading.Tasks; using Middleware; @@ -16,21 +17,27 @@ public class SagaRepository : ILoadSagaRepository where TSaga : class, ISaga { + readonly ILoadSagaRepository _loadSagaRepository; + readonly QuerySagaRepository _querySagaRepository; readonly ISagaRepositoryContextFactory _repositoryContextFactory; - public SagaRepository(ISagaRepositoryContextFactory repositoryContextFactory) + public SagaRepository(ISagaRepositoryContextFactory repositoryContextFactory, + IQuerySagaRepositoryContextFactory queryRepositoryContextFactory = null, + ILoadSagaRepositoryContextFactory loadSagaRepositoryContextFactory = null) { _repositoryContextFactory = repositoryContextFactory; + _querySagaRepository = new QuerySagaRepository(queryRepositoryContextFactory ?? NotImplementedSagaRepositoryContextFactory.Instance); + _loadSagaRepository = new LoadSagaRepository(loadSagaRepositoryContextFactory ?? NotImplementedSagaRepositoryContextFactory.Instance); } public Task Load(Guid correlationId) { - return _repositoryContextFactory.Execute(context => context.Load(correlationId)); + return _loadSagaRepository.Load(correlationId); } public Task> Find(ISagaQuery query) { - return _repositoryContextFactory.Execute>(async context => await context.Query(query).ConfigureAwait(false)); + return _querySagaRepository.Find(query); } public void Probe(ProbeContext context) @@ -38,6 +45,8 @@ public void Probe(ProbeContext context) var scope = context.CreateScope("sagaRepository"); _repositoryContextFactory.Probe(scope); + _querySagaRepository.Probe(scope); + _loadSagaRepository.Probe(scope); } public Task Send(ConsumeContext context, ISagaPolicy policy, IPipe> next) @@ -55,5 +64,39 @@ public Task SendQuery(ConsumeContext context, ISagaQuery query, ISa { return _repositoryContextFactory.SendQuery(context, query, new SendQuerySagaPipe(policy, next)); } + + + class NotImplementedSagaRepositoryContextFactory : + ILoadSagaRepositoryContextFactory, + IQuerySagaRepositoryContextFactory + { + public static readonly NotImplementedSagaRepositoryContextFactory Instance = new NotImplementedSagaRepositoryContextFactory(); + + static readonly string QueryErrorMessage = + $"Query-based saga correlation is not available when using current saga repository implementation: {TypeCache.ShortName}"; + + static readonly string LoadErrorMessage = + $"Load-based saga correlation is not available when using current saga repository implementation: {TypeCache.ShortName}"; + + NotImplementedSagaRepositoryContextFactory() + { + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + throw new NotSupportedException(LoadErrorMessage); + } + + public void Probe(ProbeContext context) + { + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + throw new NotSupportedException(QueryErrorMessage); + } + } } } diff --git a/src/MassTransit/Sagas/Saga/SagaRepositoryContext.cs b/src/MassTransit/Sagas/Saga/SagaRepositoryContext.cs deleted file mode 100644 index 87c4993b30a..00000000000 --- a/src/MassTransit/Sagas/Saga/SagaRepositoryContext.cs +++ /dev/null @@ -1,95 +0,0 @@ -namespace MassTransit.Saga -{ - using System; - using System.Threading; - using System.Threading.Tasks; - - - public interface SagaRepositoryContext : - ISagaConsumeContextFactory, - ConsumeContext - where TSaga : class, ISaga - where TMessage : class - { - /// - /// Add the saga instance, using the specified - /// - /// - /// - Task> Add(TSaga instance); - - /// - /// Insert the saga instance, if it does not already exist. - /// - /// - /// - /// A valid if the instance inserted successfully, otherwise default - /// - Task> Insert(TSaga instance); - - /// - /// Load an existing saga instance - /// - /// - /// - /// A valid if the instance loaded successfully, otherwise default - /// - Task> Load(Guid correlationId); - - /// - /// Save the saga, called after an Add, without an insert - /// - /// - /// - Task Save(SagaConsumeContext context); - - /// - /// Update the saga, called after a load or insert where the saga has not completed - /// - /// - /// - Task Update(SagaConsumeContext context); - - /// - /// Delete the saga, called after a Load when the saga is completed - /// - /// - /// - Task Delete(SagaConsumeContext context); - - /// - /// Discard the saga, called after an Add when the saga is completed within the same transaction - /// - /// - /// - Task Discard(SagaConsumeContext context); - - /// - /// Undo the changes for the saga - /// - /// - /// - Task Undo(SagaConsumeContext context); - } - - - public interface SagaRepositoryContext : - PipeContext - where TSaga : class, ISaga - { - /// - /// Query saga instances - /// - /// - /// - /// - Task> Query(ISagaQuery query, CancellationToken cancellationToken = default); - - /// - /// Load an existing saga instance - /// - /// - /// The saga, if found, or null - Task Load(Guid correlationId); - } -} diff --git a/src/MassTransit/Sagas/Saga/SagaRepositoryQueryContext.cs b/src/MassTransit/Sagas/Saga/SagaRepositoryQueryContext.cs index 3990fa72294..33585b34220 100644 --- a/src/MassTransit/Sagas/Saga/SagaRepositoryQueryContext.cs +++ b/src/MassTransit/Sagas/Saga/SagaRepositoryQueryContext.cs @@ -18,7 +18,7 @@ public interface SagaRepositoryQueryContext : public interface SagaRepositoryQueryContext : - SagaRepositoryContext, + QuerySagaRepositoryContext, IEnumerable where TSaga : class, ISaga { diff --git a/src/MassTransit/Sagas/SagaStateMachineExtensions.cs b/src/MassTransit/Sagas/SagaStateMachineExtensions.cs index da0f646e6cc..2edddf3007b 100644 --- a/src/MassTransit/Sagas/SagaStateMachineExtensions.cs +++ b/src/MassTransit/Sagas/SagaStateMachineExtensions.cs @@ -18,7 +18,7 @@ public static class SagaStateMachineExtensions /// public static ISagaQuery CreateSagaQuery(this StateMachine machine, Expression> expression, params State[] states) - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { Expression> stateExpression = machine.Accessor.GetStateExpression(states); @@ -35,7 +35,7 @@ public static ISagaQuery CreateSagaQuery(this StateMachine /// public static Func CreateSagaFilter(this StateMachine machine, Expression> expression, params State[] states) - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { Expression> stateExpression = machine.Accessor.GetStateExpression(states); diff --git a/src/MassTransit/Scheduling/BaseScheduleMessageProvider.cs b/src/MassTransit/Scheduling/BaseScheduleMessageProvider.cs index 049e614bc5c..33387cfccb8 100644 --- a/src/MassTransit/Scheduling/BaseScheduleMessageProvider.cs +++ b/src/MassTransit/Scheduling/BaseScheduleMessageProvider.cs @@ -29,19 +29,19 @@ public async Task> ScheduleSend(Uri destinationAddress, D command.Destination, message); } - public Task CancelScheduledSend(Guid tokenId) + public Task CancelScheduledSend(Guid tokenId, CancellationToken cancellationToken) { - return CancelScheduledSend(tokenId, null); + return CancelScheduledSend(tokenId, null, cancellationToken); } - public Task CancelScheduledSend(Uri destinationAddress, Guid tokenId) + public Task CancelScheduledSend(Uri destinationAddress, Guid tokenId, CancellationToken cancellationToken) { - return CancelScheduledSend(tokenId, destinationAddress); + return CancelScheduledSend(tokenId, destinationAddress, cancellationToken); } protected abstract Task ScheduleSend(ScheduleMessage message, IPipe> pipe, CancellationToken cancellationToken); - protected abstract Task CancelScheduledSend(Guid tokenId, Uri destinationAddress); + protected abstract Task CancelScheduledSend(Guid tokenId, Uri destinationAddress, CancellationToken cancellationToken); } diff --git a/src/MassTransit/Scheduling/DefaultRecurringSchedule.cs b/src/MassTransit/Scheduling/DefaultRecurringSchedule.cs index cba3b5bcce2..48a42c7d58a 100644 --- a/src/MassTransit/Scheduling/DefaultRecurringSchedule.cs +++ b/src/MassTransit/Scheduling/DefaultRecurringSchedule.cs @@ -1,7 +1,6 @@ namespace MassTransit.Scheduling { using System; - using System.Reflection; public abstract class DefaultRecurringSchedule : @@ -10,7 +9,7 @@ public abstract class DefaultRecurringSchedule : protected DefaultRecurringSchedule() { ScheduleId = TypeCache.GetShortName(GetType()); - ScheduleGroup = GetType().GetTypeInfo().Assembly.FullName.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)[0]; + ScheduleGroup = GetType().Assembly.FullName.Split(",".ToCharArray(), StringSplitOptions.RemoveEmptyEntries)[0]; TimeZoneId = TimeZoneInfo.Local.Id; StartTime = DateTime.Now; diff --git a/src/MassTransit/Scheduling/DelayedScheduleMessageProvider.cs b/src/MassTransit/Scheduling/DelayedScheduleMessageProvider.cs index 789648e13e3..1dd6babeb21 100644 --- a/src/MassTransit/Scheduling/DelayedScheduleMessageProvider.cs +++ b/src/MassTransit/Scheduling/DelayedScheduleMessageProvider.cs @@ -35,12 +35,12 @@ public async Task> ScheduleSend(Uri destinationAddress, D return new ScheduledMessageHandle(scheduleMessagePipe.ScheduledMessageId ?? NewId.NextGuid(), scheduledTime, destinationAddress, message); } - public Task CancelScheduledSend(Guid tokenId) + public Task CancelScheduledSend(Guid tokenId, CancellationToken cancellationToken) { return Task.CompletedTask; } - public Task CancelScheduledSend(Uri destinationAddress, Guid tokenId) + public Task CancelScheduledSend(Uri destinationAddress, Guid tokenId, CancellationToken cancellationToken) { return Task.CompletedTask; } diff --git a/src/MassTransit/Scheduling/EndpointRecurringMessageScheduler.cs b/src/MassTransit/Scheduling/EndpointRecurringMessageScheduler.cs index 8a9c237add0..78a4b7cb82b 100644 --- a/src/MassTransit/Scheduling/EndpointRecurringMessageScheduler.cs +++ b/src/MassTransit/Scheduling/EndpointRecurringMessageScheduler.cs @@ -1,4 +1,5 @@ -namespace MassTransit.Scheduling +#nullable enable +namespace MassTransit.Scheduling { using System; using System.Threading; @@ -9,15 +10,18 @@ public class EndpointRecurringMessageScheduler : IRecurringMessageScheduler { + readonly IBusTopology? _busTopology; readonly Func> _schedulerEndpoint; - public EndpointRecurringMessageScheduler(ISendEndpointProvider sendEndpointProvider, Uri schedulerAddress) + public EndpointRecurringMessageScheduler(ISendEndpointProvider sendEndpointProvider, Uri schedulerAddress, IBusTopology? busTopology = null) { + _busTopology = busTopology; _schedulerEndpoint = () => sendEndpointProvider.GetSendEndpoint(schedulerAddress); } - public EndpointRecurringMessageScheduler(ISendEndpoint sendEndpoint) + public EndpointRecurringMessageScheduler(ISendEndpoint sendEndpoint, IBusTopology? busTopology = null) { + _busTopology = busTopology; _schedulerEndpoint = () => Task.FromResult(sendEndpoint); } @@ -184,6 +188,147 @@ public async Task> ScheduleRecurringSend(Uri des return await Schedule(destinationAddress, schedule, send.Message, send.Pipe, cancellationToken).ConfigureAwait(false); } + public Task> ScheduleRecurringPublish(RecurringSchedule schedule, T message, + CancellationToken cancellationToken) + where T : class + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + var destinationAddress = GetPublishAddress(); + + return Schedule(destinationAddress, schedule, message, cancellationToken); + } + + public Task> ScheduleRecurringPublish(RecurringSchedule schedule, T message, IPipe> pipe, + CancellationToken cancellationToken) + where T : class + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(); + + return Schedule(destinationAddress, schedule, message, pipe, cancellationToken); + } + + public Task> ScheduleRecurringPublish(RecurringSchedule schedule, T message, IPipe pipe, + CancellationToken cancellationToken) + where T : class + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(); + + return Schedule(destinationAddress, schedule, message, pipe, cancellationToken); + } + + public Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, CancellationToken cancellationToken) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + var messageType = message.GetType(); + + var destinationAddress = GetPublishAddress(messageType); + + return MessageSchedulerConverterCache.ScheduleRecurringSend(this, destinationAddress, schedule, message, messageType, cancellationToken); + } + + public Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, Type messageType, + CancellationToken cancellationToken) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (messageType == null) + throw new ArgumentNullException(nameof(messageType)); + + var destinationAddress = GetPublishAddress(messageType); + + return MessageSchedulerConverterCache.ScheduleRecurringSend(this, destinationAddress, schedule, message, messageType, cancellationToken); + } + + public Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, IPipe pipe, + CancellationToken cancellationToken) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var messageType = message.GetType(); + + var destinationAddress = GetPublishAddress(messageType); + + return MessageSchedulerConverterCache.ScheduleRecurringSend(this, destinationAddress, schedule, message, messageType, pipe, cancellationToken); + } + + public Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, Type messageType, IPipe pipe, + CancellationToken cancellationToken) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (messageType == null) + throw new ArgumentNullException(nameof(messageType)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(messageType); + + return MessageSchedulerConverterCache.ScheduleRecurringSend(this, destinationAddress, schedule, message, messageType, pipe, cancellationToken); + } + + public async Task> ScheduleRecurringPublish(RecurringSchedule schedule, object values, + CancellationToken cancellationToken) + where T : class + { + if (values == null) + throw new ArgumentNullException(nameof(values)); + + var destinationAddress = GetPublishAddress(); + + SendTuple send = await MessageInitializerCache.InitializeMessage(values, cancellationToken).ConfigureAwait(false); + + return await Schedule(destinationAddress, schedule, send.Message, send.Pipe, cancellationToken).ConfigureAwait(false); + } + + public async Task> ScheduleRecurringPublish(RecurringSchedule schedule, object values, IPipe> pipe, + CancellationToken cancellationToken) + where T : class + { + if (values == null) + throw new ArgumentNullException(nameof(values)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(); + + SendTuple send = await MessageInitializerCache.InitializeMessage(values, pipe, cancellationToken).ConfigureAwait(false); + + return await Schedule(destinationAddress, schedule, send.Message, send.Pipe, cancellationToken).ConfigureAwait(false); + } + + public async Task> ScheduleRecurringPublish(RecurringSchedule schedule, object values, IPipe pipe, + CancellationToken cancellationToken) + where T : class + { + if (values == null) + throw new ArgumentNullException(nameof(values)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(); + + SendTuple send = await MessageInitializerCache.InitializeMessage(values, pipe, cancellationToken).ConfigureAwait(false); + + return await Schedule(destinationAddress, schedule, send.Message, send.Pipe, cancellationToken).ConfigureAwait(false); + } + public async Task CancelScheduledRecurringSend(string scheduleId, string scheduleGroup) { var command = new CancelScheduledRecurringMessageCommand(scheduleId, scheduleGroup); @@ -261,15 +406,38 @@ static ScheduleRecurringMessage CreateCommand(Uri destinationAddress, Recurri return new ScheduleRecurringMessageCommand(schedule, destinationAddress, message); } + Uri GetPublishAddress() + where T : class + { + if (_busTopology == null) + throw new InvalidOperationException("The bus topology is required to use ScheduleRecurringPublish."); + + if (_busTopology.TryGetPublishAddress(out var address)) + return address; + + throw new ArgumentException($"The publish address for the specified type was not returned: {TypeCache.ShortName}"); + } + + Uri GetPublishAddress(Type messageType) + { + if (_busTopology == null) + throw new InvalidOperationException("The bus topology is required to use ScheduleRecurringPublish."); + + if (_busTopology.TryGetPublishAddress(messageType, out var address)) + return address; + + throw new ArgumentException($"The publish address for the specified type was not returned: {TypeCache.GetShortName(messageType)}"); + } + class ScheduleRecurringMessageContextPipe : IPipe> where T : class { readonly T _payload; - readonly IPipe> _pipe; + readonly IPipe>? _pipe; - public ScheduleRecurringMessageContextPipe(T payload, IPipe> pipe) + public ScheduleRecurringMessageContextPipe(T payload, IPipe>? pipe) { _payload = payload; _pipe = pipe; @@ -281,7 +449,7 @@ public async Task Send(SendContext context) { SendContext proxy = context.CreateProxy(_payload); - await _pipe.Send(proxy).ConfigureAwait(false); + await _pipe!.Send(proxy).ConfigureAwait(false); } } diff --git a/src/MassTransit/Scheduling/EndpointScheduleMessageProvider.cs b/src/MassTransit/Scheduling/EndpointScheduleMessageProvider.cs index e402b4f6765..b3aa2833fe0 100644 --- a/src/MassTransit/Scheduling/EndpointScheduleMessageProvider.cs +++ b/src/MassTransit/Scheduling/EndpointScheduleMessageProvider.cs @@ -22,7 +22,7 @@ protected override async Task ScheduleSend(ScheduleMessage message, IPipe(new InVar.CorrelationId, InVar.Timestamp, TokenId = tokenId - }) + }, cancellationToken) .ConfigureAwait(false); } } diff --git a/src/MassTransit/Scheduling/MessageScheduler.cs b/src/MassTransit/Scheduling/MessageScheduler.cs index 0abca4aa2bb..ae1909caa4c 100644 --- a/src/MassTransit/Scheduling/MessageScheduler.cs +++ b/src/MassTransit/Scheduling/MessageScheduler.cs @@ -159,9 +159,9 @@ public async Task> ScheduleSend(Uri destinationAddress, D return await _provider.ScheduleSend(destinationAddress, scheduledTime, send.Message, send.Pipe, cancellationToken).ConfigureAwait(false); } - public Task CancelScheduledSend(Uri destinationAddress, Guid tokenId) + public Task CancelScheduledSend(Uri destinationAddress, Guid tokenId, CancellationToken cancellationToken) { - return _provider.CancelScheduledSend(destinationAddress, tokenId); + return _provider.CancelScheduledSend(destinationAddress, tokenId, cancellationToken); } public Task> SchedulePublish(DateTime scheduledTime, T message, CancellationToken cancellationToken = default) @@ -259,19 +259,19 @@ public Task> SchedulePublish(DateTime scheduledTime, obje return ScheduleSend(destinationAddress, scheduledTime, values, pipe, cancellationToken); } - public Task CancelScheduledPublish(Guid tokenId) + public Task CancelScheduledPublish(Guid tokenId, CancellationToken cancellationToken) where T : class { var destinationAddress = GetPublishAddress(); - return CancelScheduledSend(destinationAddress, tokenId); + return CancelScheduledSend(destinationAddress, tokenId, cancellationToken); } - public Task CancelScheduledPublish(Type messageType, Guid tokenId) + public Task CancelScheduledPublish(Type messageType, Guid tokenId, CancellationToken cancellationToken) { var destinationAddress = GetPublishAddress(messageType); - return CancelScheduledSend(destinationAddress, tokenId); + return CancelScheduledSend(destinationAddress, tokenId, cancellationToken); } Uri GetPublishAddress() diff --git a/src/MassTransit/Scheduling/MessageSchedulerConverterCache.cs b/src/MassTransit/Scheduling/MessageSchedulerConverterCache.cs index 484e170fd3d..92687f6cea8 100644 --- a/src/MassTransit/Scheduling/MessageSchedulerConverterCache.cs +++ b/src/MassTransit/Scheduling/MessageSchedulerConverterCache.cs @@ -154,7 +154,7 @@ public async Task ScheduleRecurringSend(IRecurringMes static class Cached { internal static readonly Lazy Converters = - new Lazy(() => new MessageSchedulerConverterCache(), LazyThreadSafetyMode.PublicationOnly); + new Lazy(() => new MessageSchedulerConverterCache()); } } } diff --git a/src/MassTransit/Scheduling/PublishRecurringMessageScheduler.cs b/src/MassTransit/Scheduling/PublishRecurringMessageScheduler.cs index 62e26728c5a..1dbdc05ee2a 100644 --- a/src/MassTransit/Scheduling/PublishRecurringMessageScheduler.cs +++ b/src/MassTransit/Scheduling/PublishRecurringMessageScheduler.cs @@ -1,4 +1,5 @@ -namespace MassTransit.Scheduling +#nullable enable +namespace MassTransit.Scheduling { using System; using System.Threading; @@ -10,11 +11,13 @@ public class PublishRecurringMessageScheduler : IRecurringMessageScheduler { + readonly IBusTopology? _busTopology; readonly IPublishEndpoint _publishEndpoint; - public PublishRecurringMessageScheduler(IPublishEndpoint publishEndpoint) + public PublishRecurringMessageScheduler(IPublishEndpoint publishEndpoint, IBusTopology? busTopology = null) { _publishEndpoint = publishEndpoint; + _busTopology = busTopology; } public Task> ScheduleRecurringSend(Uri destinationAddress, RecurringSchedule schedule, T message, @@ -177,6 +180,147 @@ public async Task> ScheduleRecurringSend(Uri des return await Schedule(destinationAddress, schedule, send.Message, send.Pipe, cancellationToken).ConfigureAwait(false); } + public Task> ScheduleRecurringPublish(RecurringSchedule schedule, T message, + CancellationToken cancellationToken) + where T : class + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + var destinationAddress = GetPublishAddress(); + + return Schedule(destinationAddress, schedule, message, cancellationToken); + } + + public Task> ScheduleRecurringPublish(RecurringSchedule schedule, T message, IPipe> pipe, + CancellationToken cancellationToken) + where T : class + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(); + + return Schedule(destinationAddress, schedule, message, pipe, cancellationToken); + } + + public Task> ScheduleRecurringPublish(RecurringSchedule schedule, T message, IPipe pipe, + CancellationToken cancellationToken) + where T : class + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(); + + return Schedule(destinationAddress, schedule, message, pipe, cancellationToken); + } + + public Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, CancellationToken cancellationToken) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + var messageType = message.GetType(); + + var destinationAddress = GetPublishAddress(messageType); + + return MessageSchedulerConverterCache.ScheduleRecurringSend(this, destinationAddress, schedule, message, messageType, cancellationToken); + } + + public Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, Type messageType, + CancellationToken cancellationToken) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (messageType == null) + throw new ArgumentNullException(nameof(messageType)); + + var destinationAddress = GetPublishAddress(messageType); + + return MessageSchedulerConverterCache.ScheduleRecurringSend(this, destinationAddress, schedule, message, messageType, cancellationToken); + } + + public Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, IPipe pipe, + CancellationToken cancellationToken) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var messageType = message.GetType(); + + var destinationAddress = GetPublishAddress(messageType); + + return MessageSchedulerConverterCache.ScheduleRecurringSend(this, destinationAddress, schedule, message, messageType, pipe, cancellationToken); + } + + public Task ScheduleRecurringPublish(RecurringSchedule schedule, object message, Type messageType, IPipe pipe, + CancellationToken cancellationToken) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + if (messageType == null) + throw new ArgumentNullException(nameof(messageType)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(messageType); + + return MessageSchedulerConverterCache.ScheduleRecurringSend(this, destinationAddress, schedule, message, messageType, pipe, cancellationToken); + } + + public async Task> ScheduleRecurringPublish(RecurringSchedule schedule, object values, + CancellationToken cancellationToken) + where T : class + { + if (values == null) + throw new ArgumentNullException(nameof(values)); + + var destinationAddress = GetPublishAddress(); + + SendTuple send = await MessageInitializerCache.InitializeMessage(values, cancellationToken).ConfigureAwait(false); + + return await Schedule(destinationAddress, schedule, send.Message, send.Pipe, cancellationToken).ConfigureAwait(false); + } + + public async Task> ScheduleRecurringPublish(RecurringSchedule schedule, object values, IPipe> pipe, + CancellationToken cancellationToken) + where T : class + { + if (values == null) + throw new ArgumentNullException(nameof(values)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(); + + SendTuple send = await MessageInitializerCache.InitializeMessage(values, pipe, cancellationToken).ConfigureAwait(false); + + return await Schedule(destinationAddress, schedule, send.Message, send.Pipe, cancellationToken).ConfigureAwait(false); + } + + public async Task> ScheduleRecurringPublish(RecurringSchedule schedule, object values, IPipe pipe, + CancellationToken cancellationToken) + where T : class + { + if (values == null) + throw new ArgumentNullException(nameof(values)); + if (pipe == null) + throw new ArgumentNullException(nameof(pipe)); + + var destinationAddress = GetPublishAddress(); + + SendTuple send = await MessageInitializerCache.InitializeMessage(values, pipe, cancellationToken).ConfigureAwait(false); + + return await Schedule(destinationAddress, schedule, send.Message, send.Pipe, cancellationToken).ConfigureAwait(false); + } + public Task CancelScheduledRecurringSend(string scheduleId, string scheduleGroup) { var command = new CancelScheduledRecurringMessageCommand(scheduleId, scheduleGroup); @@ -241,15 +385,38 @@ static ScheduleRecurringMessage CreateCommand(Uri destinationAddress, Recurri return new ScheduleRecurringMessageCommand(schedule, destinationAddress, message); } + Uri GetPublishAddress() + where T : class + { + if (_busTopology == null) + throw new InvalidOperationException("The bus topology is required to use ScheduleRecurringPublish."); + + if (_busTopology.TryGetPublishAddress(out var address)) + return address; + + throw new ArgumentException($"The publish address for the specified type was not returned: {TypeCache.ShortName}"); + } + + Uri GetPublishAddress(Type messageType) + { + if (_busTopology == null) + throw new InvalidOperationException("The bus topology is required to use ScheduleRecurringPublish."); + + if (_busTopology.TryGetPublishAddress(messageType, out var address)) + return address; + + throw new ArgumentException($"The publish address for the specified type was not returned: {TypeCache.GetShortName(messageType)}"); + } + class ScheduleRecurringMessageContextPipe : IPipe> where T : class { readonly T _payload; - readonly IPipe> _pipe; + readonly IPipe>? _pipe; - public ScheduleRecurringMessageContextPipe(T payload, IPipe> pipe) + public ScheduleRecurringMessageContextPipe(T payload, IPipe>? pipe) { _payload = payload; _pipe = pipe; @@ -261,7 +428,7 @@ public async Task Send(PublishContext context) { var proxy = new PublishContextProxy(context, _payload); - await _pipe.Send(proxy).ConfigureAwait(false); + await _pipe!.Send(proxy).ConfigureAwait(false); } } diff --git a/src/MassTransit/Scheduling/PublishScheduleMessageProvider.cs b/src/MassTransit/Scheduling/PublishScheduleMessageProvider.cs index a81bc6f681f..4ccf934e7d6 100644 --- a/src/MassTransit/Scheduling/PublishScheduleMessageProvider.cs +++ b/src/MassTransit/Scheduling/PublishScheduleMessageProvider.cs @@ -20,14 +20,14 @@ protected override Task ScheduleSend(ScheduleMessage message, IPipe(new { InVar.CorrelationId, InVar.Timestamp, TokenId = tokenId - }); + }, cancellationToken); } } } diff --git a/src/MassTransit/Scheduling/ScheduleSendPipe.cs b/src/MassTransit/Scheduling/ScheduleSendPipe.cs index 58c445bd1ed..8206ebb20b9 100644 --- a/src/MassTransit/Scheduling/ScheduleSendPipe.cs +++ b/src/MassTransit/Scheduling/ScheduleSendPipe.cs @@ -30,6 +30,8 @@ public Guid? ScheduledMessageId set => _scheduledMessageId = value; } + public Guid? MessageId => _context?.MessageId; + protected override void Send(SendContext context) { _context = context; diff --git a/src/MassTransit/Scheduling/ScheduleTokenIdCache.cs b/src/MassTransit/Scheduling/ScheduleTokenIdCache.cs index e34332b2215..a28011f0e99 100644 --- a/src/MassTransit/Scheduling/ScheduleTokenIdCache.cs +++ b/src/MassTransit/Scheduling/ScheduleTokenIdCache.cs @@ -1,7 +1,6 @@ namespace MassTransit.Scheduling { using System; - using System.Threading; /// @@ -59,8 +58,7 @@ internal static void UseTokenId(TokenIdSelector tokenIdSelector) static class Cached { - internal static Lazy> Metadata = new Lazy>( - () => new ScheduleTokenIdCache(), LazyThreadSafetyMode.PublicationOnly); + internal static Lazy> Metadata = new Lazy>(() => new ScheduleTokenIdCache()); } } } diff --git a/src/MassTransit/Serialization/BodyConsumeContext.cs b/src/MassTransit/Serialization/BodyConsumeContext.cs index ed098c14087..5d0aa9688cc 100644 --- a/src/MassTransit/Serialization/BodyConsumeContext.cs +++ b/src/MassTransit/Serialization/BodyConsumeContext.cs @@ -3,7 +3,7 @@ namespace MassTransit.Serialization { using System; using System.Collections.Generic; - using System.Reflection; + using System.Diagnostics.CodeAnalysis; using Context; @@ -24,10 +24,10 @@ public BodyConsumeContext(ReceiveContext receiveContext, SerializerContext seria public override Guid? ConversationId => SerializerContext.ConversationId; public override Guid? InitiatorId => SerializerContext.InitiatorId; public override DateTime? ExpirationTime => SerializerContext.ExpirationTime; - public override Uri? SourceAddress => SerializerContext.SourceAddress; - public override Uri? DestinationAddress => SerializerContext.DestinationAddress; - public override Uri? ResponseAddress => SerializerContext.ResponseAddress; - public override Uri? FaultAddress => SerializerContext.FaultAddress; + public override Uri SourceAddress => SerializerContext.SourceAddress!; + public override Uri DestinationAddress => SerializerContext.DestinationAddress!; + public override Uri ResponseAddress => SerializerContext.ResponseAddress!; + public override Uri FaultAddress => SerializerContext.FaultAddress!; public override DateTime? SentTime => SerializerContext.SentTime; public override Headers Headers => SerializerContext.Headers; public override HostInfo Host => SerializerContext.Host; @@ -44,7 +44,7 @@ public override bool HasMessageType(Type messageType) return SerializerContext.IsSupportedMessageType(messageType); } - public override bool TryGetMessage(out ConsumeContext? message) + public override bool TryGetMessage([NotNullWhen(true)] out ConsumeContext? message) { lock (_messageTypes) { @@ -54,13 +54,13 @@ public override bool TryGetMessage(out ConsumeContext? message) return message != null; } - if (typeof(T).GetTypeInfo().IsInterface && MessageTypeCache.IsValidMessageType) + if (typeof(T).IsInterface && MessageTypeCache.IsValidMessageType) { if (SerializerContext.IsSupportedMessageType()) { if (SerializerContext.TryGetMessage(typeof(T), out var messageObj)) { - _messageTypes[typeof(T)] = message = new MessageConsumeContext(this, ((T)messageObj)!); + _messageTypes[typeof(T)] = message = new MessageConsumeContext(this, (T)messageObj); return true; } } diff --git a/src/MassTransit/Serialization/DictionarySendHeaders.cs b/src/MassTransit/Serialization/DictionarySendHeaders.cs index 6a8057a251f..9aa7cb8a4fb 100644 --- a/src/MassTransit/Serialization/DictionarySendHeaders.cs +++ b/src/MassTransit/Serialization/DictionarySendHeaders.cs @@ -4,13 +4,14 @@ namespace MassTransit.Serialization using System; using System.Collections; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; public class DictionarySendHeaders : SendHeaders { - readonly IDictionary _headers; + readonly Dictionary _headers; public DictionarySendHeaders() { @@ -57,7 +58,7 @@ public void Set(string key, object? value, bool overwrite = true) _headers.Add(key, value); } - public bool TryGetHeader(string key, out object? value) + public bool TryGetHeader(string key, [NotNullWhen(true)] out object? value) { return _headers.TryGetValue(key, out value); } @@ -70,13 +71,13 @@ public IEnumerable> GetAll() public T? Get(string key, T? defaultValue) where T : class { - return SystemTextJsonMessageSerializer.Instance.GetValue(_headers, key, defaultValue); + return SystemTextJsonMessageSerializer.Instance.GetValue((IReadOnlyDictionary)_headers, key, defaultValue); } public T? Get(string key, T? defaultValue) where T : struct { - return SystemTextJsonMessageSerializer.Instance.GetValue(_headers, key, defaultValue); + return SystemTextJsonMessageSerializer.Instance.GetValue((IReadOnlyDictionary)_headers, key, defaultValue); } public IEnumerator GetEnumerator() diff --git a/src/MassTransit/Serialization/EnvelopeMessageContext.cs b/src/MassTransit/Serialization/EnvelopeMessageContext.cs index eafb4b6ccff..8270e6f1cb9 100644 --- a/src/MassTransit/Serialization/EnvelopeMessageContext.cs +++ b/src/MassTransit/Serialization/EnvelopeMessageContext.cs @@ -44,7 +44,7 @@ public EnvelopeMessageContext(MessageEnvelope envelope, IObjectDeserializer obje Headers GetHeaders() { return _envelope.Headers != null - ? (Headers)new ReadOnlyDictionaryHeaders(_objectDeserializer, _envelope.Headers) + ? new ReadOnlyDictionaryHeaders(_objectDeserializer, _envelope.Headers) : EmptyHeaders.Instance; } diff --git a/src/MassTransit/Serialization/EnvelopeSerializerContext.cs b/src/MassTransit/Serialization/EnvelopeSerializerContext.cs index 4ac24d360b2..04294152ff5 100644 --- a/src/MassTransit/Serialization/EnvelopeSerializerContext.cs +++ b/src/MassTransit/Serialization/EnvelopeSerializerContext.cs @@ -3,6 +3,7 @@ namespace MassTransit.Serialization { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Linq; @@ -69,7 +70,7 @@ public MessageBody SerializeObject(object? value) public abstract bool TryGetMessage(out T? message) where T : class; - public abstract bool TryGetMessage(Type messageType, out object? message); + public abstract bool TryGetMessage(Type messageType, [NotNullWhen(true)] out object? message); public abstract IMessageSerializer GetMessageSerializer(); diff --git a/src/MassTransit/Serialization/JobPropertyCollection.cs b/src/MassTransit/Serialization/JobPropertyCollection.cs new file mode 100644 index 00000000000..52ccea163b4 --- /dev/null +++ b/src/MassTransit/Serialization/JobPropertyCollection.cs @@ -0,0 +1,109 @@ +#nullable enable +namespace MassTransit.Serialization; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using Internals; + + +public class JobPropertyCollection : + ISetPropertyCollection +{ + Dictionary? _properties; + + public Dictionary Properties + { + get => _properties ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + set => _properties = value; + } + + public int Count => _properties?.Count ?? 0; + + public bool TryGet(string key, [NotNullWhen(true)] out object? value) + { + if (_properties != null) + return _properties.TryGetValue(key, out value); + + value = null; + return false; + } + + public T? Get(string key, T? defaultValue = default) + where T : class + { + return _properties == null + ? defaultValue + : SystemTextJsonMessageSerializer.Instance.GetValue((IReadOnlyDictionary)_properties, key, defaultValue); + } + + public T? Get(string key, T? defaultValue = default) + where T : struct + { + return _properties == null + ? defaultValue + : SystemTextJsonMessageSerializer.Instance.GetValue((IReadOnlyDictionary)_properties, key, defaultValue); + } + + public ISetPropertyCollection Set(string key, string? value) + { + Properties.SetValue(key, value); + + return this; + } + + public ISetPropertyCollection Set(string key, object? value, bool overwrite = true) + { + Properties.SetValue(key, value, overwrite); + + return this; + } + + public ISetPropertyCollection SetMany(IEnumerable>? properties, bool overwrite = true) + { + Properties.SetValues(properties, overwrite); + + return this; + } + + IEnumerator> IEnumerable>.GetEnumerator() + { + // ReSharper disable once NotDisposedResourceIsReturned + return _properties?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + // ReSharper disable once NotDisposedResourceIsReturned + return _properties?.GetEnumerator() ?? Enumerable.Empty>().GetEnumerator(); + } + + int IReadOnlyCollection>.Count => _properties?.Count ?? 0; + + bool IReadOnlyDictionary.ContainsKey(string key) + { + return _properties?.ContainsKey(key) ?? false; + } + + bool IReadOnlyDictionary.TryGetValue(string key, [MaybeNullWhen(false)] out object value) + { + if (_properties != null) + return _properties.TryGetValue(key, out value); + + value = null!; + return false; + } + + object IReadOnlyDictionary.this[string key] => _properties?[key] ?? throw new KeyNotFoundException(key); + + IEnumerable IReadOnlyDictionary.Keys => _properties?.Keys ?? Enumerable.Empty(); + + IEnumerable IReadOnlyDictionary.Values => _properties?.Values ?? Enumerable.Empty(); + + public static implicit operator Dictionary(JobPropertyCollection properties) + { + return properties.Properties; + } +} diff --git a/src/MassTransit/Serialization/JsonConverters/CaseInsensitiveDictionaryStringObjectJsonConverter.cs b/src/MassTransit/Serialization/JsonConverters/CaseInsensitiveDictionaryStringObjectJsonConverter.cs index f5629572b7f..544ae5f8432 100644 --- a/src/MassTransit/Serialization/JsonConverters/CaseInsensitiveDictionaryStringObjectJsonConverter.cs +++ b/src/MassTransit/Serialization/JsonConverters/CaseInsensitiveDictionaryStringObjectJsonConverter.cs @@ -195,7 +195,7 @@ static object ReadPropertyValue(ref Utf8JsonReader reader, JsonSerializerOptions if (reader.TryGetInt64(out var result)) return result; - return reader.GetDecimal(); + return reader.GetDouble(); case JsonTokenType.StartObject: return ReadObject(ref reader, options); diff --git a/src/MassTransit/Serialization/JsonConverters/CustomMessageTypeJsonConverter.cs b/src/MassTransit/Serialization/JsonConverters/CustomMessageTypeJsonConverter.cs new file mode 100644 index 00000000000..42bdb08b089 --- /dev/null +++ b/src/MassTransit/Serialization/JsonConverters/CustomMessageTypeJsonConverter.cs @@ -0,0 +1,28 @@ +namespace MassTransit.Serialization.JsonConverters; + +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + + +public class CustomMessageTypeJsonConverter : + JsonConverter + where T : class +{ + readonly JsonSerializerOptions _options; + + public CustomMessageTypeJsonConverter(JsonSerializerOptions options) + { + _options = options; + } + + public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return JsonSerializer.Deserialize(ref reader, _options); + } + + public override void Write(Utf8JsonWriter writer, T value, JsonSerializerOptions options) + { + JsonSerializer.Serialize(writer, value, _options); + } +} diff --git a/src/MassTransit/Serialization/JsonConverters/SystemTextJsonConverterFactory.cs b/src/MassTransit/Serialization/JsonConverters/SystemTextJsonConverterFactory.cs index 721d1f16f88..c2ab3165a0b 100644 --- a/src/MassTransit/Serialization/JsonConverters/SystemTextJsonConverterFactory.cs +++ b/src/MassTransit/Serialization/JsonConverters/SystemTextJsonConverterFactory.cs @@ -6,10 +6,12 @@ using System.Text.Json; using System.Text.Json.Serialization; using Batching; + using Contracts.JobService; using Courier.Contracts; using Courier.Messages; using Events; using Internals; + using JobService.Messages; using Metadata; using Scheduling; @@ -23,6 +25,8 @@ public class SystemTextJsonConverterFactory : { { typeof(Fault<>), typeof(FaultEvent<>) }, { typeof(Batch<>), typeof(MessageBatch<>) }, + { typeof(SubmitJob<>), typeof(SubmitJobCommand<>) }, + { typeof(JobCompleted<>), typeof(JobCompletedEvent<>) }, }; static SystemTextJsonConverterFactory() @@ -53,45 +57,77 @@ static SystemTextJsonConverterFactory() .Add() .Add() .Add() - .Add(); + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add() + .Add(); } public override bool CanConvert(Type typeToConvert) { - var typeInfo = typeToConvert.GetTypeInfo(); - - if (typeInfo.IsGenericType) + if (typeToConvert.IsGenericType) { - if (typeInfo.ClosesType(typeof(IDictionary<,>), out Type[] elementTypes) - || typeInfo.ClosesType(typeof(IReadOnlyDictionary<,>), out elementTypes) - || typeInfo.ClosesType(typeof(Dictionary<,>), out elementTypes) - || (typeInfo.ClosesType(typeof(IEnumerable<>), out Type[] enumerableType) + if (typeToConvert.ClosesType(typeof(IDictionary<,>), out Type[] elementTypes) + || typeToConvert.ClosesType(typeof(IReadOnlyDictionary<,>), out elementTypes) + || typeToConvert.ClosesType(typeof(Dictionary<,>), out elementTypes) + || (typeToConvert.ClosesType(typeof(IEnumerable<>), out Type[] enumerableType) && enumerableType[0].ClosesType(typeof(KeyValuePair<,>), out elementTypes) - && elementTypes[1] == typeof(object))) + && elementTypes[1] == typeof(object) + && !typeToConvert.ClosesType(typeof(IReadOnlyList<>)))) { var keyType = elementTypes[0]; - var valueType = elementTypes[1]; if (keyType != typeof(string) && keyType != typeof(Uri)) return false; - if (typeInfo.IsFSharpType()) + if (typeToConvert.IsFSharpType()) return false; return true; } } - if (!typeInfo.IsInterface) + if (!typeToConvert.IsInterface) return false; - if (_converterFactory.TryGetValue(typeInfo, out Func _)) + if (_converterFactory.TryGetValue(typeToConvert, out Func _)) return true; - if (_openTypeFactory.TryGetValue(typeInfo, out _)) + if (_openTypeFactory.TryGetValue(typeToConvert, out _)) return true; - if (typeToConvert.IsInterfaceOrConcreteClass() && MessageTypeCache.IsValidMessageType(typeToConvert) && !typeToConvert.IsValueTypeOrObject()) + if (IsConvertibleInterfaceType(typeToConvert)) return true; return false; @@ -99,14 +135,12 @@ public override bool CanConvert(Type typeToConvert) public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options) { - var typeInfo = typeToConvert.GetTypeInfo(); - - if (_converterFactory.TryGetValue(typeInfo, out Func converterFactory)) + if (_converterFactory.TryGetValue(typeToConvert, out Func converterFactory)) return converterFactory(); - if (typeInfo.IsGenericType) + if (typeToConvert.IsGenericType) { - if (!typeInfo.IsFSharpType()) + if (!typeToConvert.IsFSharpType()) { if (typeToConvert == typeof(IDictionary)) return new CaseInsensitiveDictionaryStringObjectJsonConverter>(); @@ -117,10 +151,10 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer if (typeToConvert == typeof(IEnumerable>)) return new CaseInsensitiveDictionaryStringObjectJsonConverter>>(); - if (typeInfo.ClosesType(typeof(IDictionary<,>), out Type[] elementTypes) - || typeInfo.ClosesType(typeof(IReadOnlyDictionary<,>), out elementTypes) - || typeInfo.ClosesType(typeof(Dictionary<,>), out elementTypes) - || (typeInfo.ClosesType(typeof(IEnumerable<>), out Type[] enumerableTypes) + if (typeToConvert.ClosesType(typeof(IDictionary<,>), out Type[] elementTypes) + || typeToConvert.ClosesType(typeof(IReadOnlyDictionary<,>), out elementTypes) + || typeToConvert.ClosesType(typeof(Dictionary<,>), out elementTypes) + || (typeToConvert.ClosesType(typeof(IEnumerable<>), out Type[] enumerableTypes) && enumerableTypes[0].ClosesType(typeof(KeyValuePair<,>), out elementTypes) && elementTypes[1] == typeof(object))) { @@ -139,13 +173,13 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer } } - if (typeInfo.IsGenericType && !typeInfo.IsGenericTypeDefinition) + if (typeToConvert.IsGenericType && !typeToConvert.IsGenericTypeDefinition) { - var interfaceType = typeInfo.GetGenericTypeDefinition(); + var interfaceType = typeToConvert.GetGenericTypeDefinition(); if (_openTypeFactory.TryGetValue(interfaceType, out var concreteType)) { - Type[] arguments = typeInfo.GetGenericArguments(); + Type[] arguments = typeToConvert.GetGenericArguments(); if (arguments.Length == 1 && !arguments[0].IsGenericParameter) { @@ -157,13 +191,37 @@ public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializer } } - if (typeToConvert.IsInterfaceOrConcreteClass() && MessageTypeCache.IsValidMessageType(typeToConvert) && !typeToConvert.IsValueTypeOrObject()) + if (IsConvertibleInterfaceType(typeToConvert)) { return (JsonConverter)Activator.CreateInstance( typeof(InterfaceJsonConverter<,>).MakeGenericType(typeToConvert, TypeMetadataCache.GetImplementationType(typeToConvert))); } - throw new MassTransitException($"Unsupported type for json serialization {TypeCache.GetShortName(typeInfo)}"); + throw new MassTransitException($"Unsupported type for json serialization {TypeCache.GetShortName(typeToConvert)}"); + } + + static bool IsConvertibleInterfaceType(Type typeToConvert) + { + if (!typeToConvert.IsInterfaceOrConcreteClass()) + return false; + + if (!MessageTypeCache.IsValidMessageType(typeToConvert)) + return false; + + if (typeToConvert.IsValueTypeOrObject()) + return false; + + foreach (var attribute in typeToConvert.GetCustomAttributes()) + { + switch (attribute.GetType().Name) + { + case "JsonDerivedTypeAttribute": + case "JsonPolymorphicAttribute": + return false; + } + } + + return true; } } diff --git a/src/MassTransit/Serialization/JsonMessageEnvelope.cs b/src/MassTransit/Serialization/JsonMessageEnvelope.cs index e06ffee8244..293acae922c 100644 --- a/src/MassTransit/Serialization/JsonMessageEnvelope.cs +++ b/src/MassTransit/Serialization/JsonMessageEnvelope.cs @@ -16,7 +16,7 @@ public JsonMessageEnvelope() { } - public JsonMessageEnvelope(SendContext context, object message, string[] messageTypeNames) + public JsonMessageEnvelope(SendContext context, object message) { if (context.MessageId.HasValue) MessageId = context.MessageId.Value.ToString(); @@ -45,7 +45,7 @@ public JsonMessageEnvelope(SendContext context, object message, string[] message if (context.FaultAddress != null) FaultAddress = context.FaultAddress.ToString(); - MessageType = messageTypeNames; + MessageType = context.SupportedMessageTypes; Message = message; @@ -54,7 +54,7 @@ public JsonMessageEnvelope(SendContext context, object message, string[] message SentTime = context.SentTime ?? DateTime.UtcNow; - Headers = new Dictionary(); + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair header in context.Headers.GetAll()) Headers[header.Key] = header.Value; @@ -100,7 +100,7 @@ public JsonMessageEnvelope(MessageContext context, object message, string[] mess SentTime = context.SentTime ?? DateTime.UtcNow; - Headers = new Dictionary(); + Headers = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (KeyValuePair header in context.Headers.GetAll()) Headers[header.Key] = header.Value; @@ -150,7 +150,7 @@ public JsonMessageEnvelope(MessageEnvelope envelope) public Dictionary Headers { - get => _headers ??= new Dictionary(); + get => _headers ??= new Dictionary(StringComparer.OrdinalIgnoreCase); set => _headers = value; } @@ -189,6 +189,9 @@ public void Update(SendContext context) foreach (KeyValuePair header in context.Headers.GetAll()) Headers[header.Key] = header.Value; + + if (MessageType != null) + context.SupportedMessageTypes = MessageType; } } } diff --git a/src/MassTransit/Serialization/RawMessageContext.cs b/src/MassTransit/Serialization/RawMessageContext.cs index 5df2f01bfa3..ca78c3f00cc 100644 --- a/src/MassTransit/Serialization/RawMessageContext.cs +++ b/src/MassTransit/Serialization/RawMessageContext.cs @@ -4,13 +4,12 @@ namespace MassTransit.Serialization using System.Collections; using System.Collections.Generic; using System.Linq; + using Internals; public class RawMessageContext : MessageContext { - static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1); - readonly RawSerializerOptions _options; readonly Headers _transportHeaders; @@ -56,7 +55,7 @@ public RawMessageContext(Headers headers, Uri destinationAddress, RawSerializerO { DateTime? sentTime = MessageId?.ToNewId().Timestamp; - return sentTime > _unixEpoch ? sentTime : default; + return sentTime > DateTimeConstants.Epoch ? sentTime : default; } catch (Exception) { diff --git a/src/MassTransit/Serialization/RawMessageSerializer.cs b/src/MassTransit/Serialization/RawMessageSerializer.cs index ea31ed2b980..4f31a68a74c 100644 --- a/src/MassTransit/Serialization/RawMessageSerializer.cs +++ b/src/MassTransit/Serialization/RawMessageSerializer.cs @@ -7,8 +7,7 @@ namespace MassTransit.Serialization public abstract class RawMessageSerializer { - protected virtual void SetRawMessageHeaders(SendContext context) - where T : class + protected virtual void SetRawMessageHeaders(SendContext context) { if (context.MessageId.HasValue) context.Headers.Set(MessageHeaders.MessageId, context.MessageId.Value.ToString()); @@ -25,7 +24,8 @@ protected virtual void SetRawMessageHeaders(SendContext context) if (context.RequestId.HasValue) context.Headers.Set(MessageHeaders.RequestId, context.RequestId.Value.ToString()); - context.Headers.Set(MessageHeaders.MessageType, string.Join(";", MessageTypeCache.MessageTypeNames)); + if (context.SupportedMessageTypes?.Length > 0) + context.Headers.Set(MessageHeaders.MessageType, string.Join(";", context.SupportedMessageTypes)); if (context.ResponseAddress != null) context.Headers.Set(MessageHeaders.ResponseAddress, context.ResponseAddress); diff --git a/src/MassTransit/Serialization/Serialization.cs b/src/MassTransit/Serialization/Serialization.cs index c7138544c1e..33ae8a38b67 100644 --- a/src/MassTransit/Serialization/Serialization.cs +++ b/src/MassTransit/Serialization/Serialization.cs @@ -3,6 +3,7 @@ namespace MassTransit.Serialization { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Net.Mime; @@ -57,7 +58,7 @@ public IMessageSerializer GetMessageSerializer(ContentType? contentType = null) return _defaultSerializer; } - public bool TryGetMessageSerializer(ContentType contentType, out IMessageSerializer? serializer) + public bool TryGetMessageSerializer(ContentType contentType, [NotNullWhen(true)] out IMessageSerializer? serializer) { var mediaType = contentType.MediaType; @@ -74,7 +75,7 @@ public IMessageDeserializer GetMessageDeserializer(ContentType? contentType = nu return _defaultDeserializer; } - public bool TryGetMessageDeserializer(ContentType contentType, out IMessageDeserializer? deserializer) + public bool TryGetMessageDeserializer(ContentType contentType, [NotNullWhen(true)] out IMessageDeserializer? deserializer) { var mediaType = contentType.MediaType; diff --git a/src/MassTransit/Serialization/SystemTextJsonBodyMessageSerializer.cs b/src/MassTransit/Serialization/SystemTextJsonBodyMessageSerializer.cs index 1e3c2df0543..23fe6d88424 100644 --- a/src/MassTransit/Serialization/SystemTextJsonBodyMessageSerializer.cs +++ b/src/MassTransit/Serialization/SystemTextJsonBodyMessageSerializer.cs @@ -16,25 +16,37 @@ public class SystemTextJsonBodyMessageSerializer : IMessageSerializer { readonly JsonMessageEnvelope? _envelope; + readonly string[]? _messageTypes; readonly JsonSerializerOptions _options; - readonly RawSerializerOptions _rawOptions; + readonly RawSerializerOptions? _rawOptions; object? _message; - public SystemTextJsonBodyMessageSerializer(MessageEnvelope envelope, ContentType contentType, JsonSerializerOptions options) + public SystemTextJsonBodyMessageSerializer(MessageEnvelope envelope, ContentType contentType, JsonSerializerOptions options, + string[]? messageTypes = null) { _message = envelope.Message; _options = options; + _messageTypes = messageTypes; _envelope = new JsonMessageEnvelope(envelope); ContentType = contentType; } - public SystemTextJsonBodyMessageSerializer(object message, ContentType contentType, JsonSerializerOptions options, RawSerializerOptions rawOptions) + public SystemTextJsonBodyMessageSerializer(object message, ContentType contentType, JsonSerializerOptions options, RawSerializerOptions rawOptions, + string[]? messageTypes = null) { - _message = message; + if (message is MessageEnvelope envelope) + { + _message = envelope.Message; + _envelope = new JsonMessageEnvelope(envelope); + } + else + _message = message; + _options = options; _rawOptions = rawOptions; + _messageTypes = messageTypes; ContentType = contentType; } @@ -44,17 +56,20 @@ public SystemTextJsonBodyMessageSerializer(object message, ContentType contentTy public MessageBody GetMessageBody(SendContext context) where T : class { - if (_envelope != null) + _envelope?.Update(context); + + if (_messageTypes != null) + context.SupportedMessageTypes = _messageTypes; + + if (_rawOptions.HasValue) { - _envelope.Update(context); + if (_rawOptions.Value.HasFlag(RawSerializerOptions.AddTransportHeaders)) + SetRawMessageHeaders(context); - return new SystemTextJsonMessageBody(context, _options, _envelope); + return new SystemTextJsonRawMessageBody(context, _options, _message); } - if (_rawOptions.HasFlag(RawSerializerOptions.AddTransportHeaders)) - SetRawMessageHeaders(context); - - return new SystemTextJsonRawMessageBody(context, _options, _message); + return new SystemTextJsonMessageBody(context, _options, _envelope); } public void Overlay(object message) diff --git a/src/MassTransit/Serialization/SystemTextJsonExtensions.cs b/src/MassTransit/Serialization/SystemTextJsonExtensions.cs new file mode 100644 index 00000000000..0b1e22f56fc --- /dev/null +++ b/src/MassTransit/Serialization/SystemTextJsonExtensions.cs @@ -0,0 +1,42 @@ +#nullable enable +namespace MassTransit.Serialization; + +using System; +using System.Text.Json; +using Metadata; + + +public static class SystemTextJsonExtensions +{ + public static T? GetObject(this JsonElement jsonElement, JsonSerializerOptions options) + where T : class + { + if (typeof(T).IsInterface && MessageTypeCache.IsValidMessageType) + { + var messageType = TypeMetadataCache.ImplementationType; + + if (jsonElement.Deserialize(messageType, options) is T obj) + return obj; + } + + return jsonElement.Deserialize(options); + } + + public static T? Transform(this object objectToTransform, JsonSerializerOptions options) + where T : class + { + var jsonElement = JsonSerializer.SerializeToElement(objectToTransform, options); + + return jsonElement.GetObject(options); + } + + public static object? Transform(this object objectToTransform, Type targetType, JsonSerializerOptions options) + { + var jsonElement = JsonSerializer.SerializeToElement(objectToTransform, options); + + if (targetType.IsInterface && MessageTypeCache.IsValidMessageType(targetType)) + targetType = TypeMetadataCache.GetImplementationType(targetType); + + return jsonElement.Deserialize(targetType, options); + } +} diff --git a/src/MassTransit/Serialization/SystemTextJsonMessageBody.cs b/src/MassTransit/Serialization/SystemTextJsonMessageBody.cs index 7f4f7ddf4e1..e9a3471b8f0 100644 --- a/src/MassTransit/Serialization/SystemTextJsonMessageBody.cs +++ b/src/MassTransit/Serialization/SystemTextJsonMessageBody.cs @@ -45,7 +45,7 @@ public byte[] GetBytes() try { - var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message, MessageTypeCache.MessageTypeNames); + var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message); _bytes = JsonSerializer.SerializeToUtf8Bytes(envelope, _options); @@ -70,7 +70,7 @@ public string GetString() try { - var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message, MessageTypeCache.MessageTypeNames); + var envelope = _envelope ??= new JsonMessageEnvelope(_context, _context.Message); _string = JsonSerializer.Serialize(envelope, _options); diff --git a/src/MassTransit/Serialization/SystemTextJsonMessageSerializer.cs b/src/MassTransit/Serialization/SystemTextJsonMessageSerializer.cs index 1932e6b7d67..77ab1f7dfeb 100644 --- a/src/MassTransit/Serialization/SystemTextJsonMessageSerializer.cs +++ b/src/MassTransit/Serialization/SystemTextJsonMessageSerializer.cs @@ -3,14 +3,15 @@ namespace MassTransit.Serialization { using System; using System.Net.Mime; - using System.Reflection; using System.Runtime.Serialization; using System.Text.Encodings.Web; using System.Text.Json; + using System.Text.Json.Serialization.Metadata; using Initializers; using Initializers.TypeConverters; using JsonConverters; using Metadata; + using Internals; public class SystemTextJsonMessageSerializer : @@ -36,6 +37,16 @@ static SystemTextJsonMessageSerializer() ReadCommentHandling = JsonCommentHandling.Skip, WriteIndented = true, Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping, + + #if NET8_0_OR_GREATER + // Set the TypeInfoResolver property based on whether reflection-based is enabled. + // If reflection is enabled, combine the default resolver (reflection-based) context with the custom serializer context + // Otherwise, use only the custom serializer context. + // User can overwrite it directly or by modifying the TypeInfoResolverChain. + TypeInfoResolver = JsonSerializer.IsReflectionEnabledByDefault + ? JsonTypeInfoResolver.Combine(SystemTextJsonSerializationContext.Default, new DefaultJsonTypeInfoResolver()) + : SystemTextJsonSerializationContext.Default + #endif }; Options.Converters.Add(new StringDecimalJsonConverter()); @@ -66,7 +77,11 @@ public SerializerContext Deserialize(MessageBody body, Headers headers, Uri? des { try { - var envelope = JsonSerializer.Deserialize(body.GetBytes(), Options); + JsonElement? bodyElement = body is JsonMessageBody jsonMessageBody + ? jsonMessageBody.GetJsonElement(Options) + : JsonSerializer.Deserialize(body.GetBytes(), Options); + + var envelope = bodyElement?.Deserialize(Options); if (envelope == null) throw new SerializationException("Message envelope not found"); @@ -74,9 +89,7 @@ public SerializerContext Deserialize(MessageBody body, Headers headers, Uri? des var messageTypes = envelope.MessageType ?? Array.Empty(); - var serializerContext = new SystemTextJsonSerializerContext(this, Options, ContentType, messageContext, messageTypes, envelope); - - return serializerContext; + return new SystemTextJsonSerializerContext(this, Options, ContentType, messageContext, messageTypes, envelope); } catch (SerializationException) { @@ -114,16 +127,16 @@ public MessageBody GetMessageBody(SendContext context) && typeConverter.TryConvert(text, out var result): return result; case string text: - return GetObject(JsonSerializer.Deserialize(text, Options)); + return JsonSerializer.Deserialize(text, Options).GetObject(Options); case JsonElement jsonElement: - return GetObject(jsonElement); + return jsonElement.GetObject(Options); } var element = JsonSerializer.SerializeToElement(value, Options); return element.ValueKind == JsonValueKind.Null ? defaultValue - : GetObject(element); + : element.GetObject(Options); } public T? DeserializeObject(object? value, T? defaultValue = null) @@ -137,7 +150,8 @@ public MessageBody GetMessageBody(SendContext context) return returnValue; case string text when string.IsNullOrWhiteSpace(text): return defaultValue; - case string text when TypeConverterCache.TryGetTypeConverter(out ITypeConverter? typeConverter) && typeConverter.TryConvert(text, out var result): + case string text when TypeConverterCache.TryGetTypeConverter(out ITypeConverter? typeConverter) + && typeConverter.TryConvert(text, out var result): return result; case string text: return JsonSerializer.Deserialize(text, Options); @@ -159,19 +173,5 @@ public MessageBody SerializeObject(object? value) return new SystemTextJsonObjectMessageBody(value, Options); } - - static T? GetObject(JsonElement jsonElement) - where T : class - { - if (typeof(T).GetTypeInfo().IsInterface && MessageTypeCache.IsValidMessageType) - { - var messageType = TypeMetadataCache.ImplementationType; - - if (jsonElement.Deserialize(messageType, Options) is T obj) - return obj; - } - - return jsonElement.Deserialize(Options); - } } } diff --git a/src/MassTransit/Serialization/SystemTextJsonRawMessageSerializer.cs b/src/MassTransit/Serialization/SystemTextJsonRawMessageSerializer.cs index 683e19f56cd..e47b13789da 100644 --- a/src/MassTransit/Serialization/SystemTextJsonRawMessageSerializer.cs +++ b/src/MassTransit/Serialization/SystemTextJsonRawMessageSerializer.cs @@ -39,14 +39,25 @@ public SerializerContext Deserialize(MessageBody body, Headers headers, Uri? des { try { - var jsonElement = JsonSerializer.Deserialize(body.GetBytes(), SystemTextJsonMessageSerializer.Options); + JsonElement? bodyElement; + if (body is JsonMessageBody jsonMessageBody) + bodyElement = jsonMessageBody.GetJsonElement(SystemTextJsonMessageSerializer.Options); + else + { + var bytes = body.GetBytes(); + bodyElement = bytes.Length > 0 + ? JsonSerializer.Deserialize(bytes, SystemTextJsonMessageSerializer.Options) + : null; + } + + bodyElement ??= JsonDocument.Parse("{}").RootElement; var messageTypes = headers.GetMessageTypes(); var messageContext = new RawMessageContext(headers, destinationAddress, _options); var serializerContext = new SystemTextJsonRawSerializerContext(SystemTextJsonMessageSerializer.Instance, - SystemTextJsonMessageSerializer.Options, ContentType, messageContext, messageTypes, _options, jsonElement); + SystemTextJsonMessageSerializer.Options, ContentType, messageContext, messageTypes, _options, bodyElement.Value); return serializerContext; } @@ -69,7 +80,7 @@ public MessageBody GetMessageBody(SendContext context) where T : class { if (_options.HasFlag(RawSerializerOptions.AddTransportHeaders)) - SetRawMessageHeaders(context); + SetRawMessageHeaders(context); return new SystemTextJsonRawMessageBody(context, SystemTextJsonMessageSerializer.Options); } diff --git a/src/MassTransit/Serialization/SystemTextJsonRawSerializerContext.cs b/src/MassTransit/Serialization/SystemTextJsonRawSerializerContext.cs index a9e510288ea..771d74d66b8 100644 --- a/src/MassTransit/Serialization/SystemTextJsonRawSerializerContext.cs +++ b/src/MassTransit/Serialization/SystemTextJsonRawSerializerContext.cs @@ -41,5 +41,22 @@ public override bool IsSupportedMessageType(Type messageType) || SupportedMessageTypes.Length == 0 || SupportedMessageTypes.Any(x => typeUrn.Equals(x, StringComparison.OrdinalIgnoreCase)); } + + public override IMessageSerializer GetMessageSerializer(object message, string[] messageTypes) + { + if (message == null) + throw new ArgumentNullException(nameof(message)); + + return new SystemTextJsonBodyMessageSerializer(message, ContentType, Options, _rawOptions); + } + + public override IMessageSerializer GetMessageSerializer(MessageEnvelope envelope, T message) + { + var serializer = new SystemTextJsonBodyMessageSerializer(envelope, ContentType, Options, _rawOptions); + + serializer.Overlay(message); + + return serializer; + } } } diff --git a/src/MassTransit/Serialization/SystemTextJsonSerializationContext.cs b/src/MassTransit/Serialization/SystemTextJsonSerializationContext.cs new file mode 100644 index 00000000000..8e3a95548de --- /dev/null +++ b/src/MassTransit/Serialization/SystemTextJsonSerializationContext.cs @@ -0,0 +1,74 @@ +#if NET8_0_OR_GREATER +namespace MassTransit.Serialization +{ + using System.Text.Json.Serialization; + using Batching; + using Courier.Contracts; + using Courier.Messages; + using Events; + using Metadata; + using Scheduling; + + + [JsonSerializable(typeof(Fault))] + [JsonSerializable(typeof(FaultEvent))] + // [JsonSerializable(typeof(FaultEvent<>))] + [JsonSerializable(typeof(FaultEvent))] + [JsonSerializable(typeof(ReceiveFault))] + [JsonSerializable(typeof(ReceiveFaultEvent))] + [JsonSerializable(typeof(ExceptionInfo))] + [JsonSerializable(typeof(FaultExceptionInfo))] + [JsonSerializable(typeof(HostInfo))] + [JsonSerializable(typeof(BusHostInfo))] + // [JsonSerializable(typeof(MessageBatch<>))] + [JsonSerializable(typeof(ScheduleMessage))] + [JsonSerializable(typeof(ScheduleMessageCommand))] + // [JsonSerializable(typeof(ScheduleMessageCommand<>))] + [JsonSerializable(typeof(ScheduleRecurringMessage))] + [JsonSerializable(typeof(ScheduleRecurringMessageCommand))] + // [JsonSerializable(typeof(ScheduleRecurringMessageCommand<>))] + [JsonSerializable(typeof(CancelScheduledMessage))] + [JsonSerializable(typeof(CancelScheduledRecurringMessage))] + [JsonSerializable(typeof(CancelScheduledRecurringMessageCommand))] + [JsonSerializable(typeof(PauseScheduledRecurringMessage))] + [JsonSerializable(typeof(PauseScheduledRecurringMessageCommand))] + [JsonSerializable(typeof(ResumeScheduledRecurringMessage))] + [JsonSerializable(typeof(ResumeScheduledRecurringMessageCommand))] + [JsonSerializable(typeof(MessageEnvelope))] + [JsonSerializable(typeof(JsonMessageEnvelope))] + [JsonSerializable(typeof(RoutingSlip))] + [JsonSerializable(typeof(RoutingSlipRoutingSlip))] + [JsonSerializable(typeof(Activity))] + [JsonSerializable(typeof(RoutingSlipActivity))] + [JsonSerializable(typeof(ActivityLog))] + [JsonSerializable(typeof(RoutingSlipActivityLog))] + [JsonSerializable(typeof(CompensateLog))] + [JsonSerializable(typeof(RoutingSlipCompensateLog))] + [JsonSerializable(typeof(ActivityException))] + [JsonSerializable(typeof(RoutingSlipActivityException))] + [JsonSerializable(typeof(Subscription))] + [JsonSerializable(typeof(RoutingSlipSubscription))] + [JsonSerializable(typeof(RoutingSlipCompleted))] + [JsonSerializable(typeof(RoutingSlipCompletedMessage))] + [JsonSerializable(typeof(RoutingSlipFaulted))] + [JsonSerializable(typeof(RoutingSlipFaultedMessage))] + [JsonSerializable(typeof(RoutingSlipActivityCompleted))] + [JsonSerializable(typeof(RoutingSlipActivityCompletedMessage))] + [JsonSerializable(typeof(RoutingSlipActivityFaulted))] + [JsonSerializable(typeof(RoutingSlipActivityFaultedMessage))] + [JsonSerializable(typeof(RoutingSlipActivityCompensated))] + [JsonSerializable(typeof(RoutingSlipActivityCompensatedMessage))] + [JsonSerializable(typeof(RoutingSlipActivityCompensationFailed))] + [JsonSerializable(typeof(RoutingSlipActivityCompensationFailedMessage))] + [JsonSerializable(typeof(RoutingSlipCompensationFailed))] + [JsonSerializable(typeof(RoutingSlipCompensationFailedMessage))] + [JsonSerializable(typeof(RoutingSlipTerminated))] + [JsonSerializable(typeof(RoutingSlipTerminatedMessage))] + [JsonSerializable(typeof(RoutingSlipRevised))] + [JsonSerializable(typeof(RoutingSlipRevisedMessage))] + partial class SystemTextJsonSerializationContext : + JsonSerializerContext + { + } +} +#endif diff --git a/src/MassTransit/Serialization/SystemTextJsonSerializerContext.cs b/src/MassTransit/Serialization/SystemTextJsonSerializerContext.cs index ac54614e8c3..3f091e5e0f4 100644 --- a/src/MassTransit/Serialization/SystemTextJsonSerializerContext.cs +++ b/src/MassTransit/Serialization/SystemTextJsonSerializerContext.cs @@ -3,6 +3,7 @@ namespace MassTransit.Serialization { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Net.Mime; using System.Text.Json; using System.Text.Json.Nodes; @@ -54,7 +55,7 @@ public override bool TryGetMessage(out T? message) return false; } - public override bool TryGetMessage(Type messageType, out object? message) + public override bool TryGetMessage(Type messageType, [NotNullWhen(true)] out object? message) { var jsonElement = GetJsonElement(Message); @@ -87,7 +88,7 @@ public override IMessageSerializer GetMessageSerializer(object message, string[] var envelope = new JsonMessageEnvelope(this, message, messageTypes); - return new SystemTextJsonBodyMessageSerializer(envelope, ContentType, Options); + return new SystemTextJsonBodyMessageSerializer(envelope, ContentType, Options, messageTypes); } public override Dictionary ToDictionary(T? message) diff --git a/src/MassTransit/SqlTransport/Configuration/ISqlBusFactoryConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/ISqlBusFactoryConfigurator.cs new file mode 100644 index 00000000000..ed5fd16c236 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/ISqlBusFactoryConfigurator.cs @@ -0,0 +1,48 @@ +#nullable enable +namespace MassTransit +{ + using System; + + + public interface ISqlBusFactoryConfigurator : + IBusFactoryConfigurator, + ISqlQueueEndpointConfigurator + { + new ISqlSendTopologyConfigurator SendTopology { get; } + + new ISqlPublishTopologyConfigurator PublishTopology { get; } + + /// + /// Configure the send topology of the message type + /// + /// + /// + void Send(Action> configureTopology) + where T : class; + + /// + /// Configure the send topology of the message type + /// + /// + /// + void Publish(Action>? configureTopology = null) + where T : class; + + void Publish(Type messageType, Action? configure = null); + + /// + /// In most cases, this is not needed and should not be used. However, if for any reason the default bus + /// endpoint queue name needs to be changed, this will do it. Do NOT set it to the same name as a receive + /// endpoint or you will screw things up. + /// + void OverrideDefaultBusEndpointQueueName(string value); + + /// + /// Configure a Host that can be connected. If only one host is specified, it is used as the default + /// host for receive endpoints. + /// + /// + /// + void Host(SqlHostSettings settings); + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/ISqlHostConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/ISqlHostConfigurator.cs new file mode 100644 index 00000000000..650150eae7c --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/ISqlHostConfigurator.cs @@ -0,0 +1,95 @@ +#nullable enable +namespace MassTransit; + +using System; +using System.Data; + + +public interface ISqlHostConfigurator +{ + /// + /// Set the connection string, which the underlying database provider will parse into its individual components and rebuild at runtime + /// + string? ConnectionString { set; } + + /// + /// Optional, specifies a connection tag used to identify the connection in the database + /// + string? ConnectionTag { set; } + + /// + /// The database server host name. If using SQL server with an instance, set the separately. + /// + string? Host { set; } + + /// + /// The instance name if using SQL Server instances + /// + string? InstanceName { set; } + + /// + /// Optional, only specify if a custom port is being used. + /// + int? Port { set; } + + /// + /// The database name + /// + string? Database { set; } + + /// + /// The schema to use for the transport + /// + string? Schema { set; } + + /// + /// The username for the bus to access the transport + /// + string? Username { set; } + + /// + /// The password for the username + /// + string? Password { set; } + + string? VirtualHost { set; } + + string? Area { set; } + + /// + /// Sets the isolation level used for database transactions (default: Repeatable Read) + /// + IsolationLevel IsolationLevel { set; } + + /// + /// Sets the maximum number of connections used by the SQL transport concurrently. + /// + int ConnectionLimit { set; } + + /// + /// How often database maintenance should be performed (metrics consolidation, topology cleanup, etc.) + /// + TimeSpan MaintenanceInterval { set; } + + /// + /// How many metrics events to compute in each batch + /// + int MaintenanceBatchSize { set; } + + /// + /// How often to purge auto-delete queues from the topology and all expired messages + /// + TimeSpan QueueCleanupInterval { set; } + + /// + /// Specify the license text to use + /// + /// The license text + void UseLicense(string license); + + /// + /// Specify the path to the file containing the license text + /// + /// The path to the file + void UseLicenseFile(string path); +} diff --git a/src/MassTransit/SqlTransport/Configuration/ISqlQueueConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/ISqlQueueConfigurator.cs new file mode 100644 index 00000000000..29973022199 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/ISqlQueueConfigurator.cs @@ -0,0 +1,16 @@ +namespace MassTransit +{ + using System; + + + /// + /// Configure a database transport queue + /// + public interface ISqlQueueConfigurator + { + /// + /// If specified, the queue will be automatically removed after no consumer activity within the specific idle period + /// + public TimeSpan? AutoDeleteOnIdle { set; } + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/ISqlQueueEndpointConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/ISqlQueueEndpointConfigurator.cs new file mode 100644 index 00000000000..1c813044b22 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/ISqlQueueEndpointConfigurator.cs @@ -0,0 +1,34 @@ +namespace MassTransit +{ + using System; + + + public interface ISqlQueueEndpointConfigurator : + ISqlQueueConfigurator + { + /// + /// The polling interval of the queue when notifications are not available (or trusted) + /// + TimeSpan PollingInterval { set; } + + /// + /// The message lock duration (set higher for longer-running consumers) + /// + TimeSpan LockDuration { set; } + + /// + /// The maximum time a message can remain locked before being released for redelivery by the transport (up to MaxDeliveryCount) + /// + TimeSpan MaxLockDuration { set; } + + /// + /// The maximum number of message delivery attempts by the transport before moving the message to the DLQ + /// + int MaxDeliveryCount { set; } + + /// + /// If true, messages that exist in the queue will be purged when the bus is started + /// + bool PurgeOnStartup { set; } + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/ISqlReceiveEndpointConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/ISqlReceiveEndpointConfigurator.cs new file mode 100644 index 00000000000..faa7ecba9bb --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/ISqlReceiveEndpointConfigurator.cs @@ -0,0 +1,54 @@ +#nullable enable +namespace MassTransit +{ + using System; + using SqlTransport; + + + /// + /// Configure a database transport receive endpoint + /// + public interface ISqlReceiveEndpointConfigurator : + IReceiveEndpointConfigurator, + ISqlQueueEndpointConfigurator + { + /// + /// The time to wait before the message is redelivered when faults are rethrown to the transport. + /// Defaults to 0. + /// + TimeSpan? UnlockDelay { set; } + + /// + /// Set number of concurrent messages per PartitionKey, higher value will increase throughput but will break delivery order (default: 1). + /// This applies to the concurrent receive modes only. + /// + int ConcurrentDeliveryLimit { set; } + + /// + /// Set the endpoint receive mode (changes the delivery behavior of messages to use partition keys, ordering, etc. + /// + /// + /// + void SetReceiveMode(SqlReceiveMode mode, int? concurrentDeliveryLimit = default); + + /// + /// Adds a topic subscription to the receive endpoint by message type + /// + /// + void Subscribe(Action? callback = null) + where T : class; + + /// + /// Adds a topic subscription to the receive endpoint + /// + /// The topic name + /// Configure the topic and the subscription + void Subscribe(string topicName, Action? callback = default); + + /// + /// Add middleware to the receive endpoint pipe + /// + /// + void ConfigureClient(Action>? configure); + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/ISqlTopicConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/ISqlTopicConfigurator.cs new file mode 100644 index 00000000000..588d4c28474 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/ISqlTopicConfigurator.cs @@ -0,0 +1,9 @@ +namespace MassTransit +{ + /// + /// Configures a topic for the database transport + /// + public interface ISqlTopicConfigurator + { + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/ISqlTopicSubscriptionConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/ISqlTopicSubscriptionConfigurator.cs new file mode 100644 index 00000000000..a32c8b062ba --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/ISqlTopicSubscriptionConfigurator.cs @@ -0,0 +1,14 @@ +#nullable enable +namespace MassTransit +{ + /// + /// Configures the topic subscription for the receive endpoint + /// + public interface ISqlTopicSubscriptionConfigurator : + ISqlTopicConfigurator + { + SqlSubscriptionType SubscriptionType { set; } + + string? RoutingKey { set; } + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/SqlQueueType.cs b/src/MassTransit/SqlTransport/Configuration/SqlQueueType.cs new file mode 100644 index 00000000000..4486626e736 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/SqlQueueType.cs @@ -0,0 +1,9 @@ +namespace MassTransit +{ + public enum SqlQueueType + { + Queue = 1, + ErrorQueue = 2, + DeadLetterQueue = 3 + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/SqlReceiveEndpointConfigurationExtensions.cs b/src/MassTransit/SqlTransport/Configuration/SqlReceiveEndpointConfigurationExtensions.cs new file mode 100644 index 00000000000..fffc9727326 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/SqlReceiveEndpointConfigurationExtensions.cs @@ -0,0 +1,32 @@ +#nullable enable +namespace MassTransit; + +using System; + + +public static class SqlReceiveEndpointConfigurationExtensions +{ + /// + /// Declare a ReceiveEndpoint using a unique generated queue name. This queue defaults to auto-delete + /// and non-durable. By default all services bus instances include a default receiveEndpoint that is + /// of this type (created automatically upon the first receiver binding). + /// + /// + /// + public static void ReceiveEndpoint(this ISqlBusFactoryConfigurator configurator, Action? configure = null) + { + configurator.ReceiveEndpoint(new TemporaryEndpointDefinition(), DefaultEndpointNameFormatter.Instance, configure); + } + + /// + /// Declare a receive endpoint using the endpoint . + /// + /// + /// + /// + public static void ReceiveEndpoint(this ISqlBusFactoryConfigurator configurator, IEndpointDefinition definition, + Action? configure = null) + { + configurator.ReceiveEndpoint(definition, DefaultEndpointNameFormatter.Instance, configure); + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/SqlScheduleMessageExtensions.cs b/src/MassTransit/SqlTransport/Configuration/SqlScheduleMessageExtensions.cs new file mode 100644 index 00000000000..4b225d2d6f8 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/SqlScheduleMessageExtensions.cs @@ -0,0 +1,76 @@ +namespace MassTransit +{ + using System; + using DependencyInjection; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + using Scheduling; + using SqlTransport.Configuration; + using Transports; + + + public static class SqlScheduleMessageExtensions + { + /// + /// Uses the SQL transport's built-in message scheduler + /// + /// + [Obsolete("Use the renamed UseSqlMessageScheduler instead")] + public static void UseDbMessageScheduler(this IBusFactoryConfigurator configurator) + { + UseSqlMessageScheduler(configurator); + } + + /// + /// Uses the SQL transport's built-in message scheduler + /// + /// + public static void UseSqlMessageScheduler(this IBusFactoryConfigurator configurator) + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + + var pipeBuilderConfigurator = new SqlMessageSchedulerSpecification(); + + configurator.AddPrePipeSpecification(pipeBuilderConfigurator); + } + + /// + /// Add a to the container that uses the SQL Transport message enqueue time to schedule messages. + /// + /// + public static void AddSqlMessageScheduler(this IBusRegistrationConfigurator configurator) + { + configurator.TryAddScoped(provider => + { + var busInstance = provider.GetRequiredService>().Value; + var sendEndpointProvider = provider.GetRequiredService(); + + var hostConfiguration = busInstance.HostConfiguration as ISqlHostConfiguration + ?? throw new ArgumentException("The SQL transport configuration was not found"); + + return new MessageScheduler(new SqlScheduleMessageProvider(hostConfiguration, sendEndpointProvider), busInstance.Bus.Topology); + }); + } + + /// + /// Add a to the container that uses the SQL Transport message enqueue time to schedule messages. + /// + /// + public static void AddSqlMessageScheduler(this IBusRegistrationConfigurator configurator) + where TBus : class, IBus + { + configurator.TryAddScoped(provider => + { + var busInstance = provider.GetRequiredService>().Value; + var sendEndpointProvider = provider.GetRequiredService(); + + var hostConfiguration = busInstance.HostConfiguration as ISqlHostConfiguration + ?? throw new ArgumentException("The SQL transport configuration was not found"); + + return Bind.Create( + new MessageScheduler(new SqlScheduleMessageProvider(hostConfiguration, sendEndpointProvider), busInstance.Bus.Topology)); + }); + } + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/SqlSubscriptionType.cs b/src/MassTransit/SqlTransport/Configuration/SqlSubscriptionType.cs new file mode 100644 index 00000000000..1d6666f82b8 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/SqlSubscriptionType.cs @@ -0,0 +1,9 @@ +namespace MassTransit +{ + public enum SqlSubscriptionType + { + All = 1, + RoutingKey = 2, + Pattern = 3 + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/SqlTransportMigrationOptions.cs b/src/MassTransit/SqlTransport/Configuration/SqlTransportMigrationOptions.cs new file mode 100644 index 00000000000..f93a308f359 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/SqlTransportMigrationOptions.cs @@ -0,0 +1,22 @@ +namespace MassTransit +{ + public class SqlTransportMigrationOptions + { + /// + /// If true, the database and all transport components will be created/updated on startup + /// + public bool CreateDatabase { get; set; } + + /// + /// If true, the infrastructure components for the transport will be created/updated on startup + /// + /// Use this, without CreateDatabase, if you do not have the required permissions to create databases and logins + /// + public bool CreateInfrastructure { get; set; } + + /// + /// If true, the database and all transport components will be deleted on shutdown + /// + public bool DeleteDatabase { get; set; } + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/SqlTransportOptions.cs b/src/MassTransit/SqlTransport/Configuration/SqlTransportOptions.cs new file mode 100644 index 00000000000..39976c55843 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/SqlTransportOptions.cs @@ -0,0 +1,26 @@ +#nullable enable +namespace MassTransit; + +public class SqlTransportOptions +{ + public string? Host { get; set; } + public int? Port { get; set; } + public string? Database { get; set; } + public string? Schema { get; set; } + public string? Role { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + + public string? AdminUsername { get; set; } + public string? AdminPassword { get; set; } + + /// + /// Optional, if specified, will be parsed to capture additional properties on the connection. + /// + public string? ConnectionString { get; set; } + + /// + /// If specified, changes the connection limit from the default value (10) + /// + public int? ConnectionLimit { get; set; } +} diff --git a/src/MassTransit/SqlTransport/Configuration/Topology/ISqlConsumeTopologyConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlConsumeTopologyConfigurator.cs new file mode 100644 index 00000000000..e58beb0b01b --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlConsumeTopologyConfigurator.cs @@ -0,0 +1,25 @@ +#nullable enable +namespace MassTransit; + +using System; +using System.ComponentModel; +using SqlTransport.Configuration; + + +public interface ISqlConsumeTopologyConfigurator : + IConsumeTopologyConfigurator, + ISqlConsumeTopology +{ + new ISqlMessageConsumeTopologyConfigurator GetMessageTopology() + where T : class; + + [EditorBrowsable(EditorBrowsableState.Never)] + void AddSpecification(ISqlConsumeTopologySpecification specification); + + /// + /// Bind an exchange, using the configurator + /// + /// + /// + void Subscribe(string topicName, Action? configure = null); +} diff --git a/src/MassTransit/SqlTransport/Configuration/Topology/ISqlMessageConsumeTopologyConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlMessageConsumeTopologyConfigurator.cs new file mode 100644 index 00000000000..ee2a648ed98 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlMessageConsumeTopologyConfigurator.cs @@ -0,0 +1,29 @@ +#nullable enable +namespace MassTransit; + +using System; +using SqlTransport.Topology; + + +public interface ISqlMessageConsumeTopologyConfigurator : + IMessageConsumeTopologyConfigurator, + ISqlMessageConsumeTopology + where TMessage : class +{ + /// + /// Adds the exchange bindings for this message type + /// + /// Configure the binding and the exchange + void Subscribe(Action? configure = null); +} + + +public interface IDbMessageConsumeTopologyConfigurator : + IMessageConsumeTopologyConfigurator +{ + /// + /// Apply the message topology to the builder + /// + /// + void Apply(IReceiveEndpointBrokerTopologyBuilder builder); +} diff --git a/src/MassTransit/SqlTransport/Configuration/Topology/ISqlMessagePublishTopologyConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlMessagePublishTopologyConfigurator.cs new file mode 100644 index 00000000000..12e20de872c --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlMessagePublishTopologyConfigurator.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace MassTransit +{ + public interface ISqlMessagePublishTopologyConfigurator : + IMessagePublishTopologyConfigurator, + ISqlMessagePublishTopology, + ISqlMessagePublishTopologyConfigurator + where TMessage : class + { + } + + + public interface ISqlMessagePublishTopologyConfigurator : + IMessagePublishTopologyConfigurator, + ISqlTopicConfigurator + { + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/Topology/ISqlMessageSendTopologyConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlMessageSendTopologyConfigurator.cs new file mode 100644 index 00000000000..6bfacdc158d --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlMessageSendTopologyConfigurator.cs @@ -0,0 +1,16 @@ +namespace MassTransit +{ + public interface ISqlMessageSendTopologyConfigurator : + IMessageSendTopologyConfigurator, + ISqlMessageSendTopology, + ISqlMessageSendTopologyConfigurator + where TMessage : class + { + } + + + public interface ISqlMessageSendTopologyConfigurator : + IMessageSendTopologyConfigurator + { + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/Topology/ISqlPublishTopologyConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlPublishTopologyConfigurator.cs new file mode 100644 index 00000000000..c563bd34520 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlPublishTopologyConfigurator.cs @@ -0,0 +1,15 @@ +namespace MassTransit +{ + using System; + + + public interface ISqlPublishTopologyConfigurator : + IPublishTopologyConfigurator, + ISqlPublishTopology + { + new ISqlMessagePublishTopologyConfigurator GetMessageTopology() + where T : class; + + new ISqlMessagePublishTopologyConfigurator GetMessageTopology(Type messageType); + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/Topology/ISqlSendTopologyConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlSendTopologyConfigurator.cs new file mode 100644 index 00000000000..f516d3e655a --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlSendTopologyConfigurator.cs @@ -0,0 +1,13 @@ +namespace MassTransit +{ + using System; + + + public interface ISqlSendTopologyConfigurator : + ISendTopologyConfigurator, + ISqlSendTopology + { + Action ConfigureErrorSettings { set; } + Action ConfigureDeadLetterSettings { set; } + } +} diff --git a/src/MassTransit/SqlTransport/Configuration/Topology/ISqlTopicToTopicBindingConfigurator.cs b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlTopicToTopicBindingConfigurator.cs new file mode 100644 index 00000000000..0726b2651c4 --- /dev/null +++ b/src/MassTransit/SqlTransport/Configuration/Topology/ISqlTopicToTopicBindingConfigurator.cs @@ -0,0 +1,17 @@ +#nullable enable +namespace MassTransit +{ + using System; + + + public interface ISqlTopicToTopicBindingConfigurator : + ISqlTopicSubscriptionConfigurator + { + /// + /// Creates a subscription between two topics + /// + /// Topic name of the new exchange + /// Configuration for new exchange and how to bind to it + void Subscribe(string topicName, Action? configure = null); + } +} diff --git a/src/MassTransit/SqlTransport/Exceptions/SqlEndpointAddressException.cs b/src/MassTransit/SqlTransport/Exceptions/SqlEndpointAddressException.cs new file mode 100644 index 00000000000..1a40ab4b2bb --- /dev/null +++ b/src/MassTransit/SqlTransport/Exceptions/SqlEndpointAddressException.cs @@ -0,0 +1,33 @@ +namespace MassTransit +{ + using System; + using System.Runtime.Serialization; + + + [Serializable] + public sealed class SqlEndpointAddressException : + AbstractUriException + { + public SqlEndpointAddressException() + { + } + + public SqlEndpointAddressException(Uri address, string message) + : base(address, message) + { + } + + public SqlEndpointAddressException(Uri address, string message, Exception innerException) + : base(address, message, innerException) + { + } + + #if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] + #endif + public SqlEndpointAddressException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/MassTransit/SqlTransport/Exceptions/SqlTopologyException.cs b/src/MassTransit/SqlTransport/Exceptions/SqlTopologyException.cs new file mode 100644 index 00000000000..7c56862e94f --- /dev/null +++ b/src/MassTransit/SqlTransport/Exceptions/SqlTopologyException.cs @@ -0,0 +1,34 @@ +#nullable enable +namespace MassTransit +{ + using System; + using System.Runtime.Serialization; + + + [Serializable] + public class SqlTopologyException : + MassTransitException + { + public SqlTopologyException() + { + } + + public SqlTopologyException(string? message) + : base(message) + { + } + + public SqlTopologyException(string? message, Exception? innerException) + : base(message, innerException) + { + } + + #if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] + #endif + protected SqlTopologyException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + } +} diff --git a/src/MassTransit/SqlTransport/Scheduling/SqlScheduleMessageProvider.cs b/src/MassTransit/SqlTransport/Scheduling/SqlScheduleMessageProvider.cs new file mode 100644 index 00000000000..d8cc5c71825 --- /dev/null +++ b/src/MassTransit/SqlTransport/Scheduling/SqlScheduleMessageProvider.cs @@ -0,0 +1,120 @@ +#nullable enable +namespace MassTransit.Scheduling +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using SqlTransport; + using SqlTransport.Configuration; + using Transports; + + + public class SqlScheduleMessageProvider : + IScheduleMessageProvider + { + readonly Func, CancellationToken, Task> _cancel; + readonly ConsumeContext? _context; + readonly ISqlHostConfiguration? _hostConfiguration; + readonly ISendEndpointProvider _sendEndpointProvider; + + public SqlScheduleMessageProvider(ConsumeContext context) + { + _context = context; + _sendEndpointProvider = context; + + _cancel = RetryUsingContext; + } + + public SqlScheduleMessageProvider(ISqlHostConfiguration hostConfiguration, ISendEndpointProvider sendEndpointProvider) + { + _hostConfiguration = hostConfiguration; + _sendEndpointProvider = sendEndpointProvider; + + _cancel = RetryUsingHostConfiguration; + } + + public async Task> ScheduleSend(Uri destinationAddress, DateTime scheduledTime, T message, IPipe> pipe, + CancellationToken cancellationToken) + where T : class + { + if (!MessageTypeCache.IsValidMessageType) + throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); + + var schedulePipe = new ScheduleSendPipe(pipe, scheduledTime); + + var tokenId = ScheduleTokenIdCache.GetTokenId(message); + + schedulePipe.ScheduledMessageId = tokenId; + + var endpoint = await _sendEndpointProvider.GetSendEndpoint(destinationAddress).ConfigureAwait(false); + + await endpoint.Send(message, schedulePipe, cancellationToken).ConfigureAwait(false); + + LogContext.Debug?.Log("SCHED {DestinationAddress} {MessageId} {MessageType} {DeliveryTime:G} {Token}", + destinationAddress, schedulePipe.MessageId, TypeCache.ShortName, scheduledTime, schedulePipe.ScheduledMessageId); + + return new ScheduledMessageHandle(schedulePipe.ScheduledMessageId ?? NewId.NextGuid(), scheduledTime, destinationAddress, message); + } + + public Task CancelScheduledSend(Guid tokenId, CancellationToken cancellationToken) + { + return _cancel(async clientContext => + { + var deleted = await clientContext.DeleteScheduledMessage(tokenId, cancellationToken).ConfigureAwait(false); + if (deleted) + LogContext.Debug?.Log("CANCEL {TokenId}", tokenId); + }, cancellationToken); + } + + public Task CancelScheduledSend(Uri destinationAddress, Guid tokenId, CancellationToken cancellationToken) + { + return _cancel(async clientContext => + { + var deleted = await clientContext.DeleteScheduledMessage(tokenId, cancellationToken).ConfigureAwait(false); + if (deleted) + LogContext.Debug?.Log("CANCEL {DestinationAddress} {TokenId}", destinationAddress, tokenId); + }, cancellationToken); + } + + Task RetryUsingContext(Func callback, CancellationToken cancellationToken) + { + if (!_context!.TryGetPayload(out ClientContext? clientContext)) + throw new ArgumentException("The client context was not available", nameof(_context)); + + return callback(clientContext); + } + + Task RetryUsingHostConfiguration(Func callback, CancellationToken cancellationToken) + { + var pipe = new ClientContextPipe(callback, cancellationToken); + + return _hostConfiguration.Retry(() => _hostConfiguration!.ConnectionContextSupervisor.Send(pipe, cancellationToken), cancellationToken, + _hostConfiguration!.ConnectionContextSupervisor.Stopping); + } + + + class ClientContextPipe : + IPipe + { + readonly Func _callback; + readonly CancellationToken _cancellationToken; + + public ClientContextPipe(Func callback, CancellationToken cancellationToken) + { + _callback = callback; + _cancellationToken = cancellationToken; + } + + public Task Send(ConnectionContext context) + { + var clientContext = context.CreateClientContext(_cancellationToken); + + return _callback(clientContext); + } + + public void Probe(ProbeContext context) + { + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlBusFactory.cs b/src/MassTransit/SqlTransport/SqlBusFactory.cs new file mode 100644 index 00000000000..38c192b9ddc --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlBusFactory.cs @@ -0,0 +1,45 @@ +namespace MassTransit +{ + using System; + using Configuration; + using SqlTransport.Configuration; + using SqlTransport.Topology; + using Topology; + + + public static class SqlBusFactory + { + /// + /// Create a bus using the database transport + /// + /// The configuration callback to configure the bus + /// + public static IBusControl Create(Action configure) + { + var topologyConfiguration = new SqlTopologyConfiguration(CreateMessageTopology()); + var busConfiguration = new SqlBusConfiguration(topologyConfiguration); + + var configurator = new SqlBusFactoryConfigurator(busConfiguration); + + configure(configurator); + + return configurator.Build(busConfiguration); + } + + public static IMessageTopologyConfigurator CreateMessageTopology() + { + return new MessageTopology(Cached.EntityNameFormatter); + } + + + static class Cached + { + internal static readonly IEntityNameFormatter EntityNameFormatter; + + static Cached() + { + EntityNameFormatter = new MessageNameFormatterEntityNameFormatter(new SqlMessageNameFormatter()); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlEndpointAddress.cs b/src/MassTransit/SqlTransport/SqlEndpointAddress.cs new file mode 100644 index 00000000000..bf07553c8ba --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlEndpointAddress.cs @@ -0,0 +1,154 @@ +namespace MassTransit +{ + #nullable enable + using System; + using System.Collections.Generic; + using System.Diagnostics; + using Initializers; + using Initializers.TypeConverters; + using Internals; + + + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] + public readonly struct SqlEndpointAddress + { + const string InstanceNameKey = "instance"; + const string AutoDeleteKey = "autodelete"; + const string TypeKey = "type"; + + + public enum AddressType + { + Queue = 0, + Topic = 1 + } + + + static readonly ITypeConverter _parseConverter = new EnumTypeConverter(); + + public readonly string Scheme; + public readonly string Host; + public readonly string? InstanceName; + public readonly int? Port; + public readonly string VirtualHost; + public readonly string? Area; + public readonly string Name; + + public readonly TimeSpan? AutoDeleteOnIdle; + public readonly AddressType Type; + + public SqlEndpointAddress(Uri hostAddress, Uri address, AddressType type = AddressType.Queue) + { + Port = default; + + AutoDeleteOnIdle = null; + Type = type; + + ParseLeft(hostAddress, out Scheme, out Host, out InstanceName, out Port, out VirtualHost, out Area); + + var scheme = address.Scheme.ToLowerInvariant(); + switch (scheme) + { + case SqlHostAddress.DbScheme: + + address.ParseHostPathAndEntityName(out _, out Name!); + + if (string.IsNullOrWhiteSpace(Name)) + throw new SqlEndpointAddressException(address, "Endpoint name must be specified"); + break; + + case "queue": + Name = address.AbsolutePath; + break; + + case "topic": + Area = default; + Name = address.AbsolutePath; + Type = AddressType.Topic; + break; + + default: + throw new SqlEndpointAddressException(address, "Scheme is not supported"); + } + + foreach (var (key, value) in address.SplitQueryString()) + { + switch (key) + { + case AutoDeleteKey when int.TryParse(value, out var result): + AutoDeleteOnIdle = TimeSpan.FromSeconds(result); + break; + + case TypeKey when value != null && _parseConverter.TryConvert(value, out var result): + Type = result; + break; + } + } + + if (Type != AddressType.Queue && AutoDeleteOnIdle.HasValue) + AutoDeleteOnIdle = null; + } + + public SqlEndpointAddress(Uri hostAddress, string name, TimeSpan? autoDeleteOnIdle = null, AddressType type = AddressType.Queue) + { + ParseLeft(hostAddress, out Scheme, out Host, out InstanceName, out Port, out VirtualHost, out Area); + + Name = name; + + AutoDeleteOnIdle = type == AddressType.Queue ? autoDeleteOnIdle : null; + + Type = type; + + if (type == AddressType.Topic) + Area = default; + } + + static void ParseLeft(Uri address, out string scheme, out string host, out string? instanceName, out int? port, out string virtualHost, + out string? area) + { + var hostAddress = new SqlHostAddress(address); + scheme = hostAddress.Scheme; + host = hostAddress.Host; + instanceName = hostAddress.InstanceName; + port = address.IsDefaultPort ? null : address.Port; + virtualHost = hostAddress.VirtualHost; + area = hostAddress.Area; + } + + public static implicit operator Uri(in SqlEndpointAddress address) + { + var path = address.VirtualHost == "/" ? "/" : Uri.EscapeDataString(address.VirtualHost); + if (!string.IsNullOrWhiteSpace(address.Area) && address.Type == AddressType.Queue) + path += "." + Uri.EscapeDataString(address.Area); + if (path[path.Length - 1] != '/') + path += '/'; + path += address.Name; + + var builder = new UriBuilder + { + Scheme = address.Scheme, + Host = address.Host.Trim().Trim('(', ')'), + Port = address.Port ?? -1, + Path = path + }; + + builder.Query += string.Join("&", address.GetQueryStringOptions()); + + return builder.Uri; + } + + Uri DebuggerDisplay => this; + + IEnumerable GetQueryStringOptions() + { + if (AutoDeleteOnIdle.HasValue && Type == AddressType.Queue) + yield return $"{AutoDeleteKey}={AutoDeleteOnIdle.Value.TotalSeconds:F0}"; + + if (Type != AddressType.Queue) + yield return $"{TypeKey}=topic"; + + if (!string.IsNullOrEmpty(InstanceName)) + yield return $"{InstanceNameKey}={InstanceName}"; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlHostAddress.cs b/src/MassTransit/SqlTransport/SqlHostAddress.cs new file mode 100644 index 00000000000..f97d8f9dc18 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlHostAddress.cs @@ -0,0 +1,183 @@ +#nullable enable +namespace MassTransit +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using Internals; + + + /// + /// The database host address is composed of specific parts + /// db://localhost/virtual_host_name.scope + /// db://localhost/.scope + /// + /// + /// Fragment + /// Description + /// + /// + /// Host + /// The host name from the connection string, or the host alias if configured + /// + /// + /// Virtual Host + /// + /// The name for an isolated set of topics, queues, and subscriptions in the host/schema specified by the connection string. + /// If not specified, the default virtual host is used. + /// + /// + /// + /// Area + /// The name an area, which contains one or more queues. If not specified, the default area within the virtual host is used. + /// + /// + /// + [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] + public readonly struct SqlHostAddress + { + public const string DbScheme = "db"; + + const string InstanceNameKey = "instance"; + + public readonly string Scheme; + public readonly string Host; + public readonly int? Port; + public readonly string? InstanceName; + public readonly string VirtualHost; + public readonly string? Area; + + public SqlHostAddress(Uri address) + { + var scheme = address.Scheme.ToLowerInvariant(); + switch (scheme) + { + case DbScheme: + ParseLeft(address, out Scheme, out Host, out Port, out VirtualHost, out Area); + break; + + default: + throw new ArgumentException($"The address scheme is not supported: {address.Scheme}", nameof(address)); + } + + foreach (var (key, value) in address.SplitQueryString()) + { + switch (key) + { + case InstanceNameKey when !string.IsNullOrWhiteSpace(value): + InstanceName = value; + break; + } + } + } + + public SqlHostAddress(string host, string? instanceName, int? port, string virtualHost, string? area) + { + Scheme = DbScheme; + Host = host; + InstanceName = instanceName; + Port = port; + VirtualHost = virtualHost; + Area = area; + } + + internal static void ParseLeft(Uri address, out string scheme, out string host, out int? port, out string virtualHost, out string? area) + { + scheme = address.Scheme; + host = address.Host; + port = address.IsDefaultPort ? null : address.Port; + + (virtualHost, area) = GetVirtualHostAndArea(address); + } + + static (string virtualHost, string? area) GetVirtualHostAndArea(Uri address) + { + var path = address.AbsolutePath; + + if (string.IsNullOrWhiteSpace(path)) + return ("/", null); + + if (path.Length == 1 && path[0] == '/') + return ("/", null); + + var split = path.LastIndexOf('/'); + + ReadOnlySpan span = split > 0 + ? path.AsSpan(1, split - 1) + : path.AsSpan(1); + + string virtualHost; + string? area = null; + + var areaSplit = span.IndexOf('.'); + if (areaSplit > 0) + { + virtualHost = Uri.UnescapeDataString(span.Slice(0, areaSplit).ToString()).Trim(); + area = Uri.UnescapeDataString(span.Slice(areaSplit + 1).ToString()).Trim(); + } + else + virtualHost = Uri.UnescapeDataString(span.ToString()).Trim(); + + if (string.IsNullOrWhiteSpace(virtualHost)) + return ("/", null); + + if (!IsValidSymbol(virtualHost)) + throw new SqlEndpointAddressException(address, "Virtual host must be alphanumeric"); + + if (string.IsNullOrWhiteSpace(area)) + return (virtualHost, null); + + if (!IsValidSymbol(area)) + throw new SqlEndpointAddressException(address, "Area must be alphanumeric"); + + return (virtualHost, area); + } + + public static implicit operator Uri(in SqlHostAddress address) + { + var path = address.VirtualHost == "/" ? "/" : Uri.EscapeDataString(address.VirtualHost); + if (!string.IsNullOrWhiteSpace(address.Area)) + path += "." + Uri.EscapeDataString(address.Area); + + var builder = new UriBuilder + { + Scheme = address.Scheme, + Host = address.Host.Trim().Trim('(', ')'), + Port = address.Port ?? -1, + Path = path + }; + + + builder.Query += string.Join("&", address.GetQueryStringOptions()); + + return builder.Uri; + } + + Uri DebuggerDisplay => this; + + IEnumerable GetQueryStringOptions() + { + if (!string.IsNullOrEmpty(InstanceName)) + yield return $"{InstanceNameKey}={InstanceName}"; + } + + static bool IsValidSymbol(string? className) + { + if (string.IsNullOrEmpty(className)) + return false; + + var c0 = className![0]; + if (!(char.IsLetter(c0) || c0 == '_')) + return false; + + for (var i = 1; i < className.Length; i++) + { + var c = className[i]; + if (!(char.IsLetterOrDigit(c) || c == '_')) + return false; + } + + return true; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlHostSettings.cs b/src/MassTransit/SqlTransport/SqlHostSettings.cs new file mode 100644 index 00000000000..706ff408ddd --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlHostSettings.cs @@ -0,0 +1,36 @@ +#nullable enable +namespace MassTransit +{ + using System; + using System.Data; + using Licensing; + using SqlTransport; + using SqlTransport.Configuration; + + + /// + /// Settings to configure a DbTransport host explicitly without requiring the fluent interface + /// + public interface SqlHostSettings : + ISpecification + { + Uri HostAddress { get; } + + string? ConnectionTag { get; } + + string? VirtualHost { get; } + string? Area { get; } + + IsolationLevel IsolationLevel { get; } + + int ConnectionLimit { get; } + + TimeSpan MaintenanceInterval { get; } + TimeSpan QueueCleanupInterval { get; } + int MaintenanceBatchSize { get; } + + ConnectionContextFactory CreateConnectionContextFactory(ISqlHostConfiguration configuration); + + LicenseInfo? GetLicenseInfo(); + } +} diff --git a/src/MassTransit/SqlTransport/SqlMessageContext.cs b/src/MassTransit/SqlTransport/SqlMessageContext.cs new file mode 100644 index 00000000000..7a96d22d0e2 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlMessageContext.cs @@ -0,0 +1,25 @@ +namespace MassTransit +{ + using System; + using SqlTransport; + + + public interface SqlMessageContext : + RoutingKeyConsumeContext, + PartitionKeyConsumeContext + { + SqlTransportMessage TransportMessage { get; } + + Guid TransportMessageId { get; } + long DeliveryMessageId { get; } + + string QueueName { get; } + + Guid? ConsumerId { get; } + Guid? LockId { get; } + + short Priority { get; } + DateTime EnqueueTime { get; } + int DeliveryCount { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlPublishTopologyConfigurationExtensions.cs b/src/MassTransit/SqlTransport/SqlPublishTopologyConfigurationExtensions.cs new file mode 100644 index 00000000000..2aaa824a038 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlPublishTopologyConfigurationExtensions.cs @@ -0,0 +1,73 @@ +#nullable enable +namespace MassTransit +{ + using System; + using System.Collections.Generic; + using Util; + + + public static class SqlPublishTopologyConfigurationExtensions + { + /// + /// Adds any valid message types found in the specified namespace to the publish topology + /// + /// + /// + /// + public static void AddPublishMessageTypesFromNamespaceContaining(this ISqlBusFactoryConfigurator configurator, + Action? configure = null, Func? filter = null) + { + AddPublishMessageTypesFromNamespaceContaining(configurator, typeof(T), configure, filter); + } + + /// + /// Adds any valid message types found in the specified namespace to the publish topology + /// + /// + /// The type to use to identify the assembly and namespace to scan + /// + /// + public static void AddPublishMessageTypesFromNamespaceContaining(this ISqlBusFactoryConfigurator configurator, Type type, + Action? configure = null, Func? filter = null) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + if (type.Assembly == null || type.Namespace == null) + throw new ArgumentException($"The type {TypeCache.GetShortName(type)} is not in an assembly with a valid namespace", nameof(type)); + + IEnumerable types; + + const TypeClassification typeClassification = TypeClassification.Concrete | TypeClassification.Closed | TypeClassification.Abstract + | TypeClassification.Interface; + + if (filter != null) + { + bool IsAllowed(Type candidate) + { + return MessageTypeCache.IsValidMessageType(candidate) && filter(candidate); + } + + types = AssemblyTypeCache.FindTypesInNamespace(type, IsAllowed, typeClassification); + } + else + types = AssemblyTypeCache.FindTypesInNamespace(type, MessageTypeCache.IsValidMessageType, typeClassification); + + foreach (var messageType in types) + configurator.Publish(messageType, x => configure?.Invoke(x, messageType)); + } + + /// + /// Adds the specified message types to the publish topology + /// + /// + /// + /// + public static void AddPublishMessageTypes(this ISqlBusFactoryConfigurator configurator, IEnumerable messageTypes, + Action? configure = null) + { + foreach (var messageType in messageTypes) + configurator.Publish(messageType, x => configure?.Invoke(x, messageType)); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlReceiveMode.cs b/src/MassTransit/SqlTransport/SqlReceiveMode.cs new file mode 100644 index 00000000000..75269e76cab --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlReceiveMode.cs @@ -0,0 +1,30 @@ +namespace MassTransit +{ + public enum SqlReceiveMode + { + /// + /// Messages are delivered normally in priority, enqueue_time order + /// + Normal = 0, + + /// + /// Messages are delivered in priority enqueue_time order, but only one message per PartitionKey at a time + /// + Partitioned = 1, + + /// + /// Messages are delivered in priority enqueue_time order, with additional messages fetched from the server for the same PartitionKey + /// + PartitionedConcurrent = 2, + + /// + /// Messages are delivered in first-in first-out order, but only one message per PartitionKey at a time + /// + PartitionedOrdered = 3, + + /// + /// Messages are delivered in first-in first-out order, with additional messages fetched from the server for the same PartitionKey + /// + PartitionedOrderedConcurrent = 4, + } +} diff --git a/src/MassTransit/SqlTransport/SqlSendContext.cs b/src/MassTransit/SqlTransport/SqlSendContext.cs new file mode 100644 index 00000000000..f49c194f539 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlSendContext.cs @@ -0,0 +1,23 @@ +namespace MassTransit +{ + using System; + + + public interface SqlSendContext : + SqlSendContext, + SendContext + where T : class + { + } + + + public interface SqlSendContext : + SendContext, + RoutingKeySendContext, + PartitionKeySendContext + { + Guid TransportMessageId { get; } + + public short? Priority { set; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlSendContextExtensions.cs b/src/MassTransit/SqlTransport/SqlSendContextExtensions.cs new file mode 100644 index 00000000000..68e8d005b0a --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlSendContextExtensions.cs @@ -0,0 +1,36 @@ +#nullable enable +namespace MassTransit +{ + using System; + + + public static class SqlSendContextExtensions + { + /// + /// Sets the message priority (default: 100) + /// + /// + /// + public static void SetPriority(this SendContext context, short priority) + { + if (!context.TryGetPayload(out SqlSendContext? sendContext)) + throw new ArgumentException("The DbSendContext was not available"); + + sendContext.Priority = priority == 100 ? default(short?) : priority; + } + + /// + /// Sets the message priority (default: 100) + /// + /// + /// + public static bool TrySetPriority(this SendContext context, short priority) + { + if (!context.TryGetPayload(out SqlSendContext? sendContext)) + return false; + + sendContext.Priority = priority == 100 ? default(short?) : priority; + return true; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ClientContext.cs b/src/MassTransit/SqlTransport/SqlTransport/ClientContext.cs new file mode 100644 index 00000000000..cab0b13bbaa --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ClientContext.cs @@ -0,0 +1,68 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Topology; + + + public interface ClientContext : + PipeContext + { + ConnectionContext ConnectionContext { get; } + + /// + /// Create a queue + /// + /// + /// + Task CreateQueue(Queue queue); + + /// + /// Create a topic + /// + /// + /// + Task CreateTopic(Topic topic); + + /// + /// Create a topic subscription + /// + /// + /// + Task CreateTopicSubscription(TopicToTopicSubscription subscription); + + /// + /// Create a topic subscription to a queue + /// + /// + /// + Task CreateQueueSubscription(TopicToQueueSubscription subscription); + + /// + /// Purge the specified queue (including all queue types), returning the number of messages removed + /// + /// + /// + /// + Task PurgeQueue(string queueName, CancellationToken cancellationToken); + + Task Send(string queueName, SqlMessageSendContext context) + where T : class; + + Task Publish(string topicName, SqlMessageSendContext context) + where T : class; + + Task> ReceiveMessages(string queueName, SqlReceiveMode mode, int messageLimit, int concurrentCount, + TimeSpan lockDuration); + + Task TouchQueue(string queueName); + + Task DeleteMessage(Guid lockId, long messageDeliveryId); + Task DeleteScheduledMessage(Guid tokenId, CancellationToken cancellationToken); + Task MoveMessage(Guid lockId, long messageDeliveryId, string queueName, SqlQueueType queueType, SendHeaders sendHeaders); + Task RenewLock(Guid lockId, long messageDeliveryId, TimeSpan duration); + Task Unlock(Guid lockId, long messageDeliveryId, TimeSpan delay, SendHeaders sendHeaders); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ClientContextSupervisor.cs b/src/MassTransit/SqlTransport/SqlTransport/ClientContextSupervisor.cs new file mode 100644 index 00000000000..cdaeaf2aabb --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ClientContextSupervisor.cs @@ -0,0 +1,22 @@ +namespace MassTransit.SqlTransport +{ + using Transports; + + + public class ClientContextSupervisor : + TransportPipeContextSupervisor, + IClientContextSupervisor + { + public ClientContextSupervisor(IConnectionContextSupervisor connectionContextSupervisor) + : base(new ScopeClientContextFactory(connectionContextSupervisor)) + { + connectionContextSupervisor.AddConsumeAgent(this); + } + + public ClientContextSupervisor(IClientContextSupervisor clientContextSupervisor) + : base(new SharedClientContextFactory(clientContextSupervisor)) + { + clientContextSupervisor.AddSendAgent(this); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/ConfigurationSqlHostSettings.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ConfigurationSqlHostSettings.cs new file mode 100644 index 00000000000..9bf87deec32 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ConfigurationSqlHostSettings.cs @@ -0,0 +1,114 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System; + using System.Collections.Generic; + using System.Data; + using Licensing; + + + public abstract class ConfigurationSqlHostSettings : + SqlHostSettings + { + readonly Lazy _hostAddress; + + protected ConfigurationSqlHostSettings(Uri address) + : this() + { + var hostAddress = new SqlHostAddress(address); + + Host = hostAddress.Host; + if (address.Port != -1) + Port = address.Port; + + if (!string.IsNullOrWhiteSpace(address.UserInfo)) + { + var parts = address.UserInfo.Split(':'); + Username = UriDecode(parts[0]); + + if (parts.Length >= 2) + Password = UriDecode(parts[1]); + } + + VirtualHost = hostAddress.VirtualHost; + Area = hostAddress.Area; + } + + protected ConfigurationSqlHostSettings() + { + VirtualHost = "/"; + + IsolationLevel = IsolationLevel.RepeatableRead; + + ConnectionLimit = 10; + + _hostAddress = new Lazy(FormatHostAddress); + + MaintenanceInterval = TimeSpan.FromSeconds(5); + QueueCleanupInterval = TimeSpan.FromMinutes(1); + MaintenanceBatchSize = 10000; + } + + public string? Host { get; set; } + public string? InstanceName { get; set; } + public int? Port { get; set; } + public string? Database { get; set; } + public string? Schema { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } + public string? License { get; set; } + public string? LicenseFile { get; set; } + + public IsolationLevel IsolationLevel { get; set; } + + public int ConnectionLimit { get; set; } + + public TimeSpan MaintenanceInterval { get; set; } + public TimeSpan QueueCleanupInterval { get; set; } + public int MaintenanceBatchSize { get; set; } + + public string? ConnectionTag { get; set; } + + public string? VirtualHost { get; set; } + public string? Area { get; set; } + + public abstract ConnectionContextFactory CreateConnectionContextFactory(ISqlHostConfiguration configuration); + + public LicenseInfo? GetLicenseInfo() + { + if (!string.IsNullOrWhiteSpace(License)) + return LicenseReader.Load(License!); + + if (!string.IsNullOrWhiteSpace(LicenseFile)) + return LicenseReader.LoadFromFile(LicenseFile!); + + return null; + } + + public Uri HostAddress => _hostAddress.Value; + + public virtual IEnumerable Validate() + { + if (string.IsNullOrWhiteSpace(Host)) + yield return this.Failure("Host", "Host must be specified"); + + if(ConnectionLimit < 1) + yield return this.Failure("ConnectionLimit", "must be >= 1"); + } + + static string UriDecode(string uri) + { + return Uri.UnescapeDataString(uri.Replace("+", "%2B")); + } + + Uri FormatHostAddress() + { + if (string.IsNullOrWhiteSpace(Host)) + throw new ConfigurationException("Host cannot be empty"); + if (string.IsNullOrWhiteSpace(VirtualHost)) + throw new ConfigurationException("Domain cannot be empty"); + + return new SqlHostAddress(Host!, InstanceName, Port, VirtualHost!, Area); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlBusConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlBusConfiguration.cs new file mode 100644 index 00000000000..353234ca1de --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlBusConfiguration.cs @@ -0,0 +1,17 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using MassTransit.Configuration; + + + public interface ISqlBusConfiguration : + IBusConfiguration + { + new ISqlHostConfiguration HostConfiguration { get; } + + new ISqlEndpointConfiguration BusEndpointConfiguration { get; } + + new ISqlTopologyConfiguration Topology { get; } + + ISqlEndpointConfiguration CreateEndpointConfiguration(bool isBusEndpoint = false); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlEndpointConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlEndpointConfiguration.cs new file mode 100644 index 00000000000..b446ed47bd3 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlEndpointConfiguration.cs @@ -0,0 +1,11 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using MassTransit.Configuration; + + + public interface ISqlEndpointConfiguration : + IEndpointConfiguration + { + new ISqlTopologyConfiguration Topology { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlHostConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlHostConfiguration.cs new file mode 100644 index 00000000000..a4c64608833 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlHostConfiguration.cs @@ -0,0 +1,31 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System; + using MassTransit.Configuration; + + + public interface ISqlHostConfiguration : + IHostConfiguration, + IReceiveConfigurator + { + IConnectionContextSupervisor ConnectionContextSupervisor { get; } + + SqlHostSettings Settings { get; set; } + + new ISqlBusTopology Topology { get; } + + /// + /// Apply the endpoint definition to the receive endpoint configurator + /// + /// + /// + void ApplyEndpointDefinition(ISqlReceiveEndpointConfigurator configurator, IEndpointDefinition definition); + + ISqlReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(string queueName, + Action? configure = null); + + ISqlReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(SqlReceiveSettings settings, + ISqlEndpointConfiguration endpointConfiguration, Action? configure = null); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlReceiveEndpointConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlReceiveEndpointConfiguration.cs new file mode 100644 index 00000000000..d0b128145e0 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlReceiveEndpointConfiguration.cs @@ -0,0 +1,15 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using MassTransit.Configuration; + using Transports; + + + public interface ISqlReceiveEndpointConfiguration : + IReceiveEndpointConfiguration, + ISqlEndpointConfiguration + { + ReceiveSettings Settings { get; } + + void Build(IHost host); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlTopologyConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlTopologyConfiguration.cs new file mode 100644 index 00000000000..42d1b186b3a --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/ISqlTopologyConfiguration.cs @@ -0,0 +1,15 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using MassTransit.Configuration; + + + public interface ISqlTopologyConfiguration : + ITopologyConfiguration + { + new ISqlPublishTopologyConfigurator Publish { get; } + + new ISqlSendTopologyConfigurator Send { get; } + + new ISqlConsumeTopologyConfigurator Consume { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlBusConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlBusConfiguration.cs new file mode 100644 index 00000000000..6d6257550af --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlBusConfiguration.cs @@ -0,0 +1,39 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using MassTransit.Configuration; + using Observables; + + + public class SqlBusConfiguration : + SqlEndpointConfiguration, + ISqlBusConfiguration + { + readonly BusObservable _busObservers; + + public SqlBusConfiguration(ISqlTopologyConfiguration topologyConfiguration) + : base(topologyConfiguration) + { + HostConfiguration = new SqlHostConfiguration(this, topologyConfiguration); + BusEndpointConfiguration = CreateEndpointConfiguration(true); + + _busObservers = new BusObservable(); + } + + IHostConfiguration IBusConfiguration.HostConfiguration => HostConfiguration; + IEndpointConfiguration IBusConfiguration.BusEndpointConfiguration => BusEndpointConfiguration; + IBusObserver IBusConfiguration.BusObservers => _busObservers; + + public ISqlEndpointConfiguration BusEndpointConfiguration { get; } + public ISqlHostConfiguration HostConfiguration { get; } + + public ConnectHandle ConnectBusObserver(IBusObserver observer) + { + return _busObservers.Connect(observer); + } + + public ConnectHandle ConnectEndpointConfigurationObserver(IEndpointConfigurationObserver observer) + { + return HostConfiguration.ConnectEndpointConfigurationObserver(observer); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlBusFactoryConfigurator.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlBusFactoryConfigurator.cs new file mode 100644 index 00000000000..635b458b21a --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlBusFactoryConfigurator.cs @@ -0,0 +1,130 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System; + using System.Collections.Generic; + using MassTransit.Configuration; + + + public class SqlBusFactoryConfigurator : + BusFactoryConfigurator, + ISqlBusFactoryConfigurator, + IBusFactory + { + readonly ISqlBusConfiguration _busConfiguration; + readonly ISqlHostConfiguration _hostConfiguration; + readonly SqlReceiveSettings _settings; + + public SqlBusFactoryConfigurator(ISqlBusConfiguration busConfiguration) + : base(busConfiguration) + { + _busConfiguration = busConfiguration; + _hostConfiguration = busConfiguration.HostConfiguration; + + var queueName = busConfiguration.Topology.Consume.CreateTemporaryQueueName("bus"); + _settings = new SqlReceiveSettings(busConfiguration.BusEndpointConfiguration, queueName, Defaults.TemporaryAutoDeleteOnIdle); + } + + public IReceiveEndpointConfiguration CreateBusEndpointConfiguration(Action configure) + { + return _busConfiguration.HostConfiguration.CreateReceiveEndpointConfiguration(_settings, _busConfiguration.BusEndpointConfiguration, configure); + } + + public override IEnumerable Validate() + { + foreach (var result in base.Validate()) + yield return result; + + if (string.IsNullOrWhiteSpace(_settings.QueueName)) + yield return this.Failure("Bus", "The bus queue name must not be null or empty"); + } + + public TimeSpan? AutoDeleteOnIdle + { + set => _settings.AutoDeleteOnIdle = value; + } + + public TimeSpan PollingInterval + { + set => _settings.PollingInterval = value; + } + + public TimeSpan LockDuration + { + set => _settings.LockDuration = value; + } + + public TimeSpan MaxLockDuration + { + set => _settings.MaxLockDuration = value; + } + + public int MaxDeliveryCount + { + set => _settings.MaxDeliveryCount = value; + } + + public bool PurgeOnStartup + { + set => _settings.PurgeOnStartup = value; + } + + public void Host(SqlHostSettings settings) + { + _busConfiguration.HostConfiguration.Settings = settings; + } + + public void Send(Action>? configureTopology) + where T : class + { + ISqlMessageSendTopologyConfigurator configurator = _busConfiguration.Topology.Send.GetMessageTopology(); + + configureTopology?.Invoke(configurator); + } + + public void Publish(Action>? configureTopology) + where T : class + { + ISqlMessagePublishTopologyConfigurator? configurator = _busConfiguration.Topology.Publish.GetMessageTopology(); + + configureTopology?.Invoke(configurator); + } + + public void Publish(Type messageType, Action? configure = null) + { + var configurator = _busConfiguration.Topology.Publish.GetMessageTopology(messageType); + + configure?.Invoke(configurator); + } + + public new ISqlSendTopologyConfigurator SendTopology => _busConfiguration.Topology.Send; + public new ISqlPublishTopologyConfigurator PublishTopology => _busConfiguration.Topology.Publish; + + public void OverrideDefaultBusEndpointQueueName(string queueName) + { + _settings.QueueName = queueName; + } + + public void ReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter? endpointNameFormatter, + Action? configureEndpoint) + { + _hostConfiguration.ReceiveEndpoint(definition, endpointNameFormatter, configureEndpoint); + } + + public void ReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter? endpointNameFormatter, + Action? configureEndpoint) + { + _hostConfiguration.ReceiveEndpoint(definition, endpointNameFormatter, configureEndpoint); + } + + public void ReceiveEndpoint(string queueName, Action configureEndpoint) + { + _hostConfiguration.ReceiveEndpoint(queueName, configureEndpoint); + } + + public void ReceiveEndpoint(string queueName, Action configureEndpoint) + { + _hostConfiguration.ReceiveEndpoint(queueName, configureEndpoint); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlEndpointConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlEndpointConfiguration.cs new file mode 100644 index 00000000000..8cfafb3cf27 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlEndpointConfiguration.cs @@ -0,0 +1,31 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using MassTransit.Configuration; + + + public class SqlEndpointConfiguration : + EndpointConfiguration, + ISqlEndpointConfiguration + { + public SqlEndpointConfiguration(ISqlTopologyConfiguration topologyConfiguration) + : base(topologyConfiguration) + { + Topology = topologyConfiguration; + } + + SqlEndpointConfiguration(IEndpointConfiguration parentConfiguration, ISqlTopologyConfiguration topologyConfiguration, bool isBusEndpoint) + : base(parentConfiguration, topologyConfiguration, isBusEndpoint) + { + Topology = topologyConfiguration; + } + + public new ISqlTopologyConfiguration Topology { get; } + + public ISqlEndpointConfiguration CreateEndpointConfiguration(bool isBusEndpoint) + { + var topologyConfiguration = new SqlTopologyConfiguration(Topology); + + return new SqlEndpointConfiguration(this, topologyConfiguration, isBusEndpoint); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlHostConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlHostConfiguration.cs new file mode 100644 index 00000000000..e9ecbc0e8c4 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlHostConfiguration.cs @@ -0,0 +1,170 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System; + using System.Collections.Generic; + using System.Data; + using Licensing; + using MassTransit.Configuration; + using Topology; + using Transports; + using Util; + + + public class SqlHostConfiguration : + BaseHostConfiguration, + ISqlHostConfiguration + { + readonly ISqlBusConfiguration _busConfiguration; + readonly Recycle _connectionContext; + readonly ISqlBusTopology _topology; + SqlHostSettings? _hostSettings; + LicenseInfo? _licenseInfo; + + public SqlHostConfiguration(ISqlBusConfiguration busConfiguration, ISqlTopologyConfiguration topologyConfiguration) + : base(busConfiguration) + { + _busConfiguration = busConfiguration; + + _topology = new SqlBusTopology(this, topologyConfiguration); + + ReceiveTransportRetryPolicy = Retry.CreatePolicy(x => + { + x.Handle(); + x.Handle(); + + x.Exponential(1000, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(3)); + }); + + _connectionContext = new Recycle(() => + new ConnectionContextSupervisor(this, topologyConfiguration, _hostSettings!.CreateConnectionContextFactory(this))); + } + + public IConnectionContextSupervisor ConnectionContextSupervisor => _connectionContext.Supervisor; + + public override Uri HostAddress => _hostSettings?.HostAddress ?? throw new ConfigurationException("The host was not configured."); + + ISqlBusTopology ISqlHostConfiguration.Topology => _topology; + + public override IRetryPolicy ReceiveTransportRetryPolicy { get; } + + public override IBusTopology Topology => _topology; + + public SqlHostSettings Settings + { + get => _hostSettings ?? throw new ConfigurationException("The host was not configured."); + set => _hostSettings = value ?? throw new ArgumentNullException(nameof(value)); + } + + public void ApplyEndpointDefinition(ISqlReceiveEndpointConfigurator configurator, IEndpointDefinition definition) + { + if (definition.IsTemporary) + configurator.AutoDeleteOnIdle = Defaults.TemporaryAutoDeleteOnIdle; + + base.ApplyEndpointDefinition(configurator, definition); + } + + public ISqlReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(string queueName, + Action? configure) + { + var endpointConfiguration = _busConfiguration.CreateEndpointConfiguration(); + var settings = new SqlReceiveSettings(endpointConfiguration, queueName); + + return CreateReceiveEndpointConfiguration(settings, endpointConfiguration, configure); + } + + public ISqlReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(SqlReceiveSettings settings, + ISqlEndpointConfiguration endpointConfiguration, Action? configure) + { + if (settings == null) + throw new ArgumentNullException(nameof(settings)); + if (endpointConfiguration == null) + throw new ArgumentNullException(nameof(endpointConfiguration)); + + var configuration = new SqlReceiveEndpointConfiguration(this, settings, endpointConfiguration); + + configure?.Invoke(configuration); + + Observers.EndpointConfigured(configuration); + + Add(configuration); + + return configuration; + } + + public override void ReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter? endpointNameFormatter, + Action? configureEndpoint = null) + { + var queueName = definition.GetEndpointName(endpointNameFormatter ?? DefaultEndpointNameFormatter.Instance); + + ReceiveEndpoint(queueName, configurator => + { + ApplyEndpointDefinition(configurator, definition); + configureEndpoint?.Invoke(configurator); + }); + } + + public override void ReceiveEndpoint(string queueName, Action configureEndpoint) + { + CreateReceiveEndpointConfiguration(queueName, configureEndpoint); + } + + public override IEnumerable Validate() + { + if (_hostSettings == null) + yield return this.Failure("Host", "Database must be configured"); + else + { + foreach (var result in _hostSettings.Validate()) + yield return result; + + _licenseInfo = _hostSettings.GetLicenseInfo(); + if (_licenseInfo == null) + { + yield return this.Warning("License", + "must be specified with UseLicense/UseLicenseFile or by setting the MT_LICENSE/MT_LICENSE_PATH environment variables"); + } + else + { + if (DateTime.UtcNow > _licenseInfo.Expires) + yield return this.Warning("License", $"has expired as of {_licenseInfo.Expires:D}"); + else + { + var expiresIn = _licenseInfo.Expires - DateTime.UtcNow; + + if (expiresIn < TimeSpan.FromDays(30)) + { + MassTransit.LogContext.Warning?.Log("Licensed to {Customer} - Expires on {Expires} (in {Days} days)", _licenseInfo.Customer?.Name, + _licenseInfo.Expires.ToString("D"), + (int)expiresIn.TotalDays); + } + else + { + MassTransit.LogContext.Info?.Log("Licensed to {Customer} - Expires on {Expires}", _licenseInfo.Customer?.Name, + _licenseInfo.Expires.ToString("D")); + } + } + } + } + + foreach (var result in base.Validate()) + yield return result; + } + + public override IReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(string queueName, + Action? configure) + { + return CreateReceiveEndpointConfiguration(queueName, configure); + } + + public override IHost Build() + { + var host = new SqlHost(this, _topology); + + foreach (var endpointConfiguration in GetConfiguredEndpoints()) + endpointConfiguration.Build(host); + + return host; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlHostConfigurator.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlHostConfigurator.cs new file mode 100644 index 00000000000..e1711f3eb41 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlHostConfigurator.cs @@ -0,0 +1,115 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System; + using System.Data; + + + public abstract class SqlHostConfigurator : + ISqlHostConfigurator + { + readonly ConfigurationSqlHostSettings _settings; + + protected SqlHostConfigurator(ConfigurationSqlHostSettings settings) + { + _settings = settings; + + var licensePath = Environment.GetEnvironmentVariable("MT_LICENSE_PATH"); + if (!string.IsNullOrWhiteSpace(licensePath)) + UseLicenseFile(licensePath); + else + { + var license = Environment.GetEnvironmentVariable("MT_LICENSE"); + if (!string.IsNullOrWhiteSpace(license)) + UseLicense(license); + } + } + + public abstract string? ConnectionString { set; } + + public string? ConnectionTag + { + set => _settings.ConnectionTag = value; + } + + public string? Host + { + set => _settings.Host = value; + } + + public string? InstanceName + { + set => _settings.InstanceName = value; + } + + public int? Port + { + set => _settings.Port = value; + } + + public string? Database + { + set => _settings.Database = value; + } + + public string? Schema + { + set => _settings.Schema = value; + } + + public string? Username + { + set => _settings.Username = value; + } + + public string? Password + { + set => _settings.Password = value; + } + + public string? VirtualHost + { + set => _settings.VirtualHost = value; + } + + public string? Area + { + set => _settings.Area = value; + } + + public IsolationLevel IsolationLevel + { + set => _settings.IsolationLevel = value; + } + + public int ConnectionLimit + { + set => _settings.ConnectionLimit = value; + } + + public TimeSpan MaintenanceInterval + { + set => _settings.MaintenanceInterval = value; + } + + public TimeSpan QueueCleanupInterval + { + set => _settings.QueueCleanupInterval = value; + } + + public int MaintenanceBatchSize + { + set => _settings.MaintenanceBatchSize = value; + } + + public void UseLicense(string license) + { + _settings.License = license; + } + + public void UseLicenseFile(string path) + { + _settings.LicenseFile = path; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlMessageSchedulerSpecification.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlMessageSchedulerSpecification.cs new file mode 100644 index 00000000000..0fdaa8003ce --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlMessageSchedulerSpecification.cs @@ -0,0 +1,21 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using System.Collections.Generic; + using MassTransit.Configuration; + using Middleware; + + + public class SqlMessageSchedulerSpecification : + IPipeSpecification + { + public void Apply(IPipeBuilder builder) + { + builder.AddFilter(new SqlMessageSchedulerFilter()); + } + + public IEnumerable Validate() + { + yield break; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlQueueConfigurator.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlQueueConfigurator.cs new file mode 100644 index 00000000000..9d4f0dc3184 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlQueueConfigurator.cs @@ -0,0 +1,26 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using System; + using Topology; + + + public class SqlQueueConfigurator : + ISqlQueueConfigurator, + Queue + { + protected SqlQueueConfigurator(string queueName, TimeSpan? autoDeleteOnIdle = null) + { + QueueName = queueName; + AutoDeleteOnIdle = autoDeleteOnIdle; + } + + public TimeSpan? AutoDeleteOnIdle { get; set; } + + public string QueueName { get; set; } + + protected SqlEndpointAddress GetEndpointAddress(Uri hostAddress) + { + return new SqlEndpointAddress(hostAddress, QueueName, AutoDeleteOnIdle); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlQueueSubscriptionConfigurator.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlQueueSubscriptionConfigurator.cs new file mode 100644 index 00000000000..8b881a1b6d6 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlQueueSubscriptionConfigurator.cs @@ -0,0 +1,16 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System; + + + public class SqlQueueSubscriptionConfigurator : + SqlTopicSubscriptionConfigurator + { + protected SqlQueueSubscriptionConfigurator(string topicName, SqlSubscriptionType subscriptionType = SqlSubscriptionType.All, + TimeSpan? autoDeleteOnIdle = null, string? routingKey = null) + : base(topicName, subscriptionType, routingKey) + { + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlReceiveEndpointBuilder.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlReceiveEndpointBuilder.cs new file mode 100644 index 00000000000..d16ee6399b0 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlReceiveEndpointBuilder.cs @@ -0,0 +1,68 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using MassTransit.Configuration; + using Topology; + using Transports; + + + public class SqlReceiveEndpointBuilder : + ReceiveEndpointBuilder + { + readonly ISqlReceiveEndpointConfiguration _configuration; + readonly ISqlHostConfiguration _hostConfiguration; + + public SqlReceiveEndpointBuilder(ISqlHostConfiguration hostConfiguration, ISqlReceiveEndpointConfiguration configuration) + : base(configuration) + { + _hostConfiguration = hostConfiguration; + _configuration = configuration; + } + + public override ConnectHandle ConnectConsumePipe(IPipe> pipe, ConnectPipeOptions options) + { + if (_configuration.ConfigureConsumeTopology && options.HasFlag(ConnectPipeOptions.ConfigureConsumeTopology)) + { + ISqlMessageConsumeTopologyConfigurator topology = _configuration.Topology.Consume.GetMessageTopology(); + if (topology.ConfigureConsumeTopology) + topology.Subscribe(); + } + + return base.ConnectConsumePipe(pipe, options); + } + + public SqlReceiveEndpointContext CreateReceiveEndpointContext() + { + var brokerTopology = BuildTopology(_configuration.Settings); + + var deadLetterTransport = CreateDeadLetterTransport(); + var errorTransport = CreateErrorTransport(); + + var context = new QueueSqlReceiveEndpointContext(_hostConfiguration, _configuration, brokerTopology); + + context.GetOrAddPayload(() => deadLetterTransport); + context.GetOrAddPayload(() => errorTransport); + context.GetOrAddPayload(() => _hostConfiguration.Topology); + + return context; + } + + IErrorTransport CreateErrorTransport() + { + return new SqlQueueErrorTransport(_configuration.Settings.QueueName, SqlQueueType.ErrorQueue); + } + + IDeadLetterTransport CreateDeadLetterTransport() + { + return new SqlQueueDeadLetterTransport(_configuration.Settings.QueueName, SqlQueueType.DeadLetterQueue); + } + + BrokerTopology BuildTopology(ReceiveSettings settings) + { + var topologyBuilder = new ReceiveEndpointBrokerTopologyBuilder(settings); + + _configuration.Topology.Consume.Apply(topologyBuilder); + + return topologyBuilder.BuildBrokerTopology(); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlReceiveEndpointConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlReceiveEndpointConfiguration.cs new file mode 100644 index 00000000000..3136f00e4cb --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlReceiveEndpointConfiguration.cs @@ -0,0 +1,200 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System; + using System.Collections.Generic; + using System.Text.RegularExpressions; + using MassTransit.Configuration; + using MassTransit.Middleware; + using Middleware; + using Transports; + using Util; + + + public class SqlReceiveEndpointConfiguration : + ReceiveEndpointConfiguration, + ISqlReceiveEndpointConfiguration, + ISqlReceiveEndpointConfigurator + { + static readonly Regex _regex = new Regex(@"^[A-Za-z0-9\-_\.:]+$", RegexOptions.Compiled); + + readonly IBuildPipeConfigurator _clientConfigurator; + readonly ISqlEndpointConfiguration _endpointConfiguration; + readonly ISqlHostConfiguration _hostConfiguration; + readonly Lazy _inputAddress; + readonly SqlReceiveSettings _settings; + + public SqlReceiveEndpointConfiguration(ISqlHostConfiguration hostConfiguration, SqlReceiveSettings settings, + ISqlEndpointConfiguration endpointConfiguration) + : base(hostConfiguration, endpointConfiguration) + { + _hostConfiguration = hostConfiguration; + _settings = settings; + + _endpointConfiguration = endpointConfiguration; + + _clientConfigurator = new PipeConfigurator(); + + _inputAddress = new Lazy(FormatInputAddress); + } + + public ReceiveSettings Settings => _settings; + + public override Uri HostAddress => _hostConfiguration.HostAddress; + public override Uri InputAddress => _inputAddress.Value; + + public override ReceiveEndpointContext CreateReceiveEndpointContext() + { + return CreateDbReceiveEndpointContext(); + } + + ISqlTopologyConfiguration ISqlEndpointConfiguration.Topology => _endpointConfiguration.Topology; + + public void Build(IHost host) + { + var context = CreateDbReceiveEndpointContext(); + + _clientConfigurator.UseFilter(new ConfigureSqlTopologyFilter(_settings, context.BrokerTopology, context)); + + if (_hostConfiguration.DeployTopologyOnly) + _clientConfigurator.UseFilter(new TransportReadyFilter(context)); + else + { + if (_settings.PurgeOnStartup) + _clientConfigurator.UseFilter(new PurgeOnStartupFilter(_settings.QueueName)); + + _clientConfigurator.UseFilter(new ReceiveEndpointDependencyFilter(context)); + _clientConfigurator.UseFilter(new SqlConsumerFilter(context)); + } + + IPipe clientPipe = _clientConfigurator.Build(); + + var transport = new ReceiveTransport(_hostConfiguration, context, () => context.ClientContextSupervisor, clientPipe); + + if (IsBusEndpoint && _hostConfiguration.DeployPublishTopology) + { + var publishTopology = _hostConfiguration.Topology.PublishTopology; + + var brokerTopology = publishTopology.GetPublishBrokerTopology(); + + transport.PreStartPipe = new ConfigureSqlTopologyFilter(publishTopology, brokerTopology).ToPipe(); + } + + var receiveEndpoint = new ReceiveEndpoint(transport, context); + + var queueName = _settings.QueueName ?? NewId.Next().ToString(FormatUtil.Formatter); + + host.AddReceiveEndpoint(queueName, receiveEndpoint); + + ReceiveEndpoint = receiveEndpoint; + } + + public override IEnumerable Validate() + { + if (!IsValidEntityName(_settings.QueueName)) + yield return this.Failure(_settings.QueueName, "Must be a valid queue name"); + + if (_settings.PurgeOnStartup) + yield return this.Warning(_settings.QueueName, "Existing messages will be purged on service start"); + + foreach (var result in base.Validate()) + yield return result.WithParentKey(_settings.QueueName); + } + + public TimeSpan? AutoDeleteOnIdle + { + set + { + _settings.AutoDeleteOnIdle = value; + + Changed("AutoDelete"); + } + } + + public TimeSpan PollingInterval + { + set => _settings.PollingInterval = value; + } + + public TimeSpan LockDuration + { + set => _settings.LockDuration = value; + } + + public TimeSpan MaxLockDuration + { + set => _settings.MaxLockDuration = value; + } + + public int MaxDeliveryCount + { + set => _settings.MaxDeliveryCount = value; + } + + public bool PurgeOnStartup + { + set => _settings.PurgeOnStartup = value; + } + + public void Subscribe(string topicName, Action? callback) + { + if (topicName == null) + throw new ArgumentNullException(nameof(topicName)); + + _endpointConfiguration.Topology.Consume.Subscribe(topicName, callback); + } + + public TimeSpan? UnlockDelay + { + set => _settings.UnlockDelay = value; + } + + public int ConcurrentDeliveryLimit + { + set => _settings.ConcurrentDeliveryLimit = value; + } + + public void Subscribe(Action? callback) + where T : class + { + _endpointConfiguration.Topology.Consume.GetMessageTopology().Subscribe(callback); + } + + public void SetReceiveMode(SqlReceiveMode mode, int? concurrentDeliveryLimit = default) + { + if (concurrentDeliveryLimit != null) + _settings.ConcurrentDeliveryLimit = concurrentDeliveryLimit.Value; + + _settings.ReceiveMode = mode; + } + + public void ConfigureClient(Action>? configure) + { + configure?.Invoke(_clientConfigurator); + } + + static bool IsValidEntityName(string name) + { + return _regex.Match(name).Success; + } + + SqlReceiveEndpointContext CreateDbReceiveEndpointContext() + { + var builder = new SqlReceiveEndpointBuilder(_hostConfiguration, this); + + ApplySpecifications(builder); + + return builder.CreateReceiveEndpointContext(); + } + + Uri FormatInputAddress() + { + return _settings.GetInputAddress(_hostConfiguration.HostAddress); + } + + protected override bool IsAlreadyConfigured() + { + return _inputAddress.IsValueCreated || base.IsAlreadyConfigured(); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlReceiveSettings.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlReceiveSettings.cs new file mode 100644 index 00000000000..5a746812e9a --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlReceiveSettings.cs @@ -0,0 +1,68 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using System; + + + public class SqlReceiveSettings : + SqlQueueConfigurator, + ReceiveSettings + { + readonly ISqlEndpointConfiguration _configuration; + int _concurrentDeliveryLimit; + + public SqlReceiveSettings(ISqlEndpointConfiguration configuration, string queueName, TimeSpan? autoDeleteOnIdle = null) + : base(queueName, autoDeleteOnIdle) + { + _configuration = configuration; + + PollingInterval = TimeSpan.FromSeconds(1); + ConcurrentDeliveryLimit = 1; + + LockDuration = TimeSpan.FromMinutes(1); + MaxLockDuration = TimeSpan.FromHours(12); + MaxDeliveryCount = 10; + } + + public long? QueueId { get; set; } + + public int PrefetchCount => _configuration.Transport.PrefetchCount; + + public int ConcurrentMessageLimit => _configuration.Transport.GetConcurrentMessageLimit(); + + public int ConcurrentDeliveryLimit + { + get + { + return ReceiveMode switch + { + SqlReceiveMode.Normal => 1, + SqlReceiveMode.Partitioned => 1, + SqlReceiveMode.PartitionedOrdered => 1, + _ => _concurrentDeliveryLimit + }; + } + set => _concurrentDeliveryLimit = value; + } + + public SqlReceiveMode ReceiveMode { get; set; } + + public bool PurgeOnStartup { get; set; } + + public TimeSpan LockDuration { get; set; } + + public int MaxDeliveryCount { get; set; } + + public TimeSpan PollingInterval { get; set; } + + public TimeSpan? UnlockDelay { get; set; } + + public TimeSpan MaxLockDuration { get; set; } + + public string EntityName => QueueName; + + public Uri GetInputAddress(Uri hostAddress) + { + return GetEndpointAddress(hostAddress); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlRegistrationBusFactory.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlRegistrationBusFactory.cs new file mode 100644 index 00000000000..cf6ed2a778e --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlRegistrationBusFactory.cs @@ -0,0 +1,49 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System; + using System.Collections.Generic; + using MassTransit.Configuration; + using Transports; + + + public class SqlRegistrationBusFactory : + TransportRegistrationBusFactory + { + readonly SqlBusConfiguration _busConfiguration; + readonly Action? _configure; + + public SqlRegistrationBusFactory(Action? configure) + : this(new SqlBusConfiguration(new SqlTopologyConfiguration(SqlBusFactory.CreateMessageTopology())), configure) + { + } + + SqlRegistrationBusFactory(SqlBusConfiguration busConfiguration, Action? configure) + : base(busConfiguration.HostConfiguration) + { + _configure = configure; + + _busConfiguration = busConfiguration; + } + + public override IBusInstance CreateBus(IBusRegistrationContext context, IEnumerable specifications, string busName) + { + var configurator = new SqlBusFactoryConfigurator(_busConfiguration); + + configurator.UseRawJsonSerializer(RawSerializerOptions.CopyHeaders, true); + + // var options = context.GetRequiredService>().Get(busName); + // + // configurator.Host(options.Host, options.Port, options.VHost, h => + // { + // if (!string.IsNullOrWhiteSpace(options.User)) + // h.Username(options.User); + // + // if (!string.IsNullOrWhiteSpace(options.Pass)) + // h.Password(options.Pass); + // }); + + return CreateBus(configurator, context, _configure, specifications); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlTopicConfigurator.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlTopicConfigurator.cs new file mode 100644 index 00000000000..b5c92731cee --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlTopicConfigurator.cs @@ -0,0 +1,23 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using System; + using Topology; + + + public class SqlTopicConfigurator : + ISqlTopicConfigurator, + Topic + { + public SqlTopicConfigurator(string topicName) + { + TopicName = topicName; + } + + public string TopicName { get; } + + public SqlEndpointAddress GetEndpointAddress(Uri hostAddress) + { + return new SqlEndpointAddress(hostAddress, TopicName, type: SqlEndpointAddress.AddressType.Topic); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlTopicSubscriptionConfigurator.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlTopicSubscriptionConfigurator.cs new file mode 100644 index 00000000000..12a34f8e420 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlTopicSubscriptionConfigurator.cs @@ -0,0 +1,18 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + public abstract class SqlTopicSubscriptionConfigurator : + SqlTopicConfigurator, + ISqlTopicSubscriptionConfigurator + { + protected SqlTopicSubscriptionConfigurator(string topicName, SqlSubscriptionType subscriptionType = SqlSubscriptionType.All, string? routingKey = null) + : base(topicName) + { + SubscriptionType = subscriptionType; + RoutingKey = routingKey; + } + + public string? RoutingKey { get; set; } + public SqlSubscriptionType SubscriptionType { get; set; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlTopologyConfiguration.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlTopologyConfiguration.cs new file mode 100644 index 00000000000..744b5498a75 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/SqlTopologyConfiguration.cs @@ -0,0 +1,60 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using System.Collections.Generic; + using System.Linq; + using MassTransit.Configuration; + using Topology; + + + public class SqlTopologyConfiguration : + ISqlTopologyConfiguration + { + readonly ISqlConsumeTopologyConfigurator _consumeTopology; + readonly IMessageTopologyConfigurator _messageTopology; + readonly ISqlPublishTopologyConfigurator _publishTopology; + readonly ISqlSendTopologyConfigurator _sendTopology; + + public SqlTopologyConfiguration(IMessageTopologyConfigurator messageTopology) + { + _messageTopology = messageTopology; + + _sendTopology = new SqlSendTopology(); + _sendTopology.ConnectSendTopologyConfigurationObserver(new DelegateSendTopologyConfigurationObserver(GlobalTopology.Send)); + _sendTopology.TryAddConvention(new RoutingKeySendTopologyConvention()); + _sendTopology.TryAddConvention(new PartitionKeySendTopologyConvention()); + + _publishTopology = new SqlPublishTopology(messageTopology); + _publishTopology.ConnectPublishTopologyConfigurationObserver(new DelegatePublishTopologyConfigurationObserver(GlobalTopology.Publish)); + + var observer = new PublishToSendTopologyConfigurationObserver(_sendTopology); + _publishTopology.ConnectPublishTopologyConfigurationObserver(observer); + + _consumeTopology = new SqlConsumeTopology(_publishTopology); + } + + public SqlTopologyConfiguration(ISqlTopologyConfiguration topologyConfiguration) + { + _messageTopology = topologyConfiguration.Message; + _sendTopology = topologyConfiguration.Send; + _publishTopology = topologyConfiguration.Publish; + + _consumeTopology = new SqlConsumeTopology(topologyConfiguration.Publish); + } + + IMessageTopologyConfigurator ITopologyConfiguration.Message => _messageTopology; + ISendTopologyConfigurator ITopologyConfiguration.Send => _sendTopology; + IPublishTopologyConfigurator ITopologyConfiguration.Publish => _publishTopology; + IConsumeTopologyConfigurator ITopologyConfiguration.Consume => _consumeTopology; + + ISqlPublishTopologyConfigurator ISqlTopologyConfiguration.Publish => _publishTopology; + ISqlSendTopologyConfigurator ISqlTopologyConfiguration.Send => _sendTopology; + ISqlConsumeTopologyConfigurator ISqlTopologyConfiguration.Consume => _consumeTopology; + + public IEnumerable Validate() + { + return _sendTopology.Validate() + .Concat(_publishTopology.Validate()) + .Concat(_consumeTopology.Validate()); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/ISqlConsumeTopologySpecification.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/ISqlConsumeTopologySpecification.cs new file mode 100644 index 00000000000..0dc345b5f7a --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/ISqlConsumeTopologySpecification.cs @@ -0,0 +1,11 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using Topology; + + + public interface ISqlConsumeTopologySpecification : + ISpecification + { + void Apply(IReceiveEndpointBrokerTopologyBuilder builder); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/ISqlPublishTopologySpecification.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/ISqlPublishTopologySpecification.cs new file mode 100644 index 00000000000..2ca2f361878 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/ISqlPublishTopologySpecification.cs @@ -0,0 +1,11 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using Topology; + + + public interface ISqlPublishTopologySpecification : + ISpecification + { + void Apply(IPublishEndpointBrokerTopologyBuilder builder); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/InvalidSqlConsumeTopologySpecification.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/InvalidSqlConsumeTopologySpecification.cs new file mode 100644 index 00000000000..0af7272f412 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/InvalidSqlConsumeTopologySpecification.cs @@ -0,0 +1,28 @@ +namespace MassTransit.SqlTransport.Configuration +{ + using System.Collections.Generic; + using Topology; + + + public class InvalidSqlConsumeTopologySpecification : + ISqlConsumeTopologySpecification + { + readonly string _key; + readonly string _message; + + public InvalidSqlConsumeTopologySpecification(string key, string message) + { + _key = key; + _message = message; + } + + public IEnumerable Validate() + { + yield return this.Failure(_key, _message); + } + + public void Apply(IReceiveEndpointBrokerTopologyBuilder builder) + { + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/QueueSubscriptionConsumeTopologySpecification.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/QueueSubscriptionConsumeTopologySpecification.cs new file mode 100644 index 00000000000..f01ac21fc2b --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/QueueSubscriptionConsumeTopologySpecification.cs @@ -0,0 +1,36 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System.Collections.Generic; + using Topology; + + + public class QueueSubscriptionConsumeTopologySpecification : + SqlTopicSubscriptionConfigurator, + ISqlConsumeTopologySpecification + { + public QueueSubscriptionConsumeTopologySpecification(string topicName, SqlSubscriptionType subscriptionType = SqlSubscriptionType.All, + string? routingKey = null) + : base(topicName, subscriptionType, routingKey) + { + } + + public QueueSubscriptionConsumeTopologySpecification(Topic topic, SqlSubscriptionType subscriptionType = SqlSubscriptionType.All, + string? routingKey = null) + : base(topic.TopicName, subscriptionType, routingKey) + { + } + + public IEnumerable Validate() + { + yield break; + } + + public void Apply(IReceiveEndpointBrokerTopologyBuilder builder) + { + var topicHandle = builder.CreateTopic(TopicName); + + builder.CreateQueueSubscription(topicHandle, builder.Queue, SubscriptionType, RoutingKey); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/TopicSubscriptionPublishTopologySpecification.cs b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/TopicSubscriptionPublishTopologySpecification.cs new file mode 100644 index 00000000000..becb032066a --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Configuration/Topology/TopicSubscriptionPublishTopologySpecification.cs @@ -0,0 +1,34 @@ +#nullable enable +namespace MassTransit.SqlTransport.Configuration +{ + using System.Collections.Generic; + using Topology; + + + /// + /// Used to bind an exchange to the sending + /// + public class TopicSubscriptionPublishTopologySpecification : + SqlTopicSubscriptionConfigurator, + ISqlPublishTopologySpecification + { + public TopicSubscriptionPublishTopologySpecification(string topicName, SqlSubscriptionType subscriptionType = SqlSubscriptionType.All, + string? routingKey = null) + : base(topicName, subscriptionType, routingKey) + { + } + + public IEnumerable Validate() + { + yield break; + } + + public void Apply(IPublishEndpointBrokerTopologyBuilder builder) + { + var exchangeHandle = builder.CreateTopic(TopicName); + + if (builder.Topic != null) + builder.CreateTopicSubscription(builder.Topic, exchangeHandle, SubscriptionType, RoutingKey); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ConnectionContext.cs b/src/MassTransit/SqlTransport/SqlTransport/ConnectionContext.cs new file mode 100644 index 00000000000..2f714bad7bc --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ConnectionContext.cs @@ -0,0 +1,39 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Data; + using System.Threading; + using System.Threading.Tasks; + + + public interface ConnectionContext : + PipeContext + { + Uri HostAddress { get; } + + string? Schema { get; } + + IsolationLevel IsolationLevel { get; } + + ClientContext CreateClientContext(CancellationToken cancellationToken); + + /// + /// Create a database connection + /// + /// + /// + Task CreateConnection(CancellationToken cancellationToken); + + Task DelayUntilMessageReady(long queueId, TimeSpan timeout, CancellationToken cancellationToken); + + /// + /// Executes a query within a transaction using an available connection + /// + /// + /// + /// + /// + Task Query(Func> callback, CancellationToken cancellationToken); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ConnectionContextFactory.cs b/src/MassTransit/SqlTransport/SqlTransport/ConnectionContextFactory.cs new file mode 100644 index 00000000000..2498b895c1c --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ConnectionContextFactory.cs @@ -0,0 +1,37 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Agents; + using Internals; + using Transports; + + + public abstract class ConnectionContextFactory : + IPipeContextFactory + { + public IPipeContextAgent CreateContext(ISupervisor supervisor) + { + ITransportSupervisor transportSupervisor = + supervisor as ITransportSupervisor ?? throw new ArgumentException(nameof(supervisor)); + + return supervisor.AddContext(CreateConnection(transportSupervisor)); + } + + public IActivePipeContextAgent CreateActiveContext(ISupervisor supervisor, + PipeContextHandle context, CancellationToken cancellationToken) + { + return supervisor.AddActiveContext(context, CreateSharedConnection(context.Context, cancellationToken)); + } + + static async Task CreateSharedConnection(Task context, CancellationToken cancellationToken) + { + return context.Status == TaskStatus.RanToCompletion + ? new SharedConnectionContext(context.Result, cancellationToken) + : new SharedConnectionContext(await context.OrCanceled(cancellationToken).ConfigureAwait(false), cancellationToken); + } + + protected abstract ConnectionContext CreateConnection(ITransportSupervisor supervisor); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ConnectionContextSupervisor.cs b/src/MassTransit/SqlTransport/SqlTransport/ConnectionContextSupervisor.cs new file mode 100644 index 00000000000..4f317cf7439 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ConnectionContextSupervisor.cs @@ -0,0 +1,79 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Threading.Tasks; + using Agents; + using Configuration; + using Middleware; + using Transports; + + + public class ConnectionContextSupervisor : + TransportPipeContextSupervisor, + IConnectionContextSupervisor + { + readonly ISqlHostConfiguration _hostConfiguration; + readonly ISqlTopologyConfiguration _topologyConfiguration; + + public ConnectionContextSupervisor(ISqlHostConfiguration hostConfiguration, ISqlTopologyConfiguration topologyConfiguration, + IPipeContextFactory connectionContextFactory) + : base(connectionContextFactory) + { + _hostConfiguration = hostConfiguration; + _topologyConfiguration = topologyConfiguration; + } + + public Uri NormalizeAddress(Uri address) + { + return new SqlEndpointAddress(_hostConfiguration.HostAddress, address); + } + + public Task CreatePublishTransport(SqlReceiveEndpointContext context, Uri? publishAddress) + where T : class + { + LogContext.SetCurrentIfNull(_hostConfiguration.LogContext); + + ISqlMessagePublishTopologyConfigurator publishTopology = _topologyConfiguration.Publish.GetMessageTopology(); + + var settings = publishTopology.GetSendSettings(_hostConfiguration.HostAddress); + + var brokerTopology = publishTopology.GetBrokerTopology(); + + IPipe configureTopology = new ConfigureSqlTopologyFilter(settings, brokerTopology).ToPipe(); + + var supervisor = new ClientContextSupervisor(context.ClientContextSupervisor); + + return CreateSendTransport(publishAddress!, + new TopicSendTransportContext(_hostConfiguration, context, supervisor, configureTopology, settings.EntityName)); + } + + public Task CreateSendTransport(SqlReceiveEndpointContext context, Uri address) + { + LogContext.SetCurrentIfNull(_hostConfiguration.LogContext); + + var endpointAddress = new SqlEndpointAddress(_hostConfiguration.HostAddress, address); + + var settings = _topologyConfiguration.Send.GetSendSettings(endpointAddress); + + IPipe configureTopology = new ConfigureSqlTopologyFilter(settings, settings.GetBrokerTopology()).ToPipe(); + + var supervisor = new ClientContextSupervisor(context.ClientContextSupervisor); + + return CreateSendTransport(endpointAddress, endpointAddress.Type == SqlEndpointAddress.AddressType.Queue + ? new QueueSendTransportContext(_hostConfiguration, context, supervisor, configureTopology, settings.EntityName) + : new TopicSendTransportContext(_hostConfiguration, context, supervisor, configureTopology, settings.EntityName)); + } + + Task CreateSendTransport(Uri address, SendTransportContext transportContext) + { + TransportLogMessages.CreateSendTransport(address); + + var transport = new SendTransport(transportContext); + + AddSendAgent(transport); + + return Task.FromResult(transport); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/DeadLetterSettings.cs b/src/MassTransit/SqlTransport/SqlTransport/DeadLetterSettings.cs new file mode 100644 index 00000000000..e5b7fa9778b --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/DeadLetterSettings.cs @@ -0,0 +1,15 @@ +namespace MassTransit.SqlTransport +{ + using Topology; + + + public interface DeadLetterSettings : + EntitySettings + { + /// + /// Return the BrokerTopology to apply at startup (to create exchange and queue if binding is specified) + /// + /// + BrokerTopology GetBrokerTopology(); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Defaults.cs b/src/MassTransit/SqlTransport/SqlTransport/Defaults.cs new file mode 100644 index 00000000000..0801a4a6d6e --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Defaults.cs @@ -0,0 +1,21 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.ComponentModel; + + + [EditorBrowsable(EditorBrowsableState.Never)] + public static class Defaults + { + public static TimeSpan LockDuration { get; set; } = TimeSpan.FromMinutes(5); + public static TimeSpan DefaultMessageTimeToLive { get; set; } = TimeSpan.FromDays(365 + 1); + public static TimeSpan ErrorQueueTimeToLive { get; set; } = TimeSpan.FromDays(14); + + public static TimeSpan AutoDeleteOnIdle { get; set; } = TimeSpan.FromDays(427); + public static TimeSpan TemporaryAutoDeleteOnIdle { get; set; } = TimeSpan.FromMinutes(5); + public static TimeSpan MaxAutoRenewDuration { get; set; } = TimeSpan.FromMinutes(5); + + public static TimeSpan SessionIdleTimeout { get; set; } = TimeSpan.FromSeconds(10); + public static TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromMilliseconds(100); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/EntitySettings.cs b/src/MassTransit/SqlTransport/SqlTransport/EntitySettings.cs new file mode 100644 index 00000000000..f1902163dc2 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/EntitySettings.cs @@ -0,0 +1,15 @@ +namespace MassTransit.SqlTransport +{ + using System; + + + public interface EntitySettings + { + string EntityName { get; } + + /// + /// Idle time before queue should be deleted (consumer-idle, not producer) + /// + TimeSpan? AutoDeleteOnIdle { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ErrorSettings.cs b/src/MassTransit/SqlTransport/SqlTransport/ErrorSettings.cs new file mode 100644 index 00000000000..ca1d4607d45 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ErrorSettings.cs @@ -0,0 +1,15 @@ +namespace MassTransit.SqlTransport +{ + using Topology; + + + public interface ErrorSettings : + EntitySettings + { + /// + /// Return the BrokerTopology to apply at startup (to create exchange and queue if binding is specified) + /// + /// + BrokerTopology GetBrokerTopology(); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/HostInfoCache.cs b/src/MassTransit/SqlTransport/SqlTransport/HostInfoCache.cs new file mode 100644 index 00000000000..8e6ce852a7f --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/HostInfoCache.cs @@ -0,0 +1,15 @@ +namespace MassTransit.SqlTransport +{ + using System; + using Metadata; + using Serialization; + + + public static class HostInfoCache + { + static readonly Lazy _hostInfoJson = + new Lazy(() => SystemTextJsonMessageSerializer.Instance.SerializeObject(HostMetadataCache.Host).GetString()); + + public static string HostInfoJson => _hostInfoJson.Value; + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/IClientContextSupervisor.cs b/src/MassTransit/SqlTransport/SqlTransport/IClientContextSupervisor.cs new file mode 100644 index 00000000000..8e47b3775bb --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/IClientContextSupervisor.cs @@ -0,0 +1,10 @@ +namespace MassTransit.SqlTransport +{ + using Transports; + + + public interface IClientContextSupervisor : + ITransportSupervisor + { + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/IConnectionContextSupervisor.cs b/src/MassTransit/SqlTransport/SqlTransport/IConnectionContextSupervisor.cs new file mode 100644 index 00000000000..823c2a3e0c4 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/IConnectionContextSupervisor.cs @@ -0,0 +1,19 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Threading.Tasks; + using Transports; + + + public interface IConnectionContextSupervisor : + ITransportSupervisor + { + Task CreateSendTransport(SqlReceiveEndpointContext context, Uri address); + + Task CreatePublishTransport(SqlReceiveEndpointContext context, Uri? publishAddress) + where T : class; + + Uri NormalizeAddress(Uri address); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/IQueueNotificationListener.cs b/src/MassTransit/SqlTransport/SqlTransport/IQueueNotificationListener.cs new file mode 100644 index 00000000000..c9143b1df1f --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/IQueueNotificationListener.cs @@ -0,0 +1,10 @@ +namespace MassTransit.SqlTransport +{ + using System.Threading.Tasks; + + + public interface IQueueNotificationListener + { + Task MessageReady(string queueName); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ISqlHost.cs b/src/MassTransit/SqlTransport/SqlTransport/ISqlHost.cs new file mode 100644 index 00000000000..e591854b8de --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ISqlHost.cs @@ -0,0 +1,10 @@ +namespace MassTransit.SqlTransport +{ + using Transports; + + + public interface ISqlHost : + IHost + { + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ISqlTransportConnection.cs b/src/MassTransit/SqlTransport/SqlTransport/ISqlTransportConnection.cs new file mode 100644 index 00000000000..c0a39502ea4 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ISqlTransportConnection.cs @@ -0,0 +1,10 @@ +namespace MassTransit.SqlTransport +{ + using System; + + + public interface ISqlTransportConnection : + IAsyncDisposable + { + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ISqlTransportDatabaseMigrator.cs b/src/MassTransit/SqlTransport/SqlTransport/ISqlTransportDatabaseMigrator.cs new file mode 100644 index 00000000000..bad6a824ea6 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ISqlTransportDatabaseMigrator.cs @@ -0,0 +1,13 @@ +namespace MassTransit.SqlTransport +{ + using System.Threading; + using System.Threading.Tasks; + + + public interface ISqlTransportDatabaseMigrator + { + Task CreateDatabase(SqlTransportOptions options, CancellationToken cancellationToken = default); + Task CreateInfrastructure(SqlTransportOptions options, CancellationToken cancellationToken = default); + Task DeleteDatabase(SqlTransportOptions options, CancellationToken cancellationToken = default); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/MessageDelivery.cs b/src/MassTransit/SqlTransport/SqlTransport/MessageDelivery.cs new file mode 100644 index 00000000000..c0f3690f057 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/MessageDelivery.cs @@ -0,0 +1,24 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + + + public class MessageDelivery + { + public int QueueId { get; set; } + public int Priority { get; set; } + public DateTimeOffset EnqueueTime { get; set; } + public Guid? ConsumerId { get; set; } + public Guid TransportMessageId { get; set; } + public DateTimeOffset? ExpirationTime { get; set; } + public int DeliveryCount { get; set; } + public int MaxDeliveryCount { get; set; } + public DateTimeOffset? LastDelivered { get; set; } + public long SessionNumber { get; set; } + public string? ReplyToSessionId { get; set; } + public string? GroupId { get; set; } + public int? GroupSequenceNumber { get; set; } + public string? TransportHeaders { get; set; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/MessageLockContext.cs b/src/MassTransit/SqlTransport/SqlTransport/MessageLockContext.cs new file mode 100644 index 00000000000..8fbb9c9daa2 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/MessageLockContext.cs @@ -0,0 +1,15 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Threading.Tasks; + + + public interface MessageLockContext + { + Task Complete(); + + Task Abandon(Exception exception); + Task DeadLetter(); + Task DeadLetter(Exception exception); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Middleware/ConfigureSqlTopologyFilter.cs b/src/MassTransit/SqlTransport/SqlTransport/Middleware/ConfigureSqlTopologyFilter.cs new file mode 100644 index 00000000000..97d362ac265 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Middleware/ConfigureSqlTopologyFilter.cs @@ -0,0 +1,104 @@ +#nullable enable +namespace MassTransit.SqlTransport.Middleware; + +using System; +using System.Threading.Tasks; +using Configuration; +using Topology; +using Transports; + + +/// +/// Configures the broker with the supplied topology once the model is created, to ensure +/// that the exchanges, queues, and bindings for the model are properly configured in SQS. +/// +public class ConfigureSqlTopologyFilter : + IFilter + where TSettings : class +{ + readonly BrokerTopology _brokerTopology; + readonly SqlReceiveEndpointContext? _context; + readonly TSettings _settings; + + public ConfigureSqlTopologyFilter(TSettings settings, BrokerTopology brokerTopology, SqlReceiveEndpointContext? context = null) + { + _settings = settings; + _brokerTopology = brokerTopology; + _context = context; + } + + public async Task Send(ClientContext context, IPipe next) + { + OneTimeContext> oneTimeContext = await context.OneTimeSetup>(() => + { + context.GetOrAddPayload(() => _settings); + + return ConfigureTopology(context); + }).ConfigureAwait(false); + + try + { + await next.Send(context).ConfigureAwait(false); + } + catch (Exception) + { + oneTimeContext.Evict(); + + throw; + } + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateFilterScope("configureTopology"); + + _brokerTopology.Probe(scope); + } + + async Task ConfigureTopology(ClientContext context) + { + foreach (var queue in _brokerTopology.Queues) + { + var queueId = await CreateQueue(context, queue).ConfigureAwait(false); + if (queue.QueueName == _context?.InputAddress.GetEndpointName() && _settings is SqlReceiveSettings settings) + settings.QueueId = queueId; + } + + foreach (var topic in _brokerTopology.Topics) + await CreateTopic(context, topic).ConfigureAwait(false); + + foreach (var topicSubscription in _brokerTopology.TopicSubscriptions) + await CreateTopicSubscription(context, topicSubscription).ConfigureAwait(false); + + foreach (var queueSubscription in _brokerTopology.QueueSubscriptions) + await CreateQueueSubscription(context, queueSubscription).ConfigureAwait(false); + } + + static Task CreateTopic(ClientContext context, Topic topic) + { + SqlLogMessages.CreateTopic(topic); + + return context.CreateTopic(topic); + } + + static Task CreateQueueSubscription(ClientContext context, TopicToQueueSubscription subscription) + { + SqlLogMessages.CreateQueueSubscription(subscription); + + return context.CreateQueueSubscription(subscription); + } + + static Task CreateTopicSubscription(ClientContext context, TopicToTopicSubscription subscription) + { + SqlLogMessages.CreateTopicSubscription(subscription); + + return context.CreateTopicSubscription(subscription); + } + + static Task CreateQueue(ClientContext context, Queue queue) + { + SqlLogMessages.CreateQueue(queue); + + return context.CreateQueue(queue); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Middleware/ConfigureTopologyContext.cs b/src/MassTransit/SqlTransport/SqlTransport/Middleware/ConfigureTopologyContext.cs new file mode 100644 index 00000000000..afe7b3f6934 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Middleware/ConfigureTopologyContext.cs @@ -0,0 +1,7 @@ +namespace MassTransit.SqlTransport.Middleware +{ + public interface ConfigureTopologyContext + where T : class + { + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Middleware/PurgeOnStartupFilter.cs b/src/MassTransit/SqlTransport/SqlTransport/Middleware/PurgeOnStartupFilter.cs new file mode 100644 index 00000000000..a8e0fc737f0 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Middleware/PurgeOnStartupFilter.cs @@ -0,0 +1,46 @@ +namespace MassTransit.SqlTransport.Middleware +{ + using System.Threading.Tasks; + + + /// + /// Purges the queue on startup, only once per filter instance + /// + public class PurgeOnStartupFilter : + IFilter + { + readonly string _queueName; + bool _queueAlreadyPurged; + + public PurgeOnStartupFilter(string queueName) + { + _queueName = queueName; + } + + void IProbeSite.Probe(ProbeContext context) + { + context.CreateFilterScope("purgeOnStartup"); + } + + async Task IFilter.Send(ClientContext context, IPipe next) + { + await PurgeIfRequested(context, _queueName).ConfigureAwait(false); + + await next.Send(context).ConfigureAwait(false); + } + + async Task PurgeIfRequested(ClientContext context, string queueName) + { + if (!_queueAlreadyPurged) + { + await context.PurgeQueue(queueName, context.CancellationToken).ConfigureAwait(false); + + LogContext.Debug?.Log("Purged queue {QueueName}", queueName); + + _queueAlreadyPurged = true; + } + else + LogContext.Debug?.Log("Queue {QueueName} was purged at startup, skipping", queueName); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Middleware/SqlConsumerFilter.cs b/src/MassTransit/SqlTransport/SqlTransport/Middleware/SqlConsumerFilter.cs new file mode 100644 index 00000000000..6cf9bad0c4d --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Middleware/SqlConsumerFilter.cs @@ -0,0 +1,48 @@ +namespace MassTransit.SqlTransport.Middleware +{ + using System.Threading.Tasks; + using Transports; + + + /// + /// A filter that uses the model context to create a basic consumer and connect it to the model + /// + public class SqlConsumerFilter : + IFilter + { + readonly SqlReceiveEndpointContext _context; + + public SqlConsumerFilter(SqlReceiveEndpointContext context) + { + _context = context; + } + + void IProbeSite.Probe(ProbeContext context) + { + } + + async Task IFilter.Send(ClientContext context, IPipe next) + { + var receiver = new SqlMessageReceiver(context, _context); + + await receiver.Ready.ConfigureAwait(false); + + _context.AddConsumeAgent(receiver); + + await _context.TransportObservers.NotifyReady(_context.InputAddress).ConfigureAwait(false); + + try + { + await receiver.Completed.ConfigureAwait(false); + } + finally + { + DeliveryMetrics metrics = receiver; + + await _context.TransportObservers.NotifyCompleted(_context.InputAddress, metrics).ConfigureAwait(false); + + _context.LogConsumerCompleted(metrics.DeliveryCount, metrics.ConcurrentDeliveryCount); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Middleware/SqlMessageReceiver.cs b/src/MassTransit/SqlTransport/SqlTransport/Middleware/SqlMessageReceiver.cs new file mode 100644 index 00000000000..cb80b287d84 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Middleware/SqlMessageReceiver.cs @@ -0,0 +1,211 @@ +namespace MassTransit.SqlTransport.Middleware +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading; + using System.Threading.Tasks; + using MassTransit.Middleware; + using Transports; + using Util; + + + /// + /// Receives messages from AmazonSQS, pushing them to the InboundPipe of the service endpoint. + /// + public sealed class SqlMessageReceiver : + ConsumerAgent + { + readonly ClientContext _client; + readonly SqlReceiveEndpointContext _context; + readonly OrderedChannelExecutorPool _executorPool; + readonly object _lock = new(); + readonly ReceiveSettings _receiveSettings; + CancellationTokenSource _cancellationTokenSource; + + DateTime? _lastMetricUpdate; + + /// + /// The basic consumer receives messages pushed from the broker. + /// + /// The model context for the consumer + /// The topology + public SqlMessageReceiver(ClientContext client, SqlReceiveEndpointContext context) + : base(context) + { + _client = client; + _context = context; + + _receiveSettings = client.GetPayload(); + + _executorPool = new OrderedChannelExecutorPool(_receiveSettings); + + TrySetConsumeTask(Task.Run(() => Consume())); + } + + protected override async Task ActiveAndActualAgentsCompleted(StopContext context) + { + await base.ActiveAndActualAgentsCompleted(context).ConfigureAwait(false); + + await _executorPool.DisposeAsync().ConfigureAwait(false); + } + + async Task Consume() + { + using var algorithm = new RequestRateAlgorithm(new RequestRateAlgorithmOptions + { + PrefetchCount = _receiveSettings.PrefetchCount, + ConcurrentResultLimit = _context.ConcurrentMessageLimit ?? _context.PrefetchCount, + RequestResultLimit = _receiveSettings.PrefetchCount + }); + + SetReady(); + + Task Handle(SqlTransportMessage message, CancellationToken cancellationToken) + { + var lockContext = new SqlReceiveLockContext(_context.InputAddress, message, _receiveSettings, _client); + + return _receiveSettings.ReceiveMode == SqlReceiveMode.Normal + ? HandleMessage(message, lockContext) + : _executorPool.Run(message, () => HandleMessage(message, lockContext), cancellationToken); + } + + try + { + while (!IsStopping) + await algorithm.Run((messageLimit, token) => ReceiveMessages(messageLimit, token), (m, c) => Handle(m, c), Stopping).ConfigureAwait(false); + } + catch (OperationCanceledException exception) when (exception.CancellationToken == Stopping) + { + } + catch (Exception exception) + { + LogContext.Warning?.Log(exception, "Consume Loop faulted"); + } + } + + async Task HandleMessage(SqlTransportMessage message, SqlReceiveLockContext lockContext) + { + if (IsStopping) + return; + + var context = + new SqlReceiveContext(message, message.DeliveryCount > 0, _context, _receiveSettings, _client, _client.ConnectionContext, lockContext); + try + { + await Dispatch(message.TransportMessageId, context, lockContext).ConfigureAwait(false); + } + catch (Exception exception) + { + context.LogTransportFaulted(exception); + } + finally + { + context.Dispose(); + } + + MessageHandled(); + } + + async Task> ReceiveMessages(int messageLimit, CancellationToken cancellationToken) + { + try + { + IList messages = (await _client.ReceiveMessages(_receiveSettings.EntityName, _receiveSettings.ReceiveMode, messageLimit, + _receiveSettings.ConcurrentDeliveryLimit, _receiveSettings.LockDuration).ConfigureAwait(false)).ToList(); + + if (messages.Count > 0) + return messages; + + if (_receiveSettings.AutoDeleteOnIdle.HasValue) + { + if (_lastMetricUpdate.HasValue == false + || _lastMetricUpdate.Value + new TimeSpan(_receiveSettings.AutoDeleteOnIdle.Value.Ticks / 2) > DateTime.UtcNow) + { + await _client.TouchQueue(_receiveSettings.EntityName).ConfigureAwait(false); + + _lastMetricUpdate = DateTime.UtcNow; + } + } + + await WaitForPollingIntervalOrMessageHandled(cancellationToken).ConfigureAwait(false); + + return messages; + } + catch (OperationCanceledException) + { + return Array.Empty(); + } + } + + public async Task WaitForPollingIntervalOrMessageHandled(CancellationToken cancellationToken) + { + lock (_lock) + _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); + + try + { + var delayTask = _receiveSettings.QueueId.HasValue + ? _client.ConnectionContext.DelayUntilMessageReady(_receiveSettings.QueueId.Value, _receiveSettings.PollingInterval, + _cancellationTokenSource.Token) + : Task.Delay(_receiveSettings.PollingInterval, _cancellationTokenSource.Token); + + await delayTask.ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + finally + { + lock (_lock) + { + _cancellationTokenSource.Dispose(); + _cancellationTokenSource = null; + } + } + } + + public void MessageHandled() + { + lock (_lock) + _cancellationTokenSource?.Cancel(); + } + + + class OrderedChannelExecutorPool : + IChannelExecutorPool + { + readonly IChannelExecutorPool _keyExecutorPool; + + public OrderedChannelExecutorPool(ReceiveSettings receiveSettings) + { + IHashGenerator hashGenerator = new Murmur3UnsafeHashGenerator(); + _keyExecutorPool = new PartitionChannelExecutorPool(PartitionKeyProvider, hashGenerator, + receiveSettings.ConcurrentMessageLimit, receiveSettings.ConcurrentDeliveryLimit); + } + + public Task Push(SqlTransportMessage result, Func handle, CancellationToken cancellationToken) + { + return _keyExecutorPool.Push(result, handle, cancellationToken); + } + + public Task Run(SqlTransportMessage result, Func method, CancellationToken cancellationToken = default) + { + return _keyExecutorPool.Run(result, method, cancellationToken); + } + + public ValueTask DisposeAsync() + { + return _keyExecutorPool.DisposeAsync(); + } + + static byte[] PartitionKeyProvider(SqlTransportMessage message) + { + return string.IsNullOrEmpty(message.PartitionKey) + ? [] + : Encoding.UTF8.GetBytes(message.PartitionKey); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Middleware/SqlMessageSchedulerFilter.cs b/src/MassTransit/SqlTransport/SqlTransport/Middleware/SqlMessageSchedulerFilter.cs new file mode 100644 index 00000000000..98505bfa20b --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Middleware/SqlMessageSchedulerFilter.cs @@ -0,0 +1,33 @@ +namespace MassTransit.SqlTransport.Middleware +{ + using System.Diagnostics; + using System.Threading.Tasks; + using Context; + using Scheduling; + + + /// + /// Adds the service bus message scheduler filter + /// + public class SqlMessageSchedulerFilter : + IFilter + { + void IProbeSite.Probe(ProbeContext context) + { + context.CreateFilterScope("sqlScheduler"); + } + + [DebuggerNonUserCode] + Task IFilter.Send(ConsumeContext context, IPipe next) + { + context.GetOrAddPayload(() => new ConsumeMessageSchedulerContext(context, SchedulerFactory)); + + return next.Send(context); + } + + static IMessageScheduler SchedulerFactory(ConsumeContext context) + { + return new MessageScheduler(new SqlScheduleMessageProvider(context), context.GetPayload()); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/NotificationContext.cs b/src/MassTransit/SqlTransport/SqlTransport/NotificationContext.cs new file mode 100644 index 00000000000..671796b6a16 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/NotificationContext.cs @@ -0,0 +1,8 @@ +namespace MassTransit.SqlTransport +{ + public interface NotificationContext : + PipeContext + { + ConnectHandle ConnectNotificationSink(string queueName, IQueueNotificationListener listener); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/QueueSendTransportContext.cs b/src/MassTransit/SqlTransport/SqlTransport/QueueSendTransportContext.cs new file mode 100644 index 00000000000..91ffa02da5d --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/QueueSendTransportContext.cs @@ -0,0 +1,97 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + using Configuration; + using Transports; + + + public class QueueSendTransportContext : + BaseSendTransportContext, + SendTransportContext + { + readonly IPipe _configureTopologyPipe; + readonly ISqlHostConfiguration _hostConfiguration; + readonly IClientContextSupervisor _supervisor; + + public QueueSendTransportContext(ISqlHostConfiguration hostConfiguration, ReceiveEndpointContext receiveEndpointContext, + IClientContextSupervisor supervisor, IPipe configureTopologyPipe, string entityName) + : base(hostConfiguration, receiveEndpointContext.Serialization) + { + _hostConfiguration = hostConfiguration; + _supervisor = supervisor; + + _configureTopologyPipe = configureTopologyPipe; + EntityName = entityName; + } + + public override string EntityName { get; } + public override string ActivitySystem => "db"; + + public void Probe(ProbeContext context) + { + } + + public override async Task> CreateSendContext(T message, IPipe> pipe, CancellationToken cancellationToken) + { + var sendContext = new SqlMessageSendContext(message, cancellationToken); + + await pipe.Send(sendContext).ConfigureAwait(false); + + CopyIncomingIdentifiersIfPresent(sendContext); + + return sendContext; + } + + public override IEnumerable GetAgentHandles() + { + return new IAgent[] { }; + } + + public Task Send(IPipe pipe, CancellationToken cancellationToken = default) + { + return _hostConfiguration.Retry(() => _supervisor.Send(pipe, cancellationToken), cancellationToken, _supervisor.SendStopping); + } + + public Task> CreateSendContext(ClientContext context, T message, IPipe> pipe, + CancellationToken cancellationToken) + where T : class + { + return CreateSendContext(message, pipe, cancellationToken); + } + + public async Task Send(ClientContext clientContext, SendContext sendContext) + where T : class + { + SqlMessageSendContext context = sendContext as SqlMessageSendContext + ?? throw new ArgumentException("Invalid SendContext type", nameof(sendContext)); + + await _configureTopologyPipe.Send(clientContext).ConfigureAwait(false); + + sendContext.CancellationToken.ThrowIfCancellationRequested(); + + if (Activity.Current?.IsAllDataRequested ?? false) + { + if (!string.IsNullOrWhiteSpace(context.RoutingKey)) + Activity.Current.SetTag(nameof(context.RoutingKey), context.RoutingKey); + if (!string.IsNullOrWhiteSpace(context.PartitionKey)) + Activity.Current.SetTag(nameof(context.PartitionKey), context.PartitionKey); + } + + await clientContext.Send(EntityName, context).ConfigureAwait(false); + } + + static void CopyIncomingIdentifiersIfPresent(SqlMessageSendContext context) + where T : class + { + if (context.TryGetPayload(out var consumeContext) && consumeContext.TryGetPayload(out var dbMessageContext)) + { + if (context.PartitionKey == null && dbMessageContext.PartitionKey != null) + context.PartitionKey = dbMessageContext.PartitionKey; + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/QueueSqlReceiveEndpointContext.cs b/src/MassTransit/SqlTransport/SqlTransport/QueueSqlReceiveEndpointContext.cs new file mode 100644 index 00000000000..c569b3ed25e --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/QueueSqlReceiveEndpointContext.cs @@ -0,0 +1,78 @@ +namespace MassTransit.SqlTransport +{ + using System; + using Configuration; + using Topology; + using Transports; + using Util; + + + public class QueueSqlReceiveEndpointContext : + BaseReceiveEndpointContext, + SqlReceiveEndpointContext + { + readonly Recycle _clientContext; + readonly ISqlReceiveEndpointConfiguration _configuration; + readonly ISqlHostConfiguration _hostConfiguration; + + public QueueSqlReceiveEndpointContext(ISqlHostConfiguration hostConfiguration, ISqlReceiveEndpointConfiguration configuration, + BrokerTopology brokerTopology) + : base(hostConfiguration, configuration) + { + _hostConfiguration = hostConfiguration; + _configuration = configuration; + + BrokerTopology = brokerTopology; + + _clientContext = new Recycle(() => new ClientContextSupervisor(_hostConfiguration.ConnectionContextSupervisor)); + } + + public IClientContextSupervisor ClientContextSupervisor => _clientContext.Supervisor; + + public BrokerTopology BrokerTopology { get; } + + public override void AddSendAgent(IAgent agent) + { + _clientContext.Supervisor.AddSendAgent(agent); + } + + public override void AddConsumeAgent(IAgent agent) + { + _clientContext.Supervisor.AddConsumeAgent(agent); + } + + public override Exception ConvertException(Exception exception, string message) + { + return new ConnectionException(message + _hostConfiguration.HostAddress, exception); + } + + public override void Probe(ProbeContext context) + { + context.Add("type", "Sql"); + context.Set(new + { + _configuration.Settings.QueueName, + _configuration.Settings.AutoDeleteOnIdle, + _configuration.Settings.PrefetchCount, + ConcurrentMessageLimit, + _configuration.Settings.ConcurrentDeliveryLimit, + _configuration.Settings.ReceiveMode, + _configuration.Settings.PollingInterval, + _configuration.Settings.PurgeOnStartup + }); + + var topologyScope = context.CreateScope("topology"); + BrokerTopology.Probe(topologyScope); + } + + protected override ISendTransportProvider CreateSendTransportProvider() + { + return new SqlSendTransportProvider(_hostConfiguration.ConnectionContextSupervisor, this); + } + + protected override IPublishTransportProvider CreatePublishTransportProvider() + { + return new SqlPublishTransportProvider(_hostConfiguration.ConnectionContextSupervisor, this); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ReceiveSettings.cs b/src/MassTransit/SqlTransport/SqlTransport/ReceiveSettings.cs new file mode 100644 index 00000000000..57c6fe143d7 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ReceiveSettings.cs @@ -0,0 +1,62 @@ +namespace MassTransit.SqlTransport +{ + using System; + + + /// + /// Specify the receive settings for a receive transport + /// + public interface ReceiveSettings : + EntitySettings + { + /// + /// The queue name to receive from + /// + string QueueName { get; } + + /// + /// Once the topology is configured, the queueId should be available + /// + long? QueueId { get; } + + /// + /// The number of unacknowledged messages to allow to be processed concurrently + /// + int PrefetchCount { get; } + + int ConcurrentMessageLimit { get; } + + int ConcurrentDeliveryLimit { get; } + + SqlReceiveMode ReceiveMode { get; } + + /// + /// If True, and a queue name is specified, if the queue exists and has messages, they are purged at startup + /// If the connection is reset, messages are not purged until the service is reset + /// + bool PurgeOnStartup { get; } + + /// + /// Message locks are automatically renewed, however, the actual lock duration determines how long the message remains locked + /// when the consumer process crashes. + /// + TimeSpan LockDuration { get; } + + /// + /// The maximum amount of time the lock will be renewed during message consumption before being abandoned + /// + TimeSpan MaxLockDuration { get; } + + int MaxDeliveryCount { get; } + + /// + /// How often to poll for messages when no messages exist + /// + TimeSpan PollingInterval { get; } + + /// + /// The amount of time, when a message is abandoned, before the message is available for redelivery + /// + TimeSpan? UnlockDelay { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ScopeClientContext.cs b/src/MassTransit/SqlTransport/SqlTransport/ScopeClientContext.cs new file mode 100644 index 00000000000..da7f6dc5323 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ScopeClientContext.cs @@ -0,0 +1,101 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using MassTransit.Middleware; + using Topology; + + + public class ScopeClientContext : + ScopePipeContext, + ClientContext + { + readonly ClientContext _context; + + public ScopeClientContext(ClientContext context, CancellationToken cancellationToken) + : base(context) + { + _context = context; + CancellationToken = cancellationToken; + } + + public override CancellationToken CancellationToken { get; } + + public ConnectionContext ConnectionContext => _context.ConnectionContext; + + public Task CreateQueue(Queue queue) + { + return _context.CreateQueue(queue); + } + + public Task CreateTopic(Topic topic) + { + return _context.CreateTopic(topic); + } + + public Task CreateTopicSubscription(TopicToTopicSubscription subscription) + { + return _context.CreateTopicSubscription(subscription); + } + + public Task CreateQueueSubscription(TopicToQueueSubscription subscription) + { + return _context.CreateQueueSubscription(subscription); + } + + public Task PurgeQueue(string queueName, CancellationToken cancellationToken) + { + return _context.PurgeQueue(queueName, cancellationToken); + } + + public Task Send(string queueName, SqlMessageSendContext context) + where T : class + { + return _context.Send(queueName, context); + } + + public Task Publish(string topicName, SqlMessageSendContext context) + where T : class + { + return _context.Publish(topicName, context); + } + + public Task RenewLock(Guid lockId, long messageDeliveryId, TimeSpan duration) + { + return _context.RenewLock(lockId, messageDeliveryId, duration); + } + + public Task Unlock(Guid lockId, long messageDeliveryId, TimeSpan delay, SendHeaders sendHeaders) + { + return _context.Unlock(lockId, messageDeliveryId, delay, sendHeaders); + } + + public Task> ReceiveMessages(string queueName, SqlReceiveMode mode, int messageLimit, int concurrentLimit, + TimeSpan lockDuration) + { + return _context.ReceiveMessages(queueName, mode, messageLimit, concurrentLimit, lockDuration); + } + + public Task TouchQueue(string queueName) + { + return _context.TouchQueue(queueName); + } + + public Task DeleteMessage(Guid lockId, long messageDeliveryId) + { + return _context.DeleteMessage(lockId, messageDeliveryId); + } + + public Task DeleteScheduledMessage(Guid tokenId, CancellationToken cancellationToken) + { + return _context.DeleteScheduledMessage(tokenId, cancellationToken); + } + + public Task MoveMessage(Guid lockId, long messageDeliveryId, string queueName, SqlQueueType queueType, SendHeaders sendHeaders) + { + return _context.MoveMessage(lockId, messageDeliveryId, queueName, queueType, sendHeaders); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ScopeClientContextFactory.cs b/src/MassTransit/SqlTransport/SqlTransport/ScopeClientContextFactory.cs new file mode 100644 index 00000000000..f41b817e916 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ScopeClientContextFactory.cs @@ -0,0 +1,53 @@ +namespace MassTransit.SqlTransport +{ + using System.Threading; + using System.Threading.Tasks; + using Agents; + using Internals; + + + public class ScopeClientContextFactory : + IPipeContextFactory + { + readonly IConnectionContextSupervisor _connectionContextSupervisor; + + public ScopeClientContextFactory(IConnectionContextSupervisor connectionContextSupervisor) + { + _connectionContextSupervisor = connectionContextSupervisor; + } + + IPipeContextAgent IPipeContextFactory.CreateContext(ISupervisor supervisor) + { + IAsyncPipeContextAgent asyncContext = supervisor.AddAsyncContext(); + + CreateClientContext(asyncContext, supervisor.Stopped); + + return asyncContext; + } + + IActivePipeContextAgent IPipeContextFactory.CreateActiveContext(ISupervisor supervisor, + PipeContextHandle context, CancellationToken cancellationToken) + { + return supervisor.AddActiveContext(context, CreateSharedClientContext(context.Context, cancellationToken)); + } + + static async Task CreateSharedClientContext(Task context, CancellationToken cancellationToken) + { + return context.IsCompletedSuccessfully() + ? new ScopeClientContext(context.Result, cancellationToken) + : new ScopeClientContext(await context.OrCanceled(cancellationToken).ConfigureAwait(false), cancellationToken); + } + + void CreateClientContext(IAsyncPipeContextAgent asyncContext, CancellationToken cancellationToken) + { + static Task Create(ConnectionContext connectionContext, CancellationToken createCancellationToken) + { + return Task.FromResult(connectionContext.CreateClientContext(createCancellationToken)); + } + + #pragma warning disable CS4014 + _connectionContextSupervisor.CreateAgent(asyncContext, Create, cancellationToken); + #pragma warning restore CS4014 + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SendSettings.cs b/src/MassTransit/SqlTransport/SqlTransport/SendSettings.cs new file mode 100644 index 00000000000..4235ef5aeba --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SendSettings.cs @@ -0,0 +1,23 @@ +namespace MassTransit.SqlTransport +{ + using System; + using Topology; + + + public interface SendSettings : + EntitySettings + { + /// + /// Returns the send address for the settings + /// + /// + /// + SqlEndpointAddress GetSendAddress(Uri hostAddress); + + /// + /// Return the BrokerTopology to apply at startup (to create exchange and queue if binding is specified) + /// + /// + BrokerTopology GetBrokerTopology(); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/ServiceBusHost.cs b/src/MassTransit/SqlTransport/SqlTransport/ServiceBusHost.cs new file mode 100644 index 00000000000..b61f45fa674 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/ServiceBusHost.cs @@ -0,0 +1,78 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using Configuration; + using Transports; + + + public class SqlHost : + BaseHost, + ISqlHost + { + readonly ISqlHostConfiguration _hostConfiguration; + + public SqlHost(ISqlHostConfiguration hostConfiguration, ISqlBusTopology busTopology) + : base(hostConfiguration, busTopology) + { + _hostConfiguration = hostConfiguration; + Topology = busTopology; + } + + public new ISqlBusTopology Topology { get; } + + public override HostReceiveEndpointHandle ConnectReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter? endpointNameFormatter, + Action? configureEndpoint = null) + { + return ConnectReceiveEndpoint(definition, endpointNameFormatter, configureEndpoint); + } + + public override HostReceiveEndpointHandle ConnectReceiveEndpoint(string queueName, Action? configureEndpoint = null) + { + return ConnectReceiveEndpoint(queueName, configureEndpoint); + } + + public HostReceiveEndpointHandle ConnectReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter? endpointNameFormatter = null, + Action? configureEndpoint = null) + { + var queueName = definition.GetEndpointName(endpointNameFormatter ?? DefaultEndpointNameFormatter.Instance); + + return ConnectReceiveEndpoint(queueName, configurator => + { + _hostConfiguration.ApplyEndpointDefinition(configurator, definition); + configureEndpoint?.Invoke(configurator); + }); + } + + public HostReceiveEndpointHandle ConnectReceiveEndpoint(string queueName, Action? configure = null) + { + LogContext.SetCurrentIfNull(_hostConfiguration.LogContext); + + var configuration = _hostConfiguration.CreateReceiveEndpointConfiguration(queueName, configure); + + configuration.Validate().ThrowIfContainsFailure("The receive endpoint configuration is invalid:"); + + TransportLogMessages.ConnectReceiveEndpoint(configuration.InputAddress); + + configuration.Build(this); + + return ReceiveEndpoints.Start(configuration.Settings.QueueName); + } + + protected override void Probe(ProbeContext context) + { + context.Set(new + { + Type = "Database Transport", + _hostConfiguration.HostAddress, + }); + + _hostConfiguration.ConnectionContextSupervisor.Probe(context); + } + + protected override IAgent[] GetAgentHandles() + { + return new IAgent[] { _hostConfiguration.ConnectionContextSupervisor }; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SharedClientContext.cs b/src/MassTransit/SqlTransport/SqlTransport/SharedClientContext.cs new file mode 100644 index 00000000000..9202060781f --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SharedClientContext.cs @@ -0,0 +1,101 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using MassTransit.Middleware; + using Topology; + + + public class SharedClientContext : + ProxyPipeContext, + ClientContext + { + readonly ClientContext _context; + + public SharedClientContext(ClientContext context, CancellationToken cancellationToken) + : base(context) + { + _context = context; + CancellationToken = cancellationToken; + } + + public override CancellationToken CancellationToken { get; } + + public ConnectionContext ConnectionContext => _context.ConnectionContext; + + public Task CreateQueue(Queue queue) + { + return _context.CreateQueue(queue); + } + + public Task CreateTopic(Topic topic) + { + return _context.CreateTopic(topic); + } + + public Task CreateTopicSubscription(TopicToTopicSubscription subscription) + { + return _context.CreateTopicSubscription(subscription); + } + + public Task CreateQueueSubscription(TopicToQueueSubscription subscription) + { + return _context.CreateQueueSubscription(subscription); + } + + public Task PurgeQueue(string queueName, CancellationToken cancellationToken) + { + return _context.PurgeQueue(queueName, cancellationToken); + } + + public Task Send(string queueName, SqlMessageSendContext context) + where T : class + { + return _context.Send(queueName, context); + } + + public Task Publish(string topicName, SqlMessageSendContext context) + where T : class + { + return _context.Publish(topicName, context); + } + + public Task RenewLock(Guid lockId, long messageDeliveryId, TimeSpan duration) + { + return _context.RenewLock(lockId, messageDeliveryId, duration); + } + + public Task Unlock(Guid lockId, long messageDeliveryId, TimeSpan delay, SendHeaders sendHeaders) + { + return _context.Unlock(lockId, messageDeliveryId, delay, sendHeaders); + } + + public Task> ReceiveMessages(string queueName, SqlReceiveMode mode, int messageLimit, int concurrentLimit, + TimeSpan lockDuration) + { + return _context.ReceiveMessages(queueName, mode, messageLimit, concurrentLimit, lockDuration); + } + + public Task TouchQueue(string queueName) + { + return _context.TouchQueue(queueName); + } + + public Task DeleteMessage(Guid lockId, long messageDeliveryId) + { + return _context.DeleteMessage(lockId, messageDeliveryId); + } + + public Task DeleteScheduledMessage(Guid tokenId, CancellationToken cancellationToken) + { + return _context.DeleteScheduledMessage(tokenId, cancellationToken); + } + + public Task MoveMessage(Guid lockId, long messageDeliveryId, string queueName, SqlQueueType queueType, SendHeaders sendHeaders) + { + return _context.MoveMessage(lockId, messageDeliveryId, queueName, queueType, sendHeaders); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SharedClientContextFactory.cs b/src/MassTransit/SqlTransport/SqlTransport/SharedClientContextFactory.cs new file mode 100644 index 00000000000..f68578a62de --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SharedClientContextFactory.cs @@ -0,0 +1,53 @@ +namespace MassTransit.SqlTransport +{ + using System.Threading; + using System.Threading.Tasks; + using Agents; + using Internals; + + + public class SharedClientContextFactory : + IPipeContextFactory + { + readonly IClientContextSupervisor _supervisor; + + public SharedClientContextFactory(IClientContextSupervisor supervisor) + { + _supervisor = supervisor; + } + + IPipeContextAgent IPipeContextFactory.CreateContext(ISupervisor supervisor) + { + IAsyncPipeContextAgent asyncContext = supervisor.AddAsyncContext(); + + CreateClientContext(asyncContext, supervisor.Stopped); + + return asyncContext; + } + + IActivePipeContextAgent IPipeContextFactory.CreateActiveContext(ISupervisor supervisor, + PipeContextHandle context, CancellationToken cancellationToken) + { + return supervisor.AddActiveContext(context, CreateScopeContext(context.Context, cancellationToken)); + } + + static async Task CreateScopeContext(Task context, CancellationToken cancellationToken) + { + return context.IsCompletedSuccessfully() + ? new SharedClientContext(context.Result, cancellationToken) + : new SharedClientContext(await context.OrCanceled(cancellationToken).ConfigureAwait(false), cancellationToken); + } + + void CreateClientContext(IAsyncPipeContextAgent asyncContext, CancellationToken cancellationToken) + { + static Task Create(ClientContext context, CancellationToken createCancellationToken) + { + return Task.FromResult(new SharedClientContext(context, createCancellationToken)); + } + + #pragma warning disable CS4014 + _supervisor.CreateAgent(asyncContext, Create, cancellationToken); + #pragma warning restore CS4014 + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SharedConnectionContext.cs b/src/MassTransit/SqlTransport/SqlTransport/SharedConnectionContext.cs new file mode 100644 index 00000000000..0546f183fdd --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SharedConnectionContext.cs @@ -0,0 +1,50 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Data; + using System.Threading; + using System.Threading.Tasks; + using MassTransit.Middleware; + + + public class SharedConnectionContext : + ProxyPipeContext, + ConnectionContext + { + readonly ConnectionContext _context; + + public SharedConnectionContext(ConnectionContext context, CancellationToken cancellationToken) + : base(context) + { + _context = context; + CancellationToken = cancellationToken; + } + + public override CancellationToken CancellationToken { get; } + + public Uri HostAddress => _context.HostAddress; + public string? Schema => _context.Schema; + public IsolationLevel IsolationLevel => _context.IsolationLevel; + + public ClientContext CreateClientContext(CancellationToken cancellationToken) + { + return _context.CreateClientContext(cancellationToken); + } + + public Task CreateConnection(CancellationToken cancellationToken) + { + return _context.CreateConnection(cancellationToken); + } + + public Task DelayUntilMessageReady(long queueId, TimeSpan timeout, CancellationToken cancellationToken) + { + return _context.DelayUntilMessageReady(queueId, timeout, cancellationToken); + } + + public Task Query(Func> callback, CancellationToken cancellationToken) + { + return _context.Query(callback, cancellationToken); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlClientContext.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlClientContext.cs new file mode 100644 index 00000000000..21c4736b056 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlClientContext.cs @@ -0,0 +1,49 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using MassTransit.Middleware; + using Topology; + + + public abstract class SqlClientContext : + ScopePipeContext, + ClientContext + { + protected SqlClientContext(ConnectionContext context, CancellationToken cancellationToken) + : base(context) + { + ConnectionContext = context; + CancellationToken = cancellationToken; + } + + public override CancellationToken CancellationToken { get; } + + public ConnectionContext ConnectionContext { get; } + + public abstract Task CreateQueue(Queue queue); + public abstract Task CreateTopic(Topic topic); + public abstract Task CreateTopicSubscription(TopicToTopicSubscription subscription); + public abstract Task CreateQueueSubscription(TopicToQueueSubscription subscription); + public abstract Task PurgeQueue(string queueName, CancellationToken cancellationToken); + + public abstract Task Send(string queueName, SqlMessageSendContext context) + where T : class; + + public abstract Task Publish(string topicName, SqlMessageSendContext context) + where T : class; + + public abstract Task> ReceiveMessages(string queueName, SqlReceiveMode mode, int messageLimit, int concurrentLimit, + TimeSpan lockDuration); + + public abstract Task TouchQueue(string queueName); + + public abstract Task DeleteMessage(Guid lockId, long messageDeliveryId); + public abstract Task DeleteScheduledMessage(Guid tokenId, CancellationToken cancellationToken); + public abstract Task MoveMessage(Guid lockId, long messageDeliveryId, string queueName, SqlQueueType queueType, SendHeaders sendHeaders); + public abstract Task RenewLock(Guid lockId, long messageDeliveryId, TimeSpan duration); + public abstract Task Unlock(Guid lockId, long messageDeliveryId, TimeSpan delay, SendHeaders sendHeaders); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlHeaderProvider.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlHeaderProvider.cs new file mode 100644 index 00000000000..6ff5cf9b166 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlHeaderProvider.cs @@ -0,0 +1,86 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using Transports; + + + public class SqlHeaderProvider : + IHeaderProvider + { + readonly SqlTransportMessage _message; + + public SqlHeaderProvider(SqlTransportMessage message) + { + _message = message; + } + + public IEnumerable> GetAll() + { + return _message.GetHeaders().GetAll().Concat(_message.GetTransportHeaders().GetAll()); + } + + public bool TryGetHeader(string key, [NotNullWhen(true)] out object? value) + { + switch (key) + { + case MessageHeaders.ContentType: + value = _message.ContentType; + return value != null; + case MessageHeaders.MessageType: + value = _message.MessageType; + return value != null; + case MessageHeaders.MessageId: + value = _message.MessageId; + return value != null; + case MessageHeaders.CorrelationId: + value = _message.CorrelationId; + return value != null; + case MessageHeaders.ConversationId: + value = _message.ConversationId; + return value != null; + case MessageHeaders.RequestId: + value = _message.RequestId; + return value != null; + case MessageHeaders.InitiatorId: + value = _message.InitiatorId; + return value != null; + case MessageHeaders.SourceAddress: + value = _message.SourceAddress; + return value != null; + case MessageHeaders.ResponseAddress: + value = _message.ResponseAddress; + return value != null; + case MessageHeaders.FaultAddress: + value = _message.FaultAddress; + return value != null; + case MessageHeaders.TransportMessageId: + value = _message.TransportMessageId; + return true; + case nameof(_message.MessageDeliveryId): + value = _message.MessageDeliveryId; + return true; + case nameof(_message.DeliveryCount): + value = _message.DeliveryCount; + return true; + case nameof(_message.RoutingKey): + value = _message.RoutingKey; + return value != null; + case nameof(_message.PartitionKey): + value = _message.PartitionKey; + return value != null; + } + + if (_message.GetTransportHeaders().TryGetHeader(key, out value)) + return true; + + if (_message.GetHeaders().TryGetHeader(key, out value)) + return true; + + value = default; + return false; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlLogMessages.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlLogMessages.cs new file mode 100644 index 00000000000..e15aaef03c4 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlLogMessages.cs @@ -0,0 +1,20 @@ +namespace MassTransit.SqlTransport +{ + using Logging; + using Microsoft.Extensions.Logging; + using Topology; + + + public static class SqlLogMessages + { + public static readonly LogMessage CreateTopicSubscription = LogContext.Define(LogLevel.Debug, + "Create topic subscription: {TopicSubscription}"); + + public static readonly LogMessage CreateQueueSubscription = LogContext.Define(LogLevel.Debug, + "Create queue subscription: {QueueSubscription}"); + + public static readonly LogMessage CreateTopic = LogContext.Define(LogLevel.Debug, "Create topic: {Topic}"); + + public static readonly LogMessage CreateQueue = LogContext.Define(LogLevel.Debug, "Create queue: {Queue}"); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlMessageSendContext.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlMessageSendContext.cs new file mode 100644 index 00000000000..b0aff209282 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlMessageSendContext.cs @@ -0,0 +1,48 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Collections.Generic; + using System.Threading; + using Context; + + + public class SqlMessageSendContext : + MessageSendContext, + SqlSendContext + where T : class + { + public SqlMessageSendContext(T message, CancellationToken cancellationToken) + : base(message, cancellationToken) + { + TransportMessageId = NewId.NextGuid(); + } + + public Guid TransportMessageId { get; } + + public string? PartitionKey { get; set; } + public short? Priority { get; set; } + public string? RoutingKey { get; set; } + + public override void ReadPropertiesFrom(IReadOnlyDictionary properties) + { + base.ReadPropertiesFrom(properties); + + PartitionKey = ReadString(properties, SqlTransportPropertyNames.PartitionKey); + Priority = ReadShort(properties, SqlTransportPropertyNames.Priority); + RoutingKey = ReadString(properties, SqlTransportPropertyNames.RoutingKey); + } + + public override void WritePropertiesTo(IDictionary properties) + { + base.WritePropertiesTo(properties); + + if (!string.IsNullOrWhiteSpace(PartitionKey)) + properties[SqlTransportPropertyNames.PartitionKey] = PartitionKey!; + if (Priority.HasValue) + properties[SqlTransportPropertyNames.Priority] = Priority.Value; + if (!string.IsNullOrWhiteSpace(RoutingKey)) + properties[SqlTransportPropertyNames.RoutingKey] = RoutingKey!; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlPublishTransportProvider.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlPublishTransportProvider.cs new file mode 100644 index 00000000000..62754cbd635 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlPublishTransportProvider.cs @@ -0,0 +1,27 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Threading.Tasks; + using Transports; + + + public class SqlPublishTransportProvider : + IPublishTransportProvider + { + readonly IConnectionContextSupervisor _connectionContextSupervisor; + readonly SqlReceiveEndpointContext _context; + + public SqlPublishTransportProvider(IConnectionContextSupervisor connectionContextSupervisor, SqlReceiveEndpointContext context) + { + _connectionContextSupervisor = connectionContextSupervisor; + _context = context; + } + + public Task GetPublishTransport(Uri? publishAddress) + where T : class + { + return _connectionContextSupervisor.CreatePublishTransport(_context, publishAddress); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlQueueDeadLetterTransport.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlQueueDeadLetterTransport.cs new file mode 100644 index 00000000000..286e61d14f1 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlQueueDeadLetterTransport.cs @@ -0,0 +1,27 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System.Threading.Tasks; + using Transports; + + + public class SqlQueueDeadLetterTransport : + SqlQueueMoveTransport, + IDeadLetterTransport + { + public SqlQueueDeadLetterTransport(string queueName, SqlQueueType queueType) + : base(queueName, queueType) + { + } + + public Task Send(ReceiveContext context, string? reason) + { + void PreSend(SqlTransportMessage message, SendHeaders headers) + { + headers.Set(MessageHeaders.Reason, reason ?? "Unspecified"); + } + + return Move(context, PreSend); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlQueueErrorTransport.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlQueueErrorTransport.cs new file mode 100644 index 00000000000..f5231c68a27 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlQueueErrorTransport.cs @@ -0,0 +1,30 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Threading.Tasks; + using Transports; + + + public class SqlQueueErrorTransport : + SqlQueueMoveTransport, + IErrorTransport + { + public SqlQueueErrorTransport(string queueName, SqlQueueType queueType) + : base(queueName, queueType) + { + } + + public Task Send(ExceptionReceiveContext context) + { + void PreSend(SqlTransportMessage message, SendHeaders headers) + { + headers.SetExceptionHeaders(context); + + if (message.ExpirationTime.HasValue) + message.ExpirationTime = DateTime.UtcNow + Defaults.ErrorQueueTimeToLive; + } + + return Move(context, PreSend); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlQueueMoveTransport.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlQueueMoveTransport.cs new file mode 100644 index 00000000000..25af6de801f --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlQueueMoveTransport.cs @@ -0,0 +1,39 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Threading.Tasks; + + + public class SqlQueueMoveTransport + { + readonly string _queueName; + readonly SqlQueueType _queueType; + + protected SqlQueueMoveTransport(string queueName, SqlQueueType queueType) + { + _queueName = queueName; + _queueType = queueType; + } + + protected async Task Move(ReceiveContext context, Action preSend) + { + if (!context.TryGetPayload(out SqlMessageContext? messageContext)) + throw new ArgumentException("The ReceiveContext must contain a DbMessageContext", nameof(context)); + + if (!context.TryGetPayload(out ClientContext? clientContext)) + throw new ArgumentException("The ReceiveContext must contain a ClientContext", nameof(context)); + + if (!messageContext.LockId.HasValue) + throw new ArgumentException("The LockId is not present", nameof(context)); + + var message = messageContext.TransportMessage; + + var transportHeaders = SqlTransportMessage.DeserializeHeaders(message.TransportHeaders); + + preSend(message, transportHeaders); + + await clientContext.MoveMessage(messageContext.LockId.Value, messageContext.DeliveryMessageId, _queueName, _queueType, transportHeaders); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlReceiveContext.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlReceiveContext.cs new file mode 100644 index 00000000000..61f4ed6d03f --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlReceiveContext.cs @@ -0,0 +1,67 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Collections.Generic; + using Context; + using Transports; + + + public sealed class SqlReceiveContext : + BaseReceiveContext, + SqlMessageContext, + TransportReceiveContext, + ITransportSequenceNumber + { + IHeaderProvider? _headerProvider; + + public SqlReceiveContext(SqlTransportMessage message, bool redelivered, SqlReceiveEndpointContext context, + ReceiveSettings settings, ClientContext clientContext, ConnectionContext connectionContext, SqlReceiveLockContext lockContext) + : base(redelivered, context, settings, clientContext, connectionContext, lockContext) + { + TransportMessage = message; + + Body = message.Body != null + ? new StringMessageBody(message.Body) + : new BytesMessageBody(message.BinaryBody); + } + + public override MessageBody Body { get; } + + protected override IHeaderProvider HeaderProvider => _headerProvider ??= new SqlHeaderProvider(TransportMessage); + + public SqlTransportMessage TransportMessage { get; } + + public string? RoutingKey => TransportMessage.RoutingKey; + public Guid TransportMessageId => TransportMessage.TransportMessageId; + + public Guid? ConsumerId => TransportMessage.ConsumerId; + public Guid? LockId => TransportMessage.LockId; + + public string QueueName => TransportMessage.QueueName; + public short Priority => TransportMessage.Priority; + public long DeliveryMessageId => TransportMessage.MessageDeliveryId; + public DateTime EnqueueTime => TransportMessage.EnqueueTime; + public int DeliveryCount => TransportMessage.DeliveryCount; + + public ulong? SequenceNumber => (ulong)DeliveryMessageId; + + public string? PartitionKey => TransportMessage.PartitionKey; + + public IDictionary? GetTransportProperties() + { + var properties = new Lazy>(() => new Dictionary()); + + if (!string.IsNullOrWhiteSpace(RoutingKey)) + properties.Value[SqlTransportPropertyNames.RoutingKey] = RoutingKey!; + + if (!string.IsNullOrWhiteSpace(PartitionKey)) + properties.Value[SqlTransportPropertyNames.PartitionKey] = PartitionKey!; + + if (Priority != 100) + properties.Value[SqlTransportPropertyNames.Priority] = Priority; + + return properties.IsValueCreated ? properties.Value : null; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlReceiveEndpointContext.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlReceiveEndpointContext.cs new file mode 100644 index 00000000000..518c49823f1 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlReceiveEndpointContext.cs @@ -0,0 +1,14 @@ +namespace MassTransit.SqlTransport +{ + using Topology; + using Transports; + + + public interface SqlReceiveEndpointContext : + ReceiveEndpointContext + { + IClientContextSupervisor ClientContextSupervisor { get; } + + BrokerTopology BrokerTopology { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlReceiveLockContext.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlReceiveLockContext.cs new file mode 100644 index 00000000000..ad422fa069a --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlReceiveLockContext.cs @@ -0,0 +1,223 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Transports; + using Util; + + + public class SqlReceiveLockContext : + MessageRedeliveryContext, + ReceiveLockContext + { + readonly CancellationTokenSource _activeTokenSource; + readonly ClientContext _clientContext; + readonly Uri _inputAddress; + readonly SqlTransportMessage _message; + readonly Task? _renewLockTask; + readonly ReceiveSettings _settings; + readonly DateTime _startedAt; + bool _locked; + + public SqlReceiveLockContext(Uri inputAddress, SqlTransportMessage message, ReceiveSettings settings, ClientContext clientContext) + { + _startedAt = DateTime.UtcNow; + _inputAddress = inputAddress; + _message = message; + _settings = settings; + _clientContext = clientContext; + _activeTokenSource = new CancellationTokenSource(); + _locked = true; + + if (_message.LockId.HasValue) + _renewLockTask = Task.Run(() => RenewLock()); + } + + public async Task ScheduleRedelivery(TimeSpan delay, Action? callback) + { + if (_locked == false) + return; + + _activeTokenSource.Cancel(); + + try + { + if (_renewLockTask != null) + await _renewLockTask.ConfigureAwait(false); + + if (!_clientContext.CancellationToken.IsCancellationRequested && _message.LockId != null) + { + var transportHeaders = _message.GetTransportHeaders(); + + var redeliveryCount = transportHeaders.Get(MessageHeaders.RedeliveryCount, default(int?)) ?? 0; + + transportHeaders.Set(MessageHeaders.RedeliveryCount, redeliveryCount + 1); + + var unlocked = await _clientContext.Unlock(_message.LockId.Value, _message.MessageDeliveryId, delay, transportHeaders) + .ConfigureAwait(false); + + _locked = false; + + if (unlocked) + LogContext.Debug?.Log("RESEND {DestinationAddress} {MessageId} (delay: {Delay})", _inputAddress, _message.MessageId, delay); + } + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + LogContext.Error?.Log(ex, "Schedule Redelivery faulted: {DestinationAddress} {MessageId} (lockId: {LockId})", _inputAddress, _message.MessageId, + _message.LockId); + } + finally + { + _activeTokenSource.Dispose(); + } + } + + public async Task Complete() + { + if (_locked == false) + return; + + _activeTokenSource.Cancel(); + + try + { + if (_message.LockId.HasValue) + { + await _clientContext.DeleteMessage(_message.LockId.Value, _message.MessageDeliveryId).ConfigureAwait(false); + + _locked = false; + + if (_renewLockTask != null) + await _renewLockTask.ConfigureAwait(false); + } + } + catch (Exception ex) + { + LogContext.Warning?.Log(ex, "DeleteMessage failed: {QueueName} {MessageDeliveryId} {LockId}", _settings.EntityName, _message.MessageDeliveryId, + _message.LockId); + } + finally + { + _activeTokenSource.Dispose(); + } + } + + public async Task Faulted(Exception exception) + { + if (_locked == false) + return; + + _activeTokenSource.Cancel(); + + try + { + if (_renewLockTask != null) + await _renewLockTask.ConfigureAwait(false); + + if (!_clientContext.CancellationToken.IsCancellationRequested && _message.LockId != null) + { + var headers = _message.GetTransportHeaders(); + + exception = exception.GetBaseException(); + + var exceptionMessage = ExceptionUtil.GetMessage(exception); + + headers.Set(MessageHeaders.Reason, "fault"); + headers.Set(MessageHeaders.FaultExceptionType, TypeCache.GetShortName(exception.GetType())); + headers.Set(MessageHeaders.FaultMessage, exceptionMessage); + headers.Set(MessageHeaders.FaultStackTrace, ExceptionUtil.GetStackTrace(exception)); + + await _clientContext.Unlock(_message.LockId.Value, _message.MessageDeliveryId, _settings.UnlockDelay ?? TimeSpan.Zero, headers) + .ConfigureAwait(false); + } + + _locked = false; + } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + LogContext.Error?.Log(ex, "RenewMessageLock failed: {LockId}, Original Exception: {Exception}", _message.LockId, exception); + } + finally + { + _activeTokenSource.Dispose(); + } + } + + public Task ValidateLockStatus() + { + if (_locked) + return Task.CompletedTask; + + throw new TransportException(_inputAddress, $"Message Lock Lost: {_message.LockId}"); + } + + async Task RenewLock() + { + TimeSpan CalculateDelay(TimeSpan timeout) + { + return TimeSpan.FromSeconds(timeout.TotalSeconds * 0.7); + } + + var duration = _settings.LockDuration; + + var delay = CalculateDelay(duration); + + duration = TimeSpan.FromSeconds(Math.Min(60, duration.TotalSeconds)); + + while (!_activeTokenSource.IsCancellationRequested) + { + try + { + if (delay > TimeSpan.Zero) + { + await Task.Delay(delay, _activeTokenSource.Token) + .ContinueWith(t => t, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default) + .ConfigureAwait(false); + } + + if (_activeTokenSource.IsCancellationRequested) + break; + + if (_message.LockId.HasValue) + { + if (!await _clientContext.RenewLock(_message.LockId.Value, _message.MessageDeliveryId, duration).ConfigureAwait(false)) + { + LogContext.Warning?.Log("Message Lock Lost: {InputAddress} - {MessageDeliveryId} ({LockId})", _inputAddress, + _message.MessageDeliveryId, _message.LockId); + + _locked = false; + + break; + } + } + + if (DateTime.UtcNow - _startedAt + duration >= _settings.MaxLockDuration) + break; + + delay = CalculateDelay(duration); + } + catch (TimeoutException) + { + delay = TimeSpan.Zero; + } + catch (OperationCanceledException) + { + break; + } + catch (Exception) + { + break; + } + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlSendTransportProvider.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlSendTransportProvider.cs new file mode 100644 index 00000000000..656a4bff241 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlSendTransportProvider.cs @@ -0,0 +1,30 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Threading.Tasks; + using Transports; + + + public class SqlSendTransportProvider : + ISendTransportProvider + { + readonly IConnectionContextSupervisor _connectionContextSupervisor; + readonly SqlReceiveEndpointContext _context; + + public SqlSendTransportProvider(IConnectionContextSupervisor connectionContextSupervisor, SqlReceiveEndpointContext context) + { + _connectionContextSupervisor = connectionContextSupervisor; + _context = context; + } + + public Uri NormalizeAddress(Uri address) + { + return _connectionContextSupervisor.NormalizeAddress(address); + } + + public Task GetSendTransport(Uri address) + { + return _connectionContextSupervisor.CreateSendTransport(_context, address); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlTransportMessage.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlTransportMessage.cs new file mode 100644 index 00000000000..bc00f2de8f7 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlTransportMessage.cs @@ -0,0 +1,101 @@ +#nullable enable +namespace MassTransit.SqlTransport +{ + using System; + using System.Collections.Generic; + using System.Text.Json; + using Serialization; + + + public class SqlTransportMessage + { + SendHeaders? _headers; + SendHeaders? _transportHeaders; + + public Guid TransportMessageId { get; set; } + public string QueueName { get; set; } = null!; + public short Priority { get; set; } + public long MessageDeliveryId { get; set; } + public Guid? ConsumerId { get; set; } + public Guid? LockId { get; set; } + public DateTime EnqueueTime { get; set; } + public int DeliveryCount { get; set; } + + public string? PartitionKey { get; set; } + public string? RoutingKey { get; set; } + + public string? TransportHeaders { get; set; } + + public string? ContentType { get; set; } + public string? MessageType { get; set; } + public string? Body { get; set; } + public byte[]? BinaryBody { get; set; } + + public string? Headers { get; set; } + public string? Host { get; set; } + + public Guid? MessageId { get; set; } + public Guid? RequestId { get; set; } + public Guid? CorrelationId { get; set; } + public Guid? ConversationId { get; set; } + public Guid? InitiatorId { get; set; } + + public DateTime? ExpirationTime { get; set; } + + public Uri? SourceAddress { get; set; } + public Uri? DestinationAddress { get; set; } + public Uri? ResponseAddress { get; set; } + public Uri? FaultAddress { get; set; } + + public DateTime? SentTime { get; set; } + + public SendHeaders GetHeaders() + { + return _headers ??= DeserializeHeaders(Headers); + } + + public SendHeaders GetTransportHeaders() + { + return _transportHeaders ??= DeserializeHeaders(TransportHeaders); + } + + public static SendHeaders DeserializeHeaders(string? jsonHeaders) + { + var headers = new DictionarySendHeaders(); + + if (jsonHeaders != null) + { + var elements = JsonSerializer.Deserialize>>(jsonHeaders, SystemTextJsonMessageSerializer.Options); + if (elements != null) + { + foreach (KeyValuePair element in elements) + headers.Set(element.Key, element.Value); + } + } + + return headers; + } + + HostInfo? GetHost() + { + if (Host == null) + return null; + + return JsonSerializer.Deserialize(Host, SystemTextJsonMessageSerializer.Options); + } + + static Uri? ToUri(string? value) + { + try + { + return string.IsNullOrWhiteSpace(value) + ? null + : new Uri(value); + } + catch (FormatException) + { + return default; + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlTransportMigrationHostedService.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlTransportMigrationHostedService.cs new file mode 100644 index 00000000000..e3586453458 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlTransportMigrationHostedService.cs @@ -0,0 +1,54 @@ +namespace MassTransit.SqlTransport +{ + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + + + public class SqlTransportMigrationHostedService : + IHostedService + { + readonly ILogger _logger; + readonly ISqlTransportDatabaseMigrator _migrator; + readonly SqlTransportMigrationOptions _options; + readonly SqlTransportOptions _transportOptions; + + public SqlTransportMigrationHostedService(ISqlTransportDatabaseMigrator migrator, ILogger logger, + IOptions options, IOptions dbOptions) + { + _migrator = migrator; + _logger = logger; + _options = options.Value; + _transportOptions = dbOptions.Value; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + if (_options.CreateDatabase) + { + _logger.LogInformation("MassTransit SQL Transport creating database {Database}", _transportOptions.Database); + + await _migrator.CreateDatabase(_transportOptions, cancellationToken).ConfigureAwait(false); + } + + if (_options.CreateInfrastructure) + { + _logger.LogInformation("MassTransit SQL Transport creating infrastructure for database {Database}", _transportOptions.Database); + + await _migrator.CreateInfrastructure(_transportOptions, cancellationToken).ConfigureAwait(false); + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + if (_options.DeleteDatabase) + { + _logger.LogInformation("Deleting Database {Database}", _transportOptions.Database); + + await _migrator.DeleteDatabase(_transportOptions, cancellationToken).ConfigureAwait(false); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/SqlTransportPropertyNames.cs b/src/MassTransit/SqlTransport/SqlTransport/SqlTransportPropertyNames.cs new file mode 100644 index 00000000000..bb567ae0219 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/SqlTransportPropertyNames.cs @@ -0,0 +1,9 @@ +namespace MassTransit.SqlTransport +{ + static class SqlTransportPropertyNames + { + public const string PartitionKey = "SQL-PartitionKey"; + public const string Priority = "SQL-Priority"; + public const string RoutingKey = "SQL-RoutingKey"; + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/TopicSendTransportContext.cs b/src/MassTransit/SqlTransport/SqlTransport/TopicSendTransportContext.cs new file mode 100644 index 00000000000..8e0d1778596 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/TopicSendTransportContext.cs @@ -0,0 +1,85 @@ +namespace MassTransit.SqlTransport +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + using Configuration; + using Transports; + + + public class TopicSendTransportContext : + BaseSendTransportContext, + SendTransportContext + { + readonly IPipe _configureTopologyPipe; + readonly ISqlHostConfiguration _hostConfiguration; + readonly IClientContextSupervisor _supervisor; + + public TopicSendTransportContext(ISqlHostConfiguration hostConfiguration, ReceiveEndpointContext receiveEndpointContext, + IClientContextSupervisor supervisor, IPipe configureTopologyPipe, string entityName) + : base(hostConfiguration, receiveEndpointContext.Serialization) + { + _hostConfiguration = hostConfiguration; + _supervisor = supervisor; + + _configureTopologyPipe = configureTopologyPipe; + EntityName = entityName; + } + + public override string EntityName { get; } + public override string ActivitySystem => "db"; + + public void Probe(ProbeContext context) + { + } + + public override async Task> CreateSendContext(T message, IPipe> pipe, CancellationToken cancellationToken) + { + var sendContext = new SqlMessageSendContext(message, cancellationToken); + + await pipe.Send(sendContext).ConfigureAwait(false); + + return sendContext; + } + + public override IEnumerable GetAgentHandles() + { + return new IAgent[] { }; + } + + public Task Send(IPipe pipe, CancellationToken cancellationToken = default) + { + return _hostConfiguration.Retry(() => _supervisor.Send(pipe, cancellationToken), cancellationToken, _supervisor.SendStopping); + } + + public Task> CreateSendContext(ClientContext context, T message, IPipe> pipe, + CancellationToken cancellationToken) + where T : class + { + return CreateSendContext(message, pipe, cancellationToken); + } + + public async Task Send(ClientContext clientContext, SendContext sendContext) + where T : class + { + SqlMessageSendContext context = sendContext as SqlMessageSendContext + ?? throw new ArgumentException("Invalid SendContext type", nameof(sendContext)); + + await _configureTopologyPipe.Send(clientContext).ConfigureAwait(false); + + sendContext.CancellationToken.ThrowIfCancellationRequested(); + + if (Activity.Current?.IsAllDataRequested ?? false) + { + if (!string.IsNullOrWhiteSpace(context.RoutingKey)) + Activity.Current.SetTag(nameof(context.RoutingKey), context.RoutingKey); + if (!string.IsNullOrWhiteSpace(context.PartitionKey)) + Activity.Current.SetTag(nameof(context.PartitionKey), context.PartitionKey); + } + + await clientContext.Publish(EntityName, context).ConfigureAwait(false); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/BrokerTopology.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/BrokerTopology.cs new file mode 100644 index 00000000000..c0c94bddfea --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/BrokerTopology.cs @@ -0,0 +1,11 @@ +namespace MassTransit.SqlTransport.Topology +{ + public interface BrokerTopology : + IProbeSite + { + Topic[] Topics { get; } + Queue[] Queues { get; } + TopicToTopicSubscription[] TopicSubscriptions { get; } + TopicToQueueSubscription[] QueueSubscriptions { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/BrokerTopologyBuilder.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/BrokerTopologyBuilder.cs new file mode 100644 index 00000000000..a94a7d6b723 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/BrokerTopologyBuilder.cs @@ -0,0 +1,81 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System; + using System.Threading; + using MassTransit.Topology; + + + public abstract class BrokerTopologyBuilder + { + readonly NamedEntityCollection _queues; + readonly EntityCollection _queueSubscriptions; + readonly NamedEntityCollection _topics; + readonly EntityCollection _topicSubscriptions; + long _nextId; + + protected BrokerTopologyBuilder() + { + _topics = new NamedEntityCollection(TopicEntity.EntityComparer, TopicEntity.NameComparer); + _queues = new NamedEntityCollection(QueueEntity.QueueComparer, QueueEntity.NameComparer); + + _topicSubscriptions = new EntityCollection(TopicSubscriptionEntity.EntityComparer); + _queueSubscriptions = new EntityCollection(QueueSubscriptionEntity.EntityComparer); + } + + long GetNextId() + { + return Interlocked.Increment(ref _nextId); + } + + public TopicHandle CreateTopic(string name) + { + var id = GetNextId(); + + var exchange = new TopicEntity(id, name); + + return _topics.GetOrAdd(exchange); + } + + public TopicSubscriptionHandle CreateTopicSubscription(TopicHandle source, TopicHandle destination, SqlSubscriptionType subscriptionType, + string? routingKey) + { + var id = GetNextId(); + + var sourceExchange = _topics.Get(source); + + var destinationExchange = _topics.Get(destination); + + var binding = new TopicSubscriptionEntity(id, sourceExchange, destinationExchange, subscriptionType, routingKey); + + return _topicSubscriptions.GetOrAdd(binding); + } + + public QueueHandle CreateQueue(string name, TimeSpan? autoDeleteOnIdle = null) + { + var id = GetNextId(); + + var queue = new QueueEntity(id, name, autoDeleteOnIdle); + + return _queues.GetOrAdd(queue); + } + + public QueueSubscriptionHandle CreateQueueSubscription(TopicHandle topic, QueueHandle queue, SqlSubscriptionType subscriptionType, string? routingKey) + { + var id = GetNextId(); + + var exchangeEntity = _topics.Get(topic); + + var queueEntity = _queues.Get(queue); + + var binding = new QueueSubscriptionEntity(id, exchangeEntity, queueEntity, subscriptionType, routingKey); + + return _queueSubscriptions.GetOrAdd(binding); + } + + public BrokerTopology BuildBrokerTopology() + { + return new SqlBrokerTopology(_topics, _topicSubscriptions, _queues, _queueSubscriptions); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/Queue.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/Queue.cs new file mode 100644 index 00000000000..95d26acd681 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/Queue.cs @@ -0,0 +1,15 @@ +namespace MassTransit.SqlTransport.Topology +{ + using System; + + + public interface Queue + { + string QueueName { get; } + + /// + /// Idle time before queue should be deleted (consumer-idle, not producer) + /// + TimeSpan? AutoDeleteOnIdle { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueEntity.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueEntity.cs new file mode 100644 index 00000000000..cbc26001fe0 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueEntity.cs @@ -0,0 +1,86 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System; + using System.Collections.Generic; + using System.Linq; + + + public class QueueEntity : + Queue, + QueueHandle + { + public QueueEntity(long id, string name, TimeSpan? autoDeleteOnIdle) + { + Id = id; + QueueName = name; + AutoDeleteOnIdle = autoDeleteOnIdle; + } + + public static IEqualityComparer NameComparer { get; } = new NameEqualityComparer(); + + public static IEqualityComparer QueueComparer { get; } = new QueueEntityEqualityComparer(); + + public string QueueName { get; } + public TimeSpan? AutoDeleteOnIdle { get; } + public long Id { get; } + public Queue Queue => this; + + public override string ToString() + { + return string.Join(", ", + new[] { $"name: {QueueName}", AutoDeleteOnIdle.HasValue ? $"auto-delete after {AutoDeleteOnIdle}" : "", } + .Where(x => !string.IsNullOrWhiteSpace(x))); + } + + + sealed class QueueEntityEqualityComparer : IEqualityComparer + { + public bool Equals(QueueEntity? x, QueueEntity? y) + { + if (ReferenceEquals(x, y)) + return true; + if (ReferenceEquals(x, null)) + return false; + if (ReferenceEquals(y, null)) + return false; + if (x.GetType() != y.GetType()) + return false; + return string.Equals(x.QueueName, y.QueueName) && x.AutoDeleteOnIdle == y.AutoDeleteOnIdle; + } + + public int GetHashCode(QueueEntity obj) + { + unchecked + { + var hashCode = obj.QueueName.GetHashCode(); + hashCode = (hashCode * 397) ^ obj.AutoDeleteOnIdle.GetHashCode(); + + return hashCode; + } + } + } + + + sealed class NameEqualityComparer : IEqualityComparer + { + public bool Equals(QueueEntity? x, QueueEntity? y) + { + if (ReferenceEquals(x, y)) + return true; + if (ReferenceEquals(x, null)) + return false; + if (ReferenceEquals(y, null)) + return false; + if (x.GetType() != y.GetType()) + return false; + return string.Equals(x.QueueName, y.QueueName); + } + + public int GetHashCode(QueueEntity obj) + { + return obj.QueueName.GetHashCode(); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueHandle.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueHandle.cs new file mode 100644 index 00000000000..c67546bd044 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueHandle.cs @@ -0,0 +1,11 @@ +namespace MassTransit.SqlTransport.Topology +{ + using MassTransit.Topology; + + + public interface QueueHandle : + EntityHandle + { + Queue Queue { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueSubscriptionEntity.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueSubscriptionEntity.cs new file mode 100644 index 00000000000..60af341aea7 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueSubscriptionEntity.cs @@ -0,0 +1,75 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System.Collections.Generic; + using System.Linq; + + + public class QueueSubscriptionEntity : + TopicToQueueSubscription, + QueueSubscriptionHandle + { + readonly QueueEntity _queue; + readonly TopicEntity _topic; + + public QueueSubscriptionEntity(long id, TopicEntity topic, QueueEntity queue, SqlSubscriptionType subscriptionType, string? routingKey) + { + Id = id; + SubscriptionType = subscriptionType; + RoutingKey = routingKey; + _topic = topic; + _queue = queue; + } + + public static IEqualityComparer EntityComparer { get; } = new QueueSubscriptionEntityEqualityComparer(); + + public long Id { get; } + public TopicToQueueSubscription Subscription => this; + public SqlSubscriptionType SubscriptionType { get; } + + public Topic Source => _topic.Topic; + public Queue Destination => _queue.Queue; + public string? RoutingKey { get; } + + public override string ToString() + { + return string.Join(", ", + new[] + { + $"source: {Source.TopicName}", + $"destination: {Destination.QueueName}", + $"type: {SubscriptionType}", + string.IsNullOrWhiteSpace(RoutingKey) ? "" : $"routing-key: {RoutingKey}", + }.Where(x => !string.IsNullOrWhiteSpace(x))); + } + + + sealed class QueueSubscriptionEntityEqualityComparer : IEqualityComparer + { + public bool Equals(QueueSubscriptionEntity? x, QueueSubscriptionEntity? y) + { + if (ReferenceEquals(x, y)) + return true; + if (ReferenceEquals(x, null)) + return false; + if (ReferenceEquals(y, null)) + return false; + if (x.GetType() != y.GetType()) + return false; + return x._queue.Equals(y._queue) && x._topic.Equals(y._topic) && x.SubscriptionType == y.SubscriptionType && x.RoutingKey == y.RoutingKey; + } + + public int GetHashCode(QueueSubscriptionEntity obj) + { + unchecked + { + var hashCode = obj._queue.GetHashCode(); + hashCode = (hashCode * 397) ^ obj._topic.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)obj.SubscriptionType; + hashCode = (hashCode * 397) ^ (obj.RoutingKey != null ? obj.RoutingKey.GetHashCode() : 0); + return hashCode; + } + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueSubscriptionHandle.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueSubscriptionHandle.cs new file mode 100644 index 00000000000..6c661974ab8 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/QueueSubscriptionHandle.cs @@ -0,0 +1,11 @@ +namespace MassTransit.SqlTransport.Topology +{ + using MassTransit.Topology; + + + public interface QueueSubscriptionHandle : + EntityHandle + { + TopicToQueueSubscription Subscription { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/Topic.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/Topic.cs new file mode 100644 index 00000000000..f6a17a73be4 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/Topic.cs @@ -0,0 +1,7 @@ +namespace MassTransit.SqlTransport.Topology +{ + public interface Topic + { + string TopicName { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicEntity.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicEntity.cs new file mode 100644 index 00000000000..3ca5b481e61 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicEntity.cs @@ -0,0 +1,51 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System.Collections.Generic; + + + public class TopicEntity : + Topic, + TopicHandle + { + public TopicEntity(long id, string name) + { + Id = id; + TopicName = name; + } + + public static IEqualityComparer NameComparer { get; } = new NameEqualityComparer(); + public static IEqualityComparer EntityComparer { get; } = new NameEqualityComparer(); + + public string TopicName { get; } + public long Id { get; } + public Topic Topic => this; + + public override string ToString() + { + return TopicName; + } + + + sealed class NameEqualityComparer : IEqualityComparer + { + public bool Equals(TopicEntity? x, TopicEntity? y) + { + if (ReferenceEquals(x, y)) + return true; + if (ReferenceEquals(x, null)) + return false; + if (ReferenceEquals(y, null)) + return false; + if (x.GetType() != y.GetType()) + return false; + return string.Equals(x.TopicName, y.TopicName); + } + + public int GetHashCode(TopicEntity obj) + { + return obj.TopicName.GetHashCode(); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicHandle.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicHandle.cs new file mode 100644 index 00000000000..85e0aab3529 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicHandle.cs @@ -0,0 +1,11 @@ +namespace MassTransit.SqlTransport.Topology +{ + using MassTransit.Topology; + + + public interface TopicHandle : + EntityHandle + { + Topic Topic { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicSubscriptionEntity.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicSubscriptionEntity.cs new file mode 100644 index 00000000000..5cf44120c85 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicSubscriptionEntity.cs @@ -0,0 +1,73 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System.Collections.Generic; + using System.Linq; + + + public class TopicSubscriptionEntity : + TopicToTopicSubscription, + TopicSubscriptionHandle + { + readonly TopicEntity _destination; + readonly TopicEntity _source; + + public TopicSubscriptionEntity(long id, TopicEntity source, TopicEntity destination, SqlSubscriptionType subscriptionType, string? routingKey) + { + Id = id; + SubscriptionType = subscriptionType; + RoutingKey = routingKey; + _source = source; + _destination = destination; + } + + public static IEqualityComparer EntityComparer { get; } = new TopicSubscriptionEntityEqualityComparer(); + public long Id { get; } + public TopicToTopicSubscription Subscription => this; + public SqlSubscriptionType SubscriptionType { get; } + + public Topic Source => _source.Topic; + public Topic Destination => _destination.Topic; + public string? RoutingKey { get; } + + public override string ToString() + { + return string.Join(", ", + new[] + { + $"source: {Source.TopicName}", + $"destination: {Destination.TopicName}", + $"type: {SubscriptionType}", + string.IsNullOrWhiteSpace(RoutingKey) ? "" : $"routing-key: {RoutingKey}" + }.Where(x => !string.IsNullOrWhiteSpace(x))); + } + + + sealed class TopicSubscriptionEntityEqualityComparer : IEqualityComparer + { + public bool Equals(TopicSubscriptionEntity? x, TopicSubscriptionEntity? y) + { + if (ReferenceEquals(x, y)) + return true; + if (ReferenceEquals(x, null)) + return false; + if (ReferenceEquals(y, null)) + return false; + if (x.GetType() != y.GetType()) + return false; + return x._source.Equals(y._source) && x.SubscriptionType == y.SubscriptionType && x.RoutingKey == y.RoutingKey; + } + + public int GetHashCode(TopicSubscriptionEntity obj) + { + unchecked + { + var hashCode = obj._source.GetHashCode(); + hashCode = (hashCode * 397) ^ (int)obj.SubscriptionType; + hashCode = (hashCode * 397) ^ (obj.RoutingKey != null ? obj.RoutingKey.GetHashCode() : 0); + return hashCode; + } + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicSubscriptionHandle.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicSubscriptionHandle.cs new file mode 100644 index 00000000000..eeed684ae33 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicSubscriptionHandle.cs @@ -0,0 +1,11 @@ +namespace MassTransit.SqlTransport.Topology +{ + using MassTransit.Topology; + + + public interface TopicSubscriptionHandle : + EntityHandle + { + TopicToTopicSubscription Subscription { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicToQueueSubscription.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicToQueueSubscription.cs new file mode 100644 index 00000000000..ec157344372 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicToQueueSubscription.cs @@ -0,0 +1,26 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + /// + /// The exchange to queue binding details to declare the binding to RabbitMQ + /// + public interface TopicToQueueSubscription + { + /// + /// The source exchange + /// + Topic Source { get; } + + /// + /// The destination exchange + /// + Queue Destination { get; } + + SqlSubscriptionType SubscriptionType { get; } + + /// + /// A routing key for the exchange binding + /// + string? RoutingKey { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicToTopicSubscription.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicToTopicSubscription.cs new file mode 100644 index 00000000000..2f79036330a --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/Entities/TopicToTopicSubscription.cs @@ -0,0 +1,26 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + /// + /// The exchange to exchange binding details to declare the binding to RabbitMQ + /// + public interface TopicToTopicSubscription + { + /// + /// The source exchange + /// + Topic Source { get; } + + /// + /// The destination exchange + /// + Topic Destination { get; } + + SqlSubscriptionType SubscriptionType { get; } + + /// + /// A routing key for the exchange binding + /// + string? RoutingKey { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/IBrokerTopologyBuilder.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/IBrokerTopologyBuilder.cs new file mode 100644 index 00000000000..aa30dba1caf --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/IBrokerTopologyBuilder.cs @@ -0,0 +1,46 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System; + + + public interface IBrokerTopologyBuilder + { + /// + /// Declares an exchange + /// + /// The exchange name + /// An entity handle used to reference the exchange in subsequent calls + TopicHandle CreateTopic(string name); + + /// + /// Bind an exchange to an exchange, with the specified routing key and arguments + /// + /// The source exchange + /// The destination exchange + /// + /// The binding routing key + /// An entity handle used to reference the binding in subsequent calls + TopicSubscriptionHandle CreateTopicSubscription(TopicHandle source, TopicHandle destination, + SqlSubscriptionType subscriptionType = SqlSubscriptionType.All, string? routingKey = null); + + /// + /// Declares a queue + /// + /// + /// + /// + QueueHandle CreateQueue(string name, TimeSpan? autoDeleteOnIdle = null); + + /// + /// Binds an exchange to a queue, with the specified routing key and arguments + /// + /// + /// + /// + /// + /// + QueueSubscriptionHandle CreateQueueSubscription(TopicHandle topic, QueueHandle queue, + SqlSubscriptionType subscriptionType = SqlSubscriptionType.All, string? routingKey = null); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/IPublishEndpointBrokerTopologyBuilder.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/IPublishEndpointBrokerTopologyBuilder.cs new file mode 100644 index 00000000000..b7dd12cfd1e --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/IPublishEndpointBrokerTopologyBuilder.cs @@ -0,0 +1,17 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + /// + /// A builder for creating the topology when publishing a message + /// + public interface IPublishEndpointBrokerTopologyBuilder : + IBrokerTopologyBuilder + { + /// + /// The exchange to which the message is published + /// + TopicHandle? Topic { get; set; } + + IPublishEndpointBrokerTopologyBuilder CreateImplementedBuilder(); + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/IReceiveEndpointBrokerTopologyBuilder.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/IReceiveEndpointBrokerTopologyBuilder.cs new file mode 100644 index 00000000000..8f31727715e --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/IReceiveEndpointBrokerTopologyBuilder.cs @@ -0,0 +1,16 @@ +namespace MassTransit.SqlTransport.Topology +{ + /// + /// A unique builder context should be created for each specification, so that the items added + /// by it can be combined together into a group - so that if a subsequent specification yanks + /// something that conflicts, the system can yank the group or warn that it's impacted. + /// + public interface IReceiveEndpointBrokerTopologyBuilder : + IBrokerTopologyBuilder + { + /// + /// A handle to the consuming queue + /// + QueueHandle Queue { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/PublishEndpointBrokerTopologyBuilder.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/PublishEndpointBrokerTopologyBuilder.cs new file mode 100644 index 00000000000..8d1441a7753 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/PublishEndpointBrokerTopologyBuilder.cs @@ -0,0 +1,72 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System; + + + public class PublishEndpointBrokerTopologyBuilder : + BrokerTopologyBuilder, + IPublishEndpointBrokerTopologyBuilder + { + /// + /// The topic to which the published message is sent + /// + public TopicHandle? Topic { get; set; } + + public IPublishEndpointBrokerTopologyBuilder CreateImplementedBuilder() + { + return new ImplementedBuilder(this); + } + + + class ImplementedBuilder : + IPublishEndpointBrokerTopologyBuilder + { + readonly IPublishEndpointBrokerTopologyBuilder _builder; + TopicHandle? _topic; + + public ImplementedBuilder(IPublishEndpointBrokerTopologyBuilder builder) + { + _builder = builder; + } + + public TopicHandle? Topic + { + get => _topic; + set + { + _topic = value; + if (_builder.Topic != null && _topic != null) + _builder.CreateTopicSubscription(_builder.Topic, _topic); + } + } + + public IPublishEndpointBrokerTopologyBuilder CreateImplementedBuilder() + { + return new ImplementedBuilder(this); + } + + public TopicHandle CreateTopic(string name) + { + return _builder.CreateTopic(name); + } + + public TopicSubscriptionHandle CreateTopicSubscription(TopicHandle source, TopicHandle destination, SqlSubscriptionType subscriptionType, + string? routingKey) + { + return _builder.CreateTopicSubscription(source, destination, subscriptionType, routingKey); + } + + public QueueHandle CreateQueue(string name, TimeSpan? autoDeleteOnIdle) + { + return _builder.CreateQueue(name, autoDeleteOnIdle); + } + + public QueueSubscriptionHandle CreateQueueSubscription(TopicHandle topic, QueueHandle queue, SqlSubscriptionType subscriptionType, + string? routingKey) + { + return _builder.CreateQueueSubscription(topic, queue, subscriptionType, routingKey); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/QueueSendSettings.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/QueueSendSettings.cs new file mode 100644 index 00000000000..f591b683414 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/QueueSendSettings.cs @@ -0,0 +1,37 @@ +namespace MassTransit.SqlTransport.Topology +{ + using System; + using Configuration; + + + public class QueueSendSettings : + SqlQueueConfigurator, + SendSettings + { + public QueueSendSettings(SqlEndpointAddress address) + : base(address.Name, address.AutoDeleteOnIdle) + { + } + + public QueueSendSettings(EntitySettings settings, string queueName) + : base(queueName, settings.AutoDeleteOnIdle) + { + } + + public SqlEndpointAddress GetSendAddress(Uri hostAddress) + { + return new SqlEndpointAddress(hostAddress, QueueName, AutoDeleteOnIdle); + } + + public BrokerTopology GetBrokerTopology() + { + var builder = new PublishEndpointBrokerTopologyBuilder(); + + builder.CreateQueue(QueueName, AutoDeleteOnIdle); + + return builder.BuildBrokerTopology(); + } + + public string EntityName => QueueName; + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/ReceiveEndpointBrokerTopologyBuilder.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/ReceiveEndpointBrokerTopologyBuilder.cs new file mode 100644 index 00000000000..2cad8d5827c --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/ReceiveEndpointBrokerTopologyBuilder.cs @@ -0,0 +1,14 @@ +namespace MassTransit.SqlTransport.Topology +{ + public class ReceiveEndpointBrokerTopologyBuilder : + BrokerTopologyBuilder, + IReceiveEndpointBrokerTopologyBuilder + { + public ReceiveEndpointBrokerTopologyBuilder(ReceiveSettings settings) + { + Queue = CreateQueue(settings.QueueName, settings.AutoDeleteOnIdle); + } + + public QueueHandle Queue { get; } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlBrokerTopology.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlBrokerTopology.cs new file mode 100644 index 00000000000..dee9e36f81e --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlBrokerTopology.cs @@ -0,0 +1,67 @@ +namespace MassTransit.SqlTransport.Topology +{ + using System.Collections.Generic; + using System.Linq; + + + public class SqlBrokerTopology : + BrokerTopology + { + public SqlBrokerTopology(IEnumerable topics, IEnumerable topicSubscriptions, IEnumerable queues, + IEnumerable queueSubscriptions) + { + Topics = topics.ToArray(); + Queues = queues.ToArray(); + TopicSubscriptions = topicSubscriptions.ToArray(); + QueueSubscriptions = queueSubscriptions.ToArray(); + } + + public Topic[] Topics { get; } + public Queue[] Queues { get; } + public TopicToTopicSubscription[] TopicSubscriptions { get; } + public TopicToQueueSubscription[] QueueSubscriptions { get; } + + public void Probe(ProbeContext context) + { + foreach (var topic in Topics) + { + var scope = context.CreateScope("topic"); + scope.Set(new { Name = topic.TopicName }); + } + + foreach (var queue in Queues) + { + var scope = context.CreateScope("queue"); + scope.Set(new + { + Name = queue.QueueName, + queue.AutoDeleteOnIdle + }); + } + + foreach (var subscription in TopicSubscriptions) + { + var scope = context.CreateScope("topic-subscription"); + scope.Set(new + { + Source = subscription.Source.TopicName, + Destination = subscription.Destination.TopicName, + subscription.SubscriptionType, + subscription.RoutingKey + }); + } + + foreach (var subscription in QueueSubscriptions) + { + var scope = context.CreateScope("queue-subscription"); + scope.Set(new + { + Source = subscription.Source.TopicName, + Destination = subscription.Destination.QueueName, + subscription.SubscriptionType, + subscription.RoutingKey + }); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlBusTopology.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlBusTopology.cs new file mode 100644 index 00000000000..c1da2dce5d8 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlBusTopology.cs @@ -0,0 +1,32 @@ +namespace MassTransit.SqlTransport.Topology +{ + using Configuration; + using Transports; + + + public class SqlBusTopology : + BusTopology, + ISqlBusTopology + { + readonly ISqlTopologyConfiguration _configuration; + + public SqlBusTopology(ISqlHostConfiguration hostConfiguration, ISqlTopologyConfiguration configuration) + : base(hostConfiguration, configuration) + { + _configuration = configuration; + } + + ISqlPublishTopology ISqlBusTopology.PublishTopology => _configuration.Publish; + ISqlSendTopology ISqlBusTopology.SendTopology => _configuration.Send; + + ISqlMessagePublishTopology ISqlBusTopology.Publish() + { + return _configuration.Publish.GetMessageTopology(); + } + + ISqlMessageSendTopology ISqlBusTopology.Send() + { + return _configuration.Send.GetMessageTopology(); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlConsumeTopology.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlConsumeTopology.cs new file mode 100644 index 00000000000..07699719912 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlConsumeTopology.cs @@ -0,0 +1,77 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Configuration; + + + public class SqlConsumeTopology : + ConsumeTopology, + ISqlConsumeTopologyConfigurator + { + readonly ISqlPublishTopology _publishTopology; + readonly List _specifications; + + public SqlConsumeTopology(ISqlPublishTopology publishTopology) + : base(255) + { + _publishTopology = publishTopology; + + _specifications = new List(); + } + + ISqlMessageConsumeTopology ISqlConsumeTopology.GetMessageTopology() + { + return (ISqlMessageConsumeTopology)base.GetMessageTopology(); + } + + public void AddSpecification(ISqlConsumeTopologySpecification specification) + { + if (specification == null) + throw new ArgumentNullException(nameof(specification)); + + _specifications.Add(specification); + } + + ISqlMessageConsumeTopologyConfigurator ISqlConsumeTopologyConfigurator.GetMessageTopology() + { + return (ISqlMessageConsumeTopologyConfigurator)base.GetMessageTopology(); + } + + public void Apply(IReceiveEndpointBrokerTopologyBuilder builder) + { + foreach (var specification in _specifications) + specification.Apply(builder); + + ForEach(x => x.Apply(builder)); + } + + public void Subscribe(string topicName, Action? configure = null) + { + if (string.IsNullOrWhiteSpace(topicName)) + throw new ArgumentException("Value cannot be null or whitespace.", nameof(topicName)); + + var specification = new QueueSubscriptionConsumeTopologySpecification(topicName); + + configure?.Invoke(specification); + + _specifications.Add(specification); + } + + public override IEnumerable Validate() + { + return base.Validate().Concat(_specifications.SelectMany(x => x.Validate())); + } + + protected override IMessageConsumeTopologyConfigurator CreateMessageTopology() + { + var messageTopology = new SqlMessageConsumeTopology(_publishTopology.GetMessageTopology()); + + OnMessageTopologyCreated(messageTopology); + + return messageTopology; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessageConsumeTopology.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessageConsumeTopology.cs new file mode 100644 index 00000000000..9207bdc8533 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessageConsumeTopology.cs @@ -0,0 +1,52 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Configuration; + + + public class SqlMessageConsumeTopology : + MessageConsumeTopology, + ISqlMessageConsumeTopologyConfigurator, + IDbMessageConsumeTopologyConfigurator + where TMessage : class + { + readonly ISqlMessagePublishTopology _publishTopology; + readonly List _specifications; + + public SqlMessageConsumeTopology(ISqlMessagePublishTopology publishTopology) + { + _publishTopology = publishTopology; + + _specifications = new List(); + } + + public void Apply(IReceiveEndpointBrokerTopologyBuilder builder) + { + foreach (var specification in _specifications) + specification.Apply(builder); + } + + public void Subscribe(Action? configure = null) + { + if (!IsBindableMessageType) + { + _specifications.Add(new InvalidSqlConsumeTopologySpecification(TypeCache.ShortName, "Is not a consumable message type")); + return; + } + + var specification = new QueueSubscriptionConsumeTopologySpecification(_publishTopology.Topic); + + configure?.Invoke(specification); + + _specifications.Add(specification); + } + + public override IEnumerable Validate() + { + return base.Validate().Concat(_specifications.SelectMany(x => x.Validate())); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessageNameFormatter.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessageNameFormatter.cs new file mode 100644 index 00000000000..002c699f52e --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessageNameFormatter.cs @@ -0,0 +1,25 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System; + using Transports; + + + public class SqlMessageNameFormatter : + IMessageNameFormatter + { + readonly IMessageNameFormatter _formatter; + + public SqlMessageNameFormatter(string? namespaceSeparator = null) + { + _formatter = string.IsNullOrWhiteSpace(namespaceSeparator) + ? new DefaultMessageNameFormatter("::", "--", ":", "-") + : new DefaultMessageNameFormatter("::", "--", namespaceSeparator, "-"); + } + + public string GetMessageName(Type type) + { + return _formatter.GetMessageName(type); + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessagePublishTopology.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessagePublishTopology.cs new file mode 100644 index 00000000000..0c075bb76d3 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessagePublishTopology.cs @@ -0,0 +1,100 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using Configuration; + using MassTransit.Topology; + + + public class SqlMessagePublishTopology : + MessagePublishTopology, + ISqlMessagePublishTopologyConfigurator + where TMessage : class + { + readonly List _implementedMessageTypes; + readonly SqlTopicConfigurator _topic; + + public SqlMessagePublishTopology(ISqlPublishTopology publishTopology, IMessageTopology messageTopology) + : base(publishTopology) + { + var exchangeName = messageTopology.EntityName; + + _topic = new SqlTopicConfigurator(exchangeName); + + _implementedMessageTypes = new List(); + } + + public Topic Topic => _topic; + + public void Apply(IPublishEndpointBrokerTopologyBuilder builder) + { + if (Exclude) + return; + + var topicHandle = builder.CreateTopic(_topic.TopicName); + + if (builder.Topic != null) + builder.CreateTopicSubscription(builder.Topic, topicHandle); + else + builder.Topic = topicHandle; + + foreach (var configurator in _implementedMessageTypes) + configurator.Apply(builder); + } + + public override bool TryGetPublishAddress(Uri baseAddress, [NotNullWhen(true)] out Uri? publishAddress) + { + publishAddress = _topic.GetEndpointAddress(baseAddress); + return true; + } + + public BrokerTopology GetBrokerTopology() + { + var builder = new PublishEndpointBrokerTopologyBuilder(); + + Apply(builder); + + return builder.BuildBrokerTopology(); + } + + public SendSettings GetSendSettings(Uri hostAddress) + { + return new QueueSendSettings(_topic.GetEndpointAddress(hostAddress)); + } + + public void AddImplementedMessageConfigurator(ISqlMessagePublishTopologyConfigurator configurator, bool direct) + where T : class + { + var adapter = new ImplementedTypeAdapter(configurator, direct); + + _implementedMessageTypes.Add(adapter); + } + + + class ImplementedTypeAdapter : + ISqlMessagePublishTopology + where T : class + { + readonly ISqlMessagePublishTopologyConfigurator _configurator; + readonly bool _direct; + + public ImplementedTypeAdapter(ISqlMessagePublishTopologyConfigurator configurator, bool direct) + { + _configurator = configurator; + _direct = direct; + } + + public void Apply(IPublishEndpointBrokerTopologyBuilder builder) + { + if (_direct) + { + var implementedBuilder = builder.CreateImplementedBuilder(); + + _configurator.Apply(implementedBuilder); + } + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessageSendTopology.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessageSendTopology.cs new file mode 100644 index 00000000000..afbee8fb864 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlMessageSendTopology.cs @@ -0,0 +1,12 @@ +namespace MassTransit.SqlTransport.Topology +{ + using MassTransit.Topology; + + + public class SqlMessageSendTopology : + MessageSendTopology, + ISqlMessageSendTopologyConfigurator + where TMessage : class + { + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlPublishTopology.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlPublishTopology.cs new file mode 100644 index 00000000000..c18003db925 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlPublishTopology.cs @@ -0,0 +1,85 @@ +namespace MassTransit.SqlTransport.Topology +{ + using System; + using MassTransit.Topology; + using Metadata; + + + public class SqlPublishTopology : + PublishTopology, + ISqlPublishTopologyConfigurator + { + readonly IMessageTopology _messageTopology; + + public SqlPublishTopology(IMessageTopology messageTopology) + { + _messageTopology = messageTopology; + } + + ISqlMessagePublishTopology ISqlPublishTopology.GetMessageTopology() + { + return (ISqlMessagePublishTopology)GetMessageTopology(); + } + + ISqlMessagePublishTopologyConfigurator ISqlPublishTopologyConfigurator.GetMessageTopology(Type messageType) + { + return (ISqlMessagePublishTopologyConfigurator)GetMessageTopology(messageType); + } + + public BrokerTopology GetPublishBrokerTopology() + { + var builder = new PublishEndpointBrokerTopologyBuilder(); + + ForEachMessageType(x => + { + x.Apply(builder); + + builder.Topic = null; + }); + + return builder.BuildBrokerTopology(); + } + + ISqlMessagePublishTopologyConfigurator ISqlPublishTopologyConfigurator.GetMessageTopology() + { + return (ISqlMessagePublishTopologyConfigurator)GetMessageTopology(); + } + + protected override IMessagePublishTopologyConfigurator CreateMessageTopology() + { + var messageTopology = new SqlMessagePublishTopology(this, _messageTopology.GetMessageTopology()); + + var connector = new ImplementedMessageTypeConnector(this, messageTopology); + + ImplementedMessageTypeCache.EnumerateImplementedTypes(connector); + + OnMessageTopologyCreated(messageTopology); + + return messageTopology; + } + + + class ImplementedMessageTypeConnector : + IImplementedMessageType + where TMessage : class + { + readonly SqlMessagePublishTopology _messagePublishTopologyConfigurator; + readonly ISqlPublishTopologyConfigurator _publishTopology; + + public ImplementedMessageTypeConnector(ISqlPublishTopologyConfigurator publishTopology, + SqlMessagePublishTopology messagePublishTopologyConfigurator) + { + _publishTopology = publishTopology; + _messagePublishTopologyConfigurator = messagePublishTopologyConfigurator; + } + + public void ImplementsMessageType(bool direct) + where T : class + { + ISqlMessagePublishTopologyConfigurator messageTopology = _publishTopology.GetMessageTopology(); + + _messagePublishTopologyConfigurator.AddImplementedMessageConfigurator(messageTopology, direct); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlSendTopology.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlSendTopology.cs new file mode 100644 index 00000000000..676b64b5cc5 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/SqlSendTopology.cs @@ -0,0 +1,57 @@ +#nullable enable +namespace MassTransit.SqlTransport.Topology +{ + using System; + using MassTransit.Topology; + + + public class SqlSendTopology : + SendTopology, + ISqlSendTopologyConfigurator + { + public Action? ConfigureErrorSettings { get; set; } + public Action? ConfigureDeadLetterSettings { get; set; } + + public new ISqlMessageSendTopologyConfigurator GetMessageTopology() + where T : class + { + IMessageSendTopologyConfigurator configurator = base.GetMessageTopology(); + + return (configurator as ISqlMessageSendTopologyConfigurator)!; + } + + public SendSettings GetSendSettings(SqlEndpointAddress address) + { + return address.Type == SqlEndpointAddress.AddressType.Queue + ? new QueueSendSettings(address) + : new TopicSendSettings(address); + } + + public SendSettings GetErrorSettings(ReceiveSettings settings) + { + var errorSettings = new QueueSendSettings(settings, ErrorQueueNameFormatter.FormatErrorQueueName(settings.QueueName)); + + ConfigureErrorSettings?.Invoke(errorSettings); + + return errorSettings; + } + + public SendSettings GetDeadLetterSettings(ReceiveSettings settings) + { + var deadLetterSetting = new QueueSendSettings(settings, DeadLetterQueueNameFormatter.FormatDeadLetterQueueName(settings.QueueName)); + + ConfigureDeadLetterSettings?.Invoke(deadLetterSetting); + + return deadLetterSetting; + } + + protected override IMessageSendTopologyConfigurator CreateMessageTopology(Type type) + { + var messageTopology = new SqlMessageSendTopology(); + + OnMessageTopologyCreated(messageTopology); + + return messageTopology; + } + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/TopicSendSettings.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/TopicSendSettings.cs new file mode 100644 index 00000000000..9594e709098 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/TopicSendSettings.cs @@ -0,0 +1,34 @@ +namespace MassTransit.SqlTransport.Topology +{ + using System; + using Configuration; + + + public class TopicSendSettings : + SqlTopicConfigurator, + SendSettings + { + public TopicSendSettings(SqlEndpointAddress address) + : base(address.Name) + { + } + + public SqlEndpointAddress GetSendAddress(Uri hostAddress) + { + return new SqlEndpointAddress(hostAddress, TopicName); + } + + public BrokerTopology GetBrokerTopology() + { + var builder = new PublishEndpointBrokerTopologyBuilder(); + + builder.Topic = builder.CreateTopic(TopicName); + + return builder.BuildBrokerTopology(); + } + + public string EntityName => TopicName; + + public TimeSpan? AutoDeleteOnIdle => default; + } +} diff --git a/src/MassTransit/SqlTransport/SqlTransport/Topology/TopologyLayoutExtensions.cs b/src/MassTransit/SqlTransport/SqlTransport/Topology/TopologyLayoutExtensions.cs new file mode 100644 index 00000000000..2b180835d31 --- /dev/null +++ b/src/MassTransit/SqlTransport/SqlTransport/Topology/TopologyLayoutExtensions.cs @@ -0,0 +1,26 @@ +namespace MassTransit.SqlTransport.Topology +{ + public static class TopologyLayoutExtensions + { + public static void LogResult(this BrokerTopology layout) + { + foreach (var topic in layout.Topics) + LogContext.Info?.Log("Topic: {TopicName}", topic.TopicName); + + foreach (var subscription in layout.TopicSubscriptions) + { + LogContext.Info?.Log("Topic Subscription: source {Source}, destination: {Destination}, subscriptionType: {Type} routingKey: {RoutingKey}", + subscription.Source.TopicName, subscription.Destination.TopicName, subscription.SubscriptionType, subscription.RoutingKey); + } + + foreach (var queue in layout.Queues) + LogContext.Info?.Log("Queue: {QueueName}, auto-delete: {AutoDeleteOnIdle}", queue.QueueName, queue.AutoDeleteOnIdle); + + foreach (var subscription in layout.QueueSubscriptions) + { + LogContext.Info?.Log("Queue Subscription: source {Source}, destination: {Destination}, subscriptionType: {Type} routingKey: {RoutingKey}", + subscription.Source.TopicName, subscription.Destination.QueueName, subscription.SubscriptionType, subscription.RoutingKey); + } + } + } +} diff --git a/src/MassTransit/SqlTransport/Topology/ISqlBusTopology.cs b/src/MassTransit/SqlTransport/Topology/ISqlBusTopology.cs new file mode 100644 index 00000000000..69f43141fb3 --- /dev/null +++ b/src/MassTransit/SqlTransport/Topology/ISqlBusTopology.cs @@ -0,0 +1,16 @@ +namespace MassTransit +{ + public interface ISqlBusTopology : + IBusTopology + { + new ISqlPublishTopology PublishTopology { get; } + + new ISqlSendTopology SendTopology { get; } + + new ISqlMessagePublishTopology Publish() + where T : class; + + new ISqlMessageSendTopology Send() + where T : class; + } +} diff --git a/src/MassTransit/SqlTransport/Topology/ISqlConsumeTopology.cs b/src/MassTransit/SqlTransport/Topology/ISqlConsumeTopology.cs new file mode 100644 index 00000000000..51cbc6f5c9a --- /dev/null +++ b/src/MassTransit/SqlTransport/Topology/ISqlConsumeTopology.cs @@ -0,0 +1,18 @@ +namespace MassTransit +{ + using SqlTransport.Topology; + + + public interface ISqlConsumeTopology : + IConsumeTopology + { + new ISqlMessageConsumeTopology GetMessageTopology() + where T : class; + + /// + /// Apply the entire topology to the builder + /// + /// + void Apply(IReceiveEndpointBrokerTopologyBuilder builder); + } +} diff --git a/src/MassTransit/SqlTransport/Topology/ISqlMessageConsumeTopology.cs b/src/MassTransit/SqlTransport/Topology/ISqlMessageConsumeTopology.cs new file mode 100644 index 00000000000..305de6e529d --- /dev/null +++ b/src/MassTransit/SqlTransport/Topology/ISqlMessageConsumeTopology.cs @@ -0,0 +1,8 @@ +namespace MassTransit +{ + public interface ISqlMessageConsumeTopology : + IMessageConsumeTopology + where TMessage : class + { + } +} diff --git a/src/MassTransit/SqlTransport/Topology/ISqlMessagePublishTopology.cs b/src/MassTransit/SqlTransport/Topology/ISqlMessagePublishTopology.cs new file mode 100644 index 00000000000..41b2c87a871 --- /dev/null +++ b/src/MassTransit/SqlTransport/Topology/ISqlMessagePublishTopology.cs @@ -0,0 +1,29 @@ +namespace MassTransit +{ + using System; + using SqlTransport; + using SqlTransport.Topology; + + + public interface ISqlMessagePublishTopology : + IMessagePublishTopology, + ISqlMessagePublishTopology + where TMessage : class + { + Topic Topic { get; } + + SendSettings GetSendSettings(Uri hostAddress); + + BrokerTopology GetBrokerTopology(); + } + + + public interface ISqlMessagePublishTopology + { + /// + /// Apply the message topology to the builder, including any implemented types + /// + /// The topology builder + void Apply(IPublishEndpointBrokerTopologyBuilder builder); + } +} diff --git a/src/MassTransit/SqlTransport/Topology/ISqlMessageSendTopology.cs b/src/MassTransit/SqlTransport/Topology/ISqlMessageSendTopology.cs new file mode 100644 index 00000000000..7af1c07c6fc --- /dev/null +++ b/src/MassTransit/SqlTransport/Topology/ISqlMessageSendTopology.cs @@ -0,0 +1,8 @@ +namespace MassTransit +{ + public interface ISqlMessageSendTopology : + IMessageSendTopology + where TMessage : class + { + } +} diff --git a/src/MassTransit/SqlTransport/Topology/ISqlPublishTopology.cs b/src/MassTransit/SqlTransport/Topology/ISqlPublishTopology.cs new file mode 100644 index 00000000000..3101848067d --- /dev/null +++ b/src/MassTransit/SqlTransport/Topology/ISqlPublishTopology.cs @@ -0,0 +1,14 @@ +namespace MassTransit +{ + using SqlTransport.Topology; + + + public interface ISqlPublishTopology : + IPublishTopology + { + new ISqlMessagePublishTopology GetMessageTopology() + where T : class; + + BrokerTopology GetPublishBrokerTopology(); + } +} diff --git a/src/MassTransit/SqlTransport/Topology/ISqlSendTopology.cs b/src/MassTransit/SqlTransport/Topology/ISqlSendTopology.cs new file mode 100644 index 00000000000..bb66bdd83c1 --- /dev/null +++ b/src/MassTransit/SqlTransport/Topology/ISqlSendTopology.cs @@ -0,0 +1,33 @@ +namespace MassTransit +{ + using SqlTransport; + + + public interface ISqlSendTopology : + ISendTopology + { + new ISqlMessageSendTopologyConfigurator GetMessageTopology() + where T : class; + + /// + /// Return the send settings for the specified + /// + /// + /// + SendSettings GetSendSettings(SqlEndpointAddress address); + + /// + /// Return the error settings for the queue + /// + /// + /// + SendSettings GetErrorSettings(ReceiveSettings settings); + + /// + /// Return the dead letter settings for the queue + /// + /// + /// + SendSettings GetDeadLetterSettings(ReceiveSettings settings); + } +} diff --git a/src/MassTransit/SupervisorExtensions.cs b/src/MassTransit/SupervisorExtensions.cs index c0c59b38374..33616ad6c12 100644 --- a/src/MassTransit/SupervisorExtensions.cs +++ b/src/MassTransit/SupervisorExtensions.cs @@ -148,12 +148,12 @@ async Task HandleSupervisorTask() } } - #pragma warning disable 4014 + #pragma warning disable 4014 // ReSharper disable once MethodSupportsCancellation HandleSupervisorTask().ContinueWith(_ => { }); - #pragma warning restore 4014 + #pragma warning restore 4014 return await asyncContext.Context.ConfigureAwait(false); } diff --git a/src/MassTransit/Testing/AsyncTestHarness.cs b/src/MassTransit/Testing/AsyncTestHarness.cs index ef55feff9fc..881f7c306b0 100644 --- a/src/MassTransit/Testing/AsyncTestHarness.cs +++ b/src/MassTransit/Testing/AsyncTestHarness.cs @@ -19,7 +19,7 @@ public abstract class AsyncTestHarness : protected AsyncTestHarness() { TestTimeout = Debugger.IsAttached ? TimeSpan.FromMinutes(50) : TimeSpan.FromSeconds(30); - TestInactivityTimeout = TimeSpan.FromSeconds(6); + TestInactivityTimeout = Debugger.IsAttached ? TimeSpan.FromMinutes(30) : TimeSpan.FromSeconds(6); _inactivityObserver = new Lazy(() => new AsyncInactivityObserver(TestInactivityTimeout, TestCancellationToken)); } diff --git a/src/MassTransit/Testing/ExtensionMethodsForBuses.cs b/src/MassTransit/Testing/ExtensionMethodsForBuses.cs index 743ba6317fe..9d8a318bd0e 100644 --- a/src/MassTransit/Testing/ExtensionMethodsForBuses.cs +++ b/src/MassTransit/Testing/ExtensionMethodsForBuses.cs @@ -11,6 +11,7 @@ public static class ExtensionMethodsForBuses /// /// /// + [Obsolete("Use the InactivityTask on the test harness instead")] public static IBusActivityMonitor CreateBusActivityMonitor(this IBus bus) { var sendIndicator = new BusActivitySendIndicator(); @@ -27,6 +28,7 @@ public static IBusActivityMonitor CreateBusActivityMonitor(this IBus bus) /// /// minimum time to wait to presume bus inactivity /// + [Obsolete("Use the InactivityTask on the test harness instead")] public static IBusActivityMonitor CreateBusActivityMonitor(this IBus bus, TimeSpan inactivityTimeout) { var sendIndicator = new BusActivitySendIndicator(inactivityTimeout); diff --git a/src/MassTransit/Testing/ExtensionMethodsForSagas.cs b/src/MassTransit/Testing/ExtensionMethodsForSagas.cs index c5b79e18129..ae5e8b63a9d 100644 --- a/src/MassTransit/Testing/ExtensionMethodsForSagas.cs +++ b/src/MassTransit/Testing/ExtensionMethodsForSagas.cs @@ -23,7 +23,7 @@ public static class ExtensionMethodsForSagas return TaskUtil.Faulted(new ArgumentException("Does not support IQuerySagaRepository", nameof(repository))); } - static async Task ShouldContainSaga(this ILoadSagaRepository repository, Guid correlationId, TimeSpan timeout) + public static async Task ShouldContainSaga(this ILoadSagaRepository repository, Guid correlationId, TimeSpan timeout) where TSaga : class, ISaga { var giveUpAt = DateTime.Now + timeout; @@ -40,7 +40,7 @@ public static class ExtensionMethodsForSagas return default; } - static async Task ShouldContainSaga(this IQuerySagaRepository repository, Guid correlationId, TimeSpan timeout) + public static async Task ShouldContainSaga(this IQuerySagaRepository repository, Guid correlationId, TimeSpan timeout) where TSaga : class, ISaga { var giveUpAt = DateTime.Now + timeout; @@ -69,7 +69,7 @@ public static class ExtensionMethodsForSagas return TaskUtil.Faulted(new ArgumentException("Does not support IQuerySagaRepository", nameof(repository))); } - static async Task ShouldContainSaga(this ILoadSagaRepository repository, Guid correlationId, Func condition, + public static async Task ShouldContainSaga(this ILoadSagaRepository repository, Guid correlationId, Func condition, TimeSpan timeout) where TSaga : class, ISaga { @@ -99,7 +99,7 @@ public static class ExtensionMethodsForSagas return TaskUtil.Faulted(new ArgumentException("Does not support IQuerySagaRepository", nameof(repository))); } - static async Task ShouldNotContainSaga(this ILoadSagaRepository repository, Guid correlationId, TimeSpan timeout) + public static async Task ShouldNotContainSaga(this ILoadSagaRepository repository, Guid correlationId, TimeSpan timeout) where TSaga : class, ISaga { var giveUpAt = DateTime.Now + timeout; @@ -119,7 +119,7 @@ public static class ExtensionMethodsForSagas return instance.CorrelationId; } - static async Task ShouldNotContainSaga(this IQuerySagaRepository repository, Guid correlationId, TimeSpan timeout) + public static async Task ShouldNotContainSaga(this IQuerySagaRepository repository, Guid correlationId, TimeSpan timeout) where TSaga : class, ISaga { var giveUpAt = DateTime.Now + timeout; @@ -149,7 +149,7 @@ public static class ExtensionMethodsForSagas return TaskUtil.Faulted(new ArgumentException("Does not support IQuerySagaRepository", nameof(repository))); } - static async Task ShouldContainSaga(this IQuerySagaRepository repository, Expression> filter, + public static async Task ShouldContainSaga(this IQuerySagaRepository repository, Expression> filter, TimeSpan timeout) where TSaga : class, ISaga { diff --git a/src/MassTransit/Testing/ITestHarness.cs b/src/MassTransit/Testing/ITestHarness.cs index a05a39b3017..0d77a5a1265 100644 --- a/src/MassTransit/Testing/ITestHarness.cs +++ b/src/MassTransit/Testing/ITestHarness.cs @@ -1,5 +1,6 @@ namespace MassTransit.Testing { + using System; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; @@ -11,6 +12,8 @@ public interface ITestHarness : IServiceScope Scope { get; } + IServiceProvider Provider { get; } + IEndpointNameFormatter EndpointNameFormatter { get; } /// @@ -60,6 +63,30 @@ IRequestClient GetRequestClient() Task GetConsumerEndpoint() where T : class, IConsumer; + /// + /// Use the endpoint name formatter to get the send endpoint for the message handler by message type + /// + /// The message type + /// + Task GetHandlerEndpoint() + where T : class; + + /// + /// Returns the endpoint address for the specified consumer type + /// + /// + /// + Uri GetConsumerAddress() + where T : class, IConsumer; + + /// + /// Returns the endpoint address for the specified handler type + /// + /// + /// + Uri GetHandlerAddress() + where T : class; + /// /// Use the endpoint name formatter to get the send endpoint for the saga type /// @@ -68,6 +95,14 @@ Task GetConsumerEndpoint() Task GetSagaEndpoint() where T : class, ISaga; + /// + /// Returns the endpoint address for the saga + /// + /// + /// + Uri GetSagaAddress() + where T : class, ISaga; + /// /// Use the endpoint name formatter to get the execute send endpoint for the activity type /// @@ -78,6 +113,16 @@ Task GetExecuteActivityEndpoint() where T : class, IExecuteActivity where TArguments : class; + /// + /// Returns the endpoint address for the execute activity + /// + /// + /// + /// + Uri GetExecuteActivityAddress() + where T : class, IExecuteActivity + where TArguments : class; + Task Start(); } } diff --git a/src/MassTransit/Testing/Implementations/AsyncElementList.cs b/src/MassTransit/Testing/Implementations/AsyncElementList.cs index 5f9002c5fc8..8337ef742a1 100644 --- a/src/MassTransit/Testing/Implementations/AsyncElementList.cs +++ b/src/MassTransit/Testing/Implementations/AsyncElementList.cs @@ -2,7 +2,6 @@ namespace MassTransit.Testing.Implementations { using System; using System.Collections.Generic; - using System.Linq; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Channels; @@ -15,9 +14,10 @@ public abstract class AsyncElementList : where TElement : class, IAsyncListElement { readonly Connectable> _channels; - readonly IList _messages; + readonly IDictionary _messageLookup; + readonly List _messages; + readonly CancellationToken _testCompleted; readonly TimeSpan _timeout; - CancellationToken _testCompleted; protected AsyncElementList(TimeSpan timeout, CancellationToken testCompleted = default) { @@ -25,6 +25,7 @@ protected AsyncElementList(TimeSpan timeout, CancellationToken testCompleted = d _testCompleted = testCompleted; _messages = new List(); + _messageLookup = new Dictionary(); _channels = new Connectable>(); } @@ -44,15 +45,20 @@ public async IAsyncEnumerable SelectAsync(FilterDelegate fil try { + var index = 0; + TElement[] messages; lock (_messages) messages = _messages.ToArray(); - foreach (var entry in messages) + for (; index < messages.Length; index++) { - if (filter(entry) && !returned.Contains(entry.ElementId.Value)) + var entry = messages[index]; + var elementId = entry.ElementId.Value; + + if (filter(entry) && !returned.Contains(elementId)) { - returned.Add(entry.ElementId.Value); + returned.Add(elementId); yield return entry; } } @@ -81,11 +87,14 @@ public async IAsyncEnumerable SelectAsync(FilterDelegate fil lock (_messages) messages = _messages.ToArray(); - foreach (var entry in messages) + for (; index < messages.Length; index++) { - if (filter(entry) && !returned.Contains(entry.ElementId.Value)) + var entry = messages[index]; + var elementId = entry.ElementId.Value; + + if (filter(entry) && !returned.Contains(elementId)) { - returned.Add(entry.ElementId.Value); + returned.Add(elementId); yield return entry; } } @@ -94,7 +103,7 @@ public async IAsyncEnumerable SelectAsync(FilterDelegate fil finally { handle.Disconnect(); - channel.Writer.Complete(); + channel.Writer.TryComplete(); } } @@ -180,10 +189,13 @@ protected void Add(TElement context) lock (_messages) { - if (_messages.Any(x => x.ElementId == context.ElementId)) + var elementId = context.ElementId.Value; + + if (_messageLookup.ContainsKey(elementId)) return; _messages.Add(context); + _messageLookup.Add(elementId, context); Monitor.PulseAll(_messages); } diff --git a/src/MassTransit/Testing/Implementations/BaseSagaTestHarness.cs b/src/MassTransit/Testing/Implementations/BaseSagaTestHarness.cs index 6b1a1c4f28b..77430d62eb2 100644 --- a/src/MassTransit/Testing/Implementations/BaseSagaTestHarness.cs +++ b/src/MassTransit/Testing/Implementations/BaseSagaTestHarness.cs @@ -11,9 +11,10 @@ namespace MassTransit.Testing.Implementations public abstract class BaseSagaTestHarness where TSaga : class, ISaga { - protected BaseSagaTestHarness(ISagaRepository repository, TimeSpan testTimeout) + protected BaseSagaTestHarness(IQuerySagaRepository querySagaRepository, ILoadSagaRepository loadSagaRepository, TimeSpan testTimeout) { - QuerySagaRepository = repository as IQuerySagaRepository; + QuerySagaRepository = querySagaRepository; + LoadSagaRepository = loadSagaRepository; TestTimeout = testTimeout; } @@ -21,6 +22,7 @@ protected BaseSagaTestHarness(ISagaRepository repository, TimeSpan testTi protected TimeSpan TestTimeout { get; } protected IQuerySagaRepository QuerySagaRepository { get; } + protected ILoadSagaRepository LoadSagaRepository { get; } /// /// Waits until a saga exists with the specified correlationId @@ -30,18 +32,16 @@ protected BaseSagaTestHarness(ISagaRepository repository, TimeSpan testTi /// public async Task Exists(Guid correlationId, TimeSpan? timeout = default) { - if (QuerySagaRepository == null) - throw new InvalidOperationException("The repository does not support Query operations"); + if (LoadSagaRepository == null) + throw new InvalidOperationException("The repository does not support Load operations"); var giveUpAt = DateTime.Now + (timeout ?? TestTimeout); - var query = new SagaQuery(x => x.CorrelationId == correlationId); - while (DateTime.Now < giveUpAt) { - var saga = (await QuerySagaRepository.Find(query).ConfigureAwait(false)).FirstOrDefault(); - if (saga != Guid.Empty) - return saga; + var saga = await LoadSagaRepository.Load(correlationId).ConfigureAwait(false); + if (saga != null) + return saga.CorrelationId; await Task.Delay(10).ConfigureAwait(false); } @@ -84,24 +84,22 @@ public async Task> Match(Expression> filter, TimeS /// public async Task NotExists(Guid correlationId, TimeSpan? timeout = default) { - if (QuerySagaRepository == null) - throw new InvalidOperationException("The repository does not support Query operations"); + if (LoadSagaRepository == null) + throw new InvalidOperationException("The repository does not support Load operations"); var giveUpAt = DateTime.Now + (timeout ?? TestTimeout); - var query = new SagaQuery(x => x.CorrelationId == correlationId); - - Guid? saga = default; + TSaga saga = default; while (DateTime.Now < giveUpAt) { - saga = (await QuerySagaRepository.Find(query).ConfigureAwait(false)).FirstOrDefault(); - if (saga == Guid.Empty) + saga = await LoadSagaRepository.Load(correlationId).ConfigureAwait(false); + if (saga == null) return default; await Task.Delay(10).ConfigureAwait(false); } - return saga; + return saga.CorrelationId; } } } diff --git a/src/MassTransit/Testing/Implementations/InMemoryTestHarnessBusInstance.cs b/src/MassTransit/Testing/Implementations/InMemoryTestHarnessBusInstance.cs index a4c78db8107..41e76a9ba67 100644 --- a/src/MassTransit/Testing/Implementations/InMemoryTestHarnessBusInstance.cs +++ b/src/MassTransit/Testing/Implementations/InMemoryTestHarnessBusInstance.cs @@ -41,7 +41,8 @@ public HostReceiveEndpointHandle ConnectReceiveEndpoint(IEndpointDefinition defi { return BusControl.ConnectReceiveEndpoint(definition, endpointNameFormatter, configurator => { - _busRegistrationContext.GetConfigureReceiveEndpoints().Configure(definition.GetEndpointName(endpointNameFormatter), configurator); + _busRegistrationContext.GetConfigureReceiveEndpoints() + .Configure(definition.GetEndpointName(endpointNameFormatter), configurator); configure?.Invoke(_busRegistrationContext, configurator); }); diff --git a/src/MassTransit/Testing/Implementations/RegistrationSagaStateMachineTestHarness.cs b/src/MassTransit/Testing/Implementations/RegistrationSagaStateMachineTestHarness.cs index c843a8187ee..34bb0448b38 100644 --- a/src/MassTransit/Testing/Implementations/RegistrationSagaStateMachineTestHarness.cs +++ b/src/MassTransit/Testing/Implementations/RegistrationSagaStateMachineTestHarness.cs @@ -11,15 +11,15 @@ namespace MassTransit.Testing.Implementations public class RegistrationSagaStateMachineTestHarness : BaseSagaTestHarness, ISagaStateMachineTestHarness, - #pragma warning disable CS0618 + #pragma warning disable CS0618 IStateMachineSagaTestHarness -#pragma warning restore CS0618 + #pragma warning restore CS0618 where TInstance : class, SagaStateMachineInstance where TStateMachine : SagaStateMachine { - public RegistrationSagaStateMachineTestHarness(ISagaRepositoryDecoratorRegistration registration, ISagaRepository repository, - TStateMachine stateMachine) - : base(repository, registration.TestTimeout) + public RegistrationSagaStateMachineTestHarness(ISagaRepositoryDecoratorRegistration registration, + IQuerySagaRepository querySagaRepository, ILoadSagaRepository loadSagaRepository, TStateMachine stateMachine) + : base(querySagaRepository, loadSagaRepository, registration.TestTimeout) { StateMachine = stateMachine; Consumed = registration.Consumed; diff --git a/src/MassTransit/Testing/Implementations/RegistrationSagaTestHarness.cs b/src/MassTransit/Testing/Implementations/RegistrationSagaTestHarness.cs index fd2af393f93..395ea1a1eb5 100644 --- a/src/MassTransit/Testing/Implementations/RegistrationSagaTestHarness.cs +++ b/src/MassTransit/Testing/Implementations/RegistrationSagaTestHarness.cs @@ -8,8 +8,9 @@ public class RegistrationSagaTestHarness : ISagaTestHarness where TSaga : class, ISaga { - public RegistrationSagaTestHarness(ISagaRepositoryDecoratorRegistration registration, ISagaRepository repository) - : base(repository, registration.TestTimeout) + public RegistrationSagaTestHarness(ISagaRepositoryDecoratorRegistration registration, ISagaRepository repository, + ILoadSagaRepository loadRepository, IQuerySagaRepository queryRepository) + : base(queryRepository, loadRepository, registration.TestTimeout) { Consumed = registration.Consumed; Created = registration.Created; diff --git a/src/MassTransit/Testing/Implementations/StateMachineSagaTestHarness.cs b/src/MassTransit/Testing/Implementations/StateMachineSagaTestHarness.cs index 5a64a58e3ee..8a1f5903208 100644 --- a/src/MassTransit/Testing/Implementations/StateMachineSagaTestHarness.cs +++ b/src/MassTransit/Testing/Implementations/StateMachineSagaTestHarness.cs @@ -13,8 +13,10 @@ public class StateMachineSagaTestHarness : where TInstance : class, SagaStateMachineInstance where TStateMachine : SagaStateMachine { - public StateMachineSagaTestHarness(BusTestHarness testHarness, ISagaRepository repository, TStateMachine stateMachine, string queueName) - : base(testHarness, repository, queueName) + public StateMachineSagaTestHarness(BusTestHarness testHarness, ISagaRepository repository, + IQuerySagaRepository querySagaRepository, ILoadSagaRepository loadSagaRepository, TStateMachine stateMachine, + string queueName) + : base(testHarness, repository, querySagaRepository, loadSagaRepository, queueName) { StateMachine = stateMachine; } diff --git a/src/MassTransit/Testing/InMemoryTestHarness.cs b/src/MassTransit/Testing/InMemoryTestHarness.cs index 1b765edddc0..8aebf70b422 100644 --- a/src/MassTransit/Testing/InMemoryTestHarness.cs +++ b/src/MassTransit/Testing/InMemoryTestHarness.cs @@ -27,7 +27,7 @@ public InMemoryTestHarness(string virtualHost, IEnumerable _configures; + readonly List _configures; readonly ReceivedMessageList _received; readonly CancellationToken _testCompleted; @@ -47,7 +47,7 @@ public ReceivedMessageList Fault() public ConnectHandle Connect(IConsumePipeConnector bus) { - var handles = new List(); + var handles = new List(_configures.Count); try { foreach (var configure in _configures) diff --git a/src/MassTransit/Testing/PublishedMessage.cs b/src/MassTransit/Testing/PublishedMessage.cs index 40e28977d56..5f1da9f625f 100644 --- a/src/MassTransit/Testing/PublishedMessage.cs +++ b/src/MassTransit/Testing/PublishedMessage.cs @@ -14,11 +14,13 @@ public PublishedMessage(PublishContext context, Exception exception = null) _context = context; Exception = exception; + ElementId = _context.MessageId; + StartTime = context.SentTime ?? DateTime.UtcNow; ElapsedTime = DateTime.UtcNow - StartTime; } - Guid? IAsyncListElement.ElementId => _context.MessageId; + public Guid? ElementId { get; } SendContext IPublishedMessage.Context => _context; public DateTime StartTime { get; } public TimeSpan ElapsedTime { get; } diff --git a/src/MassTransit/Testing/ReceivedMessage.cs b/src/MassTransit/Testing/ReceivedMessage.cs index c0acf36b96f..96173e3f5b5 100644 --- a/src/MassTransit/Testing/ReceivedMessage.cs +++ b/src/MassTransit/Testing/ReceivedMessage.cs @@ -15,13 +15,15 @@ public ReceivedMessage(ConsumeContext context, Exception exception = null) _context = context; _exception = exception; + ElementId = _context.MessageId; + ElapsedTime = context.ReceiveContext.ElapsedTime; StartTime = DateTime.UtcNow - ElapsedTime; if (StartTime < context.SentTime) StartTime = context.SentTime.Value; } - Guid? IAsyncListElement.ElementId => _context.MessageId; + public Guid? ElementId { get; } ConsumeContext IReceivedMessage.Context => _context; public DateTime StartTime { get; } public TimeSpan ElapsedTime { get; } diff --git a/src/MassTransit/Testing/SagaTestHarness.cs b/src/MassTransit/Testing/SagaTestHarness.cs index 07f5f2cf53b..9e4dfbd122b 100644 --- a/src/MassTransit/Testing/SagaTestHarness.cs +++ b/src/MassTransit/Testing/SagaTestHarness.cs @@ -12,8 +12,9 @@ public class SagaTestHarness : readonly SagaList _created; readonly SagaList _sagas; - public SagaTestHarness(BusTestHarness testHarness, ISagaRepository repository, string queueName) - : base(repository, testHarness.TestTimeout) + public SagaTestHarness(BusTestHarness testHarness, ISagaRepository repository, IQuerySagaRepository querySagaRepository, + ILoadSagaRepository loadSagaRepository, string queueName) + : base(querySagaRepository, loadSagaRepository, testHarness.TestTimeout) { _consumed = new ReceivedMessageList(testHarness.TestTimeout, testHarness.InactivityToken); _created = new SagaList(testHarness.TestTimeout, testHarness.InactivityToken); diff --git a/src/MassTransit/Testing/SagaTestHarnessExtensions.cs b/src/MassTransit/Testing/SagaTestHarnessExtensions.cs index 25d3bd573fb..b2bdc3b7951 100644 --- a/src/MassTransit/Testing/SagaTestHarnessExtensions.cs +++ b/src/MassTransit/Testing/SagaTestHarnessExtensions.cs @@ -10,7 +10,7 @@ public static SagaTestHarness Saga(this BusTestHarness harness, string que { var repository = new InMemorySagaRepository(); - return new SagaTestHarness(harness, repository, queueName); + return new SagaTestHarness(harness, repository, repository, repository, queueName); } public static SagaTestHarness Saga(this BusTestHarness harness, ISagaRepository repository, string queueName = null) @@ -19,7 +19,10 @@ public static SagaTestHarness Saga(this BusTestHarness harness, ISagaRepos if (repository == null) throw new ArgumentNullException(nameof(repository)); - return new SagaTestHarness(harness, repository, queueName); + var querySagaRepository = repository as IQuerySagaRepository; + var loadSagaRepository = repository as ILoadSagaRepository; + + return new SagaTestHarness(harness, repository, querySagaRepository, loadSagaRepository, queueName); } } } diff --git a/src/MassTransit/Testing/SentMessage.cs b/src/MassTransit/Testing/SentMessage.cs index e034ccefb5f..bf5b6bb8b2f 100644 --- a/src/MassTransit/Testing/SentMessage.cs +++ b/src/MassTransit/Testing/SentMessage.cs @@ -15,11 +15,13 @@ public SentMessage(SendContext context, Exception exception = null) _context = context; _exception = exception; + ElementId = _context.MessageId; + StartTime = context.SentTime ?? DateTime.UtcNow; ElapsedTime = DateTime.UtcNow - StartTime; } - Guid? IAsyncListElement.ElementId => _context.MessageId; + public Guid? ElementId { get; } SendContext ISentMessage.Context => _context; public DateTime StartTime { get; } public TimeSpan ElapsedTime { get; } diff --git a/src/MassTransit/Testing/StateMachineSagaTestingExtensions.cs b/src/MassTransit/Testing/StateMachineSagaTestingExtensions.cs index 8fbc1aada68..fb272b95a2e 100644 --- a/src/MassTransit/Testing/StateMachineSagaTestingExtensions.cs +++ b/src/MassTransit/Testing/StateMachineSagaTestingExtensions.cs @@ -19,7 +19,7 @@ public static ISagaStateMachineTestHarness StateMachin var repository = new InMemorySagaRepository(); - return new StateMachineSagaTestHarness(harness, repository, stateMachine, queueName); + return new StateMachineSagaTestHarness(harness, repository, repository, repository, stateMachine, queueName); } public static ISagaStateMachineTestHarness StateMachineSaga(this BusTestHarness harness, @@ -33,7 +33,10 @@ public static ISagaStateMachineTestHarness StateMachin if (repository == null) throw new ArgumentNullException(nameof(repository)); - return new StateMachineSagaTestHarness(harness, repository, stateMachine, queueName); + var querySagaRepository = repository as IQuerySagaRepository; + var loadSagaRepository = repository as ILoadSagaRepository; + return new StateMachineSagaTestHarness(harness, repository, querySagaRepository, loadSagaRepository, stateMachine, + queueName); } public static TInstance ContainsInState(this ISagaList sagas, Guid correlationId, TStateMachine machine, diff --git a/src/MassTransit/Testing/TelemetryMonitorExtensions.cs b/src/MassTransit/Testing/TelemetryMonitorExtensions.cs new file mode 100644 index 00000000000..87f3864b105 --- /dev/null +++ b/src/MassTransit/Testing/TelemetryMonitorExtensions.cs @@ -0,0 +1,152 @@ +#nullable enable +namespace MassTransit.Testing; + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + + +public static class TelemetryMonitorExtensions +{ + /// + /// Wraps the call on the and waits for the published message to be consumed, along with + /// all subsequently produced messages until the specified timeout. + /// + /// + /// + /// + /// + public static async Task Wait(this IPublishEndpoint publishEndpoint, Func? callback, TimeSpan? timeout = null, + TimeSpan? idleTimeout = null) + { + var methodName = GetTestMethodInfo(); + + await using var trackedActivity = new TrackedActivity(methodName, timeout, idleTimeout); + + if (callback != null) + await callback(publishEndpoint).ConfigureAwait(false); + } + + /// + /// Wraps the call on the and waits for the sent message to be consumed, along with + /// all subsequently produced messages until the specified timeout. + /// + /// + /// + /// + /// + public static async Task Wait(this ISendEndpoint sendEndpoint, Func? callback, TimeSpan? timeout = null, + TimeSpan? idleTimeout = null) + { + var methodName = GetTestMethodInfo(); + + await using var trackedActivity = new TrackedActivity(methodName, timeout, idleTimeout); + + if (callback != null) + await callback(sendEndpoint).ConfigureAwait(false); + } + + /// + /// Wraps the call on the and waits for the request to be completed, along with + /// all subsequently produced messages until the specified timeout. + /// + /// + /// + /// + /// + public static async Task> Wait(this IRequestClient client, Func, Task>> callback, + TimeSpan? timeout = null, TimeSpan? idleTimeout = null) + where T : class + where T1 : class + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + var methodName = GetTestMethodInfo(); + + await using var trackedActivity = new TrackedActivity(methodName, timeout, idleTimeout); + + Response result = await callback(client).ConfigureAwait(false); + + return result; + } + + /// + /// Wraps the call on the and waits for the request to be completed, along with + /// all subsequently produced messages until the specified timeout. + /// + /// + /// + /// + /// + public static async Task> Wait(this IRequestClient client, Func, Task>> callback, + TimeSpan? timeout = null, TimeSpan? idleTimeout = null) + where T : class + where T1 : class + where T2 : class + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + var methodName = GetTestMethodInfo(); + + await using var trackedActivity = new TrackedActivity(methodName, timeout, idleTimeout); + + Response result = await callback(client).ConfigureAwait(false); + + return result; + } + + /// + /// Wraps the call on the and waits for the request to be completed, along with + /// all subsequently produced messages until the specified timeout. + /// + /// + /// + /// + /// + public static async Task> Wait(this IRequestClient client, + Func, Task>> callback, TimeSpan? timeout = null, TimeSpan? idleTimeout = null) + where T : class + where T1 : class + where T2 : class + where T3 : class + { + if (callback == null) + throw new ArgumentNullException(nameof(callback)); + + var methodName = GetTestMethodInfo(); + + await using var trackedActivity = new TrackedActivity(methodName, timeout, idleTimeout); + + Response result = await callback(client).ConfigureAwait(false); + + return result; + } + + static string? GetTestMethodInfo() + { + var stackTrace = new StackTrace(2); + var frameCount = stackTrace.FrameCount; + for (var i = 0; i < frameCount; i++) + { + var frame = stackTrace.GetFrame(i); + if (frame == null) + continue; + + var method = frame.GetMethod(); + if (method == null) + continue; + + if (method.GetCustomAttributes(false).Any(x => + { + var name = x.GetType().Name; + return name.ToLower().Contains("test") || name.ToLower().Contains("fact"); + })) + return method.Name; + } + + return null; + } +} diff --git a/src/MassTransit/Testing/TestingServiceProviderExtensions.cs b/src/MassTransit/Testing/TestingServiceProviderExtensions.cs index 8764aa1c1a9..cb8a3c73f18 100644 --- a/src/MassTransit/Testing/TestingServiceProviderExtensions.cs +++ b/src/MassTransit/Testing/TestingServiceProviderExtensions.cs @@ -1,3 +1,4 @@ +#nullable enable namespace MassTransit.Testing { using System; @@ -6,6 +7,7 @@ namespace MassTransit.Testing using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; + using Saga; public static class TestingServiceProviderExtensions @@ -15,6 +17,15 @@ public static ITestHarness GetTestHarness(this IServiceProvider provider) return provider.GetRequiredService(); } + public static async Task StartTestHarness(this IServiceProvider provider) + { + var testHarness = provider.GetRequiredService(); + + await testHarness.Start().ConfigureAwait(false); + + return testHarness; + } + public static async Task>> ConnectPublishHandler(this ITestHarness harness, Func, bool> filter) where T : class { @@ -39,12 +50,54 @@ public static void AddTaskCompletionSource(this IBusRegistrationConfigurator configurator.AddSingleton(provider => provider.GetRequiredService().GetTask()); } + /// + /// Stop the test harness, which stops the bus and all hosted services that were started. + /// + /// + /// public static async Task Stop(this ITestHarness harness, CancellationToken cancellationToken = default) { - IHostedService[] services = harness.Scope.ServiceProvider.GetServices().ToArray(); + IHostedService[] services = harness.Provider.GetServices().ToArray(); foreach (var service in services.Reverse()) await service.StopAsync(cancellationToken).ConfigureAwait(false); } + + public static async Task RestartHostedServices(this ITestHarness harness, CancellationToken cancellationToken = default) + { + IHostedService[] services = harness.Provider.GetServices().ToArray(); + + foreach (var service in services.Reverse()) + await service.StopAsync(cancellationToken).ConfigureAwait(false); + + foreach (var service in services) + await service.StartAsync(cancellationToken).ConfigureAwait(false); + } + + /// + /// Adds a saga instance to the in-memory saga repository. + /// + /// The test harness + /// The correlationId for the newly created saga instance + /// Callback to set any additional properties on the saga instance + /// The saga type + public static void AddSagaInstance(this ITestHarness harness, Guid? correlationId = default, Action? callback = null) + where T : class, ISaga, new() + { + var dictionary = harness.Provider.GetService>(); + if (dictionary == null) + throw new ArgumentException("In-memory saga repository not found", nameof(T)); + + if (correlationId.HasValue && dictionary[correlationId.Value] != null) + { + throw new ArgumentException($"An existing saga instance with the specified correlationId already exists: {correlationId}", + nameof(correlationId)); + } + + var instance = new T { CorrelationId = correlationId ?? NewId.NextGuid() }; + callback?.Invoke(instance); + + dictionary.Add(new SagaInstance(instance)); + } } } diff --git a/src/MassTransit/Testing/TrackedActivity.cs b/src/MassTransit/Testing/TrackedActivity.cs new file mode 100644 index 00000000000..b4f2dc9524f --- /dev/null +++ b/src/MassTransit/Testing/TrackedActivity.cs @@ -0,0 +1,142 @@ +#nullable enable +namespace MassTransit.Testing; + +using System; +using System.Collections.Concurrent; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using Util; + + +class TrackedActivity : + IAsyncDisposable +{ + static readonly ActivitySource _source = new ActivitySource("MassTransit.Testing.Monitor"); + + readonly TaskCompletionSource _completed; + readonly TimeSpan _idleTimeout; + readonly ActivityListener _listener; + readonly Activity? _testActivity; + readonly TimeSpan _timeout; + readonly RollingTimer _timer; + readonly TraceInfo _traceInfo; + + public TrackedActivity(string? methodName, TimeSpan? timeout, TimeSpan? idleTimeout) + { + _idleTimeout = idleTimeout ?? TimeSpan.FromSeconds(0.05); + _timeout = timeout ?? TimeSpan.FromSeconds(30); + + Activity.DefaultIdFormat = ActivityIdFormat.W3C; + Activity.ForceDefaultIdFormat = true; + + _completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + + _listener = new ActivityListener + { + ShouldListenTo = _ => true, + Sample = Sample, + ActivityStarted = ActivityStarted, + ActivityStopped = ActivityStopped + }; + + ActivitySource.AddActivityListener(_listener); + + _testActivity = _source.StartActivity($"{methodName ?? "test"} process"); + _traceInfo = new TraceInfo { StartTime = _testActivity?.StartTimeUtc ?? DateTimeOffset.UtcNow }; + _timer = new RollingTimer(OnTimeout, _timeout, this); + _timer.Start(); + } + + public async ValueTask DisposeAsync() + { + await _completed.Task.ConfigureAwait(false); + + _testActivity?.Stop(); + + _testActivity?.Dispose(); + + _listener.Dispose(); + + _timer.Dispose(); + } + + void OnTimeout(object? state) + { + _completed.TrySetResult(true); + } + + static ActivitySamplingResult Sample(ref ActivityCreationOptions options) + { + return ActivitySamplingResult.AllDataAndRecorded; + } + + void ActivityStarted(Activity activity) + { + GetSpan(activity); + } + + void ActivityStopped(Activity activity) + { + var span = GetSpan(activity); + if (span != null) + span.Completed = true; + + if (_traceInfo.Spans.All(x => x.Value.Completed)) + _timer.Restart(_idleTimeout); + } + + SpanInfo? GetSpan(Activity activity) + { + var traceId = activity.RootId ?? ""; + if (traceId != _testActivity?.RootId) + return null; + + var span = _traceInfo.Spans.GetOrAdd(activity.Id ?? "", id => new SpanInfo + { + SpanId = id, + ParentId = activity.ParentId, + StartTime = activity.StartTimeUtc, + OperationName = activity.OperationName, + Activity = activity, + }); + + if (activity.Duration > TimeSpan.Zero) + { + span.Duration = activity.Duration; + + var traceDuration = activity.StartTimeUtc - _traceInfo.StartTime + activity.Duration; + + if (traceDuration > _traceInfo.Duration) + _traceInfo.Duration = traceDuration; + } + + return span; + } + + + class TraceInfo + { + public TraceInfo() + { + Spans = new ConcurrentDictionary(); + } + + public DateTimeOffset StartTime { get; set; } + public TimeSpan Duration { get; set; } + + public ConcurrentDictionary Spans { get; set; } + } + + + class SpanInfo + { + public string? SpanId { get; set; } + public DateTimeOffset StartTime { get; set; } + public TimeSpan Duration { get; set; } + public string? ParentId { get; set; } + public string? OperationName { get; set; } + public Activity? Activity { get; set; } + public bool Completed { get; set; } + } +} diff --git a/src/MassTransit/Topology/Configuration/CorrelationIdMessageSendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/CorrelationIdMessageSendTopologyConvention.cs index 09f9fd4d0d9..ccb68b84788 100644 --- a/src/MassTransit/Topology/Configuration/CorrelationIdMessageSendTopologyConvention.cs +++ b/src/MassTransit/Topology/Configuration/CorrelationIdMessageSendTopologyConvention.cs @@ -8,17 +8,17 @@ public class CorrelationIdMessageSendTopologyConvention : ICorrelationIdMessageSendTopologyConvention where TMessage : class { - readonly IList> _selectors; + readonly List> _selectors; public CorrelationIdMessageSendTopologyConvention() { - _selectors = new List> - { + _selectors = + [ new CorrelatedByCorrelationIdSelector(), new PropertyCorrelationIdSelector("CorrelationId"), new PropertyCorrelationIdSelector("EventId"), new PropertyCorrelationIdSelector("CommandId") - }; + ]; } bool IMessageSendTopologyConvention.TryGetMessageSendTopologyConvention(out IMessageSendTopologyConvention convention) @@ -47,9 +47,9 @@ public void SetCorrelationId(IMessageCorrelationId messageCorrelationI public bool TryGetMessageCorrelationId(out IMessageCorrelationId messageCorrelationId) { - foreach (ICorrelationIdSelector selector in _selectors) + for (var index = 0; index < _selectors.Count; index++) { - if (selector.TryGetSetCorrelationId(out messageCorrelationId)) + if (_selectors[index].TryGetSetCorrelationId(out messageCorrelationId)) return true; } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/IPartitionKeyMessageSendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/IPartitionKeyMessageSendTopologyConvention.cs similarity index 75% rename from src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/IPartitionKeyMessageSendTopologyConvention.cs rename to src/MassTransit/Topology/Configuration/IPartitionKeyMessageSendTopologyConvention.cs index 4d7cda26e7f..e7a3ee49501 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/IPartitionKeyMessageSendTopologyConvention.cs +++ b/src/MassTransit/Topology/Configuration/IPartitionKeyMessageSendTopologyConvention.cs @@ -1,6 +1,6 @@ -namespace MassTransit.AzureServiceBusTransport.Configuration +namespace MassTransit.Configuration { - using MassTransit.Configuration; + using Transports; public interface IPartitionKeyMessageSendTopologyConvention : diff --git a/src/MassTransit/Topology/Configuration/IPartitionKeySendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/IPartitionKeySendTopologyConvention.cs new file mode 100644 index 00000000000..7bc14d45247 --- /dev/null +++ b/src/MassTransit/Topology/Configuration/IPartitionKeySendTopologyConvention.cs @@ -0,0 +1,7 @@ +namespace MassTransit.Configuration +{ + public interface IPartitionKeySendTopologyConvention : + ISendTopologyConvention + { + } +} diff --git a/src/MassTransit/Topology/Configuration/IRoutingKeyMessageSendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/IRoutingKeyMessageSendTopologyConvention.cs new file mode 100644 index 00000000000..771d8272f7f --- /dev/null +++ b/src/MassTransit/Topology/Configuration/IRoutingKeyMessageSendTopologyConvention.cs @@ -0,0 +1,13 @@ +namespace MassTransit.Configuration +{ + using Transports; + + + public interface IRoutingKeyMessageSendTopologyConvention : + IMessageSendTopologyConvention + where TMessage : class + { + void SetFormatter(IRoutingKeyFormatter formatter); + void SetFormatter(IMessageRoutingKeyFormatter formatter); + } +} diff --git a/src/MassTransit/Topology/Configuration/IRoutingKeySendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/IRoutingKeySendTopologyConvention.cs new file mode 100644 index 00000000000..1779e808553 --- /dev/null +++ b/src/MassTransit/Topology/Configuration/IRoutingKeySendTopologyConvention.cs @@ -0,0 +1,7 @@ +namespace MassTransit.Configuration +{ + public interface IRoutingKeySendTopologyConvention : + ISendTopologyConvention + { + } +} diff --git a/src/MassTransit/Topology/Configuration/ISetSerializerMessageSendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/ISetSerializerMessageSendTopologyConvention.cs new file mode 100644 index 00000000000..8f9885b181e --- /dev/null +++ b/src/MassTransit/Topology/Configuration/ISetSerializerMessageSendTopologyConvention.cs @@ -0,0 +1,12 @@ +namespace MassTransit.Configuration +{ + using System.Net.Mime; + + + public interface ISetSerializerMessageSendTopologyConvention : + IMessageSendTopologyConvention + where TMessage : class + { + void SetSerializer(ContentType contentType); + } +} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/PartitionKeyMessageSendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/PartitionKeyMessageSendTopologyConvention.cs similarity index 93% rename from src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/PartitionKeyMessageSendTopologyConvention.cs rename to src/MassTransit/Topology/Configuration/PartitionKeyMessageSendTopologyConvention.cs index 8eeca1092be..ead54a4cb74 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/PartitionKeyMessageSendTopologyConvention.cs +++ b/src/MassTransit/Topology/Configuration/PartitionKeyMessageSendTopologyConvention.cs @@ -1,6 +1,6 @@ -namespace MassTransit.AzureServiceBusTransport.Configuration +namespace MassTransit.Configuration { - using MassTransit.Configuration; + using Transports; public class PartitionKeyMessageSendTopologyConvention : diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/PartitionKeySendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/PartitionKeySendTopologyConvention.cs similarity index 81% rename from src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/PartitionKeySendTopologyConvention.cs rename to src/MassTransit/Topology/Configuration/PartitionKeySendTopologyConvention.cs index e0e08e27290..4412585b24c 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/PartitionKeySendTopologyConvention.cs +++ b/src/MassTransit/Topology/Configuration/PartitionKeySendTopologyConvention.cs @@ -1,8 +1,5 @@ -namespace MassTransit.AzureServiceBusTransport.Configuration +namespace MassTransit.Configuration { - using MassTransit.Configuration; - - public class PartitionKeySendTopologyConvention : IPartitionKeySendTopologyConvention { @@ -10,8 +7,6 @@ public class PartitionKeySendTopologyConvention : public PartitionKeySendTopologyConvention() { - DefaultFormatter = new EmptyPartitionKeyFormatter(); - _cache = new TopologyConventionCache(typeof(IPartitionKeyMessageSendTopologyConvention<>), new Factory()); } @@ -20,8 +15,6 @@ bool IMessageSendTopologyConvention.TryGetMessageSendTopologyConvention(out I return _cache.GetOrAdd>().TryGetMessageSendTopologyConvention(out convention); } - public IPartitionKeyFormatter DefaultFormatter { get; set; } - class Factory : IConventionTypeFactory diff --git a/src/MassTransit/Topology/Configuration/PropertyCorrelationIdSelector.cs b/src/MassTransit/Topology/Configuration/PropertyCorrelationIdSelector.cs index b70ef2bcfb8..d4db1e7279e 100644 --- a/src/MassTransit/Topology/Configuration/PropertyCorrelationIdSelector.cs +++ b/src/MassTransit/Topology/Configuration/PropertyCorrelationIdSelector.cs @@ -1,6 +1,7 @@ namespace MassTransit.Configuration { using System; + using Internals; using Topology; @@ -17,16 +18,15 @@ public PropertyCorrelationIdSelector(string propertyName) public bool TryGetSetCorrelationId(out IMessageCorrelationId messageCorrelationId) { - var propertyInfo = typeof(T).GetProperty(_propertyName); - if (propertyInfo != null && propertyInfo.PropertyType == typeof(Guid)) + if (ReadPropertyCache.TryGetProperty(_propertyName, out IReadProperty property)) { - messageCorrelationId = new PropertyMessageCorrelationId(propertyInfo); + messageCorrelationId = new PropertyMessageCorrelationId(property); return true; } - if (propertyInfo != null && propertyInfo.PropertyType == typeof(Guid?)) + if (ReadPropertyCache.TryGetProperty(_propertyName, out IReadProperty nullableProperty)) { - messageCorrelationId = new NullablePropertyMessageCorrelationId(propertyInfo); + messageCorrelationId = new NullablePropertyMessageCorrelationId(nullableProperty); return true; } diff --git a/src/MassTransit/Topology/Configuration/RoutingKeyMessageSendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/RoutingKeyMessageSendTopologyConvention.cs new file mode 100644 index 00000000000..4a62f225dab --- /dev/null +++ b/src/MassTransit/Topology/Configuration/RoutingKeyMessageSendTopologyConvention.cs @@ -0,0 +1,47 @@ +namespace MassTransit.Configuration +{ + using Transports; + + + public class RoutingKeyMessageSendTopologyConvention : + IRoutingKeyMessageSendTopologyConvention + where TMessage : class + { + IMessageRoutingKeyFormatter _formatter; + + public RoutingKeyMessageSendTopologyConvention(IRoutingKeyFormatter formatter) + { + if (formatter != null) + SetFormatter(formatter); + } + + bool IMessageSendTopologyConvention.TryGetMessageSendTopology(out IMessageSendTopology messageSendTopology) + { + if (_formatter != null) + { + messageSendTopology = new SetRoutingKeyMessageSendTopology(_formatter); + return true; + } + + messageSendTopology = null; + return false; + } + + bool IMessageSendTopologyConvention.TryGetMessageSendTopologyConvention(out IMessageSendTopologyConvention convention) + { + convention = this as IMessageSendTopologyConvention; + + return convention != null; + } + + public void SetFormatter(IRoutingKeyFormatter formatter) + { + _formatter = new MessageRoutingKeyFormatter(formatter); + } + + public void SetFormatter(IMessageRoutingKeyFormatter formatter) + { + _formatter = formatter; + } + } +} diff --git a/src/MassTransit/Topology/Configuration/RoutingKeySendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/RoutingKeySendTopologyConvention.cs new file mode 100644 index 00000000000..3072f4c25bb --- /dev/null +++ b/src/MassTransit/Topology/Configuration/RoutingKeySendTopologyConvention.cs @@ -0,0 +1,29 @@ +namespace MassTransit.Configuration +{ + public class RoutingKeySendTopologyConvention : + IRoutingKeySendTopologyConvention + { + readonly ITopologyConventionCache _cache; + + public RoutingKeySendTopologyConvention() + { + _cache = new TopologyConventionCache(typeof(IRoutingKeyMessageSendTopologyConvention<>), new Factory()); + } + + public bool TryGetMessageSendTopologyConvention(out IMessageSendTopologyConvention convention) + where T : class + { + return _cache.GetOrAdd>().TryGetMessageSendTopologyConvention(out convention); + } + + + class Factory : + IConventionTypeFactory + { + IMessageSendTopologyConvention IConventionTypeFactory.Create() + { + return new RoutingKeyMessageSendTopologyConvention(null); + } + } + } +} diff --git a/src/MassTransit/Topology/Configuration/SetPartitionKeyMessageSendTopology.cs b/src/MassTransit/Topology/Configuration/SetPartitionKeyMessageSendTopology.cs new file mode 100644 index 00000000000..1d5c97ec475 --- /dev/null +++ b/src/MassTransit/Topology/Configuration/SetPartitionKeyMessageSendTopology.cs @@ -0,0 +1,26 @@ +namespace MassTransit.Configuration; + +using System; +using Middleware; +using Transports; + + +public class SetPartitionKeyMessageSendTopology : + IMessageSendTopology + where TMessage : class +{ + readonly IFilter> _filter; + + public SetPartitionKeyMessageSendTopology(IMessagePartitionKeyFormatter partitionKeyFormatter) + { + if (partitionKeyFormatter == null) + throw new ArgumentNullException(nameof(partitionKeyFormatter)); + + _filter = new SetPartitionKeyFilter(partitionKeyFormatter); + } + + public void Apply(ITopologyPipeBuilder> builder) + { + builder.AddFilter(_filter); + } +} diff --git a/src/MassTransit/Topology/Configuration/SetRoutingKeyMessageSendTopology.cs b/src/MassTransit/Topology/Configuration/SetRoutingKeyMessageSendTopology.cs new file mode 100644 index 00000000000..44cd94dd088 --- /dev/null +++ b/src/MassTransit/Topology/Configuration/SetRoutingKeyMessageSendTopology.cs @@ -0,0 +1,27 @@ +namespace MassTransit.Configuration +{ + using System; + using Middleware; + using Transports; + + + public class SetRoutingKeyMessageSendTopology : + IMessageSendTopology + where TMessage : class + { + readonly IFilter> _filter; + + public SetRoutingKeyMessageSendTopology(IMessageRoutingKeyFormatter routingKeyFormatter) + { + if (routingKeyFormatter == null) + throw new ArgumentNullException(nameof(routingKeyFormatter)); + + _filter = new SetRoutingKeyFilter(routingKeyFormatter); + } + + public void Apply(ITopologyPipeBuilder> builder) + { + builder.AddFilter(_filter); + } + } +} diff --git a/src/MassTransit/Topology/Configuration/SetSerializerMessageSendTopologyConvention.cs b/src/MassTransit/Topology/Configuration/SetSerializerMessageSendTopologyConvention.cs new file mode 100644 index 00000000000..90030b06243 --- /dev/null +++ b/src/MassTransit/Topology/Configuration/SetSerializerMessageSendTopologyConvention.cs @@ -0,0 +1,37 @@ +namespace MassTransit.Configuration +{ + using System.Net.Mime; + using Topology; + + + public class SetSerializerMessageSendTopologyConvention : + ISetSerializerMessageSendTopologyConvention + where TMessage : class + { + ContentType _contentType; + + bool IMessageSendTopologyConvention.TryGetMessageSendTopologyConvention(out IMessageSendTopologyConvention convention) + { + convention = this as IMessageSendTopologyConvention; + + return convention != null; + } + + bool IMessageSendTopologyConvention.TryGetMessageSendTopology(out IMessageSendTopology messageSendTopology) + { + if (_contentType != null) + { + messageSendTopology = new SetSerializerMessageSendTopology(_contentType); + return true; + } + + messageSendTopology = null; + return false; + } + + public void SetSerializer(ContentType contentType) + { + _contentType = contentType; + } + } +} diff --git a/src/MassTransit/Topology/ConsumeTopology.cs b/src/MassTransit/Topology/ConsumeTopology.cs index 0cd90a5909a..1de22c27b25 100644 --- a/src/MassTransit/Topology/ConsumeTopology.cs +++ b/src/MassTransit/Topology/ConsumeTopology.cs @@ -15,20 +15,23 @@ public class ConsumeTopology : IConsumeTopologyConfigurator, IConsumeTopologyConfigurationObserver { - readonly IList _conventions; - readonly object _lock = new object(); + readonly List _conventions; + readonly object _lock = new(); readonly int _maxQueueNameLength; - readonly ConcurrentDictionary _messageTypes; + readonly ConcurrentDictionary> _messageTypes; + readonly ConcurrentDictionary _messageTypeSelectorCache; readonly ConsumeTopologyConfigurationObservable _observers; protected ConsumeTopology(int maxQueueNameLength = 1024) { _maxQueueNameLength = maxQueueNameLength; - _messageTypes = new ConcurrentDictionary(); - _observers = new ConsumeTopologyConfigurationObservable(); + _messageTypes = new ConcurrentDictionary>(); + _messageTypeSelectorCache = new ConcurrentDictionary(); + + _conventions = new List(8); - _conventions = new List(); + _observers = new ConsumeTopologyConfigurationObservable(); _observers.Connect(this); } @@ -42,39 +45,20 @@ IMessageConsumeTopology IConsumeTopology.GetMessageTopology() return GetMessageTopology(); } - public virtual string CreateTemporaryQueueName(string tag) + IMessageConsumeTopologyConfigurator IConsumeTopologyConfigurator.GetMessageTopology() { - if (string.IsNullOrWhiteSpace(tag)) - tag = "endpoint"; - - var host = HostMetadataCache.Host; - - var sb = new StringBuilder(host.MachineName.Length + host.ProcessName.Length + tag.Length + 35); - - foreach (var c in host.MachineName) - { - if (char.IsLetterOrDigit(c) || c == '_') - sb.Append(c); - } - - sb.Append('_'); - foreach (var c in host.ProcessName) - { - if (char.IsLetterOrDigit(c) || c == '_') - sb.Append(c); - } - - sb.Append('_'); - sb.Append(tag); - sb.Append('_'); - sb.Append(NewId.Next().ToString(ZBase32Formatter.LowerCase)); + return GetMessageTopology(); + } - return ShrinkToFit(sb.ToString(), _maxQueueNameLength); + public IMessageConsumeTopologyConfigurator GetMessageTopology(Type messageType) + { + return _messageTypeSelectorCache.GetOrAdd(messageType, _ => Activation.Activate(messageType, new MessageTypeSelectorFactory(), this)) + .GetMessageTopology(); } - IMessageConsumeTopologyConfigurator IConsumeTopologyConfigurator.GetMessageTopology() + public virtual string CreateTemporaryQueueName(string tag) { - return GetMessageTopology(); + return ShrinkToFit(DefaultEndpointNameFormatter.GetTemporaryQueueName(tag), _maxQueueNameLength); } public ConnectHandle ConnectConsumeTopologyConfigurationObserver(IConsumeTopologyConfigurationObserver observer) @@ -84,30 +68,28 @@ public ConnectHandle ConnectConsumeTopologyConfigurationObserver(IConsumeTopolog public bool TryAddConvention(IConsumeTopologyConvention convention) { + var conventionType = convention.GetType(); + lock (_lock) { - if (_conventions.Any(x => x.GetType() == convention.GetType())) - return false; + for (var i = 0; i < _conventions.Count; i++) + { + if (_conventions[i].GetType() == conventionType) + return false; + } _conventions.Add(convention); + } - foreach (var messageConsumeTopologyConfigurator in _messageTypes.Values) - messageConsumeTopologyConfigurator.TryAddConvention(convention); + foreach (Lazy messageConsumeTopologyConfigurator in _messageTypes.Values) + messageConsumeTopologyConfigurator.Value.TryAddConvention(convention); - return true; - } + return true; } public virtual IEnumerable Validate() { - return _messageTypes.Values.SelectMany(x => x.Validate()); - } - - void IConsumeTopologyConfigurator.AddMessageConsumeTopology(IMessageConsumeTopology topology) - { - IMessageConsumeTopologyConfigurator messageConfiguration = GetMessageTopology(); - - messageConfiguration.Add(topology); + return _messageTypes.Values.SelectMany(x => x.Value.Validate()); } static string ShrinkToFit(string inputName, int maxLength) @@ -137,16 +119,17 @@ protected IMessageConsumeTopologyConfigurator GetMessageTopology() if (MessageTypeCache.IsValidMessageType == false) throw new ArgumentException(MessageTypeCache.InvalidMessageTypeReason, nameof(T)); - var specification = _messageTypes.GetOrAdd(typeof(T), CreateMessageTopology); + Lazy specification = _messageTypes.GetOrAdd(typeof(T), + _ => new Lazy(() => CreateMessageTopology())); - return specification as IMessageConsumeTopologyConfigurator; + return specification.Value as IMessageConsumeTopologyConfigurator; } protected bool All(Func callback) { IMessageConsumeTopologyConfigurator[] configurators; lock (_lock) - configurators = _messageTypes.Values.ToArray(); + configurators = _messageTypes.Values.Select(x => x.Value).ToArray(); if (configurators.Length == 0) return true; @@ -162,7 +145,7 @@ protected IEnumerable SelectMany(Func x.Value).ToArray(); if (configurators.Length == 0) return Enumerable.Empty(); @@ -178,7 +161,7 @@ protected void ForEach(Action callback) { IMessageConsumeTopologyConfigurator[] configurators; lock (_lock) - configurators = _messageTypes.Values.ToArray(); + configurators = _messageTypes.Values.Select(x => x.Value).ToArray(); switch (configurators.Length) { @@ -195,7 +178,7 @@ protected void ForEach(Action callback) } } - protected virtual IMessageConsumeTopologyConfigurator CreateMessageTopology(Type type) + protected virtual IMessageConsumeTopologyConfigurator CreateMessageTopology() where T : class { var messageTopology = new MessageConsumeTopology(); @@ -223,5 +206,40 @@ void ApplyConventionsToMessageTopology(IMessageConsumeTopologyConfigurator messageTopology.TryAddConvention(messageConsumeTopologyConvention); } } + + + readonly struct MessageTypeSelectorFactory : + IActivationType + { + public IMessageTypeSelector ActivateType(ConsumeTopology consumeTopology) + where T : class + { + return new MessageTypeSelector(consumeTopology); + } + } + + + interface IMessageTypeSelector + { + IMessageConsumeTopologyConfigurator GetMessageTopology(); + } + + + class MessageTypeSelector : + IMessageTypeSelector + where T : class + { + readonly ConsumeTopology _consumeTopology; + + public MessageTypeSelector(ConsumeTopology consumeTopology) + { + _consumeTopology = consumeTopology; + } + + public IMessageConsumeTopologyConfigurator GetMessageTopology() + { + return _consumeTopology.GetMessageTopology(); + } + } } } diff --git a/src/MassTransit/Topology/GlobalTopology.cs b/src/MassTransit/Topology/GlobalTopology.cs index db3bb3de5a2..29f70d506ac 100644 --- a/src/MassTransit/Topology/GlobalTopology.cs +++ b/src/MassTransit/Topology/GlobalTopology.cs @@ -2,8 +2,8 @@ namespace MassTransit { using System; using System.Collections.Generic; - using System.Threading; using Configuration; + using Contracts.JobService; using Courier.Contracts; using Topology; @@ -37,6 +37,7 @@ public class GlobalTopology : _publishToSendHandle = _publish.ConnectPublishTopologyConfigurationObserver(observer); ConfigureRoutingSlipCorrelation(); + ConfigureJobSagaCorrelation(); } public static ISendTopologyConfigurator Send => Cached.Metadata.Value.Send; @@ -106,11 +107,46 @@ void ConfigureRoutingSlipCorrelation() _send.UseCorrelationId(x => x.TrackingNumber); } + void ConfigureJobSagaCorrelation() + { + _send.UseCorrelationId(x => x.JobTypeId); + _send.UseCorrelationId(x => x.JobTypeId); + _send.UseCorrelationId(x => x.JobTypeId); + + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId>(x => x.Message.JobId); + _send.UseCorrelationId>(x => x.Message.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + _send.UseCorrelationId(x => x.JobId); + + _send.UseCorrelationId(x => x.AttemptId); + _send.UseCorrelationId(x => x.AttemptId); + _send.UseCorrelationId(x => x.AttemptId); + _send.UseCorrelationId>(x => x.Message.AttemptId); + _send.UseCorrelationId(x => x.AttemptId); + _send.UseCorrelationId(x => x.AttemptId); + } + static class Cached { - internal static readonly Lazy Metadata = - new Lazy(() => new GlobalTopology(), LazyThreadSafetyMode.PublicationOnly); + internal static readonly Lazy Metadata = new(() => new GlobalTopology()); } } } diff --git a/src/MassTransit/Topology/MessageConsumeTopology.cs b/src/MassTransit/Topology/MessageConsumeTopology.cs index 82b4db15f03..83783322819 100644 --- a/src/MassTransit/Topology/MessageConsumeTopology.cs +++ b/src/MassTransit/Topology/MessageConsumeTopology.cs @@ -10,15 +10,15 @@ public class MessageConsumeTopology : IMessageConsumeTopologyConfigurator where TMessage : class { - readonly IList> _conventions; - readonly IList> _delegateTopologies; - readonly IList> _topologies; + readonly List> _conventions; + readonly List> _delegateTopologies; + readonly List> _topologies; public MessageConsumeTopology() { - _conventions = new List>(); - _topologies = new List>(); - _delegateTopologies = new List>(); + _conventions = new List>(8); + _topologies = new List>(8); + _delegateTopologies = new List>(8); } protected bool IsBindableMessageType => GlobalTopology.IsConsumableMessageType(typeof(TMessage)); @@ -57,8 +57,13 @@ public void Apply(ITopologyPipeBuilder> builder) public bool TryAddConvention(IMessageConsumeTopologyConvention convention) { - if (_conventions.Any(x => x.GetType() == convention.GetType())) - return false; + var conventionType = convention.GetType(); + + for (var i = 0; i < _conventions.Count; i++) + { + if (_conventions[i].GetType() == conventionType) + return false; + } _conventions.Add(convention); return true; @@ -71,8 +76,7 @@ public void UpdateConvention(Func update) { if (_conventions[i] is TConvention convention) { - var updatedConvention = update(convention); - _conventions[i] = updatedConvention; + _conventions[i] = update(convention); return; } } @@ -85,8 +89,7 @@ public void AddOrUpdateConvention(Func add, Func : { readonly IReadProperty _property; - public NullablePropertyMessageCorrelationId(PropertyInfo propertyInfo) + public NullablePropertyMessageCorrelationId(IReadProperty property) { - _property = ReadPropertyCache.GetProperty(propertyInfo); + _property = property; } public bool TryGetCorrelationId(T message, out Guid correlationId) diff --git a/src/MassTransit/Topology/Topology/PropertyMessageCorrelationId.cs b/src/MassTransit/Topology/Topology/PropertyMessageCorrelationId.cs index dfe6969d343..45aa52c4ac7 100644 --- a/src/MassTransit/Topology/Topology/PropertyMessageCorrelationId.cs +++ b/src/MassTransit/Topology/Topology/PropertyMessageCorrelationId.cs @@ -1,7 +1,6 @@ namespace MassTransit.Topology { using System; - using System.Reflection; using Internals; @@ -11,9 +10,9 @@ public class PropertyMessageCorrelationId : { readonly IReadProperty _property; - public PropertyMessageCorrelationId(PropertyInfo propertyInfo) + public PropertyMessageCorrelationId(IReadProperty property) { - _property = ReadPropertyCache.GetProperty(propertyInfo); + _property = property; } public bool TryGetCorrelationId(T message, out Guid correlationId) diff --git a/src/MassTransit/Topology/Topology/SetSerializerMessageSendTopology.cs b/src/MassTransit/Topology/Topology/SetSerializerMessageSendTopology.cs new file mode 100644 index 00000000000..0a686bba58b --- /dev/null +++ b/src/MassTransit/Topology/Topology/SetSerializerMessageSendTopology.cs @@ -0,0 +1,28 @@ +namespace MassTransit.Topology +{ + using System; + using System.Net.Mime; + using Configuration; + using Middleware; + + + public class SetSerializerMessageSendTopology : + IMessageSendTopology + where T : class + { + readonly IFilter> _filter; + + public SetSerializerMessageSendTopology(ContentType contentType) + { + if (contentType == null) + throw new ArgumentNullException(nameof(contentType)); + + _filter = new SetSerializerFilter(contentType); + } + + public void Apply(ITopologyPipeBuilder> builder) + { + builder.AddFilter(_filter); + } + } +} diff --git a/src/MassTransit/Transports/AddressEqualityComparer.cs b/src/MassTransit/Transports/AddressEqualityComparer.cs index f7ea9110340..1d35e7f3fab 100644 --- a/src/MassTransit/Transports/AddressEqualityComparer.cs +++ b/src/MassTransit/Transports/AddressEqualityComparer.cs @@ -12,12 +12,12 @@ public class AddressEqualityComparer : public bool Equals(Uri x, Uri y) { return ReferenceEquals(x, y) - || x != null - && y != null - && x.Scheme.Equals(y.Scheme, StringComparison.OrdinalIgnoreCase) - && x.Host.Equals(y.Host, StringComparison.OrdinalIgnoreCase) - && x.Port.Equals(y.Port) - && x.AbsolutePath.Equals(y.AbsolutePath, StringComparison.OrdinalIgnoreCase); + || (x != null + && y != null + && x.Scheme.Equals(y.Scheme, StringComparison.OrdinalIgnoreCase) + && x.Host.Equals(y.Host, StringComparison.OrdinalIgnoreCase) + && x.Port.Equals(y.Port) + && x.AbsolutePath.Equals(y.AbsolutePath, StringComparison.OrdinalIgnoreCase)); } public int GetHashCode(Uri obj) diff --git a/src/MassTransit/Transports/BaseHost.cs b/src/MassTransit/Transports/BaseHost.cs index 8b8a8641961..c8bbfa6a2a0 100644 --- a/src/MassTransit/Transports/BaseHost.cs +++ b/src/MassTransit/Transports/BaseHost.cs @@ -151,7 +151,7 @@ public async Task Stop(CancellationToken cancellationToken) await Riders.Stop(cancellationToken).ConfigureAwait(false); - await ReceiveEndpoints.Stop(cancellationToken).ConfigureAwait(false); + await ReceiveEndpoints.StopEndpoints(cancellationToken).ConfigureAwait(false); foreach (var agent in GetAgentHandles()) await agent.Stop("Bus stopped", cancellationToken).ConfigureAwait(false); diff --git a/src/MassTransit/Transports/BaseReceiveContext.cs b/src/MassTransit/Transports/BaseReceiveContext.cs index b73cf97be9e..57de555848a 100644 --- a/src/MassTransit/Transports/BaseReceiveContext.cs +++ b/src/MassTransit/Transports/BaseReceiveContext.cs @@ -88,7 +88,7 @@ public virtual Task NotifyFaulted(ConsumeContext context, TimeSpan duratio switch (exception) { - case OperationCanceledException canceledException when canceledException.CancellationToken == context.CancellationToken: + case OperationCanceledException canceled when canceled.CancellationToken == context.CancellationToken: context.LogCanceled(duration, consumerType); break; diff --git a/src/MassTransit/Transports/BaseReceiveEndpointContext.cs b/src/MassTransit/Transports/BaseReceiveEndpointContext.cs index 6cae718d3eb..8db2cc21b7d 100644 --- a/src/MassTransit/Transports/BaseReceiveEndpointContext.cs +++ b/src/MassTransit/Transports/BaseReceiveEndpointContext.cs @@ -103,6 +103,8 @@ public ConnectHandle ConnectReceiveEndpointObserver(IReceiveEndpointObserver obs return _endpointObservers.Connect(observer); } + public TimeSpan? ConsumerStopTimeout => _hostConfiguration.ConsumerStopTimeout; + public Uri InputAddress { get; } public Task DependenciesReady { get; } diff --git a/src/MassTransit/Transports/Components/KillSwitch/StartedKillSwitchState.cs b/src/MassTransit/Transports/Components/KillSwitch/StartedKillSwitchState.cs index 81c3e190c94..b015cff64ea 100644 --- a/src/MassTransit/Transports/Components/KillSwitch/StartedKillSwitchState.cs +++ b/src/MassTransit/Transports/Components/KillSwitch/StartedKillSwitchState.cs @@ -32,11 +32,6 @@ public void Probe(ProbeContext context) }); } - public void LogThreshold() - { - LogContext.Debug?.Log("Kill Switch threshold reached, failures: {FailureCount}, attempts: {AttemptCount}", _failureCount, _attemptCount); - } - public Task PreConsume(ConsumeContext context) where T : class { @@ -68,6 +63,11 @@ public async Task ConsumeFault(ConsumeContext context, Exception exception } } + public void LogThreshold() + { + LogContext.Debug?.Log("Kill Switch threshold reached, failures: {FailureCount}, attempts: {AttemptCount}", _failureCount, _attemptCount); + } + public void Activate() { _timer = new Timer(Reset, null, _killSwitch.TrackingPeriod, _killSwitch.TrackingPeriod); diff --git a/src/MassTransit/Transports/Components/KillSwitch/StoppedKillSwitchState.cs b/src/MassTransit/Transports/Components/KillSwitch/StoppedKillSwitchState.cs index 5207fb4f269..429f1888114 100644 --- a/src/MassTransit/Transports/Components/KillSwitch/StoppedKillSwitchState.cs +++ b/src/MassTransit/Transports/Components/KillSwitch/StoppedKillSwitchState.cs @@ -9,9 +9,9 @@ namespace MassTransit.Transports.Components public class StoppedKillSwitchState : IKillSwitchState { - Stopwatch _elapsed; readonly Exception _exception; readonly IKillSwitch _killSwitch; + Stopwatch _elapsed; Timer _timer; public StoppedKillSwitchState(IKillSwitch killSwitch, Exception exception) @@ -31,12 +31,6 @@ public void Probe(ProbeContext context) }); } - public void Activate() - { - _elapsed = Stopwatch.StartNew(); - _timer = new Timer(Restart, this, _killSwitch.RestartTimeout, TimeSpan.FromMilliseconds(-1)); - } - public Task PreConsume(ConsumeContext context) where T : class { @@ -55,6 +49,12 @@ public Task ConsumeFault(ConsumeContext context, Exception exception) return Task.CompletedTask; } + public void Activate() + { + _elapsed = Stopwatch.StartNew(); + _timer = new Timer(Restart, this, _killSwitch.RestartTimeout, TimeSpan.FromMilliseconds(-1)); + } + void Restart(object state) { LogContext.SetCurrentIfNull(_killSwitch.LogContext); diff --git a/src/MassTransit/Transports/ConsumerAgent.cs b/src/MassTransit/Transports/ConsumerAgent.cs new file mode 100644 index 00000000000..0338a787237 --- /dev/null +++ b/src/MassTransit/Transports/ConsumerAgent.cs @@ -0,0 +1,250 @@ +namespace MassTransit.Transports +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Threading; + using System.Threading.Tasks; + using Internals; + using Middleware; + using Util; + + + public abstract class ConsumerAgent : + Agent, + DeliveryMetrics + { + readonly ReceiveEndpointContext _context; + readonly TaskCompletionSource _deliveryComplete; + readonly IReceivePipeDispatcher _dispatcher; + readonly object _lock = new object(); + readonly ConcurrentDictionary _pending; + Task _consumeTask; + TaskCompletionSource _consumeTaskSource; + + protected ConsumerAgent(ReceiveEndpointContext context, IEqualityComparer equalityComparer = default) + { + _context = context; + _deliveryComplete = TaskUtil.GetTask(); + + _pending = new ConcurrentDictionary(equalityComparer ?? EqualityComparer.Default); + + _dispatcher = context.CreateReceivePipeDispatcher(); + _dispatcher.ZeroActivity += HandleDeliveryComplete; + } + + protected bool IsIdle => ActiveDispatchCount == 0; + + protected long ActiveDispatchCount => _dispatcher.ActiveDispatchCount; + + protected bool IsGracefulShutdown { get; private set; } = true; + + public long DeliveryCount => _dispatcher.DispatchCount; + + public int ConcurrentDeliveryCount => _dispatcher.MaxConcurrentDispatchCount; + + Task HandleDeliveryComplete() + { + if (IsStopping) + _deliveryComplete.TrySetResult(true); + + return Task.CompletedTask; + } + + protected void TrySetManualConsumeTask() + { + if (_consumeTask != null || _consumeTaskSource != null) + return; + + lock (_lock) + { + if (_consumeTask != null || _consumeTaskSource != null) + return; + + _consumeTaskSource = TaskUtil.GetTask(); + SetConsumeTask(_consumeTaskSource.Task); + } + } + + protected void TrySetConsumeTask(Task consumeTask) + { + if (_consumeTask != null) + return; + + lock (_lock) + { + if (_consumeTask != null) + return; + + _consumeTask = consumeTask; + SetConsumeTask(_consumeTask); + } + } + + void SetConsumeTask(Task consumeTask) + { + async Task TryStop() + { + if (IsStopping) + return; + + IsGracefulShutdown = false; + + try + { + LogContext.SetCurrentIfNull(_context.LogContext); + + await this.Stop("Consume Loop Exited").ConfigureAwait(false); + } + catch (Exception exception) + { + LogContext.Warning?.Log(exception, "Stop Faulted"); + } + } + + if (consumeTask.IsCompleted) + Task.Run(() => TryStop()); + else + consumeTask.ContinueWith(_ => TryStop(), TaskContinuationOptions.RunContinuationsAsynchronously); + } + + protected override Task StopAgent(StopContext context) + { + LogContext.Debug?.Log("Consumer Stopping: {InputAddress} ({Reason})", _context.InputAddress, context.Reason); + + TrySetConsumeCompleted(); + + SetCompleted(ActiveAndActualAgentsCompleted(context)); + + return Completed; + } + + void CancelPendingConsumers() + { + foreach (var key in _pending.Keys) + { + if (_pending.TryRemove(key, out var context)) + context.Cancel(); + } + } + + protected void TrySetConsumeCompleted() + { + _consumeTaskSource?.TrySetResult(true); + } + + protected void TrySetConsumeCanceled(CancellationToken cancellationToken = default) + { + if (_consumeTaskSource == null) + return; + + CancelPendingConsumers(); + + _consumeTaskSource.TrySetCanceled(cancellationToken); + } + + protected void TrySetConsumeException(Exception exception) + { + if (_consumeTaskSource == null) + return; + + CancelPendingConsumers(); + + _consumeTaskSource.TrySetException(exception); + } + + protected virtual async Task ActiveAndActualAgentsCompleted(StopContext context) + { + if (!IsIdle) + { + CancellationTokenSource cancellationTokenSource = null; + CancellationTokenRegistration? registration = null; + + if (_context.ConsumerStopTimeout != null) + { + cancellationTokenSource = new CancellationTokenSource(_context.ConsumerStopTimeout.Value); + registration = cancellationTokenSource.Token.Register(CancelPendingConsumers); + } + + try + { + await _deliveryComplete.Task.OrCanceled(context.CancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + LogContext.Warning?.Log("Consumer stop canceled: {InputAddress}", _context.InputAddress); + + CancelPendingConsumers(); + } + finally + { + registration?.Dispose(); + cancellationTokenSource?.Dispose(); + } + } + + if (_consumeTask == null) + return; + + try + { + await _consumeTask.OrCanceled(context.CancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + catch (Exception e) + { + LogContext.Warning?.Log(e, "Consumer stop faulted: {InputAddress}", _context.InputAddress); + } + } + + protected virtual bool IsTrackable(TKey key) + { + return true; + } + + protected Task Dispatch(TKey key, TContext context, ReceiveLockContext receiveLockContext) + where TContext : BaseReceiveContext + { + var added = false; + var lockContext = receiveLockContext; + + if (IsTrackable(key)) + { + lockContext = _pending.AddOrUpdate(key, _ => + { + var current = new PendingReceiveLockContext(); + added = current.Enqueue(context, receiveLockContext); + return current; + }, (_, current) => + { + added = current.Enqueue(context, receiveLockContext); + return current; + }); + + if (!added) + { + context.LogTransportDupe(key); + return Task.CompletedTask; + } + } + + var dispatchTask = _dispatcher.Dispatch(context, lockContext); + + if (added) + { + dispatchTask.ContinueWith(_ => + { + if (_pending.TryGetValue(key, out var value)) + { + if (value.IsEmpty) + _pending.TryRemove(key, out var _); + } + }, TaskContinuationOptions.ExecuteSynchronously); + } + + return dispatchTask; + } + } +} diff --git a/src/MassTransit/Transports/Contexts/ReceiveEndpointContext.cs b/src/MassTransit/Transports/Contexts/ReceiveEndpointContext.cs index f4e6bcee2a6..7efffecb5c8 100644 --- a/src/MassTransit/Transports/Contexts/ReceiveEndpointContext.cs +++ b/src/MassTransit/Transports/Contexts/ReceiveEndpointContext.cs @@ -18,6 +18,8 @@ public interface ReceiveEndpointContext : IReceiveEndpointObserverConnector, IProbeSite { + TimeSpan? ConsumerStopTimeout { get; } + Uri InputAddress { get; } bool IsBusEndpoint { get; } diff --git a/src/MassTransit/Transports/DefaultMessageNameFormatter.cs b/src/MassTransit/Transports/DefaultMessageNameFormatter.cs index 3afc0f37286..ee9771abacf 100644 --- a/src/MassTransit/Transports/DefaultMessageNameFormatter.cs +++ b/src/MassTransit/Transports/DefaultMessageNameFormatter.cs @@ -2,7 +2,6 @@ namespace MassTransit.Transports { using System; using System.Collections.Concurrent; - using System.Reflection; using System.Text; @@ -26,14 +25,14 @@ public DefaultMessageNameFormatter(string genericArgumentSeparator, string gener _cache = new ConcurrentDictionary(); } - public MessageName GetMessageName(Type type) + public string GetMessageName(Type type) { - return new MessageName(_cache.GetOrAdd(type, CreateMessageName)); + return _cache.GetOrAdd(type, CreateMessageName); } string CreateMessageName(Type type) { - if (type.GetTypeInfo().IsGenericTypeDefinition) + if (type.IsGenericTypeDefinition) throw new ArgumentException("An open generic type cannot be used as a message name"); var sb = new StringBuilder(""); @@ -43,13 +42,12 @@ string CreateMessageName(Type type) string GetMessageName(StringBuilder sb, Type type, string scope) { - var typeInfo = type.GetTypeInfo(); - if (typeInfo.IsGenericParameter) + if (type.IsGenericParameter) return ""; - if (typeInfo.Namespace != null) + var ns = type.Namespace; + if (ns != null) { - var ns = typeInfo.Namespace; if (!ns.Equals(scope)) { sb.Append(ns); @@ -57,15 +55,15 @@ string GetMessageName(StringBuilder sb, Type type, string scope) } } - if (typeInfo.IsNested) + if (type.IsNested) { - GetMessageName(sb, typeInfo.DeclaringType, typeInfo.Namespace); + GetMessageName(sb, type.DeclaringType, ns); sb.Append(_nestedTypeSeparator); } - if (typeInfo.IsGenericType) + if (type.IsGenericType) { - var name = typeInfo.GetGenericTypeDefinition().Name; + var name = type.GetGenericTypeDefinition().Name; //remove `1 var index = name.IndexOf('`'); @@ -75,19 +73,19 @@ string GetMessageName(StringBuilder sb, Type type, string scope) sb.Append(name); sb.Append(_genericTypeSeparator); - Type[] arguments = typeInfo.GetGenericArguments(); + Type[] arguments = type.GetGenericArguments(); for (var i = 0; i < arguments.Length; i++) { if (i > 0) sb.Append(_genericArgumentSeparator); - GetMessageName(sb, arguments[i], typeInfo.Namespace); + GetMessageName(sb, arguments[i], ns); } sb.Append(_genericTypeSeparator); } else - sb.Append(typeInfo.Name); + sb.Append(type.Name); return sb.ToString(); } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/DelegatePartitionKeyFormatter.cs b/src/MassTransit/Transports/DelegatePartitionKeyFormatter.cs similarity index 91% rename from src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/DelegatePartitionKeyFormatter.cs rename to src/MassTransit/Transports/DelegatePartitionKeyFormatter.cs index 2648d89acd5..10a4a1751e9 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/DelegatePartitionKeyFormatter.cs +++ b/src/MassTransit/Transports/DelegatePartitionKeyFormatter.cs @@ -1,4 +1,4 @@ -namespace MassTransit.AzureServiceBusTransport +namespace MassTransit.Transports { using System; diff --git a/src/MassTransit/Transports/DelegateRoutingKeyFormatter.cs b/src/MassTransit/Transports/DelegateRoutingKeyFormatter.cs new file mode 100644 index 00000000000..f8af3e8c312 --- /dev/null +++ b/src/MassTransit/Transports/DelegateRoutingKeyFormatter.cs @@ -0,0 +1,22 @@ +namespace MassTransit.Transports +{ + using System; + + + public class DelegateRoutingKeyFormatter : + IMessageRoutingKeyFormatter + where TMessage : class + { + readonly Func, string> _formatter; + + public DelegateRoutingKeyFormatter(Func, string> formatter) + { + _formatter = formatter; + } + + public string FormatRoutingKey(SendContext context) + { + return _formatter(context); + } + } +} diff --git a/src/MassTransit/Transports/Fabric/MessageFabric.cs b/src/MassTransit/Transports/Fabric/MessageFabric.cs index 760589359b1..7050e575943 100644 --- a/src/MassTransit/Transports/Fabric/MessageFabric.cs +++ b/src/MassTransit/Transports/Fabric/MessageFabric.cs @@ -33,7 +33,7 @@ public void ExchangeDeclare(TContext context, string name, ExchangeType exchange public void ExchangeBind(TContext context, string source, string destination, string routingKey) { if (source.Equals(destination)) - throw new ArgumentException("The source and destination exchange cannot be the same"); + throw new ArgumentException("The source and destination exchange cannot be the same: " + source); IMessageExchange sourceExchange = GetOrAddExchange(context, source, ExchangeType.FanOut); diff --git a/src/MassTransit/Transports/Fabric/MessageQueue.cs b/src/MassTransit/Transports/Fabric/MessageQueue.cs index 57b982250eb..b0b58912bc3 100644 --- a/src/MassTransit/Transports/Fabric/MessageQueue.cs +++ b/src/MassTransit/Transports/Fabric/MessageQueue.cs @@ -80,7 +80,7 @@ public void Probe(ProbeContext context) protected override async Task StopAgent(StopContext context) { - _channel.Writer.Complete(); + _channel.Writer.TryComplete(); await _channel.Reader.Completion.ConfigureAwait(false); diff --git a/src/MassTransit/Transports/Fabric/MessageReceiverCollection.cs b/src/MassTransit/Transports/Fabric/MessageReceiverCollection.cs index b3ed1329e5f..57516b3a66f 100644 --- a/src/MassTransit/Transports/Fabric/MessageReceiverCollection.cs +++ b/src/MassTransit/Transports/Fabric/MessageReceiverCollection.cs @@ -54,7 +54,7 @@ public TopologyHandle Connect(IMessageReceiver receiver) IMessageReceiver[] connected = _receivers.Values.ToArray(); - var balancer = connected.Length == 1 + IReceiverLoadBalancer balancer = connected.Length == 1 ? new SingleReceiverLoadBalancer(connected[0]) : _balancerFactory(connected); @@ -73,15 +73,15 @@ public Task> Next(T message, CancellationToken cancellationT Task> task = _balancer.Task; if (task.IsCompletedSuccessfully()) { - var balancer = task.GetAwaiter().GetResult(); - var consumer = balancer.SelectReceiver(message); + IReceiverLoadBalancer balancer = task.GetAwaiter().GetResult(); + IMessageReceiver consumer = balancer.SelectReceiver(message); return Task.FromResult(consumer); } async Task> NextAsync() { - var balancer = await _balancer.Task.OrCanceled(cancellationToken).ConfigureAwait(false); + IReceiverLoadBalancer balancer = await _balancer.Task.OrCanceled(cancellationToken).ConfigureAwait(false); return balancer.SelectReceiver(message); } @@ -107,7 +107,7 @@ void Disconnect(long id) if (connected.Length <= 0) return; - var balancer = connected.Length == 1 + IReceiverLoadBalancer balancer = connected.Length == 1 ? new SingleReceiverLoadBalancer(connected[0]) : _balancerFactory(connected); diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IMessagePartitionKeyFormatter.cs b/src/MassTransit/Transports/IMessagePartitionKeyFormatter.cs similarity index 77% rename from src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IMessagePartitionKeyFormatter.cs rename to src/MassTransit/Transports/IMessagePartitionKeyFormatter.cs index d9e8c60ab9b..1b3473c7ab2 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IMessagePartitionKeyFormatter.cs +++ b/src/MassTransit/Transports/IMessagePartitionKeyFormatter.cs @@ -1,4 +1,4 @@ -namespace MassTransit.AzureServiceBusTransport +namespace MassTransit.Transports { public interface IMessagePartitionKeyFormatter where TMessage : class diff --git a/src/MassTransit/Transports/IMessageRoutingKeyFormatter.cs b/src/MassTransit/Transports/IMessageRoutingKeyFormatter.cs new file mode 100644 index 00000000000..36c7a0b39fd --- /dev/null +++ b/src/MassTransit/Transports/IMessageRoutingKeyFormatter.cs @@ -0,0 +1,8 @@ +namespace MassTransit.Transports +{ + public interface IMessageRoutingKeyFormatter + where TMessage : class + { + string FormatRoutingKey(SendContext context); + } +} diff --git a/src/MassTransit/Transports/IPartitionKeyFormatter.cs b/src/MassTransit/Transports/IPartitionKeyFormatter.cs new file mode 100644 index 00000000000..a4355f60161 --- /dev/null +++ b/src/MassTransit/Transports/IPartitionKeyFormatter.cs @@ -0,0 +1,13 @@ +namespace MassTransit.Transports; + +public interface IPartitionKeyFormatter +{ + /// + /// Format the partition key to be used by the transport, if supported + /// + /// The message type + /// The message send context + /// The routing key to specify in the transport + string FormatPartitionKey(SendContext context) + where T : class; +} diff --git a/src/MassTransit/Transports/IReceiveEndpointCollection.cs b/src/MassTransit/Transports/IReceiveEndpointCollection.cs index 344fede376f..78179e817b2 100644 --- a/src/MassTransit/Transports/IReceiveEndpointCollection.cs +++ b/src/MassTransit/Transports/IReceiveEndpointCollection.cs @@ -2,12 +2,12 @@ { using System.Collections.Generic; using System.Threading; + using System.Threading.Tasks; public interface IReceiveEndpointCollection : IReceiveEndpointObserverConnector, IConsumeMessageObserverConnector, - IAgent, IProbeSite { /// @@ -33,6 +33,13 @@ public interface IReceiveEndpointCollection : /// HostReceiveEndpointHandle Start(string endpointName, CancellationToken cancellationToken = default); + /// + /// Stop all receive endpoints + /// + /// + /// + Task StopEndpoints(CancellationToken cancellationToken); + IEnumerable CheckEndpointHealth(); } } diff --git a/src/MassTransit/Transports/IReceivePipeDispatcher.cs b/src/MassTransit/Transports/IReceivePipeDispatcher.cs index d965d2e19de..db2017c1a52 100644 --- a/src/MassTransit/Transports/IReceivePipeDispatcher.cs +++ b/src/MassTransit/Transports/IReceivePipeDispatcher.cs @@ -15,6 +15,6 @@ public interface IReceivePipeDispatcher : IReceiveObserverConnector, IProbeSite { - Task Dispatch(ReceiveContext context, ReceiveLockContext receiveLock = default); + Task Dispatch(ReceiveContext context, ReceiveLockContext receiveLock); } } diff --git a/src/MassTransit/Transports/IRoutingKeyFormatter.cs b/src/MassTransit/Transports/IRoutingKeyFormatter.cs new file mode 100644 index 00000000000..d3868845e69 --- /dev/null +++ b/src/MassTransit/Transports/IRoutingKeyFormatter.cs @@ -0,0 +1,14 @@ +namespace MassTransit.Transports +{ + public interface IRoutingKeyFormatter + { + /// + /// Format the routing key to be used by the transport, if supported + /// + /// The message type + /// The message send context + /// The routing key to specify in the transport + string FormatRoutingKey(SendContext context) + where T : class; + } +} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessagePartitionKeyFormatter.cs b/src/MassTransit/Transports/MessagePartitionKeyFormatter.cs similarity index 90% rename from src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessagePartitionKeyFormatter.cs rename to src/MassTransit/Transports/MessagePartitionKeyFormatter.cs index b55b50448c3..7a5cb29ef77 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessagePartitionKeyFormatter.cs +++ b/src/MassTransit/Transports/MessagePartitionKeyFormatter.cs @@ -1,4 +1,4 @@ -namespace MassTransit.AzureServiceBusTransport +namespace MassTransit.Transports { public class MessagePartitionKeyFormatter : IMessagePartitionKeyFormatter diff --git a/src/MassTransit/Transports/MessageRoutingKeyFormatter.cs b/src/MassTransit/Transports/MessageRoutingKeyFormatter.cs new file mode 100644 index 00000000000..f6e33aaa149 --- /dev/null +++ b/src/MassTransit/Transports/MessageRoutingKeyFormatter.cs @@ -0,0 +1,19 @@ +namespace MassTransit.Transports +{ + public class MessageRoutingKeyFormatter : + IMessageRoutingKeyFormatter + where TMessage : class + { + readonly IRoutingKeyFormatter _formatter; + + public MessageRoutingKeyFormatter(IRoutingKeyFormatter formatter) + { + _formatter = formatter; + } + + public string FormatRoutingKey(SendContext context) + { + return _formatter.FormatRoutingKey(context); + } + } +} diff --git a/src/MassTransit/Transports/PendingReceiveLockContext.cs b/src/MassTransit/Transports/PendingReceiveLockContext.cs new file mode 100644 index 00000000000..696e035890d --- /dev/null +++ b/src/MassTransit/Transports/PendingReceiveLockContext.cs @@ -0,0 +1,136 @@ +namespace MassTransit.Transports +{ + using System; + using System.Collections.Generic; + using System.Runtime.ExceptionServices; + using System.Threading.Tasks; + + + public class PendingReceiveLockContext : + ReceiveLockContext + { + Lock? _lockContext; + Queue _pending; + + public bool IsEmpty => _lockContext == null && (_pending == null || _pending.Count == 0); + + public Task Complete() + { + return Execute(context => context.Complete(), true); + } + + public Task Faulted(Exception exception) + { + return Execute(context => context.Faulted(exception), true); + } + + public Task ValidateLockStatus() + { + return Execute(context => context.ValidateLockStatus()); + } + + public bool Enqueue(BaseReceiveContext receiveContext, ReceiveLockContext receiveLockContext) + { + var lockContext = new Lock(receiveContext, receiveLockContext); + + lock (this) + { + if (_lockContext == null && (_pending == null || _pending.Count == 0)) + { + _lockContext = lockContext; + return true; + } + + (_pending ??= new Queue(1)).Enqueue(lockContext); + return false; + } + } + + async Task Execute(Func action, bool clearLockContext = false) + { + if (_lockContext == null) + { + lock (this) + { + if (_lockContext == null) + { + if (_pending == null || _pending.Count == 0) + return; + + _lockContext = _pending.Dequeue(); + } + } + } + + ExceptionDispatchInfo dispatchInfo; + + do + { + try + { + var lockContext = _lockContext.Value; + + if (clearLockContext) + _lockContext = null; + + await action(lockContext.ReceiveLockContext).ConfigureAwait(false); + + return; + } + catch (Exception ex) + { + dispatchInfo = ExceptionDispatchInfo.Capture(ex.GetBaseException()); + } + } + while (TryDequeue()); + + if (dispatchInfo != null) + { + dispatchInfo.Throw(); + + throw dispatchInfo.SourceException; + } + } + + bool TryDequeue() + { + lock (this) + { + if (_pending == null || _pending.Count == 0) + { + _lockContext = null; + return false; + } + + _lockContext = _pending.Dequeue(); + return true; + } + } + + public void Cancel() + { + lock (this) + { + _lockContext?.ReceiveContext.Cancel(); + if (_pending != null) + { + foreach (var pendingLock in _pending) + pendingLock.ReceiveContext.Cancel(); + } + } + } + + + readonly struct Lock + { + public readonly BaseReceiveContext ReceiveContext; + public readonly ReceiveLockContext ReceiveLockContext; + + public Lock(BaseReceiveContext receiveContext, ReceiveLockContext receiveLockContext) + { + ReceiveContext = receiveContext; + ReceiveLockContext = receiveLockContext; + } + } + } +} diff --git a/src/MassTransit/Transports/ReceiveEndpoint.cs b/src/MassTransit/Transports/ReceiveEndpoint.cs index 7eecd2b8593..804258b7a6a 100644 --- a/src/MassTransit/Transports/ReceiveEndpoint.cs +++ b/src/MassTransit/Transports/ReceiveEndpoint.cs @@ -60,6 +60,7 @@ public ReceiveEndpoint(IReceiveTransport transport, ReceiveEndpointContext conte public Uri InputAddress { get; set; } public Task Started => _started.Task; + public ConnectHandle ObserverHandle { get; set; } public ReceiveEndpointHandle Start(CancellationToken cancellationToken) { diff --git a/src/MassTransit/Transports/ReceiveEndpointCollection.cs b/src/MassTransit/Transports/ReceiveEndpointCollection.cs index c492cc291e5..ec34d6e7ad6 100644 --- a/src/MassTransit/Transports/ReceiveEndpointCollection.cs +++ b/src/MassTransit/Transports/ReceiveEndpointCollection.cs @@ -5,13 +5,11 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; - using Middleware; using Observables; using Util; public class ReceiveEndpointCollection : - Agent, IReceiveEndpointCollection { readonly SingleThreadedDictionary _endpoints; @@ -36,9 +34,10 @@ public void Add(string endpointName, ReceiveEndpoint endpoint) ? EndpointHealthResult.Healthy(endpoint, "starting") : EndpointHealthResult.Unhealthy(endpoint, "not ready", null); - var added = _endpoints.TryAdd(endpointName, key => + var added = _endpoints.TryAdd(endpointName, _ => { endpoint.ConnectReceiveEndpointObserver(new HealthResultReceiveEndpointObserver(endpoint)); + endpoint.ObserverHandle = endpoint.ConnectReceiveEndpointObserver(_receiveEndpointObservers); return endpoint; }); @@ -99,30 +98,26 @@ public IEnumerable CheckEndpointHealth() return _endpoints.Values.Select(x => x.HealthResult).ToList(); } - protected override async Task StopAgent(StopContext context) + public async Task StopEndpoints(CancellationToken cancellationToken) { ReceiveEndpoint[] endpoints = _endpoints.Values.Where(x => x.IsStarted() && !x.IsBusEndpoint).ToArray(); - await Task.WhenAll(endpoints.Select(x => x.Stop(context.CancellationToken))).ConfigureAwait(false); + await Task.WhenAll(endpoints.Select(x => x.Stop(cancellationToken))).ConfigureAwait(false); endpoints = _endpoints.Values.Where(x => x.IsStarted() && x.IsBusEndpoint).ToArray(); - await Task.WhenAll(endpoints.Select(x => x.Stop(context.CancellationToken))).ConfigureAwait(false); + await Task.WhenAll(endpoints.Select(x => x.Stop(cancellationToken))).ConfigureAwait(false); _started = false; - - await base.StopAgent(context).ConfigureAwait(false); } HostReceiveEndpointHandle StartEndpoint(string endpointName, ReceiveEndpoint endpoint, CancellationToken cancellationToken) { try { - var observerHandle = endpoint.ConnectReceiveEndpointObserver(_receiveEndpointObservers); - void RemoveEndpoint() { - observerHandle.Disconnect(); + endpoint.ObserverHandle.Disconnect(); Remove(endpointName); } diff --git a/src/MassTransit/Transports/ReceiveEndpointDispatcher.cs b/src/MassTransit/Transports/ReceiveEndpointDispatcher.cs index a6c206360cc..2c80df4fde2 100644 --- a/src/MassTransit/Transports/ReceiveEndpointDispatcher.cs +++ b/src/MassTransit/Transports/ReceiveEndpointDispatcher.cs @@ -4,6 +4,7 @@ namespace MassTransit.Transports using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Context; using Internals; @@ -82,7 +83,7 @@ public async Task Dispatch(byte[] body, IReadOnlyDictionary head try { - await _dispatcher.Dispatch(context).ConfigureAwait(false); + await _dispatcher.Dispatch(context, NoLockReceiveContext.Instance).ConfigureAwait(false); } finally { diff --git a/src/MassTransit/Transports/ReceiveEndpointLoggingExtensions.cs b/src/MassTransit/Transports/ReceiveEndpointLoggingExtensions.cs index 7df879a56fb..1abd4140a68 100644 --- a/src/MassTransit/Transports/ReceiveEndpointLoggingExtensions.cs +++ b/src/MassTransit/Transports/ReceiveEndpointLoggingExtensions.cs @@ -23,6 +23,9 @@ public static class ReceiveEndpointLoggingExtensions static readonly LogMessage _logReceiveFault = LogContext.Define(LogLevel.Error, "R-FAULT {InputAddress} {MessageId} {Duration}"); + static readonly LogMessage _logReceiveDupe = LogContext.Define(LogLevel.Warning, + "R-DUPE {InputAddress} {MessageId} {TransportMessageId}"); + static readonly LogMessage _logSent = LogContext.DefineMessage(LogLevel.Debug, "SEND {DestinationAddress} {MessageId} {MessageType}"); @@ -103,9 +106,18 @@ public static void LogFaulted(this ReceiveContext context, Exception exception) _logReceiveFault(context.InputAddress, GetMessageId(context), context.ElapsedTime, exception); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static void LogTransportDupe(this ReceiveContext context, TTransportMessageId transportMessageId) + { + _logReceiveDupe(context.InputAddress, GetMessageId(context), transportMessageId); + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void LogTransportFaulted(this ReceiveContext context, Exception exception) { + if (exception.GetBaseException() is OperationCanceledException && context.CancellationToken.IsCancellationRequested) + return; + _logFault(context.InputAddress, GetMessageId(context), exception); } diff --git a/src/MassTransit/Transports/ReceivePipeDispatcher.cs b/src/MassTransit/Transports/ReceivePipeDispatcher.cs index 634c7aedcd8..45b73952cb5 100644 --- a/src/MassTransit/Transports/ReceivePipeDispatcher.cs +++ b/src/MassTransit/Transports/ReceivePipeDispatcher.cs @@ -44,27 +44,31 @@ public DeliveryMetrics GetMetrics() public event ZeroActiveDispatchHandler ZeroActivity; - public async Task Dispatch(ReceiveContext context, ReceiveLockContext receiveLock = default) + public async Task Dispatch(ReceiveContext context, ReceiveLockContext receiveLock) { LogContext.SetCurrentIfNull(_hostConfiguration.ReceiveLogContext); var active = StartDispatch(); StartedActivity? activity = LogContext.Current?.StartReceiveActivity(_activityName, _inputAddress, _endpointName, context); + StartedInstrument? instrument = LogContext.Current?.StartReceiveInstrument(context); + try { if (_observers.Count > 0) await _observers.PreReceive(context).ConfigureAwait(false); - if (receiveLock != null) - await receiveLock.ValidateLockStatus().ConfigureAwait(false); + var validateLockStatusTask = receiveLock.ValidateLockStatus(); + if (validateLockStatusTask.Status != TaskStatus.RanToCompletion) + await validateLockStatusTask.ConfigureAwait(false); await _receivePipe.Send(context).ConfigureAwait(false); await context.ReceiveCompleted.ConfigureAwait(false); - if (receiveLock != null) - await receiveLock.Complete().ConfigureAwait(false); + var receiveLockCompleteTask = receiveLock.Complete(); + if (receiveLockCompleteTask.Status != TaskStatus.RanToCompletion) + await receiveLockCompleteTask.ConfigureAwait(false); if (_observers.Count > 0) await _observers.PostReceive(context).ConfigureAwait(false); @@ -78,27 +82,35 @@ public async Task Dispatch(ReceiveContext context, ReceiveLockContext receiveLoc { try { - await receiveLock.Faulted(ex).ConfigureAwait(false); + var receiveLockFaultedTask = receiveLock.Faulted(ex); + if (receiveLockFaultedTask.Status != TaskStatus.RanToCompletion) + await receiveLockFaultedTask.ConfigureAwait(false); activity?.AddExceptionEvent(ex); + instrument?.AddException(ex); } catch (Exception releaseLockException) { var aggregateException = new AggregateException("ReceiveLock.Faulted threw an exception", releaseLockException, ex); activity?.AddExceptionEvent(aggregateException); + instrument?.AddException(aggregateException); throw aggregateException; } } else + { activity?.AddExceptionEvent(ex); + instrument?.AddException(ex); + } throw; } finally { activity?.Stop(); + instrument?.Stop(); await active.Complete().ConfigureAwait(false); } diff --git a/src/MassTransit/Transports/ReceiveTransport.cs b/src/MassTransit/Transports/ReceiveTransport.cs index 872d431be36..154a75db1ed 100644 --- a/src/MassTransit/Transports/ReceiveTransport.cs +++ b/src/MassTransit/Transports/ReceiveTransport.cs @@ -101,8 +101,6 @@ protected override async Task StopAgent(StopContext context) { LogContext.SetCurrentIfNull(_context.LogContext); - TransportLogMessages.StoppingReceiveTransport(_context.InputAddress); - if (_supervisor != null) await _supervisor.Stop(context).ConfigureAwait(false); @@ -147,7 +145,7 @@ async Task Run() if (retryContext == null && !policyContext.CanRetry(exception, out retryContext)) { - LogContext.Debug?.Log(exception, "ReceiveTransport Cannot Retry: {InputAddress}", _context.InputAddress); + LogContext.Error?.Log(exception, "ReceiveTransport Cannot Retry: {InputAddress}", _context.InputAddress); break; } } @@ -189,7 +187,9 @@ async Task RunTransport() if (_preStartPipe.IsNotEmpty()) await _supervisor.Send(_preStartPipe, Stopping).ConfigureAwait(false); - await _context.OnTransportStartup(_supervisor, Stopping).ConfigureAwait(false); + // Nothing connected to the pipe, so signal early we are available + if (!_context.ReceivePipe.Connected.IsCompleted) + await _context.OnTransportStartup(_supervisor, Stopping).ConfigureAwait(false); if (!IsStopping) await _supervisor.Send(_transportPipe, Stopped).ConfigureAwait(false); diff --git a/src/MassTransit/Transports/SendEndpointCache.cs b/src/MassTransit/Transports/SendEndpointCache.cs index ab0d3069649..a171796cfb0 100644 --- a/src/MassTransit/Transports/SendEndpointCache.cs +++ b/src/MassTransit/Transports/SendEndpointCache.cs @@ -14,7 +14,7 @@ public class SendEndpointCache : public SendEndpointCache() { - var options = new CacheOptions {Capacity = SendEndpointCacheDefaults.Capacity}; + var options = new CacheOptions { Capacity = SendEndpointCacheDefaults.Capacity }; var policy = new TimeToLiveCachePolicy>(SendEndpointCacheDefaults.MaxAge); _cache = new MassTransitCache, ITimeToLiveCacheValue>>(policy, options); diff --git a/src/MassTransit/Transports/SendTransport.cs b/src/MassTransit/Transports/SendTransport.cs index ba81490ae77..68c32f3dadc 100644 --- a/src/MassTransit/Transports/SendTransport.cs +++ b/src/MassTransit/Transports/SendTransport.cs @@ -84,6 +84,7 @@ public async Task Send(TContext context) SendContext sendContext = await _sendTransportContext.CreateSendContext(context, _message, _pipe, _cancellationToken).ConfigureAwait(false); StartedActivity? activity = LogContext.Current?.StartSendActivity(_sendTransportContext, sendContext); + StartedInstrument? instrument = LogContext.Current?.StartSendInstrument(_sendTransportContext, sendContext); try { if (_sendTransportContext.SendObservers.Count > 0) @@ -105,12 +106,14 @@ public async Task Send(TContext context) await _sendTransportContext.SendObservers.SendFault(sendContext, ex).ConfigureAwait(false); activity?.AddExceptionEvent(ex); + instrument?.AddException(ex); throw; } finally { activity?.Stop(); + instrument?.Stop(); } } diff --git a/src/MassTransit/Transports/TransportLogMessages.cs b/src/MassTransit/Transports/TransportLogMessages.cs index dee0acf5572..a15af1fb2b2 100644 --- a/src/MassTransit/Transports/TransportLogMessages.cs +++ b/src/MassTransit/Transports/TransportLogMessages.cs @@ -20,10 +20,7 @@ public static class TransportLogMessages "Disconnected: {Host}"); public static readonly LogMessage StoppingSendTransport = LogContext.Define(LogLevel.Debug, - "Stopping send transport: {Destination}"); - - public static readonly LogMessage StoppingReceiveTransport = LogContext.Define(LogLevel.Debug, - "Stopping receive transport: {InputAddress}"); + "Send Transport Stopping: {Destination}"); public static readonly LogMessage ConnectReceiveEndpoint = LogContext.Define(LogLevel.Debug, "Connect receive endpoint: {InputAddress}"); diff --git a/src/MassTransit/Transports/TransportStartExtensions.cs b/src/MassTransit/Transports/TransportStartExtensions.cs index 93ced48761e..25461e422de 100644 --- a/src/MassTransit/Transports/TransportStartExtensions.cs +++ b/src/MassTransit/Transports/TransportStartExtensions.cs @@ -12,17 +12,11 @@ public static async Task OnTransportStartup(this ReceiveEndpointContext conte CancellationToken cancellationToken) where T : class, PipeContext { - // Nothing connected to the pipe, so signal early we are available - if (!context.ReceivePipe.Connected.IsCompleted) - { - using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, supervisor.ConsumeStopping); - - var pipe = new WaitForConnectionPipe(context, tokenSource.Token); + using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, supervisor.ConsumeStopping); - await supervisor.Send(pipe, cancellationToken).ConfigureAwait(false); - } + var pipe = new WaitForConnectionPipe(context, tokenSource.Token); - await context.DependenciesReady.OrCanceled(cancellationToken).ConfigureAwait(false); + await supervisor.Send(pipe, cancellationToken).ConfigureAwait(false); } diff --git a/src/MassTransit/Util/ChannelExecutor.cs b/src/MassTransit/Util/ChannelExecutor.cs index f0c63a18cce..3a82eaadf30 100644 --- a/src/MassTransit/Util/ChannelExecutor.cs +++ b/src/MassTransit/Util/ChannelExecutor.cs @@ -60,7 +60,7 @@ public async ValueTask DisposeAsync() if (_disposed) return; - _channel.Writer.Complete(); + _channel.Writer.TryComplete(); await _readerTask.ConfigureAwait(false); @@ -121,7 +121,7 @@ async Task RunMethod() public async Task Run(Func> method, CancellationToken cancellationToken = default) { - var future = new Future(method, cancellationToken); + var future = new RunFuture(method, cancellationToken); await _channel.Writer.WriteAsync(future, cancellationToken).ConfigureAwait(false); @@ -250,6 +250,51 @@ public async Task Run() } + readonly struct RunFuture : + IFuture + { + readonly CancellationToken _cancellationToken; + readonly TaskCompletionSource _completion; + readonly Func> _method; + + public RunFuture(Func> method, CancellationToken cancellationToken) + { + _method = method; + _cancellationToken = cancellationToken; + _completion = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + /// + /// The post-execution result, which can be awaited + /// + public Task Completed => _completion.Task; + + public async Task Run() + { + if (_cancellationToken.IsCancellationRequested) + { + _completion.TrySetCanceled(_cancellationToken); + return; + } + + try + { + var result = await _method().ConfigureAwait(false); + + _completion.TrySetResult(result); + } + catch (OperationCanceledException exception) when (exception.CancellationToken == _cancellationToken) + { + _completion.TrySetCanceled(exception.CancellationToken); + } + catch (Exception exception) + { + _completion.TrySetException(exception); + } + } + } + + class SynchronousFuture : IFuture { diff --git a/src/MassTransit/Util/ChartTable.cs b/src/MassTransit/Util/ChartTable.cs index 61dcc1853a1..5e78084655b 100644 --- a/src/MassTransit/Util/ChartTable.cs +++ b/src/MassTransit/Util/ChartTable.cs @@ -9,7 +9,7 @@ namespace MassTransit.Util public class ChartTable { readonly int _chartWidth; - readonly IList _lines; + readonly List _lines; public ChartTable(int chartWidth = 60) { diff --git a/src/MassTransit/Util/PartitionChannelExecutorPool.cs b/src/MassTransit/Util/PartitionChannelExecutorPool.cs index a29d836197b..0d797755000 100644 --- a/src/MassTransit/Util/PartitionChannelExecutorPool.cs +++ b/src/MassTransit/Util/PartitionChannelExecutorPool.cs @@ -12,7 +12,7 @@ public class PartitionChannelExecutorPool : { readonly IHashGenerator _hashGenerator; readonly PartitionKeyProvider _partitionKeyProvider; - readonly Lazy[] _partitions; + readonly Lazy[] _partitions; public PartitionChannelExecutorPool(PartitionKeyProvider partitionKeyProvider, IHashGenerator hashGenerator, int concurrencyLimit, int concurrentDeliveryLimit = 1) @@ -20,13 +20,13 @@ public PartitionChannelExecutorPool(PartitionKeyProvider partitionKeyProvider _partitionKeyProvider = partitionKeyProvider; _hashGenerator = hashGenerator; _partitions = Enumerable.Range(0, concurrencyLimit) - .Select(x => new Lazy(() => new ChannelExecutor(concurrentDeliveryLimit))) + .Select(_ => new Lazy(() => new TaskExecutor(concurrentDeliveryLimit))) .ToArray(); } public async ValueTask DisposeAsync() { - foreach (Lazy partition in _partitions) + foreach (Lazy partition in _partitions) { if (partition.IsValueCreated) await partition.Value.DisposeAsync().ConfigureAwait(false); @@ -45,7 +45,7 @@ public Task Run(T partition, Func method, CancellationToken cancellationTo return executor.Run(method, cancellationToken); } - ChannelExecutor GetChannelExecutor(T partition) + TaskExecutor GetChannelExecutor(T partition) { if (_partitions.Length == 1) return _partitions[0].Value; diff --git a/src/MassTransit/Util/RollingTimer.cs b/src/MassTransit/Util/RollingTimer.cs index 23a765162f8..d2e9d944f7d 100644 --- a/src/MassTransit/Util/RollingTimer.cs +++ b/src/MassTransit/Util/RollingTimer.cs @@ -14,7 +14,7 @@ public class RollingTimer : readonly TimerCallback _callback; readonly object _lock = new object(); readonly object _state; - readonly TimeSpan _timeout; + TimeSpan _timeout; Timer _timer; int _triggered; @@ -65,10 +65,13 @@ public void Stop() /// /// Restarts the existing timer, creates and starts a new timer if it does not exist. /// - public void Restart() + public void Restart(TimeSpan? timeout = null) { lock (_lock) { + if (timeout.HasValue) + _timeout = timeout.Value; + if (_timer == null) StartInternal(); else diff --git a/src/MassTransit/Util/Scanning/AssemblyFinder.cs b/src/MassTransit/Util/Scanning/AssemblyFinder.cs index c6123e8528d..8219e162ce9 100644 --- a/src/MassTransit/Util/Scanning/AssemblyFinder.cs +++ b/src/MassTransit/Util/Scanning/AssemblyFinder.cs @@ -26,7 +26,7 @@ public static IEnumerable FindAssemblies(AssemblyLoadFailure loadFailu if (Path.IsPathRooted(binPath)) return FindAssemblies(binPath, loadFailure, includeExeFiles, filter); - string[] binPaths = binPath.Split(';'); + var binPaths = binPath.Split(';'); return binPaths.SelectMany(bin => { var path = Path.Combine(assemblyPath, bin); diff --git a/src/MassTransit/Util/Scanning/AssemblyScanTypeInfo.cs b/src/MassTransit/Util/Scanning/AssemblyScanTypeInfo.cs index 866281dfa77..9dae62bf4b5 100644 --- a/src/MassTransit/Util/Scanning/AssemblyScanTypeInfo.cs +++ b/src/MassTransit/Util/Scanning/AssemblyScanTypeInfo.cs @@ -76,7 +76,7 @@ IEnumerable SelectLists(TypeClassification classification) var open = classification.HasFlag(TypeClassification.Open); var closed = classification.HasFlag(TypeClassification.Closed); - if (open && closed || !open && !closed) + if ((open && closed) || (!open && !closed)) { yield return OpenTypes; yield return ClosedTypes; diff --git a/src/MassTransit/Util/Scanning/AssemblyScanner.cs b/src/MassTransit/Util/Scanning/AssemblyScanner.cs index a45cff72f0f..d64f274542c 100644 --- a/src/MassTransit/Util/Scanning/AssemblyScanner.cs +++ b/src/MassTransit/Util/Scanning/AssemblyScanner.cs @@ -40,7 +40,7 @@ public void AssemblyContainingType() public void AssemblyContainingType(Type type) { - _assemblies.Add(type.GetTypeInfo().Assembly); + _assemblies.Add(type.Assembly); } public void Exclude(Func exclude) @@ -184,7 +184,7 @@ static Assembly FindTheCallingAssembly() { var trace = new StackTrace(false); var thisAssembly = System.Reflection.Assembly.GetExecutingAssembly(); - var mtAssembly = typeof(IBus).GetTypeInfo().Assembly; + var mtAssembly = typeof(IBus).Assembly; Assembly callingAssembly = null; for (var i = 0; i < trace.FrameCount; i++) @@ -193,7 +193,7 @@ static Assembly FindTheCallingAssembly() var declaringType = frame.GetMethod().DeclaringType; if (declaringType != null) { - var assembly = declaringType.GetTypeInfo().Assembly; + var assembly = declaringType.Assembly; if (assembly != thisAssembly && assembly != mtAssembly) { callingAssembly = assembly; diff --git a/src/MassTransit/Util/Scanning/AssemblyTypeList.cs b/src/MassTransit/Util/Scanning/AssemblyTypeList.cs index bc19c3c982a..84ac2641b5d 100644 --- a/src/MassTransit/Util/Scanning/AssemblyTypeList.cs +++ b/src/MassTransit/Util/Scanning/AssemblyTypeList.cs @@ -3,14 +3,13 @@ using System; using System.Collections.Generic; using System.Linq; - using System.Reflection; public class AssemblyTypeList { - public readonly IList Abstract = new List(); - public readonly IList Concrete = new List(); - public readonly IList Interface = new List(); + public readonly List Abstract = new List(); + public readonly List Concrete = new List(); + public readonly List Interface = new List(); public IEnumerable> SelectTypes(TypeClassification classification) { @@ -42,11 +41,11 @@ public IEnumerable AllTypes() public void Add(Type type) { - if (type.GetTypeInfo().IsInterface) + if (type.IsInterface) Interface.Add(type); - else if (type.GetTypeInfo().IsAbstract) + else if (type.IsAbstract) Abstract.Add(type); - else if (type.GetTypeInfo().IsClass) + else if (type.IsClass) Concrete.Add(type); } } diff --git a/src/MassTransit/Util/SingleThreadedDictionary.cs b/src/MassTransit/Util/SingleThreadedDictionary.cs index 96015a5367e..fd711abd69b 100644 --- a/src/MassTransit/Util/SingleThreadedDictionary.cs +++ b/src/MassTransit/Util/SingleThreadedDictionary.cs @@ -66,7 +66,7 @@ public bool TryAdd(TKey key, Func valueFactory) return false; var value = valueFactory(key); - _dictionary = new Dictionary(_dictionary, _comparer) {[key] = value}; + _dictionary = new Dictionary(_dictionary, _comparer) { [key] = value }; return true; } diff --git a/src/MassTransit/Util/TaskExecutor.cs b/src/MassTransit/Util/TaskExecutor.cs new file mode 100644 index 00000000000..c73b0f6693d --- /dev/null +++ b/src/MassTransit/Util/TaskExecutor.cs @@ -0,0 +1,330 @@ +namespace MassTransit.Util; + +using System; +using System.Linq; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + + +/// +/// The successor to , now with a more optimized execution pipeline resulting in +/// lower memory usage and reduced overhead. +/// +public class TaskExecutor : + IAsyncDisposable +{ + readonly Task _readerTask; + readonly Channel _taskChannel; + bool _disposed; + + public TaskExecutor(int concurrencyLimit = 1) + { + if (concurrencyLimit < 1) + throw new ArgumentOutOfRangeException(nameof(concurrencyLimit), concurrencyLimit, "Must be >= 1"); + + _taskChannel = Channel.CreateUnbounded(new UnboundedChannelOptions + { + AllowSynchronousContinuations = true, + SingleReader = concurrencyLimit == 1, + SingleWriter = false + }); + + _readerTask = concurrencyLimit == 1 + ? Task.Run(() => SingleReader()) + : Task.WhenAll(Enumerable.Range(0, concurrencyLimit).Select(_ => Task.Run(() => MultipleReader()))); + } + + public TaskExecutor(int prefetchCount, int concurrencyLimit = 1) + { + if (concurrencyLimit < 1) + throw new ArgumentOutOfRangeException(nameof(concurrencyLimit), concurrencyLimit, "Must be >= 1"); + + _taskChannel = Channel.CreateBounded(new BoundedChannelOptions(prefetchCount) + { + AllowSynchronousContinuations = true, + SingleReader = concurrencyLimit == 1, + SingleWriter = false + }); + + _readerTask = concurrencyLimit == 1 + ? Task.Run(() => SingleReader()) + : Task.WhenAll(Enumerable.Range(0, concurrencyLimit).Select(_ => Task.Run(() => MultipleReader()))); + } + + public async ValueTask DisposeAsync() + { + if (_disposed) + return; + + _taskChannel.Writer.TryComplete(); + + await _readerTask.ConfigureAwait(false); + + _disposed = true; + } + + public async Task Run(Action method, CancellationToken cancellationToken = default) + { + var future = new ActionFuture(method, cancellationToken); + + await _taskChannel.Writer.WriteAsync(future, cancellationToken).ConfigureAwait(false); + + await future.Completed.ConfigureAwait(false); + } + + public async Task Run(Func method, CancellationToken cancellationToken = default) + { + var future = new TaskFuture(method, cancellationToken); + + await _taskChannel.Writer.WriteAsync(future, cancellationToken).ConfigureAwait(false); + + await future.Completed.ConfigureAwait(false); + } + + public async Task Push(Action method, CancellationToken cancellationToken = default) + { + var future = new ActionFuture(method, cancellationToken); + + await _taskChannel.Writer.WriteAsync(future, cancellationToken).ConfigureAwait(false); + } + + public async Task Push(Func method, CancellationToken cancellationToken = default) + { + var future = new TaskFuture(method, cancellationToken); + + await _taskChannel.Writer.WriteAsync(future, cancellationToken).ConfigureAwait(false); + } + + public async Task Run(Func method, CancellationToken cancellationToken = default) + { + var future = new FuncFuture(method, cancellationToken); + + await _taskChannel.Writer.WriteAsync(future, cancellationToken).ConfigureAwait(false); + + return await future.Completed.ConfigureAwait(false); + } + + public async Task Run(Func> method, CancellationToken cancellationToken = default) + { + var future = new TaskFuture(method, cancellationToken); + + await _taskChannel.Writer.WriteAsync(future, cancellationToken).ConfigureAwait(false); + + return await future.Completed.ConfigureAwait(false); + } + + async Task MultipleReader() + { + try + { + while (await _taskChannel.Reader.WaitToReadAsync().ConfigureAwait(false)) + { + if (!_taskChannel.Reader.TryRead(out var future)) + continue; + + await future.Run().ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + LogContext.Warning?.Log(exception, "MultipleReader faulted"); + } + } + + async Task SingleReader() + { + try + { + while (await _taskChannel.Reader.WaitToReadAsync().ConfigureAwait(false)) + { + if (!_taskChannel.Reader.TryPeek(out var future)) + continue; + + await future.Run().ConfigureAwait(false); + + await _taskChannel.Reader.ReadAsync().ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + LogContext.Warning?.Log(exception, "SingleReader faulted"); + } + } + + + interface IFuture + { + ValueTask Run(); + } + + + class BaseFuture + { + protected readonly CancellationToken CancellationToken; + protected readonly TaskCompletionSource Source; + + protected BaseFuture(CancellationToken cancellationToken) + { + CancellationToken = cancellationToken; + + Source = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + } + + protected bool IsCancellationRequested => CancellationToken.IsCancellationRequested; + + public Task Completed => Source.Task; + } + + + class BaseFuture : + BaseFuture + { + protected BaseFuture(CancellationToken cancellationToken) + : base(cancellationToken) + { + } + } + + + class TaskFuture : + BaseFuture, + IFuture + { + readonly Func> _method; + + public TaskFuture(Func> method, CancellationToken cancellationToken) + : base(cancellationToken) + { + _method = method; + } + + public async ValueTask Run() + { + if (IsCancellationRequested) + { + Source.SetException(new OperationCanceledException(CancellationToken)); + return; + } + + try + { + var result = await _method().ConfigureAwait(false); + + Source.SetResult(result); + } + catch (Exception exception) + { + Source.SetException(exception); + } + } + } + + + class TaskFuture : + BaseFuture, + IFuture + { + readonly Func _method; + + public TaskFuture(Func method, CancellationToken cancellationToken) + : base(cancellationToken) + { + _method = method; + } + + public async ValueTask Run() + { + if (IsCancellationRequested) + { + Source.SetException(new OperationCanceledException(CancellationToken)); + return; + } + + try + { + await _method().ConfigureAwait(false); + + Source.SetResult(true); + } + catch (Exception exception) + { + Source.SetException(exception); + } + } + } + + + class FuncFuture : + BaseFuture, + IFuture + { + readonly Func _method; + + public FuncFuture(Func method, CancellationToken cancellationToken) + : base(cancellationToken) + { + _method = method; + } + + public async ValueTask Run() + { + if (IsCancellationRequested) + { + Source.SetException(new OperationCanceledException(CancellationToken)); + return; + } + + try + { + var result = _method(); + + Source.SetResult(result); + } + catch (Exception exception) + { + Source.SetException(exception); + } + } + } + + + class ActionFuture : + BaseFuture, + IFuture + { + readonly Action _method; + + public ActionFuture(Action method, CancellationToken cancellationToken) + : base(cancellationToken) + { + _method = method; + } + + public async ValueTask Run() + { + if (IsCancellationRequested) + { + Source.SetException(new OperationCanceledException(CancellationToken)); + return; + } + + try + { + _method(); + + Source.SetResult(true); + } + catch (Exception exception) + { + Source.SetException(exception); + } + } + } +} diff --git a/src/MassTransit/Util/TaskUtil.cs b/src/MassTransit/Util/TaskUtil.cs index ec34998eee3..ed151a727bd 100644 --- a/src/MassTransit/Util/TaskUtil.cs +++ b/src/MassTransit/Util/TaskUtil.cs @@ -1,9 +1,15 @@ +#nullable enable namespace MassTransit.Util { using System; + using System.Collections.Generic; + using System.Diagnostics; + using System.Diagnostics.CodeAnalysis; + using System.Reflection; using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; + using Internals; public static class TaskUtil @@ -18,7 +24,7 @@ public static class TaskUtil /// /// /// - public static Task Default() + public static Task Default() { return Cached.DefaultValueTask; } @@ -82,14 +88,14 @@ public static CancellationTokenRegistration RegisterTask(this CancellationToken if (!cancellationToken.CanBeCanceled) throw new ArgumentException("The cancellationToken must support cancellation", nameof(cancellationToken)); - TaskCompletionSource source = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); + var source = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); cancelTask = source.Task; return cancellationToken.Register(SetCompleted, source); } - static void SetCompleted(object obj) + static void SetCompleted(object? obj) { if (obj is TaskCompletionSource source) source.SetCompleted(); @@ -103,7 +109,7 @@ public static CancellationTokenRegistration RegisterIfCanBeCanceled(this Cancell return default; } - static void Cancel(object obj) + static void Cancel(object? obj) { if (obj is CancellationTokenSource source) source.Cancel(); @@ -123,34 +129,24 @@ public static void Await(Func taskFactory, CancellationToken cancellationT if (taskFactory == null) throw new ArgumentNullException(nameof(taskFactory)); - var previousContext = SynchronizationContext.Current; - try + using (InitializeExecutionEnvironment()) { - using var syncContext = new SingleThreadSynchronizationContext(cancellationToken); - SynchronizationContext.SetSynchronizationContext(syncContext); - - var t = taskFactory(); - if (t == null) + var task = taskFactory(); + if (task == null) throw new InvalidOperationException("The taskFactory must return a Task"); - var awaiter = t.GetAwaiter(); + if (cancellationToken.CanBeCanceled) + task = task.OrCanceled(cancellationToken); - while (!awaiter.IsCompleted) + var awaiter = new TaskAwaitAdapter(task); + if (!awaiter.IsCompleted) { - if (cancellationToken.IsCancellationRequested) - throw new OperationCanceledException("The task was not completed before being canceled"); - - Thread.Sleep(3); + var dispatch = SynchronizationDispatcher.FromCurrentSynchronizationContext(); + dispatch.WaitForCompletion(awaiter); } - syncContext.SetComplete(); - awaiter.GetResult(); } - finally - { - SynchronizationContext.SetSynchronizationContext(previousContext); - } } public static void Await(Task task, CancellationToken cancellationToken = default) @@ -158,30 +154,21 @@ public static void Await(Task task, CancellationToken cancellationToken = defaul if (task == null) throw new ArgumentNullException(nameof(task)); - var previousContext = SynchronizationContext.Current; - try + using (InitializeExecutionEnvironment()) { - using var syncContext = new SingleThreadSynchronizationContext(cancellationToken); - SynchronizationContext.SetSynchronizationContext(syncContext); + if (cancellationToken.CanBeCanceled) + task = task.OrCanceled(cancellationToken); - var awaiter = task.GetAwaiter(); + var awaiter = new TaskAwaitAdapter(task); - while (!awaiter.IsCompleted) + if (!awaiter.IsCompleted) { - if (cancellationToken.IsCancellationRequested) - throw new OperationCanceledException("The task was not completed before being canceled"); - - Thread.Sleep(3); + var waitStrategy = SynchronizationDispatcher.FromCurrentSynchronizationContext(); + waitStrategy.WaitForCompletion(awaiter); } - syncContext.SetComplete(); - awaiter.GetResult(); } - finally - { - SynchronizationContext.SetSynchronizationContext(previousContext); - } } public static T Await(Func> taskFactory, CancellationToken cancellationToken = default) @@ -189,34 +176,67 @@ public static T Await(Func> taskFactory, CancellationToken cancellati if (taskFactory == null) throw new ArgumentNullException(nameof(taskFactory)); - var previousContext = SynchronizationContext.Current; - try + using (InitializeExecutionEnvironment()) { - using var syncContext = new SingleThreadSynchronizationContext(cancellationToken); - SynchronizationContext.SetSynchronizationContext(syncContext); - - Task t = taskFactory(); - if (t == null) + Task? task = taskFactory(); + if (task == null) throw new InvalidOperationException("The taskFactory must return a Task"); - TaskAwaiter awaiter = t.GetAwaiter(); + if (cancellationToken.CanBeCanceled) + task = task.OrCanceled(cancellationToken); - while (!awaiter.IsCompleted) + var awaiter = new TaskAwaitAdapter(task); + if (!awaiter.IsCompleted) { - if (cancellationToken.IsCancellationRequested) - throw new OperationCanceledException("The task was not completed before being canceled"); - - Thread.Sleep(3); + var dispatch = SynchronizationDispatcher.FromCurrentSynchronizationContext(); + dispatch.WaitForCompletion(awaiter); } - syncContext.SetComplete(); - - return awaiter.GetResult(); + return awaiter.GetResultOfT(); } - finally + } + + static void ContinueOnSameSynchronizationContext(AwaitAdapter awaiter, Action continuation) + { + if (continuation is null) + throw new ArgumentNullException(nameof(continuation)); + + var context = SynchronizationContext.Current; + + awaiter.OnCompleted(() => { - SynchronizationContext.SetSynchronizationContext(previousContext); + if (context is null || SynchronizationContext.Current == context) + continuation.Invoke(); + else + context.Post(_ => continuation.Invoke(), continuation); + }); + } + + static IDisposable? InitializeExecutionEnvironment() + { + if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA) + { + var context = SynchronizationContext.Current; + if (context is null || context.GetType() == typeof(SynchronizationContext)) + { + var singleThreadedContext = new SingleThreadedSynchronizationContext(TimeSpan.FromSeconds(10)); + + SetSynchronizationContext(singleThreadedContext); + + return new DisposableAction(() => + { + SetSynchronizationContext(context); + singleThreadedContext.Dispose(); + }); + } } + + return null; + } + + static void SetSynchronizationContext(SynchronizationContext? syncContext) + { + SynchronizationContext.SetSynchronizationContext(syncContext); } @@ -230,7 +250,7 @@ static class Cached static class Cached { - public static readonly Task DefaultValueTask = Task.FromResult(default); + public static readonly Task DefaultValueTask = Task.FromResult(default); public static readonly Task CanceledTask = GetCanceledTask(); static Task GetCanceledTask() @@ -242,50 +262,446 @@ static Task GetCanceledTask() } - sealed class SingleThreadSynchronizationContext : + sealed class SingleThreadedSynchronizationContext : SynchronizationContext, IDisposable { - readonly CancellationToken _cancellationToken; - readonly ChannelExecutor _executor; - bool _completed; + const string ShutdownTimeoutMessage = "Work posted to the synchronization context did not complete within ten seconds."; + + readonly Queue _queue = new(); - public SingleThreadSynchronizationContext(CancellationToken cancellationToken) + readonly TimeSpan _shutdownTimeout; + Status _status; + Stopwatch? _timeSinceShutdown; + + public SingleThreadedSynchronizationContext(TimeSpan shutdownTimeout) { - _cancellationToken = cancellationToken; - _executor = new ChannelExecutor(1); + _shutdownTimeout = shutdownTimeout; } public void Dispose() { - _executor.DisposeAsync().GetAwaiter().GetResult(); + ShutDown(); } - public override void Post(SendOrPostCallback callback, object state) + public override void Post(SendOrPostCallback d, object? state) { - if (callback == null) - throw new ArgumentNullException(nameof(callback)); + if (d == null) + throw new ArgumentNullException(nameof(d)); - if (_completed) - throw new TaskSchedulerException("The synchronization context was already completed"); + AddWork(new ScheduledWork(d, state, null)); + } - try + public override void Send(SendOrPostCallback d, object? state) + { + if (d == null) + throw new ArgumentNullException(nameof(d)); + + if (Current == this) + d.Invoke(state); + else { - _executor?.Push(async () => callback(state), _cancellationToken); + using var finished = new ManualResetEventSlim(); + + AddWork(new ScheduledWork(d, state, finished)); + finished.Wait(); } - catch (InvalidOperationException) + } + + void AddWork(ScheduledWork work) + { + lock (_queue) { + switch (_status) + { + case Status.ShuttingDown: + if (_timeSinceShutdown!.Elapsed < _shutdownTimeout) + break; + goto case Status.ShutDown; + + case Status.ShutDown: + throw ErrorAndGetExceptionForShutdownTimeout(); + } + + _queue.Enqueue(work); + Monitor.Pulse(_queue); } } - public override void Send(SendOrPostCallback d, object state) + public void ShutDown() { - throw new NotSupportedException("Synchronously sending is not supported."); + lock (_queue) + { + switch (_status) + { + case Status.ShuttingDown: + case Status.ShutDown: + return; + } + + _timeSinceShutdown = Stopwatch.StartNew(); + _status = Status.ShuttingDown; + Monitor.Pulse(_queue); + } + } + + public void Run() + { + lock (_queue) + { + switch (_status) + { + case Status.Running: + throw new InvalidOperationException("SingleThreadedSynchronizationContext.Run may not be reentered."); + + case Status.ShuttingDown: + case Status.ShutDown: + throw new InvalidOperationException("This SingleThreadedSynchronizationContext has been shut down."); + } + + _status = Status.Running; + } + + while (TryTake(out var scheduledWork)) + scheduledWork.Execute(); } - public void SetComplete() + bool TryTake(out ScheduledWork scheduledWork) + { + lock (_queue) + { + while (_queue.Count == 0) + { + if (_status == Status.ShuttingDown) + { + _status = Status.ShutDown; + scheduledWork = default; + return false; + } + + Monitor.Wait(_queue); + } + + if (_status == Status.ShuttingDown && _timeSinceShutdown!.Elapsed > _shutdownTimeout) + { + _status = Status.ShutDown; + throw ErrorAndGetExceptionForShutdownTimeout(); + } + + scheduledWork = _queue.Dequeue(); + } + + return true; + } + + static Exception ErrorAndGetExceptionForShutdownTimeout() + { + return new InvalidOperationException(ShutdownTimeoutMessage); + } + + + struct ScheduledWork + { + readonly SendOrPostCallback _callback; + readonly object? _state; + readonly ManualResetEventSlim? _finished; + + public ScheduledWork(SendOrPostCallback callback, object? state, ManualResetEventSlim? finished) + { + _callback = callback; + _state = state; + _finished = finished; + } + + public void Execute() + { + _callback.Invoke(_state); + _finished?.Set(); + } + } + + + enum Status + { + NotStarted, + Running, + ShuttingDown, + ShutDown + } + } + + + abstract class AwaitAdapter + { + public abstract bool IsCompleted { get; } + public abstract void OnCompleted(Action action); + public abstract void GetResult(); + } + + + sealed class TaskAwaitAdapter : + AwaitAdapter + { + readonly TaskAwaiter _awaiter; + + public TaskAwaitAdapter(Task task) + { + _awaiter = task.GetAwaiter(); + } + + public override bool IsCompleted => _awaiter.IsCompleted; + + public override void OnCompleted(Action action) + { + _awaiter.UnsafeOnCompleted(action); + } + + public override void GetResult() + { + _awaiter.GetResult(); + } + } + + + sealed class TaskAwaitAdapter : + AwaitAdapter + { + readonly TaskAwaiter _awaiter; + + public TaskAwaitAdapter(Task task) + { + _awaiter = task.GetAwaiter(); + } + + public override bool IsCompleted => _awaiter.IsCompleted; + + public override void OnCompleted(Action action) + { + _awaiter.UnsafeOnCompleted(action); + } + + public override void GetResult() + { + _awaiter.GetResult(); + } + + public T GetResultOfT() + { + return _awaiter.GetResult(); + } + } + + + abstract class SynchronizationDispatcher + { + public abstract void WaitForCompletion(AwaitAdapter awaiter); + + public static SynchronizationDispatcher FromCurrentSynchronizationContext() + { + var context = SynchronizationContext.Current; + + if (context is SingleThreadedSynchronizationContext) + return SingleThreadedSynchronizationDispatcher.Instance; + + return WindowsFormsSynchronizationDispatcher.GetIfApplicable() + ?? WpfSynchronizationDispatcher.GetIfApplicable() + ?? NoSynchronizationDispatcher.Instance; + } + + + sealed class NoSynchronizationDispatcher : + SynchronizationDispatcher + { + public static readonly NoSynchronizationDispatcher Instance = new(); + + NoSynchronizationDispatcher() + { + } + + public override void WaitForCompletion(AwaitAdapter awaiter) + { + awaiter.GetResult(); + } + } + + + sealed class WindowsFormsSynchronizationDispatcher : + SynchronizationDispatcher + { + static WindowsFormsSynchronizationDispatcher? _instance; + readonly Action _applicationExit; + + readonly Action _applicationRun; + + WindowsFormsSynchronizationDispatcher(Action applicationRun, Action applicationExit) + { + _applicationRun = applicationRun; + _applicationExit = applicationExit; + } + + public static SynchronizationDispatcher? GetIfApplicable() + { + if (!IsApplicable(SynchronizationContext.Current)) + return null; + + if (_instance is null) + { + var applicationType = + SynchronizationContext.Current.GetType().Assembly.GetType("System.Windows.Forms.Application", true)!; + + var applicationRun = (Action)applicationType + .GetMethod("Run", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, null, Type.EmptyTypes, null)! + .CreateDelegate(typeof(Action)); + + var applicationExit = (Action)applicationType + .GetMethod("Exit", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, null, Type.EmptyTypes, null)! + .CreateDelegate(typeof(Action)); + + _instance = new WindowsFormsSynchronizationDispatcher(applicationRun, applicationExit); + } + + return _instance; + } + + static bool IsApplicable([NotNullWhen(true)] SynchronizationContext? context) + { + return context?.GetType().FullName == "System.Windows.Forms.WindowsFormsSynchronizationContext"; + } + + public override void WaitForCompletion(AwaitAdapter awaiter) + { + var context = SynchronizationContext.Current; + + if (!IsApplicable(context)) + throw new InvalidOperationException("This dispatch must only be used from a WindowsFormsSynchronizationContext."); + + if (awaiter.IsCompleted) + return; + + context.Post(_ => ContinueOnSameSynchronizationContext(awaiter, _applicationExit), awaiter); + + try + { + _applicationRun.Invoke(); + } + finally + { + SynchronizationContext.SetSynchronizationContext(context); + } + } + } + + + sealed class WpfSynchronizationDispatcher : + SynchronizationDispatcher + { + static WpfSynchronizationDispatcher? _instance; + readonly MethodInfo _dispatcherFrameSetContinueProperty; + readonly Type _dispatcherFrameType; + + readonly MethodInfo _dispatcherPushFrame; + + WpfSynchronizationDispatcher(MethodInfo dispatcherPushFrame, + MethodInfo dispatcherFrameSetContinueProperty, + Type dispatcherFrameType) + { + _dispatcherPushFrame = dispatcherPushFrame; + _dispatcherFrameSetContinueProperty = dispatcherFrameSetContinueProperty; + _dispatcherFrameType = dispatcherFrameType; + } + + public static SynchronizationDispatcher? GetIfApplicable() + { + var context = SynchronizationContext.Current; + + if (!IsApplicable(context)) + return null; + + if (_instance is null) + { + var assemblyType = context.GetType().Assembly; + var dispatcherType = assemblyType.GetType("System.Windows.Threading.Dispatcher", true)!; + var dispatcherFrameType = assemblyType.GetType("System.Windows.Threading.DispatcherFrame", true)!; + + var dispatcherPushFrame = dispatcherType + .GetMethod("PushFrame", BindingFlags.Public | BindingFlags.Static | BindingFlags.DeclaredOnly, null, new[] { dispatcherFrameType }, + null)!; + + var dispatcherSetFrameContinue = dispatcherFrameType + .GetProperty("Continue")? + .GetSetMethod()!; + + _instance = new WpfSynchronizationDispatcher( + dispatcherPushFrame, + dispatcherSetFrameContinue, + dispatcherFrameType); + } + + return _instance; + } + + static bool IsApplicable([NotNullWhen(true)] SynchronizationContext? context) + { + return context?.GetType().FullName == "System.Windows.Threading.DispatcherSynchronizationContext"; + } + + public override void WaitForCompletion(AwaitAdapter awaiter) + { + var context = SynchronizationContext.Current; + + if (!IsApplicable(context)) + throw new InvalidOperationException("This dispatch must only be used from a DispatcherSynchronizationContext."); + + if (awaiter.IsCompleted) + return; + + var frame = Activator.CreateInstance(_dispatcherFrameType, true); + + context.Post(_ => ContinueOnSameSynchronizationContext(awaiter, () => _dispatcherFrameSetContinueProperty.Invoke(frame, [false])), awaiter); + + _dispatcherPushFrame.Invoke(null, [frame]); + } + } + } + + + sealed class SingleThreadedSynchronizationDispatcher : + SynchronizationDispatcher + { + public static readonly SingleThreadedSynchronizationDispatcher Instance = new(); + + SingleThreadedSynchronizationDispatcher() + { + } + + public override void WaitForCompletion(AwaitAdapter awaiter) + { + var context = SynchronizationContext.Current as SingleThreadedSynchronizationContext + ?? throw new InvalidOperationException("This dispatch must only be used from a SingleThreadedSynchronizationContext."); + + if (awaiter.IsCompleted) + return; + + context.Post(_ => ContinueOnSameSynchronizationContext(awaiter, context.ShutDown), awaiter); + + context.Run(); + } + } + + + sealed class DisposableAction : + IDisposable + { + Action? _action; + + public DisposableAction(Action action) + { + _action = action; + } + + public void Dispose() { - _completed = true; + Interlocked.Exchange(ref _action, null)?.Invoke(); } } } diff --git a/src/MassTransit/Util/TextTable.cs b/src/MassTransit/Util/TextTable.cs index 4d8c14a9686..8ed7316534f 100644 --- a/src/MassTransit/Util/TextTable.cs +++ b/src/MassTransit/Util/TextTable.cs @@ -29,8 +29,8 @@ public class TextTable typeof(float) }; - readonly IList _columns; - readonly IList _rows; + readonly List _columns; + readonly List _rows; Type[] _columnTypes; public TextTable(params string[] columns) diff --git a/src/NewId/NewId.csproj b/src/NewId/NewId.csproj deleted file mode 100644 index f424e2e9a4a..00000000000 --- a/src/NewId/NewId.csproj +++ /dev/null @@ -1,24 +0,0 @@ - - - - - netstandard2.0 - MassTransit - - - - MassTransit;NewId - $(Description) - - - - - - - - - - - - - diff --git a/src/Persistence/MassTransit.AmazonS3/AmazonS3/MessageData/AmazonS3MessageDataRepository.cs b/src/Persistence/MassTransit.AmazonS3/AmazonS3/MessageData/AmazonS3MessageDataRepository.cs new file mode 100644 index 00000000000..520410702f5 --- /dev/null +++ b/src/Persistence/MassTransit.AmazonS3/AmazonS3/MessageData/AmazonS3MessageDataRepository.cs @@ -0,0 +1,151 @@ +namespace MassTransit.AmazonS3.MessageData +{ + using System; + using System.Collections.Generic; + using System.IO; + using System.Threading; + using System.Threading.Tasks; + using Amazon.S3; + using Amazon.S3.Model; + using Amazon.S3.Transfer; + using Amazon.S3.Util; + using Util; + + + public class AmazonS3MessageDataRepository : + IMessageDataRepository, + IBusObserver + { + const string RuleName = "s3-messagedata-rule"; + readonly string _bucket; + readonly IAmazonS3 _s3Client; + + public AmazonS3MessageDataRepository(string bucket) + : this(new AmazonS3Client(), bucket) + { + } + + public AmazonS3MessageDataRepository(AmazonS3Config config, string bucket) + : this(new AmazonS3Client(config), bucket) + { + } + + public AmazonS3MessageDataRepository(IAmazonS3 client, string bucket) + { + _s3Client = client; + _bucket = bucket; + } + + public void PostCreate(IBus bus) + { + } + + public void CreateFaulted(Exception exception) + { + } + + public Task PreStop(IBus bus) + { + return Task.CompletedTask; + } + + public Task PostStart(IBus bus, Task busReady) + { + return Task.CompletedTask; + } + + public Task StartFaulted(IBus bus, Exception exception) + { + return Task.CompletedTask; + } + + public async Task PreStart(IBus bus) + { + try + { + var bucketExists = await AmazonS3Util.DoesS3BucketExistV2Async(_s3Client, _bucket); + if (!bucketExists) + { + try + { + await _s3Client.PutBucketAsync(new PutBucketRequest + { + BucketName = _bucket, + UseClientRegion = true + }); + } + catch (AmazonS3Exception exception) + { + LogContext.Warning?.Log(exception, "Amazon S3 Bucket does not exist: {Address}", _bucket); + } + } + + if (MessageDataDefaults.TimeToLive != null && MessageDataDefaults.TimeToLive.Value.Days > 0) + { + // Do no delete life cycle rule if TimeToLive is not available. Allow user to create rule in S3 console. + await _s3Client.DeleteLifecycleConfigurationAsync(_bucket); + await _s3Client.PutLifecycleConfigurationAsync(new PutLifecycleConfigurationRequest + { + BucketName = _bucket, + Configuration = new LifecycleConfiguration + { + Rules = new List + { + new LifecycleRule + { + Id = RuleName, + Expiration = new LifecycleRuleExpiration { Days = MessageDataDefaults.TimeToLive.Value.Days } + } + } + } + }); + } + } + catch (Exception exception) + { + LogContext.Error?.Log(exception, "S3 Storage failure."); + } + } + + public Task PostStop(IBus bus) + { + return Task.CompletedTask; + } + + public Task StopFaulted(IBus bus, Exception exception) + { + return Task.CompletedTask; + } + + public async Task Get(Uri address, CancellationToken cancellationToken = new CancellationToken()) + { + var filePath = ParseFilePath(address); + var transferUtility = new TransferUtility(_s3Client); + return await transferUtility.OpenStreamAsync(_bucket, filePath, cancellationToken); + } + + public async Task Put(Stream stream, TimeSpan? timeToLive = null, CancellationToken cancellationToken = new CancellationToken()) + { + var filePath = FormatUtil.Formatter.Format(NewId.Next().ToSequentialGuid().ToByteArray()); + var transferUtility = new TransferUtility(_s3Client); + await transferUtility.UploadAsync(stream, _bucket, filePath, cancellationToken); + return new Uri($"urn:file:{filePath.Replace(Path.DirectorySeparatorChar, ':')}"); + } + + static string ParseFilePath(Uri address) + { + if (address.Scheme != "urn") + throw new ArgumentException("The address must be a urn"); + + var parts = address.Segments[0].Split(':'); + if (parts[0] != "file") + throw new ArgumentException("The address must be a urn:file"); + + var length = parts.Length - 1; + var elements = new string[length]; + Array.Copy(parts, 1, elements, 0, length); + + return Path.Combine(elements); + } + } +} diff --git a/src/Persistence/MassTransit.AmazonS3/Configuration/AmazonS3ClientExtensions.cs b/src/Persistence/MassTransit.AmazonS3/Configuration/AmazonS3ClientExtensions.cs new file mode 100644 index 00000000000..f26b0c468e9 --- /dev/null +++ b/src/Persistence/MassTransit.AmazonS3/Configuration/AmazonS3ClientExtensions.cs @@ -0,0 +1,14 @@ +namespace MassTransit +{ + using Amazon.S3; + using AmazonS3.MessageData; + + + public static class AmazonS3ClientExtensions + { + public static AmazonS3MessageDataRepository CreateMessageDataRepository(this AmazonS3Client client, string bucket) + { + return new AmazonS3MessageDataRepository(client, bucket); + } + } +} diff --git a/src/Persistence/MassTransit.AmazonS3/Configuration/AmazonS3MessageDataRepositorySelectorExtensions.cs b/src/Persistence/MassTransit.AmazonS3/Configuration/AmazonS3MessageDataRepositorySelectorExtensions.cs new file mode 100644 index 00000000000..a1de06a92ab --- /dev/null +++ b/src/Persistence/MassTransit.AmazonS3/Configuration/AmazonS3MessageDataRepositorySelectorExtensions.cs @@ -0,0 +1,52 @@ +namespace MassTransit +{ + using System; + using Amazon.S3; + using AmazonS3.MessageData; + using Configuration; + + + public static class AmazonS3MessageDataRepositorySelectorExtensions + { + /// + /// Use Amazon S3 Storage for message data storage + /// + /// + /// Bucket for s3. + /// + /// + public static IMessageDataRepository AmazonS3(this IMessageDataRepositorySelector selector, string bucket) + { + if (selector is null) + throw new ArgumentNullException(nameof(selector)); + + if (string.IsNullOrEmpty(bucket)) + throw new ArgumentNullException(nameof(bucket)); + + var repository = new AmazonS3MessageDataRepository(bucket); + + return repository; + } + + /// + /// Use Amazon S3 Storage for message data storage + /// + /// + /// Bucket for s3. + /// Amazon s3 config. + /// + /// + public static IMessageDataRepository AmazonS3(this IMessageDataRepositorySelector selector, string bucket, AmazonS3Config config) + { + if (selector is null) + throw new ArgumentNullException(nameof(selector)); + + if (string.IsNullOrEmpty(bucket)) + throw new ArgumentNullException(nameof(bucket)); + + var repository = new AmazonS3MessageDataRepository(config, bucket); + + return repository; + } + } +} diff --git a/src/Persistence/MassTransit.AmazonS3/MassTransit.AmazonS3.csproj b/src/Persistence/MassTransit.AmazonS3/MassTransit.AmazonS3.csproj new file mode 100644 index 00000000000..9283c85660a --- /dev/null +++ b/src/Persistence/MassTransit.AmazonS3/MassTransit.AmazonS3.csproj @@ -0,0 +1,28 @@ + + + + + + netstandard2.0;net6.0;net8.0 + + + + $(TargetFrameworks);net472 + + + + MassTransit + + + + MassTransit.AmazonS3 + MassTransit.AmazonS3 + MassTransit;S3;Storage;ServiceBus + MassTransit S3 Storage persistence support; $(Description) + + + + + + + diff --git a/src/Persistence/MassTransit.AmazonS3/MassTransit.AmazonS3.csproj.DotSettings b/src/Persistence/MassTransit.AmazonS3/MassTransit.AmazonS3.csproj.DotSettings new file mode 100644 index 00000000000..71ef70e5bcb --- /dev/null +++ b/src/Persistence/MassTransit.AmazonS3/MassTransit.AmazonS3.csproj.DotSettings @@ -0,0 +1,5 @@ + + True diff --git a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/NewtonsoftJsonCosmosClientFactory.cs b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/NewtonsoftJsonCosmosClientFactory.cs index 09fa457027b..811382874e4 100644 --- a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/NewtonsoftJsonCosmosClientFactory.cs +++ b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/NewtonsoftJsonCosmosClientFactory.cs @@ -6,6 +6,7 @@ namespace MassTransit.AzureCosmos using System.Linq; using Internals; using Microsoft.Azure.Cosmos; + using Microsoft.Extensions.Options; using Newtonsoft.Json; using Saga; @@ -19,14 +20,16 @@ public class NewtonsoftJsonCosmosClientFactory : IDisposable { readonly CosmosAuthSettings _authSettings; + readonly CosmosClientOptions _clientOptions; readonly ConcurrentDictionary> _clients; - public NewtonsoftJsonCosmosClientFactory(CosmosAuthSettings authSettings) + public NewtonsoftJsonCosmosClientFactory(CosmosAuthSettings authSettings, IOptions clientOptions) { if (authSettings == null) throw new ArgumentNullException(nameof(authSettings)); _authSettings = authSettings; + _clientOptions = clientOptions.Value; _clients = new ConcurrentDictionary>(); } @@ -51,13 +54,11 @@ Lazy CreateClient() { return new Lazy(() => { - var clientOptions = new CosmosClientOptions(); - var serializerSettings = GetSerializerSettingsIfNeeded(); if (serializerSettings != null) - clientOptions.Serializer = new NewtonsoftJsonCosmosSerializer(serializerSettings); + _clientOptions.Serializer = new NewtonsoftJsonCosmosSerializer(serializerSettings); - return CosmosClientFactory.CreateClient(_authSettings, clientOptions); + return CosmosClientFactory.CreateClient(_authSettings, _clientOptions); }); } diff --git a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepository.cs b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepository.cs index 2daccda9bef..1ae79e862e7 100644 --- a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepository.cs +++ b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepository.cs @@ -32,7 +32,7 @@ public static ISagaRepository Create(CosmosClient client, string database var repositoryFactory = new CosmosSagaRepositoryContextFactory(databaseContext, consumeContextFactory); - return new SagaRepository(repositoryFactory); + return new SagaRepository(repositoryFactory, repositoryFactory, repositoryFactory); } } } diff --git a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepositoryContext.cs b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepositoryContext.cs index 67d31689de2..d5f8459b757 100644 --- a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepositoryContext.cs @@ -177,7 +177,8 @@ async Task> CreateSagaConsumeContext(Respons public class CosmosSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext + QuerySagaRepositoryContext, + LoadSagaRepositoryContext where TSaga : class, ISaga { readonly DatabaseContext _context; @@ -188,6 +189,26 @@ public CosmosSagaRepositoryContext(DatabaseContext context, CancellationT _context = context; } + public async Task Load(Guid correlationId) + { + try + { + var options = _context.GetItemRequestOptions(); + + var id = correlationId.ToString(); + var partitionKey = new PartitionKey(id); + + ItemResponse response = await _context.Container.ReadItemAsync(id, partitionKey, options, CancellationToken) + .ConfigureAwait(false); + + return response.Resource; + } + catch (CosmosException e) when (e.StatusCode == HttpStatusCode.NotFound) + { + return default; + } + } + public async Task> Query(ISagaQuery query, CancellationToken cancellationToken = default) { QueryRequestOptions queryOptions = null; @@ -208,25 +229,5 @@ public async Task> Query(ISagaQuery que return new LoadedSagaRepositoryQueryContext(this, instances); } - - public async Task Load(Guid correlationId) - { - try - { - var options = _context.GetItemRequestOptions(); - - var id = correlationId.ToString(); - var partitionKey = new PartitionKey(id); - - ItemResponse response = await _context.Container.ReadItemAsync(id, partitionKey, options, CancellationToken) - .ConfigureAwait(false); - - return response.Resource; - } - catch (CosmosException e) when (e.StatusCode == HttpStatusCode.NotFound) - { - return default; - } - } } } diff --git a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepositoryContextFactory.cs index ad7bea021c0..ddf8734a101 100644 --- a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/Saga/CosmosSagaRepositoryContextFactory.cs @@ -9,7 +9,9 @@ namespace MassTransit.AzureCosmos.Saga public class CosmosSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + IQuerySagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISaga { readonly DatabaseContext _context; @@ -21,6 +23,18 @@ public CosmosSagaRepositoryContextFactory(DatabaseContext context, ISagaC _factory = factory; } + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + public void Probe(ProbeContext context) { context.Add("persistence", "cosmosdb"); @@ -50,7 +64,7 @@ public async Task SendQuery(ConsumeContext context, ISagaQuery quer await next.Send(queryContext).ConfigureAwait(false); } - public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + Task ExecuteAsyncMethod(Func, Task> asyncMethod, CancellationToken cancellationToken) where T : class { var repositoryContext = new CosmosSagaRepositoryContext(_context, cancellationToken); diff --git a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/SystemTextJsonCosmosClientFactory.cs b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/SystemTextJsonCosmosClientFactory.cs index 0b6d969d619..263eb2c39fd 100644 --- a/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/SystemTextJsonCosmosClientFactory.cs +++ b/src/Persistence/MassTransit.Azure.Cosmos/AzureCosmos/SystemTextJsonCosmosClientFactory.cs @@ -5,6 +5,7 @@ namespace MassTransit.AzureCosmos using System.Collections.Generic; using System.Text.Json; using Microsoft.Azure.Cosmos; + using Microsoft.Extensions.Options; using Saga; using Serialization; @@ -18,15 +19,17 @@ public class SystemTextJsonCosmosClientFactory : IDisposable { readonly CosmosAuthSettings _authSettings; - readonly ConcurrentDictionary> _clients; + readonly CosmosClientOptions _clientOptions; readonly JsonNamingPolicy _namingPolicy; + readonly ConcurrentDictionary> _clients; - public SystemTextJsonCosmosClientFactory(CosmosAuthSettings authSettings, JsonNamingPolicy namingPolicy) + public SystemTextJsonCosmosClientFactory(CosmosAuthSettings authSettings, IOptions clientOptions, JsonNamingPolicy namingPolicy) { if (authSettings == null) throw new ArgumentNullException(nameof(authSettings)); _authSettings = authSettings; + _clientOptions = clientOptions.Value; _namingPolicy = namingPolicy; _clients = new ConcurrentDictionary>(); @@ -52,13 +55,11 @@ Lazy CreateClient() { return new Lazy(() => { - var clientOptions = new CosmosClientOptions(); - var options = GetSerializerOptions(); if (options != null) - clientOptions.Serializer = new SystemTextJsonCosmosSerializer(options); + _clientOptions.Serializer = new SystemTextJsonCosmosSerializer(options); - return CosmosClientFactory.CreateClient(_authSettings, clientOptions); + return CosmosClientFactory.CreateClient(_authSettings, _clientOptions); }); } diff --git a/src/Persistence/MassTransit.Azure.Cosmos/Configuration/Configuration/CosmosSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.Azure.Cosmos/Configuration/Configuration/CosmosSagaRepositoryConfigurator.cs index 11ab52686fb..b105977281e 100644 --- a/src/Persistence/MassTransit.Azure.Cosmos/Configuration/Configuration/CosmosSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.Azure.Cosmos/Configuration/Configuration/CosmosSagaRepositoryConfigurator.cs @@ -9,6 +9,7 @@ namespace MassTransit.Configuration using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; + using Microsoft.Extensions.Options; using Saga; using Serialization; @@ -163,18 +164,23 @@ public void Register(ISagaRepositoryRegistrationConfigurator configurator _registerClientFactory(configurator); configurator.TryAddSingleton(DatabaseContextFactory); + configurator.RegisterLoadSagaRepository>(); + configurator.RegisterQuerySagaRepository>(); configurator.RegisterSagaRepository, SagaConsumeContextFactory, TSaga>, CosmosSagaRepositoryContextFactory>(); + configurator.AddOptions(); } void RegisterNewtonsoftJsonClientFactory(ISagaRepositoryRegistrationConfigurator configurator) { - configurator.TryAddSingleton(provider => new NewtonsoftJsonCosmosClientFactory(_settings)); + configurator.TryAddSingleton(provider => + new NewtonsoftJsonCosmosClientFactory(_settings, provider.GetRequiredService>())); } void RegisterSystemTextJsonClientFactory(ISagaRepositoryRegistrationConfigurator configurator) { - configurator.TryAddSingleton(provider => new SystemTextJsonCosmosClientFactory(_settings, PropertyNamingPolicy)); + configurator.TryAddSingleton(provider => + new SystemTextJsonCosmosClientFactory(_settings, provider.GetRequiredService>(), PropertyNamingPolicy)); } DatabaseContext DatabaseContextFactory(IServiceProvider provider) diff --git a/src/Persistence/MassTransit.Azure.Cosmos/Configuration/CosmosRepositoryConfigurationExtensions.cs b/src/Persistence/MassTransit.Azure.Cosmos/Configuration/CosmosRepositoryConfigurationExtensions.cs index a777a0d987c..e292fd4af0d 100644 --- a/src/Persistence/MassTransit.Azure.Cosmos/Configuration/CosmosRepositoryConfigurationExtensions.cs +++ b/src/Persistence/MassTransit.Azure.Cosmos/Configuration/CosmosRepositoryConfigurationExtensions.cs @@ -4,7 +4,9 @@ namespace MassTransit using Azure.Core; using AzureCosmos; using Configuration; + using Microsoft.Azure.Cosmos; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; using Serialization; @@ -98,6 +100,95 @@ public static ISagaRegistrationConfigurator CosmosRepository(this return configurator.CosmosRepository(new CosmosAuthSettings(accountEndpoint, tokenCredential), configure); } + /// + /// Configure the Job Service saga state machines to use Azure Cosmos + /// + /// + /// The cosmos service endpoint to use + /// The cosmos account key or resource token to use to create the client. + /// + /// + public static IJobSagaRegistrationConfigurator CosmosRepository(this IJobSagaRegistrationConfigurator configurator, string accountEndpoint, + string authKeyOrResourceToken, Action configure) + { + var registrationProvider = new CosmosSagaRepositoryRegistrationProvider(x => + { + x.AccountEndpoint = accountEndpoint; + x.AuthKeyOrResourceToken = authKeyOrResourceToken; + + configure?.Invoke(x); + }); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + + /// + /// Configure the Job Service saga state machines to use Azure Cosmos + /// + /// + /// + /// The connection string to the cosmos account. ex: + /// AccountEndpoint=https://XXXXX.documents.azure.com:443/;AccountKey=SuperSecretKey; + /// + /// + /// + public static IJobSagaRegistrationConfigurator CosmosRepository(this IJobSagaRegistrationConfigurator configurator, string connectionString, + Action configure) + { + var registrationProvider = new CosmosSagaRepositoryRegistrationProvider(x => + { + x.ConnectionString = connectionString; + + configure?.Invoke(x); + }); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + + /// + /// Configure the Job Service saga state machines to use Azure Cosmos + /// + /// + /// The cosmos service endpoint to use + /// The token to provide AAD token for authorization. + /// + /// + public static IJobSagaRegistrationConfigurator CosmosRepository(this IJobSagaRegistrationConfigurator configurator, string accountEndpoint, + TokenCredential tokenCredential, Action configure) + { + var registrationProvider = new CosmosSagaRepositoryRegistrationProvider(x => + { + x.AccountEndpoint = accountEndpoint; + x.TokenCredential = tokenCredential; + + configure?.Invoke(x); + }); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + + /// + /// Configure the Job Service saga state machines to use Azure Cosmos + /// + /// + /// + /// + public static IJobSagaRegistrationConfigurator CosmosRepository(this IJobSagaRegistrationConfigurator configurator, + Action configure) + { + var registrationProvider = new CosmosSagaRepositoryRegistrationProvider(configure); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + /// /// Use the Cosmos saga repository for sagas configured by type (without a specific generic call to AddSaga/AddSagaStateMachine) /// @@ -176,7 +267,8 @@ public static void SetCosmosSagaRepositoryProvider(this IRegistrationConfigurato static IServiceCollection AddCosmosClientFactory(this IServiceCollection collection, CosmosAuthSettings authSettings) { return collection.AddSingleton(provider => - new SystemTextJsonCosmosClientFactory(authSettings, SystemTextJsonMessageSerializer.Options.PropertyNamingPolicy)); + new SystemTextJsonCosmosClientFactory(authSettings, provider.GetRequiredService>(), + SystemTextJsonMessageSerializer.Options.PropertyNamingPolicy)); } /// @@ -228,7 +320,8 @@ public static IServiceCollection AddCosmosClientFactory(this IServiceCollection /// static IServiceCollection AddNewtonsoftCosmosClientFactory(this IServiceCollection collection, CosmosAuthSettings authSettings) { - return collection.AddSingleton(provider => new NewtonsoftJsonCosmosClientFactory(authSettings)); + return collection.AddSingleton(provider => + new NewtonsoftJsonCosmosClientFactory(authSettings, provider.GetRequiredService>())); } /// diff --git a/src/Persistence/MassTransit.Azure.Cosmos/MassTransit.Azure.Cosmos.csproj b/src/Persistence/MassTransit.Azure.Cosmos/MassTransit.Azure.Cosmos.csproj index 2077a941104..4bd3c797f74 100644 --- a/src/Persistence/MassTransit.Azure.Cosmos/MassTransit.Azure.Cosmos.csproj +++ b/src/Persistence/MassTransit.Azure.Cosmos/MassTransit.Azure.Cosmos.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -22,7 +22,6 @@ - diff --git a/src/Persistence/MassTransit.Azure.Storage/MassTransit.Azure.Storage.csproj b/src/Persistence/MassTransit.Azure.Storage/MassTransit.Azure.Storage.csproj index b30f79a2777..3df56dfb55f 100644 --- a/src/Persistence/MassTransit.Azure.Storage/MassTransit.Azure.Storage.csproj +++ b/src/Persistence/MassTransit.Azure.Storage/MassTransit.Azure.Storage.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -23,7 +23,6 @@ - diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/AuditRecord.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/AuditRecord.cs index b88089d665c..49b1cced2fb 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/AuditRecord.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/AuditRecord.cs @@ -4,12 +4,12 @@ namespace MassTransit.AzureTable using System.Text; using System.Text.Json; using Audit; - using Microsoft.Azure.Cosmos.Table; + using Azure; + using Azure.Data.Tables; using Serialization; - public class AuditRecord : - TableEntity + public class AuditRecord : ITableEntity { static readonly char[] _disallowedCharacters; @@ -34,6 +34,10 @@ static AuditRecord() public string Custom { get; set; } public string Headers { get; set; } public string Message { get; set; } + public string RowKey { get; set; } + public string PartitionKey { get; set; } + public ETag ETag { get; set; } = ETag.All; + public DateTimeOffset? Timestamp { get; set; } internal static AuditRecord Create(T message, MessageAuditMetadata metadata, IPartitionKeyFormatter partitionKeyFormatter) where T : class diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/AzureTableAuditStore.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/AzureTableAuditStore.cs index 55ff9693356..a1fc263751b 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/AzureTableAuditStore.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/AzureTableAuditStore.cs @@ -2,16 +2,16 @@ { using System.Threading.Tasks; using Audit; - using Microsoft.Azure.Cosmos.Table; + using Azure.Data.Tables; public class AzureTableAuditStore : IMessageAuditStore { readonly IPartitionKeyFormatter _partitionKeyFormatter; - readonly CloudTable _table; + readonly TableClient _table; - public AzureTableAuditStore(CloudTable table, IPartitionKeyFormatter partitionKeyFormatter) + public AzureTableAuditStore(TableClient table, IPartitionKeyFormatter partitionKeyFormatter) { _table = table; _partitionKeyFormatter = partitionKeyFormatter; @@ -20,8 +20,7 @@ public AzureTableAuditStore(CloudTable table, IPartitionKeyFormatter partitionKe Task IMessageAuditStore.StoreMessage(T message, MessageAuditMetadata metadata) { var auditRecord = AuditRecord.Create(message, metadata, _partitionKeyFormatter); - var insertOrMergeOperation = TableOperation.InsertOrMerge(auditRecord); - return _table.ExecuteAsync(insertOrMergeOperation); + return _table.UpsertEntityAsync(auditRecord); } } } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/DatabaseContext.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/DatabaseContext.cs index 802c568895b..20be2291bda 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/DatabaseContext.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/DatabaseContext.cs @@ -1,6 +1,6 @@ namespace MassTransit.AzureTable { - using Microsoft.Azure.Cosmos.Table; + using Azure.Data.Tables; public interface DatabaseContext @@ -8,7 +8,7 @@ public interface DatabaseContext { ISagaKeyFormatter Formatter { get; } - CloudTable Table { get; } + TableClient Table { get; } IEntityConverter Converter { get; } } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/ICloudTableProvider.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/ICloudTableProvider.cs index 474e3b6fb51..a5c91386ac0 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/ICloudTableProvider.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/ICloudTableProvider.cs @@ -1,11 +1,11 @@ namespace MassTransit.AzureTable { - using Microsoft.Azure.Cosmos.Table; + using Azure.Data.Tables; public interface ICloudTableProvider where TSaga : class, ISaga { - CloudTable GetCloudTable(); + TableClient GetCloudTable(); } } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/IEntityConverter.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/IEntityConverter.cs index 0a093994ac5..d02e55a0f6a 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/IEntityConverter.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/IEntityConverter.cs @@ -1,13 +1,12 @@ namespace MassTransit.AzureTable { using System.Collections.Generic; - using Microsoft.Azure.Cosmos.Table; public interface IEntityConverter where T : class { - IDictionary GetDictionary(T entity); - T GetObject(IDictionary entityProperties); + IDictionary GetDictionary(T entity); + T GetObject(IDictionary entityProperties); } } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableDatabaseContext.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableDatabaseContext.cs index 8ca0e092ec3..bf23cc4af1e 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableDatabaseContext.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableDatabaseContext.cs @@ -1,13 +1,13 @@ namespace MassTransit.AzureTable.Saga { - using Microsoft.Azure.Cosmos.Table; + using Azure.Data.Tables; public class AzureTableDatabaseContext : DatabaseContext where TSaga : class, ISaga { - public AzureTableDatabaseContext(CloudTable table, ISagaKeyFormatter keyFormatter) + public AzureTableDatabaseContext(TableClient table, ISagaKeyFormatter keyFormatter) { Table = table; Formatter = keyFormatter; @@ -16,7 +16,7 @@ public AzureTableDatabaseContext(CloudTable table, ISagaKeyFormatter keyF } public ISagaKeyFormatter Formatter { get; } - public CloudTable Table { get; } + public TableClient Table { get; } public IEntityConverter Converter { get; } } } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepository.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepository.cs index 5417ead3a31..fc382751a8d 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepository.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepository.cs @@ -1,14 +1,14 @@ namespace MassTransit.AzureTable.Saga { using System; + using Azure.Data.Tables; using MassTransit.Saga; - using Microsoft.Azure.Cosmos.Table; public static class AzureTableSagaRepository where TSaga : class, ISaga { - public static ISagaRepository Create(Func tableFactory, ISagaKeyFormatter keyFormatter) + public static ISagaRepository Create(Func tableFactory, ISagaKeyFormatter keyFormatter) { var consumeContextFactory = new SagaConsumeContextFactory, TSaga>(); @@ -16,10 +16,10 @@ public static ISagaRepository Create(Func tableFactory, ISaga var repositoryContextFactory = new AzureTableSagaRepositoryContextFactory(cloudTableProvider, consumeContextFactory, keyFormatter); - return new SagaRepository(repositoryContextFactory); + return new SagaRepository(repositoryContextFactory, loadSagaRepositoryContextFactory: repositoryContextFactory); } - public static ISagaRepository Create(Func tableFactory) + public static ISagaRepository Create(Func tableFactory) { return Create(tableFactory, new ConstPartitionSagaKeyFormatter(typeof(TSaga).Name)); } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepositoryContext.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepositoryContext.cs index 8bedf3675b9..0a40c088809 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepositoryContext.cs @@ -4,10 +4,11 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; + using Azure; + using Azure.Data.Tables; using Context; using Logging; using MassTransit.Saga; - using Microsoft.Azure.Cosmos.Table; using Middleware; @@ -39,12 +40,13 @@ public async Task> Insert(TSaga instance) { try { - var result = await TableInsert(instance).ConfigureAwait(false); - if (result.Result is DynamicTableEntity tableEntity) + (Task insert, var entity) = TableInsert(instance); + var result = await insert.ConfigureAwait(false); + if (!result.IsError) { _consumeContext.LogInsert(instance.CorrelationId); - return await CreateSagaConsumeContext(tableEntity, SagaConsumeContextMode.Insert).ConfigureAwait(false); + return await CreateSagaConsumeContext(entity, SagaConsumeContextMode.Insert).ConfigureAwait(false); } } catch (Exception ex) @@ -59,18 +61,18 @@ public async Task> Load(Guid correlationId) { var (partitionKey, rowKey) = _context.Formatter.Format(correlationId); - var operation = TableOperation.Retrieve(partitionKey, rowKey); - var result = await _context.Table.ExecuteAsync(operation, CancellationToken).ConfigureAwait(false); + NullableResponse result = await _context.Table.GetEntityIfExistsAsync(partitionKey, rowKey).ConfigureAwait(false); - if (result.Result is DynamicTableEntity tableEntity) - return await CreateSagaConsumeContext(tableEntity, SagaConsumeContextMode.Load).ConfigureAwait(false); + if (result.HasValue) + return await CreateSagaConsumeContext(new TableEntity(result.Value), SagaConsumeContextMode.Load).ConfigureAwait(false); return default; } public Task Save(SagaConsumeContext context) { - return TableInsert(context.Saga); + (Task insert, _) = TableInsert(context.Saga); + return insert; } public async Task Update(SagaConsumeContext context) @@ -79,19 +81,12 @@ public async Task Update(SagaConsumeContext context) try { - IDictionary entityProperties = _context.Converter.GetDictionary(instance); - - var (partitionKey, rowKey) = _context.Formatter.Format(instance.CorrelationId); - var eTag = context.GetPayload(); + IDictionary dict = _context.Converter.GetDictionary(instance); + var entity = new TableEntity(dict) { ETag = new ETag(eTag.ETag) }; + (entity.PartitionKey, entity.RowKey) = _context.Formatter.Format(instance.CorrelationId); - var operation = TableOperation.Replace(new DynamicTableEntity(partitionKey, rowKey) - { - Properties = entityProperties, - ETag = eTag.ETag - }); - - await _context.Table.ExecuteAsync(operation, context.CancellationToken).ConfigureAwait(false); + await _context.Table.UpdateEntityAsync(entity, entity.ETag, TableUpdateMode.Replace, context.CancellationToken).ConfigureAwait(false); } catch (Exception exception) { @@ -103,15 +98,9 @@ public async Task Delete(SagaConsumeContext context) { var instance = context.Saga; - IDictionary entityProperties = _context.Converter.GetDictionary(instance); - var (partitionKey, rowKey) = _context.Formatter.Format(instance.CorrelationId); - var eTag = context.GetPayload(); - - var operation = TableOperation.Delete(new DynamicTableEntity(partitionKey, rowKey, eTag.ETag, entityProperties)); - - await _context.Table.ExecuteAsync(operation, context.CancellationToken).ConfigureAwait(false); + await _context.Table.DeleteEntityAsync(partitionKey, rowKey, new ETag(eTag.ETag), context.CancellationToken).ConfigureAwait(false); } public Task Discard(SagaConsumeContext context) @@ -130,25 +119,23 @@ public Task> CreateSagaConsumeContext(ConsumeCon return _factory.CreateSagaConsumeContext(_context, consumeContext, instance, mode); } - Task TableInsert(TSaga instance) + (Task, TableEntity) TableInsert(TSaga instance) { - IDictionary entityProperties = _context.Converter.GetDictionary(instance); + IDictionary dict = _context.Converter.GetDictionary(instance); + var entity = new TableEntity(dict); + (entity.PartitionKey, entity.RowKey) = _context.Formatter.Format(instance.CorrelationId); - var (partitionKey, rowKey) = _context.Formatter.Format(instance.CorrelationId); - - var operation = TableOperation.Insert(new DynamicTableEntity(partitionKey, rowKey) {Properties = entityProperties}); - - return _context.Table.ExecuteAsync(operation, CancellationToken); + return (_context.Table.AddEntityAsync(entity, CancellationToken), entity); } - async Task> CreateSagaConsumeContext(DynamicTableEntity entity, SagaConsumeContextMode mode) + async Task> CreateSagaConsumeContext(TableEntity entity, SagaConsumeContextMode mode) { - var instance = _context.Converter.GetObject(entity.Properties); + var instance = _context.Converter.GetObject(entity); SagaConsumeContext sagaConsumeContext = await _factory.CreateSagaConsumeContext(_context, _consumeContext, instance, mode) .ConfigureAwait(false); - var eTag = new SagaETag(entity.ETag); + var eTag = new SagaETag(entity.ETag.ToString()); sagaConsumeContext.AddOrUpdatePayload(() => eTag, _ => eTag); @@ -159,7 +146,7 @@ async Task> CreateSagaConsumeContext(Dynamic public class CosmosTableSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext + LoadSagaRepositoryContext where TSaga : class, ISaga { readonly DatabaseContext _context; @@ -170,22 +157,16 @@ public CosmosTableSagaRepositoryContext(DatabaseContext context, Cancella _context = context; } - public Task> Query(ISagaQuery query, CancellationToken cancellationToken) - { - throw new NotImplementedByDesignException("Azure Table saga repository does not support queries"); - } - public async Task Load(Guid correlationId) { var (partitionKey, rowKey) = _context.Formatter.Format(correlationId); - var operation = TableOperation.Retrieve(partitionKey, rowKey); - var result = await _context.Table.ExecuteAsync(operation, CancellationToken).ConfigureAwait(false); - if (result.Result is DynamicTableEntity entity) - return _context.Converter.GetObject(entity.Properties); + NullableResponse result = await _context.Table + .GetEntityIfExistsAsync(partitionKey, rowKey, cancellationToken: CancellationToken).ConfigureAwait(false); - - return default; + return result.HasValue + ? _context.Converter.GetObject(new TableEntity(result.Value)) + : default; } } } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepositoryContextFactory.cs index 6448fbb1d38..7bb6766e18b 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/AzureTableSagaRepositoryContextFactory.cs @@ -3,12 +3,13 @@ namespace MassTransit.AzureTable.Saga using System; using System.Threading; using System.Threading.Tasks; + using Azure.Data.Tables; using MassTransit.Saga; - using Microsoft.Azure.Cosmos.Table; public class AzureTableSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISaga { readonly ICloudTableProvider _cloudTableProvider; @@ -24,13 +25,24 @@ public AzureTableSagaRepositoryContextFactory(ICloudTableProvider cloudTa _keyFormatter = keyFormatter; } - public AzureTableSagaRepositoryContextFactory(CloudTable cloudTable, + public AzureTableSagaRepositoryContextFactory(TableClient cloudTable, ISagaConsumeContextFactory, TSaga> factory, ISagaKeyFormatter keyFormatter) : this(new ConstCloudTableProvider(cloudTable), factory, keyFormatter) { } + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + var database = _cloudTableProvider.GetCloudTable(); + + var databaseContext = new AzureTableDatabaseContext(database, _keyFormatter); + var repositoryContext = new CosmosTableSagaRepositoryContext(databaseContext, cancellationToken); + + return asyncMethod(repositoryContext); + } + public void Probe(ProbeContext context) { context.Add("persistence", "azuretable"); @@ -53,16 +65,5 @@ public async Task SendQuery(ConsumeContext context, ISagaQuery quer { throw new NotImplementedByDesignException("Azure Table repository does not support queries"); } - - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) - where T : class - { - var database = _cloudTableProvider.GetCloudTable(); - - var databaseContext = new AzureTableDatabaseContext(database, _keyFormatter); - var repositoryContext = new CosmosTableSagaRepositoryContext(databaseContext, cancellationToken); - - return await asyncMethod(repositoryContext).ConfigureAwait(false); - } } } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ConstCloudTableProvider.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ConstCloudTableProvider.cs index 99b66b78893..bba90c3884e 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ConstCloudTableProvider.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ConstCloudTableProvider.cs @@ -1,20 +1,20 @@ namespace MassTransit.AzureTable.Saga { - using Microsoft.Azure.Cosmos.Table; + using Azure.Data.Tables; public class ConstCloudTableProvider : ICloudTableProvider where TSaga : class, ISaga { - readonly CloudTable _cloudTable; + readonly TableClient _cloudTable; - public ConstCloudTableProvider(CloudTable cloudTable) + public ConstCloudTableProvider(TableClient cloudTable) { _cloudTable = cloudTable; } - public CloudTable GetCloudTable() + public TableClient GetCloudTable() { return _cloudTable; } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/DelegateCloudTableProvider.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/DelegateCloudTableProvider.cs index cf3a3275f11..3147dc6c1c0 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/DelegateCloudTableProvider.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/DelegateCloudTableProvider.cs @@ -1,21 +1,21 @@ namespace MassTransit.AzureTable.Saga { using System; - using Microsoft.Azure.Cosmos.Table; + using Azure.Data.Tables; public class DelegateCloudTableProvider : ICloudTableProvider where TSaga : class, ISaga { - readonly Func _cloudTable; + readonly Func _cloudTable; - public DelegateCloudTableProvider(Func cloudTable) + public DelegateCloudTableProvider(Func cloudTable) { _cloudTable = cloudTable; } - public CloudTable GetCloudTable() + public TableClient GetCloudTable() { return _cloudTable(); } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityConverter.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityConverter.cs index fedecf0a9cc..cbb3492d2de 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityConverter.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityConverter.cs @@ -2,7 +2,6 @@ namespace MassTransit.AzureTable.Saga { using System; using System.Collections.Generic; - using Microsoft.Azure.Cosmos.Table; public class EntityConverter : @@ -16,25 +15,21 @@ public EntityConverter(IList> converters) _converters = converters; } - public IDictionary GetDictionary(T entity) + public IDictionary GetDictionary(T entity) { - var entityProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); - for (int i = 0; i < _converters.Count; i++) - { + var entityProperties = new Dictionary(StringComparer.OrdinalIgnoreCase); + for (var i = 0; i < _converters.Count; i++) _converters[i].FromEntity(entity, entityProperties); - } return entityProperties; } - public T GetObject(IDictionary entityProperties) + public T GetObject(IDictionary entityProperties) { var entity = (T)Activator.CreateInstance(typeof(T)); - for (int i = 0; i < _converters.Count; i++) - { + for (var i = 0; i < _converters.Count; i++) _converters[i].ToEntity(entity, entityProperties); - } return entity; } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityPropertyConverter.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityPropertyConverter.cs index 20cf97448b2..d988942eaa9 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityPropertyConverter.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityPropertyConverter.cs @@ -4,17 +4,16 @@ namespace MassTransit.AzureTable.Saga using System.Collections.Generic; using Initializers; using Internals; - using Microsoft.Azure.Cosmos.Table; public class EntityPropertyConverter : IEntityPropertyConverter where TEntity : class { - readonly ITypeConverter _fromEntity; + readonly ITypeConverter _fromEntity; readonly string _name; readonly IReadProperty _read; - readonly ITypeConverter _toEntity; + readonly ITypeConverter _toEntity; readonly IWriteProperty _write; public EntityPropertyConverter(string name) @@ -23,14 +22,14 @@ public EntityPropertyConverter(string name) _read = ReadPropertyCache.GetProperty(name); _write = WritePropertyCache.GetProperty(name); - _toEntity = EntityPropertyTypeConverter.Instance as ITypeConverter + _toEntity = EntityPropertyTypeConverter.Instance as ITypeConverter ?? throw new ArgumentException("Invalid property type"); - _fromEntity = EntityPropertyTypeConverter.Instance as ITypeConverter + _fromEntity = EntityPropertyTypeConverter.Instance as ITypeConverter ?? throw new ArgumentException("Invalid property type"); } - public void ToEntity(TEntity entity, IDictionary entityProperties) + public void ToEntity(TEntity entity, IDictionary entityProperties) { if (entityProperties.TryGetValue(_name, out var entityProperty)) { @@ -39,7 +38,7 @@ public void ToEntity(TEntity entity, IDictionary entityP } } - public void FromEntity(TEntity entity, IDictionary entityProperties) + public void FromEntity(TEntity entity, IDictionary entityProperties) { var propertyValue = _read.Get(entity); diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityPropertyTypeConverter.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityPropertyTypeConverter.cs index c817d66f5ef..2af126c7925 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityPropertyTypeConverter.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/EntityPropertyTypeConverter.cs @@ -4,50 +4,49 @@ namespace MassTransit.AzureTable.Saga using Initializers; using Initializers.TypeConverters; using Internals; - using Microsoft.Azure.Cosmos.Table; public class EntityPropertyTypeConverter : - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter, - ITypeConverter + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter, + ITypeConverter { public static readonly EntityPropertyTypeConverter Instance = new EntityPropertyTypeConverter(); static readonly TimeSpanTypeConverter _timeSpanConverter = new TimeSpanTypeConverter(); @@ -56,11 +55,11 @@ public class EntityPropertyTypeConverter : { } - public bool TryConvert(EntityProperty input, out bool result) + public bool TryConvert(object input, out bool result) { - if (input.PropertyType == EdmType.Boolean) + if (input is bool value) { - result = input.BooleanValue.HasValue && input.BooleanValue.Value; + result = value; return true; } @@ -68,11 +67,11 @@ public bool TryConvert(EntityProperty input, out bool result) return false; } - public bool TryConvert(EntityProperty input, out bool? result) + public bool TryConvert(object input, out bool? result) { - if (input.PropertyType == EdmType.Boolean) + if (input is bool value) { - result = input.BooleanValue; + result = value; return true; } @@ -80,11 +79,11 @@ public bool TryConvert(EntityProperty input, out bool? result) return false; } - public bool TryConvert(EntityProperty input, out byte[] result) + public bool TryConvert(object input, out byte[] result) { - if (input.PropertyType == EdmType.Binary) + if (input is byte[] value) { - result = input.BinaryValue; + result = value; return true; } @@ -92,11 +91,17 @@ public bool TryConvert(EntityProperty input, out byte[] result) return false; } - public bool TryConvert(EntityProperty input, out DateTime result) + public bool TryConvert(object input, out DateTime result) { - if (input.PropertyType == EdmType.DateTime) + if (input is DateTime dt) { - result = input.DateTime ?? default; + result = dt; + return true; + } + + if(input is DateTimeOffset dto) + { + result = dto.UtcDateTime; return true; } @@ -104,23 +109,30 @@ public bool TryConvert(EntityProperty input, out DateTime result) return false; } - public bool TryConvert(EntityProperty input, out DateTime? result) + public bool TryConvert(object input, out DateTime? result) { - if (input.PropertyType == EdmType.DateTime) + if (input is DateTime dt) + { + result = dt; + return true; + } + + if (input is DateTimeOffset dto) { - result = input.DateTime; + result = dto.UtcDateTime; return true; } + result = default; return false; } - public bool TryConvert(EntityProperty input, out DateTimeOffset result) + public bool TryConvert(object input, out DateTimeOffset result) { - if (input.PropertyType == EdmType.DateTime) + if (input is DateTimeOffset dto) { - result = input.DateTimeOffsetValue ?? default; + result = dto; return true; } @@ -128,11 +140,11 @@ public bool TryConvert(EntityProperty input, out DateTimeOffset result) return false; } - public bool TryConvert(EntityProperty input, out DateTimeOffset? result) + public bool TryConvert(object input, out DateTimeOffset? result) { - if (input.PropertyType == EdmType.DateTime) + if (input is DateTimeOffset dto) { - result = input.DateTimeOffsetValue; + result = dto; return true; } @@ -140,11 +152,11 @@ public bool TryConvert(EntityProperty input, out DateTimeOffset? result) return false; } - public bool TryConvert(EntityProperty input, out double result) + public bool TryConvert(object input, out double result) { - if (input.PropertyType == EdmType.Double) + if (input is double) { - result = input.DoubleValue ?? default; + result = input as double? ?? default; return true; } @@ -152,11 +164,11 @@ public bool TryConvert(EntityProperty input, out double result) return false; } - public bool TryConvert(EntityProperty input, out double? result) + public bool TryConvert(object input, out double? result) { - if (input.PropertyType == EdmType.Double) + if (input is double value) { - result = input.DoubleValue; + result = value; return true; } @@ -164,11 +176,11 @@ public bool TryConvert(EntityProperty input, out double? result) return false; } - public bool TryConvert(bool? input, out EntityProperty result) + public bool TryConvert(object input, out Guid result) { - if (input.HasValue) + if (input is Guid) { - result = new EntityProperty(input); + result = input as Guid? ?? default; return true; } @@ -176,17 +188,11 @@ public bool TryConvert(bool? input, out EntityProperty result) return false; } - public bool TryConvert(bool input, out EntityProperty result) - { - result = new EntityProperty(input); - return true; - } - - public bool TryConvert(byte[] input, out EntityProperty result) + public bool TryConvert(object input, out Guid? result) { - if (input != null) + if (input is Guid value) { - result = new EntityProperty(input); + result = value; return true; } @@ -194,11 +200,11 @@ public bool TryConvert(byte[] input, out EntityProperty result) return false; } - public bool TryConvert(DateTime? input, out EntityProperty result) + public bool TryConvert(object input, out int result) { - if (input.HasValue) + if (input is int) { - result = new EntityProperty(input); + result = input as int? ?? default; return true; } @@ -206,17 +212,23 @@ public bool TryConvert(DateTime? input, out EntityProperty result) return false; } - public bool TryConvert(DateTime input, out EntityProperty result) + public bool TryConvert(object input, out int? result) { - result = new EntityProperty(input); - return true; + if (input is int value) + { + result = value; + return true; + } + + result = default; + return false; } - public bool TryConvert(TimeSpan? input, out EntityProperty result) + public bool TryConvert(object input, out long result) { - if (input.HasValue && _timeSpanConverter.TryConvert(input.Value, out var text)) + if (input is long) { - result = new EntityProperty(text); + result = input as long? ?? default; return true; } @@ -224,11 +236,11 @@ public bool TryConvert(TimeSpan? input, out EntityProperty result) return false; } - public bool TryConvert(TimeSpan input, out EntityProperty result) + public bool TryConvert(object input, out long? result) { - if (_timeSpanConverter.TryConvert(input, out var text)) + if (input is long value) { - result = new EntityProperty(text); + result = value; return true; } @@ -236,11 +248,11 @@ public bool TryConvert(TimeSpan input, out EntityProperty result) return false; } - public bool TryConvert(DateTimeOffset? input, out EntityProperty result) + public bool TryConvert(bool? input, out object result) { if (input.HasValue) { - result = new EntityProperty(input); + result = input; return true; } @@ -248,17 +260,17 @@ public bool TryConvert(DateTimeOffset? input, out EntityProperty result) return false; } - public bool TryConvert(DateTimeOffset input, out EntityProperty result) + public bool TryConvert(bool input, out object result) { - result = new EntityProperty(input); + result = input; return true; } - public bool TryConvert(double? input, out EntityProperty result) + public bool TryConvert(byte[] input, out object result) { - if (input.HasValue) + if (input != null) { - result = new EntityProperty(input); + result = input; return true; } @@ -266,17 +278,11 @@ public bool TryConvert(double? input, out EntityProperty result) return false; } - public bool TryConvert(double input, out EntityProperty result) - { - result = new EntityProperty(input); - return true; - } - - public bool TryConvert(Guid? input, out EntityProperty result) + public bool TryConvert(DateTime? input, out object result) { if (input.HasValue) { - result = new EntityProperty(input); + result = input; return true; } @@ -284,17 +290,17 @@ public bool TryConvert(Guid? input, out EntityProperty result) return false; } - public bool TryConvert(Guid input, out EntityProperty result) + public bool TryConvert(DateTime input, out object result) { - result = new EntityProperty(input); + result = input; return true; } - public bool TryConvert(int? input, out EntityProperty result) + public bool TryConvert(DateTimeOffset? input, out object result) { if (input.HasValue) { - result = new EntityProperty(input); + result = input; return true; } @@ -302,17 +308,17 @@ public bool TryConvert(int? input, out EntityProperty result) return false; } - public bool TryConvert(int input, out EntityProperty result) + public bool TryConvert(DateTimeOffset input, out object result) { - result = new EntityProperty(input); + result = input; return true; } - public bool TryConvert(long? input, out EntityProperty result) + public bool TryConvert(double? input, out object result) { if (input.HasValue) { - result = new EntityProperty(input); + result = input; return true; } @@ -320,23 +326,17 @@ public bool TryConvert(long? input, out EntityProperty result) return false; } - public bool TryConvert(long input, out EntityProperty result) + public bool TryConvert(double input, out object result) { - result = new EntityProperty(input); + result = input; return true; } - public bool TryConvert(string input, out EntityProperty result) + public bool TryConvert(Guid? input, out object result) { - result = new EntityProperty(input); - return true; - } - - public bool TryConvert(Uri input, out EntityProperty result) - { - if (input != null) + if (input.HasValue) { - result = new EntityProperty(input.ToString()); + result = input; return true; } @@ -344,11 +344,17 @@ public bool TryConvert(Uri input, out EntityProperty result) return false; } - public bool TryConvert(Version input, out EntityProperty result) + public bool TryConvert(Guid input, out object result) { - if (input != null) + result = input; + return true; + } + + public bool TryConvert(int? input, out object result) + { + if (input.HasValue) { - result = new EntityProperty(input.ToString()); + result = input; return true; } @@ -356,11 +362,17 @@ public bool TryConvert(Version input, out EntityProperty result) return false; } - public bool TryConvert(EntityProperty input, out Guid result) + public bool TryConvert(int input, out object result) + { + result = input; + return true; + } + + public bool TryConvert(long? input, out object result) { - if (input.PropertyType == EdmType.Guid) + if (input.HasValue) { - result = input.GuidValue ?? default; + result = input; return true; } @@ -368,23 +380,23 @@ public bool TryConvert(EntityProperty input, out Guid result) return false; } - public bool TryConvert(EntityProperty input, out Guid? result) + public bool TryConvert(long input, out object result) { - if (input.PropertyType == EdmType.Guid) - { - result = input.GuidValue; - return true; - } + result = input; + return true; + } - result = default; - return false; + public bool TryConvert(string input, out object result) + { + result = input; + return true; } - public bool TryConvert(EntityProperty input, out int result) + public bool TryConvert(TimeSpan? input, out object result) { - if (input.PropertyType == EdmType.Int32) + if (input.HasValue && _timeSpanConverter.TryConvert(input.Value, out var text)) { - result = input.Int32Value ?? default; + result = text; return true; } @@ -392,11 +404,11 @@ public bool TryConvert(EntityProperty input, out int result) return false; } - public bool TryConvert(EntityProperty input, out int? result) + public bool TryConvert(TimeSpan input, out object result) { - if (input.PropertyType == EdmType.Int32) + if (_timeSpanConverter.TryConvert(input, out var text)) { - result = input.Int32Value; + result = text; return true; } @@ -404,11 +416,11 @@ public bool TryConvert(EntityProperty input, out int? result) return false; } - public bool TryConvert(EntityProperty input, out long result) + public bool TryConvert(Uri input, out object result) { - if (input.PropertyType == EdmType.Int64) + if (input != null) { - result = input.Int64Value ?? default; + result = input.ToString(); return true; } @@ -416,11 +428,11 @@ public bool TryConvert(EntityProperty input, out long result) return false; } - public bool TryConvert(EntityProperty input, out long? result) + public bool TryConvert(Version input, out object result) { - if (input.PropertyType == EdmType.Int64) + if (input != null) { - result = input.Int64Value; + result = input.ToString(); return true; } @@ -428,12 +440,12 @@ public bool TryConvert(EntityProperty input, out long? result) return false; } - public bool TryConvert(EntityProperty input, out string result) + public bool TryConvert(object input, out string result) { - if (input.PropertyType == EdmType.String - && input.StringValue != null) + if (input is string value + && input.ToString() != null) { - result = input.StringValue; + result = value; return true; } @@ -441,13 +453,13 @@ public bool TryConvert(EntityProperty input, out string result) return false; } - public bool TryConvert(EntityProperty input, out TimeSpan result) + public bool TryConvert(object input, out TimeSpan result) { - if (input.PropertyType == EdmType.String) + if (input is string) { - result = string.IsNullOrWhiteSpace(input.StringValue) + result = string.IsNullOrWhiteSpace(input.ToString()) ? default - : _timeSpanConverter.TryConvert(input.StringValue, out var value) + : _timeSpanConverter.TryConvert(input.ToString(), out var value) ? value : default; return true; @@ -457,13 +469,13 @@ public bool TryConvert(EntityProperty input, out TimeSpan result) return false; } - public bool TryConvert(EntityProperty input, out TimeSpan? result) + public bool TryConvert(object input, out TimeSpan? result) { - if (input.PropertyType == EdmType.String) + if (input is string) { - result = string.IsNullOrWhiteSpace(input.StringValue) + result = string.IsNullOrWhiteSpace(input.ToString()) ? default - : _timeSpanConverter.TryConvert(input.StringValue, out var value) + : _timeSpanConverter.TryConvert(input.ToString(), out var value) ? value : default; return true; @@ -473,13 +485,13 @@ public bool TryConvert(EntityProperty input, out TimeSpan? result) return false; } - public bool TryConvert(EntityProperty input, out Uri result) + public bool TryConvert(object input, out Uri result) { - if (input.PropertyType == EdmType.String - && input.StringValue != null - && Uri.IsWellFormedUriString(input.StringValue, UriKind.RelativeOrAbsolute)) + if (input is string + && input.ToString() != null + && Uri.IsWellFormedUriString(input.ToString(), UriKind.RelativeOrAbsolute)) { - result = new Uri(input.StringValue); + result = new Uri(input.ToString()); return true; } @@ -487,11 +499,11 @@ public bool TryConvert(EntityProperty input, out Uri result) return false; } - public bool TryConvert(EntityProperty input, out Version result) + public bool TryConvert(object input, out Version result) { - if (input.PropertyType == EdmType.String - && input.StringValue != null - && Version.TryParse(input.StringValue, out var version)) + if (input is string + && input.ToString() != null + && Version.TryParse(input.ToString(), out var version)) { result = version; return true; @@ -503,8 +515,8 @@ public bool TryConvert(EntityProperty input, out Version result) public static bool IsSupported(Type propertyType) { - var fromType = typeof(ITypeConverter<,>).MakeGenericType(typeof(EntityProperty), propertyType); - var toType = typeof(ITypeConverter<,>).MakeGenericType(propertyType, typeof(EntityProperty)); + var fromType = typeof(ITypeConverter<,>).MakeGenericType(typeof(object), propertyType); + var toType = typeof(ITypeConverter<,>).MakeGenericType(propertyType, typeof(object)); return typeof(EntityPropertyTypeConverter).HasInterface(fromType) && typeof(EntityPropertyTypeConverter).HasInterface(toType); } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/IEntityPropertyConverter.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/IEntityPropertyConverter.cs index 9fc943d854e..063f7f38234 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/IEntityPropertyConverter.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/IEntityPropertyConverter.cs @@ -1,13 +1,12 @@ namespace MassTransit.AzureTable.Saga { using System.Collections.Generic; - using Microsoft.Azure.Cosmos.Table; public interface IEntityPropertyConverter where TEntity : class { - void ToEntity(TEntity entity, IDictionary entityProperties); - void FromEntity(TEntity entity, IDictionary entityProperties); + void ToEntity(TEntity entity, IDictionary entityProperties); + void FromEntity(TEntity entity, IDictionary entityProperties); } } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ObjectEntityPropertyConverter.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ObjectEntityPropertyConverter.cs index 7c951841b5f..febbec228f4 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ObjectEntityPropertyConverter.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ObjectEntityPropertyConverter.cs @@ -2,7 +2,6 @@ namespace MassTransit.AzureTable.Saga { using System.Collections.Generic; using Internals; - using Microsoft.Azure.Cosmos.Table; using Serialization; @@ -22,23 +21,23 @@ public ObjectEntityPropertyConverter(string name) _write = WritePropertyCache.GetProperty(name); } - public void ToEntity(TEntity entity, IDictionary entityProperties) + public void ToEntity(TEntity entity, IDictionary entityProperties) { if (entityProperties.TryGetValue(_name, out var entityProperty)) { - var propertyValue = ObjectDeserializer.Deserialize(entityProperty.StringValue); + var propertyValue = ObjectDeserializer.Deserialize(entityProperty.ToString()); _write.Set(entity, propertyValue); } } - public void FromEntity(TEntity entity, IDictionary entityProperties) + public void FromEntity(TEntity entity, IDictionary entityProperties) { var propertyValue = _read.Get(entity); var text = ObjectDeserializer.Serialize(propertyValue); if (!string.IsNullOrWhiteSpace(text)) - entityProperties.Add(_name, new EntityProperty(text)); + entityProperties.Add(_name, text); } } } diff --git a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ValueTypeEntityPropertyConverter.cs b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ValueTypeEntityPropertyConverter.cs index 3f50365938c..73af1de4fe4 100644 --- a/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ValueTypeEntityPropertyConverter.cs +++ b/src/Persistence/MassTransit.Azure.Table/AzureTable/Saga/ValueTypeEntityPropertyConverter.cs @@ -2,7 +2,6 @@ namespace MassTransit.AzureTable.Saga { using System.Collections.Generic; using Internals; - using Microsoft.Azure.Cosmos.Table; using Serialization; @@ -22,24 +21,24 @@ public ValueTypeEntityPropertyConverter(string name) _write = WritePropertyCache.GetProperty(name); } - public void ToEntity(TEntity entity, IDictionary entityProperties) + public void ToEntity(TEntity entity, IDictionary entityProperties) { if (entityProperties.TryGetValue(_name, out var entityProperty)) { - var propertyValue = ObjectDeserializer.Deserialize(entityProperty.StringValue); + TProperty? propertyValue = ObjectDeserializer.Deserialize(entityProperty.ToString()); if (propertyValue.HasValue) _write.Set(entity, propertyValue.Value); } } - public void FromEntity(TEntity entity, IDictionary entityProperties) + public void FromEntity(TEntity entity, IDictionary entityProperties) { var propertyValue = _read.Get(entity); var text = ObjectDeserializer.Serialize(propertyValue); if (!string.IsNullOrWhiteSpace(text)) - entityProperties.Add(_name, new EntityProperty(text)); + entityProperties.Add(_name, text); } } } diff --git a/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableAuditStoreConfiguratorExtensions.cs b/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableAuditStoreConfiguratorExtensions.cs index 64969f4dc22..035e4a2251e 100644 --- a/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableAuditStoreConfiguratorExtensions.cs +++ b/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableAuditStoreConfiguratorExtensions.cs @@ -1,8 +1,8 @@ namespace MassTransit { using System; + using Azure.Data.Tables; using AzureTable; - using Microsoft.Azure.Cosmos.Table; public static class AzureTableAuditStoreConfiguratorExtensions @@ -11,71 +11,67 @@ public static class AzureTableAuditStoreConfiguratorExtensions /// Supply your storage account and table name for audit logs. Default Partition Key Strategy and no filters will be applied. /// /// - /// Your cloud storage account. + /// Service client used to perform all service level operations. /// The name of the table for which the Audit Logs will be persisted. - public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, CloudStorageAccount storageAccount, string auditTableName) + public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, TableServiceClient tableServiceClient, string auditTableName) { - var tableClient = storageAccount.CreateCloudTableClient(); - var table = tableClient.GetTableReference(auditTableName); - table.CreateIfNotExists(); + var tableClient = tableServiceClient.GetTableClient(auditTableName); + tableClient.CreateIfNotExists(); - ConfigureAuditStore(configurator, table); + ConfigureAuditStore(configurator, tableClient); } /// /// Supply your storage account, table name and filter to be used. Default Partition Key Strategy will be applied. /// /// - /// Your cloud storage account. + /// Service client used to perform all service level operations. /// The name of the table for which the Audit Logs will be persisted. /// Message Filter to exclude or include messages from audit based on requirements - public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, CloudStorageAccount storageAccount, string auditTableName, + public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, TableServiceClient tableServiceClient, string auditTableName, Action configureFilter) { - var tableClient = storageAccount.CreateCloudTableClient(); - var table = tableClient.GetTableReference(auditTableName); - table.CreateIfNotExists(); + var tableClient = tableServiceClient.GetTableClient(auditTableName); + tableClient.CreateIfNotExists(); - ConfigureAuditStore(configurator, table, configureFilter); + ConfigureAuditStore(configurator, tableClient, configureFilter); } /// /// Supply your storage account, table name and partition key strategy based on the message type and audit information. No Filters will be applied. /// /// - /// Your cloud storage account. + /// Service client used to perform all service level operations. /// The name of the table for which the Audit Logs will be persisted. /// /// Using the message type and audit information or otherwise, specify the partition key strategy /// - public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, CloudStorageAccount storageAccount, string auditTableName, + public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, TableServiceClient tableServiceClient, string auditTableName, IPartitionKeyFormatter partitionKeyFormatter) { - var tableClient = storageAccount.CreateCloudTableClient(); - var table = tableClient.GetTableReference(auditTableName); - table.CreateIfNotExists(); + var tableClient = tableServiceClient.GetTableClient(auditTableName); + tableClient.CreateIfNotExists(); - ConfigureAuditStore(configurator, table, null, partitionKeyFormatter); + ConfigureAuditStore(configurator, tableClient, null, partitionKeyFormatter); } /// /// Supply your storage account, table name, partition key strategy and message filter to be applied. /// /// - /// Your cloud storage account. + /// Service client used to perform all service level operations. /// The name of the table for which the Audit Logs will be persisted. /// /// Using the message type and audit information or otherwise, specify the partition key strategy /// /// Message Filter to exclude or include messages from audit based on requirements - public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, CloudStorageAccount storageAccount, string auditTableName, + public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, TableServiceClient tableServiceClient, string auditTableName, IPartitionKeyFormatter partitionKeyFormatter, Action configureFilter) { - var tableClient = storageAccount.CreateCloudTableClient(); - var table = tableClient.GetTableReference(auditTableName); - table.CreateIfNotExists(); + var tableClient = tableServiceClient.GetTableClient(auditTableName); + tableClient.CreateIfNotExists(); - ConfigureAuditStore(configurator, table, configureFilter, partitionKeyFormatter); + ConfigureAuditStore(configurator, tableClient, configureFilter, partitionKeyFormatter); } /// @@ -87,7 +83,7 @@ public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configur /// Using the message type and audit information or otherwise, specify the partition key strategy /// /// Message Filter to exclude or include messages from audit based on requirements - public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, CloudTable table, + public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, TableClient table, IPartitionKeyFormatter partitionKeyFormatter, Action configureFilter) { ConfigureAuditStore(configurator, table, configureFilter, partitionKeyFormatter); @@ -99,7 +95,7 @@ public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configur /// /// Your Azure Cloud Table /// Message Filter to exclude or include messages from audit based on requirements - public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, CloudTable table, + public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, TableClient table, Action configureFilter) { ConfigureAuditStore(configurator, table, configureFilter); @@ -113,17 +109,17 @@ public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configur /// /// Using the message type and audit information or otherwise, specify the partition key strategy /// - public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, CloudTable table, IPartitionKeyFormatter partitionKeyFormatter) + public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, TableClient table, IPartitionKeyFormatter partitionKeyFormatter) { ConfigureAuditStore(configurator, table, null, partitionKeyFormatter); } - public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, CloudTable table) + public static void UseAzureTableAuditStore(this IBusFactoryConfigurator configurator, TableClient table) { ConfigureAuditStore(configurator, table); } - static void ConfigureAuditStore(IBusFactoryConfigurator configurator, CloudTable table, Action configureFilter = default, + static void ConfigureAuditStore(IBusFactoryConfigurator configurator, TableClient table, Action configureFilter = default, IPartitionKeyFormatter formatter = default) { var auditStore = new AzureTableAuditStore(table, formatter ?? new DefaultPartitionKeyFormatter()); diff --git a/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableJobServiceConfigurationExtensions.cs b/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableJobServiceConfigurationExtensions.cs index 0a164cbaccc..40ae1728ab8 100644 --- a/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableJobServiceConfigurationExtensions.cs +++ b/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableJobServiceConfigurationExtensions.cs @@ -1,15 +1,15 @@ namespace MassTransit { using System; + using Azure.Data.Tables; using AzureTable; using AzureTable.Saga; - using Microsoft.Azure.Cosmos.Table; public static class AzureTableJobServiceConfigurationExtensions { public static void UseAzureTableSagaRepository(this IJobServiceConfigurator configurator, - Func contextFactory, + Func contextFactory, ISagaKeyFormatter jobTypeKeyFormatter, ISagaKeyFormatter jobKeyFormatter, ISagaKeyFormatter jobAttemptKeyFormatter) @@ -22,7 +22,7 @@ public static void UseAzureTableSagaRepository(this IJobServiceConfigurator conf } public static void UseAzureTableSagaRepository(this IJobServiceConfigurator configurator, - Func contextFactory) + Func contextFactory) { UseAzureTableSagaRepository(configurator, contextFactory, new ConstPartitionSagaKeyFormatter(nameof(JobTypeSaga)), diff --git a/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableRepositoryRegistrationExtensions.cs b/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableRepositoryRegistrationExtensions.cs index b55db1c93ea..0f5785ba924 100644 --- a/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableRepositoryRegistrationExtensions.cs +++ b/src/Persistence/MassTransit.Azure.Table/Configuration/AzureTableRepositoryRegistrationExtensions.cs @@ -28,6 +28,22 @@ public static ISagaRegistrationConfigurator AzureTableRepository(this ISag return configurator; } + /// + /// Configure the Job Service saga state machines to use Azure Table Storage + /// + /// + /// + /// + public static IJobSagaRegistrationConfigurator AzureTableRepository(this IJobSagaRegistrationConfigurator configurator, + Action configure) + { + var registrationProvider = new AzureTableSagaRepositoryRegistrationProvider(configure); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + /// /// Use the Azure Table saga repository for sagas configured by type (without a specific generic call to AddSaga/AddSagaStateMachine) /// diff --git a/src/Persistence/MassTransit.Azure.Table/Configuration/Configuration/AzureTableSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.Azure.Table/Configuration/Configuration/AzureTableSagaRepositoryConfigurator.cs index 498cf9ae54a..1964ad14f4f 100644 --- a/src/Persistence/MassTransit.Azure.Table/Configuration/Configuration/AzureTableSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.Azure.Table/Configuration/Configuration/AzureTableSagaRepositoryConfigurator.cs @@ -2,9 +2,9 @@ namespace MassTransit.Configuration { using System; using System.Collections.Generic; + using Azure.Data.Tables; using AzureTable; using AzureTable.Saga; - using Microsoft.Azure.Cosmos.Table; using Microsoft.Extensions.DependencyInjection.Extensions; using Saga; @@ -14,7 +14,7 @@ public class AzureTableSagaRepositoryConfigurator : ISpecification where TSaga : class, ISaga { - Func _connectionFactory; + Func _connectionFactory; Func> _formatterFactory = provider => new ConstPartitionSagaKeyFormatter(typeof(TSaga).Name); @@ -23,7 +23,7 @@ public class AzureTableSagaRepositoryConfigurator : /// Supply factory for retrieving the Cloud Table. /// /// - public void ConnectionFactory(Func connectionFactory) + public void ConnectionFactory(Func connectionFactory) { _connectionFactory = provider => connectionFactory(); } @@ -32,7 +32,7 @@ public void ConnectionFactory(Func connectionFactory) /// Supply factory for retrieving the Cloud Table. /// /// - public void ConnectionFactory(Func connectionFactory) + public void ConnectionFactory(Func connectionFactory) { _connectionFactory = connectionFactory; } @@ -52,13 +52,13 @@ public IEnumerable Validate() yield return this.Failure("ConnectionFactory", "must be specified"); } - public void Register(ISagaRepositoryRegistrationConfigurator configurator) - where T : class, ISaga + public void Register(ISagaRepositoryRegistrationConfigurator configurator) { configurator.TryAddSingleton>(provider => new ConstCloudTableProvider(_connectionFactory(provider))); configurator.TryAddSingleton(_formatterFactory); - configurator.RegisterSagaRepository, SagaConsumeContextFactory, T>, - AzureTableSagaRepositoryContextFactory>(); + configurator.RegisterLoadSagaRepository>(); + configurator.RegisterSagaRepository, SagaConsumeContextFactory, TSaga>, + AzureTableSagaRepositoryContextFactory>(); } } } diff --git a/src/Persistence/MassTransit.Azure.Table/Configuration/IAzureTableSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.Azure.Table/Configuration/IAzureTableSagaRepositoryConfigurator.cs index c0f80a65645..252ce5ac65c 100644 --- a/src/Persistence/MassTransit.Azure.Table/Configuration/IAzureTableSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.Azure.Table/Configuration/IAzureTableSagaRepositoryConfigurator.cs @@ -1,8 +1,8 @@ namespace MassTransit { using System; + using Azure.Data.Tables; using AzureTable; - using Microsoft.Azure.Cosmos.Table; public interface IAzureTableSagaRepositoryConfigurator : @@ -23,12 +23,12 @@ public interface IAzureTableSagaRepositoryConfigurator /// Use a simple factory method to create the connection /// /// - void ConnectionFactory(Func connectionFactory); + void ConnectionFactory(Func connectionFactory); /// /// Supply factory for retrieving the Cloud Table. /// /// - void ConnectionFactory(Func connectionFactory); + void ConnectionFactory(Func connectionFactory); } } diff --git a/src/Persistence/MassTransit.Azure.Table/MassTransit.Azure.Table.csproj b/src/Persistence/MassTransit.Azure.Table/MassTransit.Azure.Table.csproj index 777e8568587..e07db64ade2 100644 --- a/src/Persistence/MassTransit.Azure.Table/MassTransit.Azure.Table.csproj +++ b/src/Persistence/MassTransit.Azure.Table/MassTransit.Azure.Table.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -25,7 +25,7 @@ - + diff --git a/src/Persistence/MassTransit.DapperIntegration/Configuration/DatabaseContextFactory.cs b/src/Persistence/MassTransit.DapperIntegration/Configuration/DatabaseContextFactory.cs new file mode 100644 index 00000000000..c0a5bcd6ebb --- /dev/null +++ b/src/Persistence/MassTransit.DapperIntegration/Configuration/DatabaseContextFactory.cs @@ -0,0 +1,8 @@ +namespace MassTransit; + +using System.Data; +using DapperIntegration.Saga; + + +public delegate DatabaseContext DatabaseContextFactory(IDbConnection connection, IDbTransaction transaction) + where TSaga : class, ISaga; diff --git a/src/Persistence/MassTransit.DapperIntegration/Configuration/IDapperSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.DapperIntegration/Configuration/IDapperSagaRepositoryConfigurator.cs index 7299573f780..f7e00984553 100644 --- a/src/Persistence/MassTransit.DapperIntegration/Configuration/IDapperSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.DapperIntegration/Configuration/IDapperSagaRepositoryConfigurator.cs @@ -1,17 +1,20 @@ -namespace MassTransit -{ - using System.Data; +namespace MassTransit; +using System.Data; - public interface IDapperSagaRepositoryConfigurator - { - IsolationLevel IsolationLevel { set; } - } +public interface IDapperSagaRepositoryConfigurator +{ + IsolationLevel IsolationLevel { set; } +} - public interface IDapperSagaRepositoryConfigurator : - IDapperSagaRepositoryConfigurator - where TSaga : class, ISaga - { - } + +public interface IDapperSagaRepositoryConfigurator : + IDapperSagaRepositoryConfigurator + where TSaga : class, ISaga +{ + /// + /// Set the database context factory to allow customization of the Dapper interaction/queries + /// + DatabaseContextFactory ContextFactory { set; } } diff --git a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Configuration/DapperSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Configuration/DapperSagaRepositoryConfigurator.cs index 4e42e3e714e..d1b9a41f457 100644 --- a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Configuration/DapperSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Configuration/DapperSagaRepositoryConfigurator.cs @@ -1,3 +1,4 @@ +#nullable enable namespace MassTransit.DapperIntegration.Configuration { using System.Collections.Generic; @@ -24,18 +25,21 @@ public DapperSagaRepositoryConfigurator(string connectionString, IsolationLevel public IsolationLevel IsolationLevel { get; set; } + public DatabaseContextFactory? ContextFactory { get; set; } + public IEnumerable Validate() { if (string.IsNullOrWhiteSpace(_connectionString)) yield return this.Failure("ConnectionString", "must be specified"); } - public void Register(ISagaRepositoryRegistrationConfigurator configurator) - where T : class, ISaga + public void Register(ISagaRepositoryRegistrationConfigurator configurator) { - configurator.TryAddSingleton(new DapperOptions(_connectionString, IsolationLevel)); - configurator.RegisterSagaRepository, SagaConsumeContextFactory, T>, - DapperSagaRepositoryContextFactory>(); + configurator.TryAddSingleton(new DapperOptions(_connectionString, IsolationLevel, ContextFactory)); + configurator.RegisterLoadSagaRepository>(); + configurator.RegisterQuerySagaRepository>(); + configurator.RegisterSagaRepository, SagaConsumeContextFactory, TSaga>, + DapperSagaRepositoryContextFactory>(); } } } diff --git a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperDatabaseContext.cs b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperDatabaseContext.cs index 1962a1b5832..eeaae5ce93e 100644 --- a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperDatabaseContext.cs +++ b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperDatabaseContext.cs @@ -12,8 +12,7 @@ namespace MassTransit.DapperIntegration.Saga public class DapperDatabaseContext : - DatabaseContext, - IDisposable + DatabaseContext { readonly SqlConnection _connection; readonly SemaphoreSlim _inUse; @@ -106,16 +105,18 @@ public async Task> QueryAsync(Expression> filter } } - public void Dispose() + public void Commit() + { + _transaction.Commit(); + } + + public ValueTask DisposeAsync() { _inUse.Dispose(); _transaction.Dispose(); _connection.Dispose(); - } - public void Commit() - { - _transaction.Commit(); + return default; } static string GetTableName() diff --git a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperOptions.cs b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperOptions.cs index 2ac03b21c56..7ae468ec504 100644 --- a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperOptions.cs +++ b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperOptions.cs @@ -1,17 +1,21 @@ +#nullable enable namespace MassTransit.DapperIntegration.Saga { using System.Data; public class DapperOptions + where TSaga : class, ISaga { - public DapperOptions(string connectionString, IsolationLevel isolationLevel) + public DapperOptions(string connectionString, IsolationLevel isolationLevel, DatabaseContextFactory? contextFactory) { ConnectionString = connectionString; IsolationLevel = isolationLevel; + ContextFactory = contextFactory; } public string ConnectionString { get; } public IsolationLevel IsolationLevel { get; } + public DatabaseContextFactory? ContextFactory { get; } } } diff --git a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperSagaRepositoryContext.cs b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperSagaRepositoryContext.cs index c432599edbb..28d6cd06f15 100644 --- a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperSagaRepositoryContext.cs @@ -84,7 +84,8 @@ public Task Undo(SagaConsumeContext context) public class DapperSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext + QuerySagaRepositoryContext, + LoadSagaRepositoryContext where TSaga : class, ISaga { readonly DatabaseContext _context; @@ -95,16 +96,16 @@ public DapperSagaRepositoryContext(DatabaseContext context, CancellationT _context = context; } + public Task Load(Guid correlationId) + { + return _context.LoadAsync(correlationId, CancellationToken); + } + public async Task> Query(ISagaQuery query, CancellationToken cancellationToken = default) { IEnumerable instances = await _context.QueryAsync(query.FilterExpression, cancellationToken).ConfigureAwait(false); return new LoadedSagaRepositoryQueryContext(this, instances); } - - public Task Load(Guid correlationId) - { - return _context.LoadAsync(correlationId, CancellationToken); - } } } diff --git a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperSagaRepositoryContextFactory.cs index c13034565ed..c07f0d34ca0 100644 --- a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DapperSagaRepositoryContextFactory.cs @@ -9,7 +9,9 @@ namespace MassTransit.DapperIntegration.Saga public class DapperSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + IQuerySagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISaga { readonly ISagaConsumeContextFactory, TSaga> _factory; @@ -21,6 +23,18 @@ public DapperSagaRepositoryContextFactory(DapperOptions options, ISagaCon _factory = factory; } + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + public void Probe(ProbeContext context) { context.Add("persistence", "dapper"); @@ -29,7 +43,7 @@ public void Probe(ProbeContext context) public async Task Send(ConsumeContext context, IPipe> next) where T : class { - using DapperDatabaseContext databaseContext = await CreateDatabaseContext(context.CancellationToken).ConfigureAwait(false); + await using DatabaseContext databaseContext = await CreateDatabaseContext(context.CancellationToken).ConfigureAwait(false); var repositoryContext = new DapperSagaRepositoryContext(databaseContext, context, _factory); @@ -41,7 +55,7 @@ public async Task Send(ConsumeContext context, IPipe(ConsumeContext context, ISagaQuery query, IPipe> next) where T : class { - using DapperDatabaseContext databaseContext = await CreateDatabaseContext(context.CancellationToken).ConfigureAwait(false); + await using DatabaseContext databaseContext = await CreateDatabaseContext(context.CancellationToken).ConfigureAwait(false); IEnumerable instances = await databaseContext.QueryAsync(query.FilterExpression, context.CancellationToken).ConfigureAwait(false); @@ -54,11 +68,10 @@ public async Task SendQuery(ConsumeContext context, ISagaQuery quer databaseContext.Commit(); } - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + async Task ExecuteAsyncMethod(Func, Task> asyncMethod, CancellationToken cancellationToken) where T : class { - using DapperDatabaseContext databaseContext = await CreateDatabaseContext(cancellationToken).ConfigureAwait(false); - + await using DatabaseContext databaseContext = await CreateDatabaseContext(cancellationToken).ConfigureAwait(false); var sagaRepositoryContext = new DapperSagaRepositoryContext(databaseContext, cancellationToken); var result = await asyncMethod(sagaRepositoryContext).ConfigureAwait(false); @@ -68,7 +81,7 @@ public async Task Execute(Func, Task> asyn return result; } - async Task> CreateDatabaseContext(CancellationToken cancellationToken) + async Task> CreateDatabaseContext(CancellationToken cancellationToken) { var connection = new SqlConnection(_options.ConnectionString); SqlTransaction transaction = null; @@ -78,7 +91,9 @@ async Task> CreateDatabaseContext(CancellationToken transaction = connection.BeginTransaction(_options.IsolationLevel); - return new DapperDatabaseContext(connection, transaction); + return _options.ContextFactory != null + ? _options.ContextFactory(connection, transaction) + : new DapperDatabaseContext(connection, transaction); } catch (Exception) { diff --git a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DatabaseContext.cs b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DatabaseContext.cs index bc9cba05d0c..32b8fc19905 100644 --- a/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DatabaseContext.cs +++ b/src/Persistence/MassTransit.DapperIntegration/DapperIntegration/Saga/DatabaseContext.cs @@ -7,7 +7,8 @@ namespace MassTransit.DapperIntegration.Saga using System.Threading.Tasks; - public interface DatabaseContext + public interface DatabaseContext : + IAsyncDisposable { Task DeleteAsync(T instance, CancellationToken cancellationToken) where T : class, ISaga; @@ -23,5 +24,7 @@ Task InsertAsync(T instance, CancellationToken cancellationToken = default) Task UpdateAsync(T instance, CancellationToken cancellationToken = default) where T : class, ISaga; + + void Commit(); } } diff --git a/src/Persistence/MassTransit.DapperIntegration/DapperSagaRepository.cs b/src/Persistence/MassTransit.DapperIntegration/DapperSagaRepository.cs index 83e8f432b13..38ca4de08e4 100644 --- a/src/Persistence/MassTransit.DapperIntegration/DapperSagaRepository.cs +++ b/src/Persistence/MassTransit.DapperIntegration/DapperSagaRepository.cs @@ -12,10 +12,10 @@ public static ISagaRepository Create(string connectionString, IsolationLe { var consumeContextFactory = new SagaConsumeContextFactory, TSaga>(); - var options = new DapperOptions(connectionString, isolationLevel); + var options = new DapperOptions(connectionString, isolationLevel, null); var repositoryContextFactory = new DapperSagaRepositoryContextFactory(options, consumeContextFactory); - return new SagaRepository(repositoryContextFactory); + return new SagaRepository(repositoryContextFactory, repositoryContextFactory, repositoryContextFactory); } } } diff --git a/src/Persistence/MassTransit.DapperIntegration/MassTransit.DapperIntegration.csproj b/src/Persistence/MassTransit.DapperIntegration/MassTransit.DapperIntegration.csproj index d4327b31899..9d27bd89fe9 100644 --- a/src/Persistence/MassTransit.DapperIntegration/MassTransit.DapperIntegration.csproj +++ b/src/Persistence/MassTransit.DapperIntegration/MassTransit.DapperIntegration.csproj @@ -1,11 +1,11 @@  - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -21,9 +21,9 @@ + - diff --git a/src/Persistence/MassTransit.DynamoDbIntegration/Configuration/Configuration/DynamoDbSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.DynamoDbIntegration/Configuration/Configuration/DynamoDbSagaRepositoryConfigurator.cs index 6e77b272f5f..76bf698d95a 100644 --- a/src/Persistence/MassTransit.DynamoDbIntegration/Configuration/Configuration/DynamoDbSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.DynamoDbIntegration/Configuration/Configuration/DynamoDbSagaRepositoryConfigurator.cs @@ -47,13 +47,13 @@ public IEnumerable Validate() yield return this.Failure("Expiration", "If specified, must be > 30 seconds"); } - public void Register(ISagaRepositoryRegistrationConfigurator configurator) - where T : class, ISagaVersion + public void Register(ISagaRepositoryRegistrationConfigurator configurator) { configurator.TryAddSingleton(_contextFactory); - configurator.TryAddSingleton(new DynamoDbSagaRepositoryOptions(TableName, Expiration)); - configurator.RegisterSagaRepository, SagaConsumeContextFactory, T>, - DynamoDbSagaRepositoryContextFactory>(); + configurator.TryAddSingleton(new DynamoDbSagaRepositoryOptions(TableName, Expiration)); + configurator.RegisterLoadSagaRepository>(); + configurator.RegisterSagaRepository, SagaConsumeContextFactory, TSaga>, + DynamoDbSagaRepositoryContextFactory>(); } } } diff --git a/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepository.cs b/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepository.cs index 549d3fb8094..8201b996f69 100644 --- a/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepository.cs +++ b/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepository.cs @@ -16,7 +16,7 @@ public static ISagaRepository Create(Func dynamoDbFacto var repositoryContextFactory = new DynamoDbSagaRepositoryContextFactory(dynamoDbFactory, consumeContextFactory, options); - return new SagaRepository(repositoryContextFactory); + return new SagaRepository(repositoryContextFactory, loadSagaRepositoryContextFactory: repositoryContextFactory); } } } diff --git a/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepositoryContext.cs b/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepositoryContext.cs index d50f4e5a4fc..8673711c086 100644 --- a/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepositoryContext.cs @@ -102,7 +102,7 @@ public Task> CreateSagaConsumeContext(ConsumeCon public class DynamoDbSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext, + LoadSagaRepositoryContext, IDisposable where TSaga : class, ISagaVersion { @@ -119,11 +119,6 @@ public void Dispose() _context.Dispose(); } - public Task> Query(ISagaQuery query, CancellationToken cancellationToken) - { - throw new NotImplementedByDesignException("DynamoDb saga repository does not support queries"); - } - public Task Load(Guid correlationId) { return _context.Load(correlationId); diff --git a/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepositoryContextFactory.cs index 904e439f4c4..321ccef0327 100644 --- a/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.DynamoDbIntegration/DynamoDbIntegration/Saga/DynamoDbSagaRepositoryContextFactory.cs @@ -8,7 +8,8 @@ namespace MassTransit.DynamoDbIntegration.Saga public class DynamoDbSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISagaVersion { readonly Func _databaseFactory; @@ -33,12 +34,7 @@ public DynamoDbSagaRepositoryContextFactory(Func databaseFacto _options = options; } - public void Probe(ProbeContext context) - { - context.Add("persistence", "dynamodb"); - } - - public async Task Send(ConsumeContext context, IPipe> next) + public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) where T : class { var database = _databaseFactory(); @@ -46,9 +42,9 @@ public async Task Send(ConsumeContext context, IPipe(database, _options); try { - var repositoryContext = new DynamoDbSagaRepositoryContext(databaseContext, context, _factory); + var repositoryContext = new DynamoDbSagaRepositoryContext(databaseContext, cancellationToken); - await next.Send(repositoryContext).ConfigureAwait(false); + return await asyncMethod(repositoryContext).ConfigureAwait(false); } finally { @@ -56,13 +52,12 @@ public async Task Send(ConsumeContext context, IPipe(ConsumeContext context, ISagaQuery query, IPipe> next) - where T : class + public void Probe(ProbeContext context) { - throw new NotImplementedByDesignException("DynamoDb saga repository does not support queries"); + context.Add("persistence", "dynamodb"); } - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + public async Task Send(ConsumeContext context, IPipe> next) where T : class { var database = _databaseFactory(); @@ -70,14 +65,20 @@ public async Task Execute(Func, Task> asyn var databaseContext = new DynamoDbDatabaseContext(database, _options); try { - var repositoryContext = new DynamoDbSagaRepositoryContext(databaseContext, cancellationToken); + var repositoryContext = new DynamoDbSagaRepositoryContext(databaseContext, context, _factory); - return await asyncMethod(repositoryContext).ConfigureAwait(false); + await next.Send(repositoryContext).ConfigureAwait(false); } finally { databaseContext.Dispose(); } } + + public async Task SendQuery(ConsumeContext context, ISagaQuery query, IPipe> next) + where T : class + { + throw new NotImplementedByDesignException("DynamoDb saga repository does not support queries"); + } } } diff --git a/src/Persistence/MassTransit.DynamoDbIntegration/MassTransit.DynamoDbIntegration.csproj b/src/Persistence/MassTransit.DynamoDbIntegration/MassTransit.DynamoDbIntegration.csproj index 43dd63a3c44..34ed8a54f1f 100644 --- a/src/Persistence/MassTransit.DynamoDbIntegration/MassTransit.DynamoDbIntegration.csproj +++ b/src/Persistence/MassTransit.DynamoDbIntegration/MassTransit.DynamoDbIntegration.csproj @@ -2,11 +2,11 @@ - netstandard2.0;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -22,7 +22,6 @@ - diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/Configuration/EntityFrameworkSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/Configuration/EntityFrameworkSagaRepositoryConfigurator.cs index ad28885b362..f463f4a21a0 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/Configuration/EntityFrameworkSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/Configuration/EntityFrameworkSagaRepositoryConfigurator.cs @@ -105,8 +105,10 @@ public void Register(ISagaRepositoryRegistrationConfigurator configurator else configurator.TryAddSingleton(provider => CreatePessimisticLockStrategy()); - configurator.RegisterSagaRepository, - EntityFrameworkSagaRepositoryContextFactory>(); + configurator.RegisterLoadSagaRepository>(); + configurator.RegisterQuerySagaRepository>(); + configurator + .RegisterSagaRepository, EntityFrameworkSagaRepositoryContextFactory>(); } static void AddDbContext(IServiceCollection collection, @@ -148,13 +150,9 @@ static void CheckContextConstructors() ISagaRepositoryLockStrategy CreateOptimisticLockStrategy() { - ILoadQueryProvider queryProvider = new DefaultSagaLoadQueryProvider(); - if (_queryCustomization != null) - queryProvider = new CustomSagaLoadQueryProvider(queryProvider, _queryCustomization); + var queryExecutor = new OptimisticLoadQueryExecutor(_queryCustomization); - var queryExecutor = new OptimisticLoadQueryExecutor(queryProvider); - - return new OptimisticSagaRepositoryLockStrategy(queryProvider, queryExecutor, _isolationLevel); + return new OptimisticSagaRepositoryLockStrategy(queryExecutor, _queryCustomization, _isolationLevel); } ISagaRepositoryLockStrategy CreatePessimisticLockStrategy() diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkAuditStoreConfiguratorExtensions.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkAuditStoreConfiguratorExtensions.cs index ae84966c298..9e4918f8881 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkAuditStoreConfiguratorExtensions.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkAuditStoreConfiguratorExtensions.cs @@ -7,6 +7,21 @@ namespace MassTransit public static class EntityFrameworkAuditStoreConfiguratorExtensions { + /// + /// Configure Audit Store against Entity Framework Core + /// + /// + /// The DB Context Options configuration builder + /// Name of the table to store audit records. + /// Name of the audit table's db schema + /// Configure messages to exclude or include from auditing. + public static void UseEntityFrameworkCoreAuditStore(this IBusFactoryConfigurator configurator, DbContextOptionsBuilder dbContextOptions, + string auditTableName, string auditTableSchema, + Action configureFilter) + { + ConfigureAuditStore(configurator, dbContextOptions, auditTableName, auditTableSchema, configureFilter); + } + /// /// Configure Audit Store against Entity Framework Core /// @@ -18,7 +33,7 @@ public static void UseEntityFrameworkCoreAuditStore(this IBusFactoryConfigurator string auditTableName, Action configureFilter) { - ConfigureAuditStore(configurator, dbContextOptions, auditTableName, configureFilter); + ConfigureAuditStore(configurator, dbContextOptions, auditTableName, configureFilter: configureFilter); } /// @@ -27,16 +42,17 @@ public static void UseEntityFrameworkCoreAuditStore(this IBusFactoryConfigurator /// /// The DB Context Options configuration builder /// Name of the table to store audit records. + /// Name of the audit table's db schema public static void UseEntityFrameworkCoreAuditStore(this IBusFactoryConfigurator configurator, DbContextOptionsBuilder dbContextOptions, - string auditTableName) + string auditTableName, string auditTableSchema = null) { - ConfigureAuditStore(configurator, dbContextOptions, auditTableName); + ConfigureAuditStore(configurator, dbContextOptions, auditTableName, auditTableSchema); } static void ConfigureAuditStore(IBusFactoryConfigurator configurator, DbContextOptionsBuilder dbContextOptions, string auditTableName, - Action configureFilter = default) + string auditTableSchema = null, Action configureFilter = default) { - var auditStore = new EntityFrameworkAuditStore(dbContextOptions.Options, auditTableName); + var auditStore = new EntityFrameworkAuditStore(dbContextOptions.Options, auditTableName, auditTableSchema); configurator.ConnectSendAuditObservers(auditStore, configureFilter); configurator.ConnectConsumeAuditObserver(auditStore, configureFilter); diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkCoreSagaRepositoryRegistrationExtensions.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkCoreSagaRepositoryRegistrationExtensions.cs index b88fb915c86..788eb1bb25a 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkCoreSagaRepositoryRegistrationExtensions.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkCoreSagaRepositoryRegistrationExtensions.cs @@ -72,6 +72,22 @@ public static ISagaRegistrationConfigurator EntityFrameworkRepository + /// Configure the Job Service saga state machines to use Entity Framework Core as the saga repository + /// + /// + /// + /// + public static IJobSagaRegistrationConfigurator EntityFrameworkRepository(this IJobSagaRegistrationConfigurator configurator, + Action configure = null) + { + var registrationProvider = new EntityFrameworkSagaRepositoryRegistrationProvider(configure); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + /// /// Use the EntityFramework saga repository for sagas configured by type (without a specific generic call to AddSaga/AddSagaStateMachine) /// @@ -233,6 +249,32 @@ public static IEntityFrameworkSagaRepositoryConfigurator UseMySql(this IEntityFr return configurator; } + /// + /// Configure the repository for use with SQLite + /// + /// + /// + /// + public static IEntityFrameworkSagaRepositoryConfigurator UseSqlite(this IEntityFrameworkSagaRepositoryConfigurator configurator) + where T : class, ISaga + { + configurator.LockStatementProvider = new SqliteLockStatementProvider(); + + return configurator; + } + + /// + /// Configure the repository for use with SQLite + /// + /// + /// + public static IEntityFrameworkSagaRepositoryConfigurator UseSqlite(this IEntityFrameworkSagaRepositoryConfigurator configurator) + { + configurator.LockStatementProvider = new SqliteLockStatementProvider(); + + return configurator; + } + /// /// Create EntityFramework saga repository /// diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkOutboxConfigurationExtensions.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkOutboxConfigurationExtensions.cs index b16cd52a857..3fcdab3a013 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkOutboxConfigurationExtensions.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/Configuration/EntityFrameworkOutboxConfigurationExtensions.cs @@ -3,6 +3,7 @@ namespace MassTransit { using System; using Configuration; + using DependencyInjection; using EntityFrameworkCoreIntegration; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; @@ -27,12 +28,36 @@ public static void AddEntityFrameworkOutbox(this IBusRegistrationCon outboxConfigurator.Configure(configure); } + /// + /// Configure the Entity Framework outbox on the receive endpoint + /// + /// + /// Configuration service provider + /// + public static void UseEntityFrameworkOutbox(this IReceiveEndpointConfigurator configurator, IRegistrationContext context, + Action? configure = null) + where TDbContext : DbContext + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + var observer = new OutboxConsumePipeSpecificationObserver(configurator, context); + + configure?.Invoke(observer); + + configurator.ConnectConsumerConfigurationObserver(observer); + configurator.ConnectSagaConfigurationObserver(observer); + } + /// /// Configure the Entity Framework outbox on the receive endpoint /// /// /// Configuration service provider /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseEntityFrameworkOutbox(this IReceiveEndpointConfigurator configurator, IServiceProvider provider, Action? configure = null) where TDbContext : DbContext @@ -42,7 +67,7 @@ public static void UseEntityFrameworkOutbox(this IReceiveEndpointCon if (provider == null) throw new ArgumentNullException(nameof(provider)); - var observer = new OutboxConsumePipeSpecificationObserver(configurator, provider); + var observer = new OutboxConsumePipeSpecificationObserver(configurator, provider, LegacySetScopedConsumeContext.Instance); configure?.Invoke(observer); @@ -62,6 +87,19 @@ public static IEntityFrameworkOutboxConfigurator UseSqlServer(this IEntityFramew return configurator; } + /// + /// Configure the outbox for use with SQL Server + /// + /// + /// Set to false when using multiple DbContexts + /// + public static IEntityFrameworkOutboxConfigurator UseSqlServer(this IEntityFrameworkOutboxConfigurator configurator, bool enableSchemaCaching) + { + configurator.LockStatementProvider = new SqlServerLockStatementProvider(enableSchemaCaching); + + return configurator; + } + /// /// Configure the outbox for use with Postgres /// @@ -74,6 +112,19 @@ public static IEntityFrameworkOutboxConfigurator UsePostgres(this IEntityFramewo return configurator; } + /// + /// Configure the outbox for use with Postgres + /// + /// + /// Set to false when using multiple DbContexts + /// + public static IEntityFrameworkOutboxConfigurator UsePostgres(this IEntityFrameworkOutboxConfigurator configurator, bool enableSchemaCaching) + { + configurator.LockStatementProvider = new PostgresLockStatementProvider(enableSchemaCaching); + + return configurator; + } + /// /// Configure the outbox for use with MySQL /// @@ -86,10 +137,80 @@ public static IEntityFrameworkOutboxConfigurator UseMySql(this IEntityFrameworkO return configurator; } + /// + /// Configure the outbox for use with MySQL + /// + /// + /// Set to false when using multiple DbContexts + /// + public static IEntityFrameworkOutboxConfigurator UseMySql(this IEntityFrameworkOutboxConfigurator configurator, bool enableSchemaCaching) + { + configurator.LockStatementProvider = new MySqlLockStatementProvider(enableSchemaCaching); + + return configurator; + } + + /// + /// Configure the outbox for use with SQLite + /// + /// + /// + public static IEntityFrameworkOutboxConfigurator UseSqlite(this IEntityFrameworkOutboxConfigurator configurator) + { + configurator.LockStatementProvider = new SqliteLockStatementProvider(); + + return configurator; + } + + /// + /// Configure the outbox for use with SQLite + /// + /// + /// Set to false when using multiple DbContexts + /// + public static IEntityFrameworkOutboxConfigurator UseSqlite(this IEntityFrameworkOutboxConfigurator configurator, bool enableSchemaCaching) + { + configurator.LockStatementProvider = new SqliteLockStatementProvider(enableSchemaCaching); + + return configurator; + } + + /// + /// Adds all three entities (, , and ) + /// to the DbContext. If this method is used, the , , and + /// methods should not be used. + /// + /// + /// Optional, to customize all three entity model builders + public static void AddTransactionalOutboxEntities(this ModelBuilder modelBuilder, Action? callback = null) + { + modelBuilder.AddInboxStateEntity(callback); + modelBuilder.AddOutboxStateEntity(callback); + modelBuilder.AddOutboxMessageEntity(callback); + } + + /// + /// Adds the entity to the DbContext. If used, the method should not be used. + /// + /// + /// Optional, to customize the entity model builder public static void AddInboxStateEntity(this ModelBuilder modelBuilder, Action>? callback = null) { EntityTypeBuilder inbox = modelBuilder.Entity(); + inbox.ConfigureInboxStateEntity(); + + callback?.Invoke(inbox); + } + + /// + /// Configures the entity using an already created . + /// + /// The model builder + public static void ConfigureInboxStateEntity(this EntityTypeBuilder inbox) + { + inbox.OptOutOfEntityFrameworkConventions(); + inbox.Property(p => p.Id); inbox.HasKey(p => p.Id); @@ -115,14 +236,30 @@ public static void AddInboxStateEntity(this ModelBuilder modelBuilder, Action p.Delivered); inbox.Property(p => p.LastSequenceNumber); - - callback?.Invoke(inbox); } + /// + /// Adds the entity to the DbContext. If used, the method should not be used. + /// + /// + /// Optional, to customize the entity model builder public static void AddOutboxStateEntity(this ModelBuilder modelBuilder, Action>? callback = null) { EntityTypeBuilder outbox = modelBuilder.Entity(); + outbox.ConfigureOutboxStateEntity(); + + callback?.Invoke(outbox); + } + + /// + /// Configures the entity using an already created . + /// + /// The model builder + public static void ConfigureOutboxStateEntity(this EntityTypeBuilder outbox) + { + outbox.OptOutOfEntityFrameworkConventions(); + outbox.Property(p => p.OutboxId); outbox.HasKey(p => p.OutboxId); @@ -135,14 +272,30 @@ public static void AddOutboxStateEntity(this ModelBuilder modelBuilder, Action p.Delivered); outbox.Property(p => p.LastSequenceNumber); - - callback?.Invoke(outbox); } + /// + /// Adds the entity to the DbContext. If used, the method should not be used. + /// + /// + /// Optional, to customize the entity model builder public static void AddOutboxMessageEntity(this ModelBuilder modelBuilder, Action>? callback = null) { EntityTypeBuilder outbox = modelBuilder.Entity(); + outbox.ConfigureOutboxMessageEntity(); + + callback?.Invoke(outbox); + } + + /// + /// Configures the entity using an already created . + /// + /// The model builder + public static void ConfigureOutboxMessageEntity(this EntityTypeBuilder outbox) + { + outbox.OptOutOfEntityFrameworkConventions(); + outbox.Property(p => p.SequenceNumber); outbox.HasKey(p => p.SequenceNumber); @@ -174,13 +327,25 @@ public static void AddOutboxMessageEntity(this ModelBuilder modelBuilder, Action p.InboxConsumerId, p.SequenceNumber }).IsUnique(); - - outbox.Property(p => p.OutboxId); + outbox.HasOne().WithMany().IsRequired(false) + .HasForeignKey(p => new + { + p.InboxMessageId, + p.InboxConsumerId + }).HasPrincipalKey(p => new + { + p.MessageId, + p.ConsumerId + }); + + outbox.Property(p => p.OutboxId).IsRequired(false); outbox.HasIndex(p => new { p.OutboxId, p.SequenceNumber, }).IsUnique(); + outbox.HasOne().WithMany().IsRequired(false) + .HasForeignKey(p => p.OutboxId); outbox.Property(p => p.Headers); @@ -188,10 +353,22 @@ public static void AddOutboxMessageEntity(this ModelBuilder modelBuilder, Action outbox.Property(p => p.ContentType) .HasMaxLength(256); + outbox.Property(p => p.MessageType); outbox.Property(p => p.Body); + } - callback?.Invoke(outbox); + /// + /// Configures the entity type builder to opt out of Entity Framework conventions. + /// This method sets the maximum length of all properties to null, effectively removing any length constraints. + /// + /// The EntityTypeBuilder instance to configure. + internal static void OptOutOfEntityFrameworkConventions(this EntityTypeBuilder builder) + { + foreach (var properties in builder.Metadata.GetProperties()) + { + properties.SetMaxLength(null); + } } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/AuditDbContext.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/AuditDbContext.cs index 95005e298ef..da62220eec8 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/AuditDbContext.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/AuditDbContext.cs @@ -7,21 +7,24 @@ public class AuditDbContext : DbContext { readonly string _auditTableName; + readonly string _auditTableSchema; - protected AuditDbContext(string auditTableName) + protected AuditDbContext(string auditTableName, string auditTableSchema = null) { _auditTableName = auditTableName; + _auditTableSchema = auditTableSchema; } - public AuditDbContext(DbContextOptions options, string auditTableName) + public AuditDbContext(DbContextOptions options, string auditTableName, string auditTableSchema = null) : base(options) { _auditTableName = auditTableName; + _auditTableSchema = auditTableSchema; } protected override void OnModelCreating(ModelBuilder modelBuilder) { - modelBuilder.ApplyConfiguration(new AuditMapping(_auditTableName)); + modelBuilder.ApplyConfiguration(new AuditMapping(_auditTableName, _auditTableSchema)); } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/AuditMapping.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/AuditMapping.cs index a90b87592b6..9f5a5b4b30f 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/AuditMapping.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/AuditMapping.cs @@ -9,15 +9,24 @@ public class AuditMapping : IEntityTypeConfiguration { readonly string _tableName; + readonly string _schemaName; - public AuditMapping(string tableName) + public AuditMapping(string tableName, string schemaName = null) { _tableName = tableName; + _schemaName = schemaName; } public void Configure(EntityTypeBuilder builder) { - builder.ToTable(_tableName); + if (string.IsNullOrWhiteSpace(_schemaName)) + { + builder.ToTable(_tableName); + } + else + { + builder.ToTable(_tableName, _schemaName); + } builder.HasKey(x => x.AuditRecordId); builder.Property(x => x.AuditRecordId) diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/EntityFrameworkAuditStore.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/EntityFrameworkAuditStore.cs index 3c08b69bcc0..eb3bb1e0c25 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/EntityFrameworkAuditStore.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Audit/EntityFrameworkAuditStore.cs @@ -9,15 +9,17 @@ public class EntityFrameworkAuditStore : IMessageAuditStore { readonly string _auditTableName; + readonly string _auditTableSchema; readonly DbContextOptions _contextOptions; - public EntityFrameworkAuditStore(DbContextOptions contextOptions, string auditTableName) + public EntityFrameworkAuditStore(DbContextOptions contextOptions, string auditTableName, string auditTableSchema = null) { _contextOptions = contextOptions; _auditTableName = auditTableName; + _auditTableSchema = auditTableSchema; } - public DbContext AuditContext => new AuditDbContext(_contextOptions, _auditTableName); + public DbContext AuditContext => new AuditDbContext(_contextOptions, _auditTableName, _auditTableSchema); async Task IMessageAuditStore.StoreMessage(T message, MessageAuditMetadata metadata) { diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/BusOutboxDeliveryService.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/BusOutboxDeliveryService.cs index 96842720440..d5762c2f14a 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/BusOutboxDeliveryService.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/BusOutboxDeliveryService.cs @@ -6,6 +6,7 @@ namespace MassTransit.EntityFrameworkCoreIntegration using System.Linq; using System.Threading; using System.Threading.Tasks; + using Internals; using Logging; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Storage; @@ -29,7 +30,9 @@ public class BusOutboxDeliveryService : readonly ILogger _logger; readonly IBusOutboxNotification _notification; readonly OutboxDeliveryServiceOptions _options; + readonly Func> _outboxMessagesQuery; readonly IServiceProvider _provider; + string _getOutboxIdStatement; public BusOutboxDeliveryService(IBusControl busControl, IOptions options, @@ -45,6 +48,13 @@ public BusOutboxDeliveryService(IBusControl busControl, IOptions + context.Set() + .Where(x => x.OutboxId == outboxId && x.SequenceNumber > lastSequenceNumber) + .OrderBy(x => x.SequenceNumber) + .Take(limit) + .AsNoTracking()); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -52,22 +62,31 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) using var algorithm = new RequestRateAlgorithm(new RequestRateAlgorithmOptions { PrefetchCount = _options.QueryMessageLimit, - RequestResultLimit = 10 + RequestResultLimit = 10, }); while (!stoppingToken.IsCancellationRequested) { try { - await _notification.WaitForDelivery(stoppingToken).ConfigureAwait(false); - await _busControl.WaitForHealthStatus(BusHealthStatus.Healthy, stoppingToken).ConfigureAwait(false); - await algorithm.Run(DeliverOutbox, stoppingToken).ConfigureAwait(false); + var count = await algorithm.Run(DeliverOutbox, stoppingToken).ConfigureAwait(false); + if (count > 0) + continue; + + await _notification.WaitForDelivery(stoppingToken).ConfigureAwait(false); } catch (OperationCanceledException) { } + catch (DbUpdateConcurrencyException) + { + } + catch (InvalidOperationException exception) when (exception.InnerException != null + && exception.InnerException.Message.Contains("concurrent update")) + { + } catch (Exception exception) { _logger.LogError(exception, "ProcessMessageBatch faulted"); @@ -76,23 +95,6 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) } async Task DeliverOutbox(int resultLimit, CancellationToken cancellationToken) - { - var resultCount = 0; - - for (var i = 0; i < resultLimit; i++) - { - var messageCount = await DeliverOutbox(cancellationToken).ConfigureAwait(false); - if (messageCount > 0) - resultCount++; - - if (messageCount == 0) - break; - } - - return resultCount; - } - - async Task DeliverOutbox(CancellationToken cancellationToken) { var scope = _provider.CreateAsyncScope(); @@ -145,14 +147,9 @@ async Task Execute() { throw; } - catch (DbUpdateConcurrencyException) + catch (InvalidOperationException exception) when (exception.InnerException != null + && exception.InnerException.Message.Contains("concurrent update")) { - await RollbackTransaction(transaction).ConfigureAwait(false); - throw; - } - catch (DbUpdateException) - { - await RollbackTransaction(transaction).ConfigureAwait(false); throw; } catch (Exception) @@ -162,16 +159,17 @@ async Task Execute() } } - var messageCount = 0; + var executionStrategy = dbContext.Database.CreateExecutionStrategy(); - var executeResult = 1; - while (executeResult >= 0) + var messageCount = 0; + while (messageCount < resultLimit) { - var executionStrategy = dbContext.Database.CreateExecutionStrategy(); - if (executionStrategy is ExecutionStrategy) - executeResult = await executionStrategy.ExecuteAsync(() => Execute()).ConfigureAwait(false); - else - executeResult = await Execute().ConfigureAwait(false); + var executeResult = executionStrategy is ExecutionStrategy + ? await executionStrategy.ExecuteAsync(() => Execute()).ConfigureAwait(false) + : await Execute().ConfigureAwait(false); + + if (executeResult < 0) + break; if (executeResult > 0) messageCount += executeResult; @@ -192,7 +190,6 @@ static async Task RemoveOutbox(TDbContext dbContext, OutboxState outboxState, Ca { List messages = await dbContext.Set() .Where(x => x.OutboxId == outboxState.OutboxId) - .OrderBy(x => x.SequenceNumber) .ToListAsync(cancellationToken); dbContext.RemoveRange(messages); @@ -213,12 +210,9 @@ async Task DeliverOutboxMessages(TDbContext dbContext, OutboxState outboxSt var lastSequenceNumber = outboxState.LastSequenceNumber ?? 0; - List messages = await dbContext.Set() - .Where(x => x.OutboxId == outboxState.OutboxId && x.SequenceNumber > lastSequenceNumber) - .OrderBy(x => x.SequenceNumber) - .Take(messageLimit) - .AsNoTracking() - .ToListAsync(cancellationToken).ConfigureAwait(false); + IList messages = await _outboxMessagesQuery(dbContext, outboxState.OutboxId, lastSequenceNumber, messageLimit) + .ToListAsync(cancellationToken) + .ConfigureAwait(false); var sentSequenceNumber = 0L; @@ -249,13 +243,22 @@ async Task DeliverOutboxMessages(TDbContext dbContext, OutboxState outboxSt var endpoint = await _busControl.GetSendEndpoint(message.DestinationAddress).ConfigureAwait(false); StartedActivity? activity = LogContext.Current?.StartOutboxDeliverActivity(message); + StartedInstrument? instrument = LogContext.Current?.StartOutboxDeliveryInstrument(message); + try { await endpoint.Send(new SerializedMessageBody(), pipe, token.Token).ConfigureAwait(false); } + catch (Exception ex) + { + activity?.AddExceptionEvent(ex); + instrument?.AddException(ex); + throw; + } finally { activity?.Stop(); + instrument?.Stop(); } sentSequenceNumber = message.SequenceNumber; @@ -288,16 +291,15 @@ async Task DeliverOutboxMessages(TDbContext dbContext, OutboxState outboxSt if (messageIndex == messages.Count && messages.Count < messageLimit) { + outboxState.Delivered = DateTime.UtcNow; + if (hasLastSequenceNumber == false) { dbContext.Remove(outboxState); dbContext.RemoveRange(messages); } else - { - outboxState.Delivered = DateTime.UtcNow; dbContext.Update(outboxState); - } saveChanges = true; diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/DbContextOutboxConsumeContext.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/DbContextOutboxConsumeContext.cs index 3012757c7a6..56256399cb7 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/DbContextOutboxConsumeContext.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/DbContextOutboxConsumeContext.cs @@ -19,6 +19,7 @@ public class DbContextOutboxConsumeContext : { readonly TDbContext _dbContext; readonly InboxState _inboxState; + readonly DbSet _outboxMessageSet; readonly IDbContextTransaction _transaction; public DbContextOutboxConsumeContext(ConsumeContext context, OutboxConsumeOptions options, IServiceProvider provider, TDbContext dbContext, @@ -28,6 +29,8 @@ public DbContextOutboxConsumeContext(ConsumeContext context, OutboxCon _dbContext = dbContext; _transaction = transaction; _inboxState = inboxState; + + _outboxMessageSet = dbContext.Set(); } public override Guid? MessageId => _inboxState.MessageId; @@ -46,7 +49,7 @@ public override async Task SetConsumed() _inboxState.Consumed = DateTime.UtcNow; _dbContext.Update(_inboxState); - await _dbContext.SaveChangesAsync(CancellationToken); + await _dbContext.SaveChangesAsync(CancellationToken).ConfigureAwait(false); LogContext.Debug?.Log("Outbox Consumed: {MessageId} {Consumed}", MessageId, _inboxState.Consumed); } @@ -56,7 +59,7 @@ public override async Task SetDelivered() _inboxState.Delivered = DateTime.UtcNow; _dbContext.Update(_inboxState); - await _dbContext.SaveChangesAsync(CancellationToken); + await _dbContext.SaveChangesAsync(CancellationToken).ConfigureAwait(false); LogContext.Debug?.Log("Outbox Delivered: {MessageId} {Delivered}", MessageId, _inboxState.Delivered); } @@ -70,7 +73,7 @@ public override async Task> LoadOutboxMessages() .OrderBy(x => x.SequenceNumber) .Take(Options.MessageDeliveryLimit + 1) .AsNoTracking() - .ToListAsync(CancellationToken); + .ToListAsync(CancellationToken).ConfigureAwait(false); for (var i = 0; i < messages.Count; i++) messages[i].Deserialize(SerializerContext); @@ -90,11 +93,11 @@ public override async Task RemoveOutboxMessages() { List messages = await _dbContext.Set() .Where(x => x.InboxMessageId == MessageId && x.InboxConsumerId == ConsumerId) - .ToListAsync(CancellationToken); + .ToListAsync(CancellationToken).ConfigureAwait(false); _dbContext.RemoveRange(messages); - await _dbContext.SaveChangesAsync(CancellationToken); + await _dbContext.SaveChangesAsync(CancellationToken).ConfigureAwait(false); if (messages.Count > 0) LogContext.Debug?.Log("Outbox removed {Count} messages: {MessageId}", messages.Count, MessageId); @@ -103,7 +106,7 @@ public override async Task RemoveOutboxMessages() public override Task AddSend(SendContext context) where T : class { - return _dbContext.Set().AddSend(context, SerializerContext, MessageId, ConsumerId); + return _outboxMessageSet.AddSend(context, SerializerContext, MessageId, ConsumerId); } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkConsumeContextScopedBusContext.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkConsumeContextScopedBusContext.cs new file mode 100644 index 00000000000..0d3c8bdc1b3 --- /dev/null +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkConsumeContextScopedBusContext.cs @@ -0,0 +1,45 @@ +#nullable enable +namespace MassTransit.EntityFrameworkCoreIntegration; + +using System; +using Clients; +using DependencyInjection; +using Microsoft.EntityFrameworkCore; +using Middleware.Outbox; + + +public class EntityFrameworkConsumeContextScopedBusContext : + EntityFrameworkScopedBusContext + where TBus : class, IBus + where TDbContext : DbContext +{ + readonly TBus _bus; + readonly IClientFactory _clientFactory; + readonly ConsumeContext _consumeContext; + readonly IServiceProvider _provider; + + public EntityFrameworkConsumeContextScopedBusContext(TBus bus, TDbContext dbContext, IBusOutboxNotification notification, IClientFactory clientFactory, + IServiceProvider provider, ConsumeContext consumeContext) + : base(bus, dbContext, notification, clientFactory, provider) + { + _bus = bus; + _clientFactory = clientFactory; + _provider = provider; + _consumeContext = consumeContext; + } + + protected override IPublishEndpointProvider GetPublishEndpointProvider() + { + return new ScopedConsumePublishEndpointProvider(_bus, _consumeContext, _provider); + } + + protected override ISendEndpointProvider GetSendEndpointProvider() + { + return new ScopedConsumeSendEndpointProvider(_bus, _consumeContext, _provider); + } + + protected override ScopedClientFactory GetClientFactory() + { + return new ScopedClientFactory(new ClientFactory(new ScopedClientFactoryContext(_clientFactory, _provider)), _consumeContext); + } +} diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkOutboxContextFactory.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkOutboxContextFactory.cs index d728e9f9963..85237efb1aa 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkOutboxContextFactory.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkOutboxContextFactory.cs @@ -15,9 +15,9 @@ public class EntityFrameworkOutboxContextFactory : where TDbContext : DbContext { readonly TDbContext _dbContext; - readonly IServiceProvider _provider; readonly IsolationLevel _isolationLevel; readonly ILockStatementProvider _lockStatementProvider; + readonly IServiceProvider _provider; string _lockStatement; public EntityFrameworkOutboxContextFactory(TDbContext dbContext, IServiceProvider provider, IOptions options) @@ -88,19 +88,17 @@ async Task Execute() return continueProcessing; } - catch (DbUpdateConcurrencyException) - { - await RollbackTransaction(transaction).ConfigureAwait(false); - throw; - } - catch (DbUpdateException) - { - await RollbackTransaction(transaction).ConfigureAwait(false); - throw; - } catch (Exception) { - await RollbackTransaction(transaction).ConfigureAwait(false); + try + { + await transaction.RollbackAsync(CancellationToken.None).ConfigureAwait(false); + } + catch (Exception innerException) + { + LogContext.Warning?.Log(innerException, "Transaction rollback failed"); + } + throw; } } @@ -121,17 +119,5 @@ public void Probe(ProbeContext context) var scope = context.CreateFilterScope("outboxContextFactory"); scope.Add("provider", "entityFrameworkCore"); } - - static async Task RollbackTransaction(IDbContextTransaction transaction) - { - try - { - await transaction.RollbackAsync(CancellationToken.None).ConfigureAwait(false); - } - catch (Exception innerException) - { - LogContext.Warning?.Log(innerException, "Transaction rollback failed"); - } - } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkOutboxExtensions.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkOutboxExtensions.cs index 0105dd1e8a1..97cab717ef0 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkOutboxExtensions.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkOutboxExtensions.cs @@ -10,7 +10,7 @@ namespace MassTransit.EntityFrameworkCoreIntegration public static class EntityFrameworkOutboxExtensions { - public static async Task AddSend(this DbSet collection, SendContext context, IObjectDeserializer deserializer, + public static Task AddSend(this DbSet collection, SendContext context, IObjectDeserializer deserializer, Guid? inboxMessageId = null, Guid? inboxConsumerId = null, Guid? outboxId = null) where T : class { @@ -34,6 +34,7 @@ public static async Task AddSend(this DbSet collection, SendCo FaultAddress = context.FaultAddress, SentTime = context.SentTime ?? now, ContentType = context.ContentType?.ToString() ?? context.Serialization.DefaultContentType.ToString(), + MessageType = string.Join(";", context.SupportedMessageTypes), Body = body.GetString(), InboxMessageId = inboxMessageId, InboxConsumerId = inboxConsumerId, @@ -56,7 +57,10 @@ public static async Task AddSend(this DbSet collection, SendCo outboxMessage.Properties = deserializer.SerializeDictionary(properties); } - await collection.AddAsync(outboxMessage, context.CancellationToken).ConfigureAwait(false); + lock (collection) + collection.Add(outboxMessage); + + return Task.CompletedTask; } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkSagaRepository.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkSagaRepository.cs index 46b908cb081..e7820e20dad 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkSagaRepository.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkSagaRepository.cs @@ -14,12 +14,8 @@ public static class EntityFrameworkSagaRepository public static ISagaRepository CreateOptimistic(ISagaDbContextFactory dbContextFactory, Func, IQueryable> queryCustomization = null) { - ILoadQueryProvider queryProvider = new DefaultSagaLoadQueryProvider(); - if (queryCustomization != null) - queryProvider = new CustomSagaLoadQueryProvider(queryProvider, queryCustomization); - - var queryExecutor = new OptimisticLoadQueryExecutor(queryProvider); - var lockStrategy = new OptimisticSagaRepositoryLockStrategy(queryProvider, queryExecutor, IsolationLevel.ReadCommitted); + var queryExecutor = new OptimisticLoadQueryExecutor(queryCustomization); + var lockStrategy = new OptimisticSagaRepositoryLockStrategy(queryExecutor, queryCustomization, IsolationLevel.ReadCommitted); return CreateRepository(dbContextFactory, lockStrategy); } @@ -55,7 +51,7 @@ static ISagaRepository CreateRepository(ISagaDbContextFactory dbCo var repositoryFactory = new EntityFrameworkSagaRepositoryContextFactory(dbContextFactory, consumeContextFactory, lockStrategy); - return new SagaRepository(repositoryFactory); + return new SagaRepository(repositoryFactory, repositoryFactory, repositoryFactory); } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkScopedBusContext.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkScopedBusContext.cs index df148e434bc..80b212791e2 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkScopedBusContext.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkScopedBusContext.cs @@ -23,8 +23,9 @@ public class EntityFrameworkScopedBusContext : readonly TBus _bus; readonly IClientFactory _clientFactory; readonly TDbContext _dbContext; - readonly object _lock = new object(); readonly IBusOutboxNotification _notification; + readonly DbSet _outboxMessageSet; + readonly DbSet _outboxStateSet; readonly IServiceProvider _provider; Guid _outboxId; EntityEntry? _outboxState; @@ -42,25 +43,25 @@ public EntityFrameworkScopedBusContext(TBus bus, TDbContext dbContext, IBusOutbo _provider = provider; _outboxId = NewId.NextGuid(); + + _outboxMessageSet = dbContext.Set(); + _outboxStateSet = dbContext.Set(); } public void Dispose() { - lock (_lock) - { - if (WasCommitted()) - _notification.Delivered(); + if (WasCommitted()) + _notification.Delivered(); - _outboxState = null; - } + _outboxState = null; } public Task AddSend(SendContext context) where T : class { - if (_outboxState == null || WasCommitted()) + lock (_outboxStateSet) { - lock (_lock) + if (_outboxState == null || WasCommitted()) { if (WasCommitted()) { @@ -77,7 +78,7 @@ public Task AddSend(SendContext context) } } - return _dbContext.Set().AddSend(context, SystemTextJsonMessageSerializer.Instance, outboxId: _outboxId); + return _outboxMessageSet.AddSend(context, SystemTextJsonMessageSerializer.Instance, outboxId: _outboxId); } public object? GetService(Type serviceType) @@ -85,28 +86,31 @@ public Task AddSend(SendContext context) return _provider.GetService(serviceType); } - public ISendEndpointProvider SendEndpointProvider + public ISendEndpointProvider SendEndpointProvider => _sendEndpointProvider ??= new OutboxSendEndpointProvider(this, GetSendEndpointProvider()); + + public IPublishEndpoint PublishEndpoint => + _publishEndpoint ??= new PublishEndpoint(new OutboxPublishEndpointProvider(this, GetPublishEndpointProvider())); + + public IScopedClientFactory ClientFactory => _scopedClientFactory ??= GetClientFactory(); + + bool WasCommitted() { - get { return _sendEndpointProvider ??= new OutboxSendEndpointProvider(this, _bus); } + return _outboxState?.State == EntityState.Unchanged; } - public IPublishEndpoint PublishEndpoint + protected virtual ScopedClientFactory GetClientFactory() { - get { return _publishEndpoint ??= new PublishEndpoint(new OutboxPublishEndpointProvider(this, _bus)); } + return new ScopedClientFactory(new ClientFactory(new ScopedClientFactoryContext(_clientFactory, _provider)), null); } - public IScopedClientFactory ClientFactory + protected virtual IPublishEndpointProvider GetPublishEndpointProvider() { - get - { - return _scopedClientFactory ??= - new ScopedClientFactory(new ClientFactory(new ScopedClientFactoryContext(_clientFactory, _provider)), null); - } + return _bus; } - bool WasCommitted() + protected virtual ISendEndpointProvider GetSendEndpointProvider() { - return _outboxState?.State == EntityState.Unchanged; + return _bus; } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkScopedBusContextProvider.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkScopedBusContextProvider.cs index c8dc6b7bde8..38a49940737 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkScopedBusContextProvider.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/EntityFrameworkScopedBusContextProvider.cs @@ -13,11 +13,16 @@ public class EntityFrameworkScopedBusContextProvider : where TDbContext : DbContext { public EntityFrameworkScopedBusContextProvider(TBus bus, TDbContext dbContext, IBusOutboxNotification notification, - Bind clientFactory, - ScopedConsumeContextProvider consumeContextProvider, IServiceProvider provider) + Bind clientFactory, Bind consumeContextProvider, + IScopedConsumeContextProvider globalConsumeContextProvider, IServiceProvider provider) { - if (consumeContextProvider.HasContext) - Context = new ConsumeContextScopedBusContext(consumeContextProvider.GetContext(), clientFactory.Value); + if (consumeContextProvider.Value.HasContext) + Context = new ConsumeContextScopedBusContext(consumeContextProvider.Value.GetContext(), clientFactory.Value); + else if (globalConsumeContextProvider.HasContext) + { + Context = new EntityFrameworkConsumeContextScopedBusContext(bus, dbContext, notification, clientFactory.Value, provider, + globalConsumeContextProvider.GetContext()); + } else Context = new EntityFrameworkScopedBusContext(bus, dbContext, notification, clientFactory.Value, provider); } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/FutureSagaDbContext.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/FutureSagaDbContext.cs index 481802ef164..15b7fcf34e9 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/FutureSagaDbContext.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/FutureSagaDbContext.cs @@ -7,7 +7,12 @@ namespace MassTransit.EntityFrameworkCoreIntegration public class FutureSagaDbContext : SagaDbContext { - public FutureSagaDbContext(DbContextOptions options) + public FutureSagaDbContext(DbContextOptions options) + : base(options) + { + } + + protected FutureSagaDbContext(DbContextOptions options) : base(options) { } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/ILoadQueryProvider.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/ILoadQueryProvider.cs deleted file mode 100644 index 145146fd9a5..00000000000 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/ILoadQueryProvider.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MassTransit.EntityFrameworkCoreIntegration -{ - using System.Linq; - using Microsoft.EntityFrameworkCore; - - - public interface ILoadQueryProvider - where TSaga : class, ISaga - { - IQueryable GetQueryable(DbContext dbContext); - } -} diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/InboxCleanupService.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/InboxCleanupService.cs index 07ce288a5f8..2a3ea908c23 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/InboxCleanupService.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/InboxCleanupService.cs @@ -47,7 +47,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) removed = await CleanUpInboxState(stoppingToken).ConfigureAwait(false); } - catch (OperationCanceledException exception) when (exception.CancellationToken == stoppingToken) + catch (OperationCanceledException) { } catch (DbUpdateConcurrencyException exception) diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobAttemptSagaMap.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobAttemptSagaMap.cs index c3dab0d2ed5..6c74f1ca13d 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobAttemptSagaMap.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobAttemptSagaMap.cs @@ -16,6 +16,8 @@ public JobAttemptSagaMap(bool optimistic) protected override void Configure(EntityTypeBuilder entity, ModelBuilder model) { + entity.OptOutOfEntityFrameworkConventions(); + entity.Property(x => x.CurrentState); entity.Ignore(x => x.Version); @@ -29,6 +31,10 @@ protected override void Configure(EntityTypeBuilder entity, Mode entity.Ignore(x => x.RowVersion); entity.Property(x => x.JobId); + entity.HasOne().WithMany() + .HasForeignKey(x => x.JobId) + .OnDelete(DeleteBehavior.Cascade); + entity.Property(x => x.RetryAttempt); entity.HasIndex(x => new diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobSagaMap.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobSagaMap.cs index d0112ea8cb8..0df1df60eaa 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobSagaMap.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobSagaMap.cs @@ -16,6 +16,8 @@ public JobSagaMap(bool optimistic) protected override void Configure(EntityTypeBuilder entity, ModelBuilder model) { + entity.OptOutOfEntityFrameworkConventions(); + entity.Property(x => x.CurrentState); entity.Ignore(x => x.Version); @@ -49,6 +51,25 @@ protected override void Configure(EntityTypeBuilder entity, ModelBuilde entity.Property(x => x.JobSlotWaitToken); entity.Property(x => x.JobRetryDelayToken); + + entity.Property(x => x.IncompleteAttempts) + .HasJsonConversion(); + + entity.Property(x => x.LastProgressValue); + entity.Property(x => x.LastProgressLimit); + entity.Property(x => x.LastProgressSequenceNumber); + + entity.Property(x => x.JobState) + .HasJsonConversion(); + + entity.Property(x => x.JobProperties) + .HasJsonConversion(); + + entity.Property(x => x.CronExpression); + entity.Property(x => x.TimeZoneId); + entity.Property(x => x.StartDate); + entity.Property(x => x.EndDate); + entity.Property(x => x.NextStartDate); } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobTypeSagaMap.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobTypeSagaMap.cs index 01f4826d734..d471706f429 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobTypeSagaMap.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/JobTypeSagaMap.cs @@ -16,6 +16,8 @@ public JobTypeSagaMap(bool optimistic) protected override void Configure(EntityTypeBuilder entity, ModelBuilder model) { + entity.OptOutOfEntityFrameworkConventions(); + entity.Property(x => x.CurrentState); entity.Ignore(x => x.Version); @@ -30,6 +32,8 @@ protected override void Configure(EntityTypeBuilder entity, ModelBu entity.Property(x => x.ActiveJobCount); entity.Property(x => x.ConcurrentJobLimit); + entity.Property(x => x.GlobalConcurrentJobLimit); + entity.Property(x => x.Name); entity.Property(x => x.OverrideJobLimit); entity.Property(x => x.OverrideLimitExpiration); @@ -39,6 +43,9 @@ protected override void Configure(EntityTypeBuilder entity, ModelBu entity.Property(x => x.Instances) .HasJsonConversion(); + + entity.Property(x => x.Properties) + .HasJsonConversion(); } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/MySqlLockStatementProvider.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/MySqlLockStatementProvider.cs index 41568edcd44..7c9422eba32 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/MySqlLockStatementProvider.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/MySqlLockStatementProvider.cs @@ -4,7 +4,7 @@ public class MySqlLockStatementProvider : SqlLockStatementProvider { public MySqlLockStatementProvider(bool enableSchemaCaching = true) - : base(string.Empty, new MySqlLockStatementFormatter(), enableSchemaCaching) + : base(new MySqlLockStatementFormatter(), enableSchemaCaching) { } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/OptimisticFutureSagaDbContext.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/OptimisticFutureSagaDbContext.cs index 620889b864a..3472dc5e213 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/OptimisticFutureSagaDbContext.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/OptimisticFutureSagaDbContext.cs @@ -7,7 +7,7 @@ namespace MassTransit.EntityFrameworkCoreIntegration public class OptimisticFutureSagaDbContext : FutureSagaDbContext { - public OptimisticFutureSagaDbContext(DbContextOptions options) + public OptimisticFutureSagaDbContext(DbContextOptions options) : base(options) { } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/OutboxMessage.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/OutboxMessage.cs index 6cfb0244818..f3f293b2e6a 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/OutboxMessage.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/OutboxMessage.cs @@ -45,6 +45,7 @@ public class OutboxMessage : public Guid MessageId { get; set; } public string ContentType { get; set; } = null!; + public string MessageType { get; set; } = null!; public string Body { get; set; } = null!; diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/PostgresLockStatementFormatter.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/PostgresLockStatementFormatter.cs index 94a8a933230..b08a3c12c6c 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/PostgresLockStatementFormatter.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/PostgresLockStatementFormatter.cs @@ -8,7 +8,7 @@ public class PostgresLockStatementFormatter : { public void Create(StringBuilder sb, string schema, string table) { - sb.AppendFormat("SELECT * FROM \"{0}\".\"{1}\" WHERE ", schema, table); + sb.AppendFormat("SELECT *, xmin FROM {0} WHERE ", FormatTableName(schema, table)); } public void AppendColumn(StringBuilder sb, int index, string columnName) @@ -26,7 +26,12 @@ public void Complete(StringBuilder sb) public void CreateOutboxStatement(StringBuilder sb, string schema, string table, string columnName) { - sb.AppendFormat(@"SELECT * FROM ""{0}"".""{1}"" ORDER BY ""{2}"" LIMIT 1 FOR UPDATE SKIP LOCKED", schema, table, columnName); + sb.AppendFormat(@"SELECT *, xmin FROM {0} ORDER BY ""{1}"" LIMIT 1 FOR UPDATE SKIP LOCKED", FormatTableName(schema, table), columnName); + } + + static string FormatTableName(string schema, string table) + { + return string.IsNullOrEmpty(schema) ? $"\"{table}\"" : $"\"{schema}\".\"{table}\""; } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/PostgresLockStatementProvider.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/PostgresLockStatementProvider.cs index e47b7e5bbee..78f0b698e96 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/PostgresLockStatementProvider.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/PostgresLockStatementProvider.cs @@ -3,10 +3,8 @@ namespace MassTransit.EntityFrameworkCoreIntegration public class PostgresLockStatementProvider : SqlLockStatementProvider { - const string DefaultSchemaName = "public"; - public PostgresLockStatementProvider(bool enableSchemaCaching = true) - : base(DefaultSchemaName, new PostgresLockStatementFormatter(), enableSchemaCaching) + : base(new PostgresLockStatementFormatter(), enableSchemaCaching) { } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/CustomSagaLoadQueryProvider.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/CustomSagaLoadQueryProvider.cs deleted file mode 100644 index 4ff0f50c79c..00000000000 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/CustomSagaLoadQueryProvider.cs +++ /dev/null @@ -1,26 +0,0 @@ -namespace MassTransit.EntityFrameworkCoreIntegration.Saga -{ - using System; - using System.Linq; - using Microsoft.EntityFrameworkCore; - - - public class CustomSagaLoadQueryProvider : - ILoadQueryProvider - where TSaga : class, ISaga - { - readonly Func, IQueryable> _customize; - readonly ILoadQueryProvider _source; - - public CustomSagaLoadQueryProvider(ILoadQueryProvider source, Func, IQueryable> customize) - { - _source = source; - _customize = customize; - } - - public IQueryable GetQueryable(DbContext dbContext) - { - return _customize(_source.GetQueryable(dbContext)); - } - } -} diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/DbContextSagaRepositoryContext.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/DbContextSagaRepositoryContext.cs index 52cabbc3471..423ff88020c 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/DbContextSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/DbContextSagaRepositoryContext.cs @@ -99,8 +99,6 @@ public async Task Update(SagaConsumeContext context) await _inUse.WaitAsync(context.CancellationToken).ConfigureAwait(false); try { - _dbContext.Set().Update(context.Saga); - await _dbContext.SaveChangesAsync(CancellationToken).ConfigureAwait(false); } finally @@ -154,7 +152,8 @@ public Task> CreateSagaConsumeContext(ConsumeCon public class DbContextSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext + QuerySagaRepositoryContext, + LoadSagaRepositoryContext where TSaga : class, ISaga { readonly DbContext _dbContext; @@ -165,6 +164,13 @@ public DbContextSagaRepositoryContext(DbContext dbContext, CancellationToken can _dbContext = dbContext; } + public Task Load(Guid correlationId) + { + return _dbContext.Set() + .AsNoTracking() + .SingleOrDefaultAsync(x => x.CorrelationId == correlationId); + } + public async Task> Query(ISagaQuery query, CancellationToken cancellationToken) { IList results = await _dbContext.Set() @@ -176,12 +182,5 @@ public async Task> Query(ISagaQuery que return new DefaultSagaRepositoryQueryContext(this, results); } - - public Task Load(Guid correlationId) - { - return _dbContext.Set() - .AsNoTracking() - .SingleOrDefaultAsync(x => x.CorrelationId == correlationId); - } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/DefaultSagaLoadQueryProvider.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/DefaultSagaLoadQueryProvider.cs deleted file mode 100644 index 2de2bf27705..00000000000 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/DefaultSagaLoadQueryProvider.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MassTransit.EntityFrameworkCoreIntegration.Saga -{ - using System.Linq; - using Microsoft.EntityFrameworkCore; - - - public class DefaultSagaLoadQueryProvider : - ILoadQueryProvider - where TSaga : class, ISaga - { - public IQueryable GetQueryable(DbContext dbContext) - { - return dbContext.Set(); - } - } -} diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/EntityFrameworkSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/EntityFrameworkSagaRepositoryContextFactory.cs index 55e7074a798..8242ae3190a 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/EntityFrameworkSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/EntityFrameworkSagaRepositoryContextFactory.cs @@ -11,7 +11,9 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Saga public class EntityFrameworkSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + IQuerySagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISaga { readonly ISagaConsumeContextFactory _consumeContextFactory; @@ -26,6 +28,18 @@ public EntityFrameworkSagaRepositoryContextFactory(ISagaDbContextFactory _lockStrategy = lockStrategy; } + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + public void Probe(ProbeContext context) { var dbContext = _dbContextFactory.Create(); @@ -117,7 +131,7 @@ await WithinTransaction(dbContext, context.CancellationToken, () => SendQueryAsy } } - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + async Task ExecuteAsyncMethod(Func, Task> asyncMethod, CancellationToken cancellationToken) where T : class { var dbContext = _dbContextFactory.Create(); diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticLoadQueryExecutor.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticLoadQueryExecutor.cs index aa8eaece564..68143ac2bf5 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticLoadQueryExecutor.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticLoadQueryExecutor.cs @@ -1,6 +1,7 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Saga { using System; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -10,16 +11,28 @@ public class OptimisticLoadQueryExecutor : ILoadQueryExecutor where TSaga : class, ISaga { - readonly ILoadQueryProvider _provider; + readonly Func> _compiledQuery; + readonly Func, IQueryable> _queryCustomization; - public OptimisticLoadQueryExecutor(ILoadQueryProvider provider) + public OptimisticLoadQueryExecutor(Func, IQueryable> queryCustomization = null) { - _provider = provider; + _queryCustomization = queryCustomization; + + if (queryCustomization == null) + { + _compiledQuery = EF.CompileAsyncQuery((DbContext context, Guid id) => + context.Set().AsTracking().SingleOrDefault(x => x.CorrelationId == id)); + } } public Task Load(DbContext dbContext, Guid correlationId, CancellationToken cancellationToken) { - return _provider.GetQueryable(dbContext).AsTracking().SingleOrDefaultAsync(x => x.CorrelationId == correlationId, cancellationToken); + if (_compiledQuery != null) + return _compiledQuery(dbContext, correlationId); + + IQueryable queryable = _queryCustomization(dbContext.Set()); + + return queryable.AsTracking().SingleOrDefaultAsync(x => x.CorrelationId == correlationId, cancellationToken); } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticSagaLockContext.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticSagaLockContext.cs index 6b658b3c685..6d4469da784 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticSagaLockContext.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticSagaLockContext.cs @@ -1,5 +1,6 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Saga { + using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -17,20 +18,25 @@ public class OptimisticSagaLockContext : { readonly CancellationToken _cancellationToken; readonly DbContext _context; - readonly ILoadQueryProvider _provider; readonly ISagaQuery _query; + readonly Func, IQueryable> _queryCustomization; - public OptimisticSagaLockContext(DbContext context, ISagaQuery query, CancellationToken cancellationToken, ILoadQueryProvider provider) + public OptimisticSagaLockContext(DbContext context, ISagaQuery query, CancellationToken cancellationToken, + Func, IQueryable> queryCustomization) { _context = context; _query = query; _cancellationToken = cancellationToken; - _provider = provider; + _queryCustomization = queryCustomization; } public async Task> Load() { - List instances = await _provider.GetQueryable(_context) + IQueryable queryable = _context.Set(); + if (_queryCustomization != null) + queryable = _queryCustomization(queryable); + + List instances = await queryable.AsTracking() .Where(_query.FilterExpression) .ToListAsync(_cancellationToken) .ConfigureAwait(false); diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticSagaRepositoryLockStrategy.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticSagaRepositoryLockStrategy.cs index ed1eba9c346..42b132a5111 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticSagaRepositoryLockStrategy.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/Saga/OptimisticSagaRepositoryLockStrategy.cs @@ -2,6 +2,7 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Saga { using System; using System.Data; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Microsoft.EntityFrameworkCore; @@ -12,12 +13,13 @@ public class OptimisticSagaRepositoryLockStrategy : where TSaga : class, ISaga { readonly ILoadQueryExecutor _executor; - readonly ILoadQueryProvider _provider; + readonly Func, IQueryable> _queryCustomization; - public OptimisticSagaRepositoryLockStrategy(ILoadQueryProvider provider, ILoadQueryExecutor executor, IsolationLevel isolationLevel) + public OptimisticSagaRepositoryLockStrategy(ILoadQueryExecutor executor, Func, IQueryable> queryCustomization, + IsolationLevel isolationLevel) { - _provider = provider; _executor = executor; + _queryCustomization = queryCustomization; IsolationLevel = isolationLevel; } @@ -31,7 +33,7 @@ public Task Load(DbContext context, Guid correlationId, CancellationToken public async Task> CreateLockContext(DbContext context, ISagaQuery query, CancellationToken cancellationToken) { - return new OptimisticSagaLockContext(context, query, cancellationToken, _provider); + return new OptimisticSagaLockContext(context, query, cancellationToken, _queryCustomization); } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlLockStatementProvider.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlLockStatementProvider.cs index 634d59823f4..f5299d11e40 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlLockStatementProvider.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlLockStatementProvider.cs @@ -18,12 +18,18 @@ public class SqlLockStatementProvider : public SqlLockStatementProvider(string defaultSchema, ILockStatementFormatter formatter, bool enableSchemaCaching = true) { - DefaultSchema = defaultSchema ?? throw new ArgumentNullException(nameof(defaultSchema)); + DefaultSchema = defaultSchema; _formatter = formatter; _enableSchemaCaching = enableSchemaCaching; } + public SqlLockStatementProvider(ILockStatementFormatter formatter, bool enableSchemaCaching = true) + { + _formatter = formatter; + _enableSchemaCaching = enableSchemaCaching; + } + string DefaultSchema { get; } public virtual string GetRowLockStatement(DbContext context) diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlServerLockStatementFormatter.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlServerLockStatementFormatter.cs index 5310c7b296c..5c4f0a61ed2 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlServerLockStatementFormatter.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlServerLockStatementFormatter.cs @@ -6,9 +6,19 @@ namespace MassTransit.EntityFrameworkCoreIntegration public class SqlServerLockStatementFormatter : ILockStatementFormatter { + readonly bool _serializable; + + public SqlServerLockStatementFormatter(bool serializable) + { + _serializable = serializable; + } + public void Create(StringBuilder sb, string schema, string table) { - sb.AppendFormat("SELECT * FROM [{0}].{1} WITH (UPDLOCK, ROWLOCK, SERIALIZABLE) WHERE ", schema, table); + sb.AppendFormat("SELECT * FROM {0} WITH (UPDLOCK, ROWLOCK", FormatTableName(schema, table)); + if (_serializable) + sb.Append(", SERIALIZABLE"); + sb.Append(") WHERE "); } public void AppendColumn(StringBuilder sb, int index, string columnName) @@ -25,7 +35,12 @@ public void Complete(StringBuilder sb) public void CreateOutboxStatement(StringBuilder sb, string schema, string table, string columnName) { - sb.AppendFormat(@"SELECT TOP 1 * FROM [{0}].{1} WITH (UPDLOCK, ROWLOCK, READPAST) ORDER BY {2}", schema, table, columnName); + sb.AppendFormat(@"SELECT TOP 1 * FROM {0} WITH (UPDLOCK, ROWLOCK, READPAST) ORDER BY {1}", FormatTableName(schema, table), columnName); + } + + static string FormatTableName(string schema, string table) + { + return string.IsNullOrEmpty(schema) ? $"{table}" : $"[{schema}].{table}"; } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlServerLockStatementProvider.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlServerLockStatementProvider.cs index f3e2de5d464..608edeb6106 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlServerLockStatementProvider.cs +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqlServerLockStatementProvider.cs @@ -3,15 +3,13 @@ public class SqlServerLockStatementProvider : SqlLockStatementProvider { - const string DefaultSchemaName = "dbo"; - - public SqlServerLockStatementProvider(bool enableSchemaCaching = true) - : base(DefaultSchemaName, new SqlServerLockStatementFormatter(), enableSchemaCaching) + public SqlServerLockStatementProvider(bool enableSchemaCaching = true, bool serializable = false) + : base(new SqlServerLockStatementFormatter(serializable), enableSchemaCaching) { } - public SqlServerLockStatementProvider(string schemaName, bool enableSchemaCaching = true) - : base(schemaName, new SqlServerLockStatementFormatter(), enableSchemaCaching) + public SqlServerLockStatementProvider(string schemaName, bool enableSchemaCaching = true, bool serializable = false) + : base(schemaName, new SqlServerLockStatementFormatter(serializable), enableSchemaCaching) { } } diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqliteLockStatementFormatter.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqliteLockStatementFormatter.cs new file mode 100644 index 00000000000..5fb5db037c5 --- /dev/null +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqliteLockStatementFormatter.cs @@ -0,0 +1,28 @@ +namespace MassTransit.EntityFrameworkCoreIntegration +{ + using System.Text; + + + public class SqliteLockStatementFormatter : + ILockStatementFormatter + { + public void Create(StringBuilder sb, string schema, string table) + { + sb.Append($@"SELECT * FROM ""{table}"" WHERE "); + } + + public void AppendColumn(StringBuilder sb, int index, string columnName) + { + sb.Append(index == 0 ? $@"""{columnName}"" = @p0" : $@" AND ""{columnName}"" = @p{index}"); + } + + public void Complete(StringBuilder sb) + { + } + + public void CreateOutboxStatement(StringBuilder sb, string schema, string table, string columnName) + { + sb.Append($@"SELECT * FROM ""{table}"" ORDER BY ""{columnName}"" LIMIT 1"); + } + } +} diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqliteLockStatementProvider.cs b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqliteLockStatementProvider.cs new file mode 100644 index 00000000000..d29983b5142 --- /dev/null +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/EntityFrameworkCoreIntegration/SqliteLockStatementProvider.cs @@ -0,0 +1,16 @@ +namespace MassTransit.EntityFrameworkCoreIntegration +{ + public class SqliteLockStatementProvider : + SqlLockStatementProvider + { + public SqliteLockStatementProvider(bool enableSchemaCaching = true) + : base(new SqliteLockStatementFormatter(), enableSchemaCaching) + { + } + + public SqliteLockStatementProvider(string schemaName, bool enableSchemaCaching = true) + : base(schemaName, new SqliteLockStatementFormatter(), enableSchemaCaching) + { + } + } +} diff --git a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/MassTransit.EntityFrameworkCoreIntegration.csproj b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/MassTransit.EntityFrameworkCoreIntegration.csproj index 62d21726399..6ac728bf6f2 100644 --- a/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/MassTransit.EntityFrameworkCoreIntegration.csproj +++ b/src/Persistence/MassTransit.EntityFrameworkCoreIntegration/MassTransit.EntityFrameworkCoreIntegration.csproj @@ -2,7 +2,7 @@ - netstandard2.0;net6.0 + netstandard2.0;net6.0;net8.0 MassTransit @@ -18,7 +18,6 @@ - diff --git a/src/Persistence/MassTransit.EntityFrameworkIntegration/Configuration/Configuration/EntityFrameworkSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.EntityFrameworkIntegration/Configuration/Configuration/EntityFrameworkSagaRepositoryConfigurator.cs index 30f8b90ee5c..50938edd398 100644 --- a/src/Persistence/MassTransit.EntityFrameworkIntegration/Configuration/Configuration/EntityFrameworkSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.EntityFrameworkIntegration/Configuration/Configuration/EntityFrameworkSagaRepositoryConfigurator.cs @@ -84,8 +84,10 @@ public void Register(ISagaRepositoryRegistrationConfigurator configurator else configurator.TryAddSingleton(provider => CreatePessimisticLockStrategy()); - configurator.RegisterSagaRepository, - EntityFrameworkSagaRepositoryContextFactory>(); + configurator.RegisterLoadSagaRepository>(); + configurator.RegisterQuerySagaRepository>(); + configurator + .RegisterSagaRepository, EntityFrameworkSagaRepositoryContextFactory>(); } ISagaRepositoryLockStrategy CreateOptimisticLockStrategy() diff --git a/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/EntityFrameworkSagaRepository.cs b/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/EntityFrameworkSagaRepository.cs index 9744951d7eb..003ba97aea4 100644 --- a/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/EntityFrameworkSagaRepository.cs +++ b/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/EntityFrameworkSagaRepository.cs @@ -55,7 +55,7 @@ static ISagaRepository CreateRepository(ISagaDbContextFactory dbCo var repositoryFactory = new EntityFrameworkSagaRepositoryContextFactory(dbContextFactory, consumeContextFactory, lockStrategy); - return new SagaRepository(repositoryFactory); + return new SagaRepository(repositoryFactory, repositoryFactory, repositoryFactory); } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/Saga/DbContextSagaRepositoryContext.cs b/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/Saga/DbContextSagaRepositoryContext.cs index 87e8f9e53b0..5f288c73497 100644 --- a/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/Saga/DbContextSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/Saga/DbContextSagaRepositoryContext.cs @@ -145,7 +145,8 @@ public Task> CreateSagaConsumeContext(ConsumeCon public class DbContextSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext + QuerySagaRepositoryContext, + LoadSagaRepositoryContext where TSaga : class, ISaga { readonly DbContext _dbContext; @@ -156,6 +157,13 @@ public DbContextSagaRepositoryContext(DbContext dbContext, CancellationToken can _dbContext = dbContext; } + public Task Load(Guid correlationId) + { + return _dbContext.Set() + .AsNoTracking() + .SingleOrDefaultAsync(x => x.CorrelationId == correlationId); + } + public async Task> Query(ISagaQuery query, CancellationToken cancellationToken) { IList results = await _dbContext.Set() @@ -167,12 +175,5 @@ public async Task> Query(ISagaQuery que return new DefaultSagaRepositoryQueryContext(this, results); } - - public Task Load(Guid correlationId) - { - return _dbContext.Set() - .AsNoTracking() - .SingleOrDefaultAsync(x => x.CorrelationId == correlationId); - } } } diff --git a/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/Saga/EntityFrameworkSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/Saga/EntityFrameworkSagaRepositoryContextFactory.cs index 0399549b3d4..1fd54ef70f5 100644 --- a/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/Saga/EntityFrameworkSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.EntityFrameworkIntegration/EntityFrameworkIntegration/Saga/EntityFrameworkSagaRepositoryContextFactory.cs @@ -12,7 +12,9 @@ namespace MassTransit.EntityFrameworkIntegration.Saga public class EntityFrameworkSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + IQuerySagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISaga { readonly ISagaConsumeContextFactory _consumeContextFactory; @@ -27,6 +29,18 @@ public EntityFrameworkSagaRepositoryContextFactory(ISagaDbContextFactory _lockStrategy = lockStrategy; } + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + public void Probe(ProbeContext context) { var dbContext = _dbContextFactory.Create(); @@ -89,7 +103,7 @@ await WithinTransaction(dbContext, async () => } } - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + async Task ExecuteAsyncMethod(Func, Task> asyncMethod, CancellationToken cancellationToken) where T : class { var dbContext = _dbContextFactory.Create(); diff --git a/src/Persistence/MassTransit.EntityFrameworkIntegration/MassTransit.EntityFrameworkIntegration.csproj b/src/Persistence/MassTransit.EntityFrameworkIntegration/MassTransit.EntityFrameworkIntegration.csproj index dc7ebe05db4..5c7ee507776 100644 --- a/src/Persistence/MassTransit.EntityFrameworkIntegration/MassTransit.EntityFrameworkIntegration.csproj +++ b/src/Persistence/MassTransit.EntityFrameworkIntegration/MassTransit.EntityFrameworkIntegration.csproj @@ -6,7 +6,7 @@ - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -22,7 +22,6 @@ - diff --git a/src/Persistence/MassTransit.MartenIntegration/Configuration/Configuration/MartenSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.MartenIntegration/Configuration/Configuration/MartenSagaRepositoryConfigurator.cs index 461fdf40a57..1342b05b4a4 100644 --- a/src/Persistence/MassTransit.MartenIntegration/Configuration/Configuration/MartenSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.MartenIntegration/Configuration/Configuration/MartenSagaRepositoryConfigurator.cs @@ -5,6 +5,7 @@ namespace MassTransit.Configuration using Marten; using Marten.Schema.Identity; using MartenIntegration.Saga; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Npgsql; using Saga; @@ -15,47 +16,85 @@ public class MartenSagaRepositoryConfigurator : ISpecification where TSaga : class, ISaga { - Action _configureOptions; + readonly Action> _configureSchema; + Action _configureMarten; + public MartenSagaRepositoryConfigurator(Action> configureSchema = null) + { + _configureSchema = configureSchema; + } + + [Obsolete("Use AddMarten to configure the connection. Visit https://masstransit.io/obsolete for details.")] public void Connection(string connectionString, Action configure = null) { void ConfigureOptions(StoreOptions options) { options.Connection(connectionString); - options.Schema.For().Identity(x => x.CorrelationId).IdStrategy(new NoOpIdGeneration()); - configure?.Invoke(options); } - _configureOptions = ConfigureOptions; + _configureMarten = ConfigureOptions; } + [Obsolete("Use AddMarten to configure the connection. Visit https://masstransit.io/obsolete for details.")] public void Connection(Func connectionFactory, Action configure = null) { void ConfigureOptions(StoreOptions options) { options.Connection(connectionFactory); - options.Schema.For().Identity(x => x.CorrelationId).IdStrategy(new NoOpIdGeneration()); - configure?.Invoke(options); } - _configureOptions = ConfigureOptions; + _configureMarten = ConfigureOptions; } public IEnumerable Validate() { - if (_configureOptions == null) - yield return this.Failure("Connection", "must be specified"); + yield break; + } + + public void Register(ISagaRepositoryRegistrationConfigurator configurator) + { + if (_configureMarten != null) + { + configurator.AddMarten(options => + { + _configureMarten(options); + }); + } + + configurator.TryAddEnumerable(ServiceDescriptor.Singleton(Factory)); + configurator.RegisterLoadSagaRepository>(); + configurator.RegisterQuerySagaRepository>(); + configurator.RegisterSagaRepository, + MartenSagaRepositoryContextFactory>(); + } + + MartenSagaRepositoryStoreOptionsConfigurator Factory(IServiceProvider provider) + { + return new MartenSagaRepositoryStoreOptionsConfigurator(_configureSchema); } - public void Register(ISagaRepositoryRegistrationConfigurator configurator) - where T : class, ISaga + + class MartenSagaRepositoryStoreOptionsConfigurator : + IConfigureMarten { - configurator.TryAddSingleton(provider => DocumentStore.For(_configureOptions)); - configurator.RegisterSagaRepository, MartenSagaRepositoryContextFactory>(); + readonly Action> _configure; + + public MartenSagaRepositoryStoreOptionsConfigurator(Action> configure) + { + _configure = configure; + } + + public void Configure(IServiceProvider services, StoreOptions options) + { + MartenRegistry.DocumentMappingExpression mappingExpression = + options.Schema.For().Identity(x => x.CorrelationId).IdStrategy(new NoOpIdGeneration()).UseOptimisticConcurrency(true); + + _configure?.Invoke(mappingExpression); + } } } } diff --git a/src/Persistence/MassTransit.MartenIntegration/Configuration/Configuration/MartenSagaRepositoryRegistrationProvider.cs b/src/Persistence/MassTransit.MartenIntegration/Configuration/Configuration/MartenSagaRepositoryRegistrationProvider.cs index 836f9b9c1e5..a2b3fb912a1 100644 --- a/src/Persistence/MassTransit.MartenIntegration/Configuration/Configuration/MartenSagaRepositoryRegistrationProvider.cs +++ b/src/Persistence/MassTransit.MartenIntegration/Configuration/Configuration/MartenSagaRepositoryRegistrationProvider.cs @@ -1,36 +1,23 @@ namespace MassTransit.Configuration { - using System; - using Marten; - using Npgsql; - - public class MartenSagaRepositoryRegistrationProvider : ISagaRepositoryRegistrationProvider { - readonly Action _configure; - readonly Func _connectionFactory; - readonly string _connectionString; - - public MartenSagaRepositoryRegistrationProvider(string connectionString, Action configure) - { - _connectionString = connectionString; - _configure = configure; - } + readonly bool _optimisticConcurrency; - public MartenSagaRepositoryRegistrationProvider(Func connectionFactory, Action configure) + public MartenSagaRepositoryRegistrationProvider(bool optimisticConcurrency = false) { - _connectionFactory = connectionFactory; - _configure = configure; + _optimisticConcurrency = optimisticConcurrency; } public virtual void Configure(ISagaRegistrationConfigurator configurator) where TSaga : class, ISaga { - if (_connectionFactory != null) - configurator.MartenRepository(_connectionFactory, options => _configure?.Invoke(options)); - else - configurator.MartenRepository(_connectionString, options => _configure?.Invoke(options)); + configurator.MartenRepository(schema => + { + if (_optimisticConcurrency) + schema.UseOptimisticConcurrency(true); + }); } } } diff --git a/src/Persistence/MassTransit.MartenIntegration/Configuration/IMartenSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.MartenIntegration/Configuration/IMartenSagaRepositoryConfigurator.cs index 455a766a73d..f200739b6d7 100644 --- a/src/Persistence/MassTransit.MartenIntegration/Configuration/IMartenSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.MartenIntegration/Configuration/IMartenSagaRepositoryConfigurator.cs @@ -7,7 +7,10 @@ namespace MassTransit public interface IMartenSagaRepositoryConfigurator { + [Obsolete("Use AddMarten to configure the connection. Visit https://masstransit.io/obsolete for details.")] void Connection(string connectionString, Action configure = null); + + [Obsolete("Use AddMarten to configure the connection. Visit https://masstransit.io/obsolete for details.")] void Connection(Func connectionFactory, Action configure = null); } diff --git a/src/Persistence/MassTransit.MartenIntegration/Configuration/MartenSagaRepositoryRegistrationExtensions.cs b/src/Persistence/MassTransit.MartenIntegration/Configuration/MartenSagaRepositoryRegistrationExtensions.cs index 4683c41bdcc..0a60341883e 100644 --- a/src/Persistence/MassTransit.MartenIntegration/Configuration/MartenSagaRepositoryRegistrationExtensions.cs +++ b/src/Persistence/MassTransit.MartenIntegration/Configuration/MartenSagaRepositoryRegistrationExtensions.cs @@ -8,6 +8,33 @@ namespace MassTransit public static class MartenSagaRepositoryRegistrationExtensions { + /// + /// Configures the saga to use the Marten saga repository. + /// + /// + /// Optional, allows additional configuration on the saga schema + /// The saga type + public static ISagaRegistrationConfigurator MartenRepository(this ISagaRegistrationConfigurator configurator, + Action> configure = null) + where T : class, ISaga + { + var repositoryConfigurator = new MartenSagaRepositoryConfigurator(configure); + + configurator.Repository(x => repositoryConfigurator.Register(x)); + + return configurator; + } + + /// + /// Use the Marten saga repository for any sagas without an explicitly configured saga repository. + /// + /// + /// If true, optimistic concurrency will be used for any configured saga types + public static void SetMartenSagaRepositoryProvider(this IRegistrationConfigurator configurator, bool optimisticConcurrency = false) + { + configurator.SetSagaRepositoryProvider(new MartenSagaRepositoryRegistrationProvider(optimisticConcurrency)); + } + /// /// Adds a Marten saga repository to the registration /// @@ -16,6 +43,7 @@ public static class MartenSagaRepositoryRegistrationExtensions /// /// /// + [Obsolete("AddMarten should be used to set up and configure Marten and use MartenRepository() with no arguments.")] public static ISagaRegistrationConfigurator MartenRepository(this ISagaRegistrationConfigurator configurator, string connectionString, Action configureOptions) where T : class, ISaga @@ -24,8 +52,6 @@ public static ISagaRegistrationConfigurator MartenRepository(this ISagaReg repositoryConfigurator.Connection(connectionString, configureOptions); - repositoryConfigurator.Validate().ThrowIfContainsFailure("The Marten saga repository configuration is invalid:"); - configurator.Repository(x => repositoryConfigurator.Register(x)); return configurator; @@ -39,6 +65,7 @@ public static ISagaRegistrationConfigurator MartenRepository(this ISagaReg /// /// /// + [Obsolete("AddMarten should be used to set up and configure Marten and use MartenRepository() with no arguments.")] public static ISagaRegistrationConfigurator MartenRepository(this ISagaRegistrationConfigurator configurator, Func connectionFactory, Action configureOptions) where T : class, ISaga @@ -47,8 +74,6 @@ public static ISagaRegistrationConfigurator MartenRepository(this ISagaReg repositoryConfigurator.Connection(connectionFactory, configureOptions); - repositoryConfigurator.Validate().ThrowIfContainsFailure("The Marten saga repository configuration is invalid:"); - configurator.Repository(x => repositoryConfigurator.Register(x)); return configurator; @@ -61,6 +86,7 @@ public static ISagaRegistrationConfigurator MartenRepository(this ISagaReg /// The Marten configuration string /// /// + [Obsolete("AddMarten should be used to set up and configure Marten and use MartenRepository() with no arguments.")] public static ISagaRegistrationConfigurator MartenRepository(this ISagaRegistrationConfigurator configurator, string connectionString) where T : class, ISaga { @@ -68,47 +94,80 @@ public static ISagaRegistrationConfigurator MartenRepository(this ISagaReg repositoryConfigurator.Connection(connectionString); - repositoryConfigurator.Validate().ThrowIfContainsFailure("The Marten saga repository configuration is invalid:"); - configurator.Repository(x => repositoryConfigurator.Register(x)); return configurator; } /// - /// Use the Marten saga repository for sagas configured by type (without a specific generic call to AddSaga/AddSagaStateMachine) + /// Use the Marten saga repository for sagas configured by type (without a specific repository specified via AddSaga/AddSagaStateMachine) /// /// /// + [Obsolete("AddMarten should be used to set up and configure Marten and use SetMartenSagaRepositoryProvider() with no arguments.")] public static void SetMartenSagaRepositoryProvider(this IRegistrationConfigurator configurator, string connectionString) { - configurator.SetSagaRepositoryProvider(new MartenSagaRepositoryRegistrationProvider(connectionString, x => - { - })); + var martenRegistrationPipeline = configurator.AddMarten( + options => + { + options.Connection(connectionString); + }); + +#if NET6_0_OR_GREATER + martenRegistrationPipeline.ApplyAllDatabaseChangesOnStartup(); +#endif + + configurator.SetSagaRepositoryProvider(new MartenSagaRepositoryRegistrationProvider()); } /// - /// Use the Marten saga repository for sagas configured by type (without a specific generic call to AddSaga/AddSagaStateMachine) + /// Use the Marten saga repository for sagas configured by type (without a specific repository specified via AddSaga/AddSagaStateMachine) /// /// /// /// + [Obsolete("AddMarten should be used to set up and configure Marten and use SetMartenSagaRepositoryProvider() with no arguments.")] public static void SetMartenSagaRepositoryProvider(this IRegistrationConfigurator configurator, string connectionString, Action configureOptions) { - configurator.SetSagaRepositoryProvider(new MartenSagaRepositoryRegistrationProvider(connectionString, configureOptions)); + var martenRegistrationPipeline = configurator.AddMarten( + options => + { + options.Connection(connectionString); + + configureOptions?.Invoke(options); + }); + +#if NET6_0_OR_GREATER + martenRegistrationPipeline.ApplyAllDatabaseChangesOnStartup(); +#endif + + configurator.SetSagaRepositoryProvider(new MartenSagaRepositoryRegistrationProvider()); } /// - /// Use the Marten saga repository for sagas configured by type (without a specific generic call to AddSaga/AddSagaStateMachine) + /// Use the Marten saga repository for sagas configured by type (without a specific repository specified via AddSaga/AddSagaStateMachine) /// /// /// /// + [Obsolete("AddMarten should be used to set up and configure Marten and use SetMartenSagaRepositoryProvider() with no arguments.")] public static void SetMartenSagaRepositoryProvider(this IRegistrationConfigurator configurator, Func connectionFactory, Action configureOptions) { - configurator.SetSagaRepositoryProvider(new MartenSagaRepositoryRegistrationProvider(connectionFactory, configureOptions)); + var martenRegistrationPipeline = configurator.AddMarten( + options => + { + options.Connection(connectionFactory); + + configureOptions?.Invoke(options); + }); + +#if NET6_0_OR_GREATER + martenRegistrationPipeline.ApplyAllDatabaseChangesOnStartup(); +#endif + + configurator.SetSagaRepositoryProvider(new MartenSagaRepositoryRegistrationProvider()); } } } diff --git a/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepository.cs b/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepository.cs index f70e02506ec..22887270597 100644 --- a/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepository.cs +++ b/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepository.cs @@ -11,8 +11,8 @@ public static ISagaRepository Create(IDocumentStore documentStore) { var consumeContextFactory = new SagaConsumeContextFactory(); - ISagaRepositoryContextFactory repositoryContextFactory = new MartenSagaRepositoryContextFactory(documentStore, consumeContextFactory); - return new SagaRepository(repositoryContextFactory); + var repositoryContextFactory = new MartenSagaRepositoryContextFactory(documentStore, consumeContextFactory); + return new SagaRepository(repositoryContextFactory, repositoryContextFactory, repositoryContextFactory); } } } diff --git a/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepositoryContext.cs b/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepositoryContext.cs index c9ce424c641..743968939b3 100644 --- a/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepositoryContext.cs @@ -111,7 +111,8 @@ public Task Undo(SagaConsumeContext context) public class MartenSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext + QuerySagaRepositoryContext, + LoadSagaRepositoryContext where TSaga : class, ISaga { readonly IQuerySession _session; diff --git a/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepositoryContextFactory.cs index 15f300073a7..9e0c8fa0271 100644 --- a/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.MartenIntegration/MartenIntegration/Saga/MartenSagaRepositoryContextFactory.cs @@ -10,7 +10,9 @@ namespace MassTransit.MartenIntegration.Saga public class MartenSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + IQuerySagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISaga { readonly IDocumentStore _documentStore; @@ -22,6 +24,18 @@ public MartenSagaRepositoryContextFactory(IDocumentStore documentStore, ISagaCon _factory = factory; } + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + public async Task Send(ConsumeContext context, IPipe> next) where T : class { @@ -49,10 +63,20 @@ public async Task SendQuery(ConsumeContext context, ISagaQuery quer await next.Send(queryContext).ConfigureAwait(false); } - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken) + public void Probe(ProbeContext context) + { + context.Add("persistence", "marten"); + } + + async Task ExecuteAsyncMethod(Func, Task> asyncMethod, CancellationToken cancellationToken) where T : class { + +#if NET6_0_OR_GREATER + var session = _documentStore.LightweightSession(); +#else var session = _documentStore.OpenSession(); +#endif try { var repositoryContext = new MartenSagaRepositoryContext(session, cancellationToken); @@ -64,10 +88,5 @@ public async Task Execute(Func, Task> asyn session.Dispose(); } } - - public void Probe(ProbeContext context) - { - context.Add("persistence", "marten"); - } } } diff --git a/src/Persistence/MassTransit.MartenIntegration/MassTransit.MartenIntegration.csproj b/src/Persistence/MassTransit.MartenIntegration/MassTransit.MartenIntegration.csproj index 484038b947d..0e54697b331 100644 --- a/src/Persistence/MassTransit.MartenIntegration/MassTransit.MartenIntegration.csproj +++ b/src/Persistence/MassTransit.MartenIntegration/MassTransit.MartenIntegration.csproj @@ -1,11 +1,7 @@  - netstandard2.0;netstandard2.1;net6.0 - - - - $(TargetFrameworks);net462 + net6.0;net8.0 @@ -21,7 +17,7 @@ - + diff --git a/src/Persistence/MassTransit.MongoDbIntegration/Configuration/Configuration/MongoDbOutboxConfigurator.cs b/src/Persistence/MassTransit.MongoDbIntegration/Configuration/Configuration/MongoDbOutboxConfigurator.cs index f67797fed5d..8adcb8dd3a1 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/Configuration/Configuration/MongoDbOutboxConfigurator.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/Configuration/Configuration/MongoDbOutboxConfigurator.cs @@ -90,32 +90,23 @@ IMongoCollection CollectionFactory(IServiceProvider provider) static void RegisterClassMaps() { - if (!BsonClassMap.IsClassMapRegistered(typeof(InboxState))) + BsonClassMap.TryRegisterClassMap(new BsonClassMap(cfg => { - BsonClassMap.RegisterClassMap(new BsonClassMap(cfg => - { - cfg.AutoMap(); - cfg.MapIdProperty(x => x.Id); - })); - } + cfg.AutoMap(); + cfg.MapIdProperty(x => x.Id); + })); - if (!BsonClassMap.IsClassMapRegistered(typeof(OutboxState))) + BsonClassMap.TryRegisterClassMap(new BsonClassMap(cfg => { - BsonClassMap.RegisterClassMap(new BsonClassMap(cfg => - { - cfg.AutoMap(); - cfg.MapIdProperty(x => x.OutboxId); - })); - } + cfg.AutoMap(); + cfg.MapIdProperty(x => x.OutboxId); + })); - if (!BsonClassMap.IsClassMapRegistered(typeof(OutboxMessage))) + BsonClassMap.TryRegisterClassMap(new BsonClassMap(cfg => { - BsonClassMap.RegisterClassMap(new BsonClassMap(cfg => - { - cfg.AutoMap(); - cfg.MapIdProperty(x => x.Id); - })); - } + cfg.AutoMap(); + cfg.MapIdProperty(x => x.Id); + })); } } } diff --git a/src/Persistence/MassTransit.MongoDbIntegration/Configuration/Configuration/MongoDbSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.MongoDbIntegration/Configuration/Configuration/MongoDbSagaRepositoryConfigurator.cs index 8e10e210148..773831b6b7a 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/Configuration/Configuration/MongoDbSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/Configuration/Configuration/MongoDbSagaRepositoryConfigurator.cs @@ -24,6 +24,7 @@ public MongoDbSagaRepositoryConfigurator() { cfg.AutoMap(); cfg.MapIdProperty(x => x.CorrelationId); + cfg.MapProperty(x => x.Version).SetIgnoreIfDefault(false); })); } @@ -41,13 +42,11 @@ public override IEnumerable Validate() yield return this.Failure("ClassMapFactory", "must be specified"); } - public void Register(ISagaRepositoryRegistrationConfigurator configurator) - where T : class, ISagaVersion + public void Register(ISagaRepositoryRegistrationConfigurator configurator) { IMongoCollection MongoDbCollectionFactory(IServiceProvider provider) { - if (!BsonClassMap.IsClassMapRegistered(typeof(TSaga))) - BsonClassMap.RegisterClassMap(_classMapFactory(provider)); + BsonClassMap.TryRegisterClassMap(_classMapFactory(provider)); var database = ProviderDatabaseFactory(provider); var collectionNameFormatter = CollectionNameFormatterFactory(provider); @@ -67,8 +66,10 @@ IMongoCollection MongoDbCollectionFactory(IServiceProvider provider) configurator.TryAddScoped(provider => provider.GetRequiredService().GetCollection()); - configurator.RegisterSagaRepository, SagaConsumeContextFactory, T>, - MongoDbSagaRepositoryContextFactory>(); + configurator.RegisterLoadSagaRepository>(); + configurator.RegisterQuerySagaRepository>(); + configurator.RegisterSagaRepository, SagaConsumeContextFactory, TSaga>, + MongoDbSagaRepositoryContextFactory>(); } } } diff --git a/src/Persistence/MassTransit.MongoDbIntegration/Configuration/MongoDbOutboxConfigurationExtensions.cs b/src/Persistence/MassTransit.MongoDbIntegration/Configuration/MongoDbOutboxConfigurationExtensions.cs index 3c98cc15fb8..e2a2031a95e 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/Configuration/MongoDbOutboxConfigurationExtensions.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/Configuration/MongoDbOutboxConfigurationExtensions.cs @@ -3,13 +3,14 @@ namespace MassTransit { using System; using Configuration; + using DependencyInjection; using MongoDbIntegration; public static class MongoDbOutboxConfigurationExtensions { /// - /// Configures the Entity Framework Outbox on the bus, which can subsequently be used to configure + /// Configures the Mongo DB outbox on the bus, which can subsequently be used to configure /// the transactional outbox on a receive endpoint. /// /// @@ -23,12 +24,35 @@ public static void AddMongoDbOutbox(this IBusRegistrationConfigurator configurat outboxConfigurator.Configure(configure); } + /// + /// Configure the Mongo DB outbox on the receive endpoint + /// + /// + /// Configuration service provider + /// + public static void UseMongoDbOutbox(this IReceiveEndpointConfigurator configurator, IRegistrationContext context, + Action? configure = null) + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (context == null) + throw new ArgumentNullException(nameof(context)); + + var observer = new OutboxConsumePipeSpecificationObserver(configurator, context); + + configure?.Invoke(observer); + + configurator.ConnectConsumerConfigurationObserver(observer); + configurator.ConnectSagaConfigurationObserver(observer); + } + /// /// Configure the Entity Framework outbox on the receive endpoint /// /// /// Configuration service provider /// + [Obsolete("Use the IRegistrationContext overload instead. Visit https://masstransit.io/obsolete for details.")] public static void UseMongoDbOutbox(this IReceiveEndpointConfigurator configurator, IServiceProvider provider, Action? configure = null) { @@ -37,7 +61,7 @@ public static void UseMongoDbOutbox(this IReceiveEndpointConfigurator configurat if (provider == null) throw new ArgumentNullException(nameof(provider)); - var observer = new OutboxConsumePipeSpecificationObserver(configurator, provider); + var observer = new OutboxConsumePipeSpecificationObserver(configurator, provider, LegacySetScopedConsumeContext.Instance); configure?.Invoke(observer); diff --git a/src/Persistence/MassTransit.MongoDbIntegration/Configuration/MongoDbSagaRepositoryRegistrationExtensions.cs b/src/Persistence/MassTransit.MongoDbIntegration/Configuration/MongoDbSagaRepositoryRegistrationExtensions.cs index 7cc1ceba097..d526fd14c7c 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/Configuration/MongoDbSagaRepositoryRegistrationExtensions.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/Configuration/MongoDbSagaRepositoryRegistrationExtensions.cs @@ -67,6 +67,66 @@ public static ISagaRegistrationConfigurator MongoDbRepository(this }); } + /// + /// Configure the Job Service saga state machines to use MongoDB + /// + /// + /// + /// + public static IJobSagaRegistrationConfigurator MongoDbRepository(this IJobSagaRegistrationConfigurator configurator, + Action configure) + { + var registrationProvider = new MongoDbSagaRepositoryRegistrationProvider(configure); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + + /// + /// Configure the Job Service saga state machines to use MongoDB + /// + /// + /// The connection string for the MongoDB database + /// + /// + public static IJobSagaRegistrationConfigurator MongoDbRepository(this IJobSagaRegistrationConfigurator configurator, string connectionString, + Action configure) + { + var registrationProvider = new MongoDbSagaRepositoryRegistrationProvider(r => + { + r.Connection = connectionString; + + configure?.Invoke(r); + }); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + + /// + /// Configure the Job Service saga state machines to use MongoDB + /// + /// + /// A ready to use MongoDB database + /// + /// + public static IJobSagaRegistrationConfigurator MongoDbRepository(this IJobSagaRegistrationConfigurator configurator, IMongoDatabase database, + Action configure) + { + var registrationProvider = new MongoDbSagaRepositoryRegistrationProvider(r => + { + r.Database(database); + + configure?.Invoke(r); + }); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + /// /// Use the MongoDB saga repository for sagas configured by type (without a specific generic call to AddSaga/AddSagaStateMachine) /// diff --git a/src/Persistence/MassTransit.MongoDbIntegration/Exceptions/MongoDbSaveEventException.cs b/src/Persistence/MassTransit.MongoDbIntegration/Exceptions/MongoDbSaveEventException.cs index c3fe47b92bf..73e1e132e06 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/Exceptions/MongoDbSaveEventException.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/Exceptions/MongoDbSaveEventException.cs @@ -24,6 +24,9 @@ public MongoDbSaveEventException(Guid trackingNumber, string message, Exception TrackingNumber = trackingNumber; } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MongoDbSaveEventException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -32,6 +35,9 @@ protected MongoDbSaveEventException(SerializationInfo info, StreamingContext con public Guid TrackingNumber { get; private set; } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MassTransit.MongoDbIntegration.csproj b/src/Persistence/MassTransit.MongoDbIntegration/MassTransit.MongoDbIntegration.csproj index 45649329eb5..f4391a4d189 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MassTransit.MongoDbIntegration.csproj +++ b/src/Persistence/MassTransit.MongoDbIntegration/MassTransit.MongoDbIntegration.csproj @@ -1,11 +1,12 @@  + - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -22,7 +23,6 @@ - diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MassTransitMongoDbConventions.cs b/src/Persistence/MassTransit.MongoDbIntegration/MassTransitMongoDbConventions.cs index e0aa84b4056..05a58117f3f 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MassTransitMongoDbConventions.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MassTransitMongoDbConventions.cs @@ -46,20 +46,17 @@ public MassTransitMongoDbConventions(ConventionFilter filter = default) static bool IsMassTransitClass(Type type) { - return type.FullName.StartsWith("MassTransit") || IsSagaClass(type) && type != typeof(AuditDocument); + return type.FullName.StartsWith("MassTransit") || (IsSagaClass(type) && type != typeof(AuditDocument)); } static bool IsSagaClass(Type type) { - return type.GetTypeInfo().IsClass && typeof(ISagaVersion).IsAssignableFrom(type); + return type.IsClass && typeof(ISagaVersion).IsAssignableFrom(type); } public static void RegisterClass(Expression> id) { - if (BsonClassMap.IsClassMapRegistered(typeof(T))) - return; - - BsonClassMap.RegisterClassMap(x => + BsonClassMap.TryRegisterClassMap(x => { x.AutoMap(); x.SetIdMember(x.GetMemberMap(id)); @@ -68,10 +65,7 @@ public static void RegisterClass(Expression> id) public static void RegisterClass() { - if (BsonClassMap.IsClassMapRegistered(typeof(T))) - return; - - BsonClassMap.RegisterClassMap(x => + BsonClassMap.TryRegisterClassMap(x => { x.AutoMap(); x.SetDiscriminatorIsRequired(true); diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Audit/MongoDbAuditStore.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Audit/MongoDbAuditStore.cs index ec75726bbac..121ce6e7d17 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Audit/MongoDbAuditStore.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Audit/MongoDbAuditStore.cs @@ -8,7 +8,8 @@ namespace MassTransit.MongoDbIntegration.Audit using MongoDB.Driver; - public class MongoDbAuditStore : IMessageAuditStore + public class MongoDbAuditStore : + IMessageAuditStore { readonly IMongoCollection _collection; @@ -17,14 +18,14 @@ static MongoDbAuditStore() if (BsonClassMap.IsClassMapRegistered(typeof(AuditDocument))) return; - // easiest way to metadata since keys wont become element names, therefore subject to validation - // will allow keys like $correlationId to be kept - var headersSerializer = new DictionaryInterfaceImplementerSerializer( - DictionaryRepresentation.ArrayOfDocuments - ); - - BsonClassMap.RegisterClassMap(x => + BsonClassMap.TryRegisterClassMap(x => { + // easiest way to metadata since keys wont become element names, therefore subject to validation + // will allow keys like $correlationId to be kept + var headersSerializer = new DictionaryInterfaceImplementerSerializer( + DictionaryRepresentation.ArrayOfDocuments + ); + x.AutoMap(); x.MapIdMember(doc => doc.AuditId); x.MapMember(doc => doc.Headers).SetSerializer(headersSerializer); diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/BusOutboxDeliveryService.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/BusOutboxDeliveryService.cs index 17b3ee737be..bb21c83fe55 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/BusOutboxDeliveryService.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/BusOutboxDeliveryService.cs @@ -2,6 +2,7 @@ namespace MassTransit.MongoDbIntegration { using System; using System.Collections.Generic; + using System.Linq; using System.Threading; using System.Threading.Tasks; using Logging; @@ -15,7 +16,6 @@ namespace MassTransit.MongoDbIntegration using MongoDB.Driver; using Outbox; using Serialization; - using Util; public class BusOutboxDeliveryService : @@ -40,32 +40,37 @@ public BusOutboxDeliveryService(IBusControl busControl, IOptions 0) + continue; + + await _notification.WaitForDelivery(stoppingToken).ConfigureAwait(false); } catch (OperationCanceledException) { } catch (Exception exception) { - _logger.LogError(exception, "ProcessMessageBatch faulted"); + _logger.LogError(exception, "ProcessOutboxes faulted"); } } } + async Task ProcessOutboxes(CancellationToken cancellationToken) + { + List outboxIds = (await GetOutboxes(_options.QueryMessageLimit, cancellationToken).ConfigureAwait(false)).ToList(); + + await Task.WhenAll(outboxIds.Select(outboxId => DeliverOutbox(outboxId, cancellationToken))).ConfigureAwait(false); + + return outboxIds.Count; + } + async Task> GetOutboxes(int resultLimit, CancellationToken cancellationToken) { var scope = _provider.CreateAsyncScope(); @@ -135,26 +140,27 @@ async Task Execute() } else { - if (outboxState.Delivered != null) + if (outboxState.Delivered.HasValue) { await RemoveOutbox(messageCollection, stateCollection, outboxState, cancellationToken).ConfigureAwait(false); continueProcessing = false; } else + { continueProcessing = await DeliverOutboxMessages(messageCollection, outboxState, cancellationToken).ConfigureAwait(false); - outboxState.Version++; + outboxState.Version++; - FilterDefinition updateFilter = - builder.Eq(x => x.OutboxId, outboxId) & builder.Lt(x => x.Version, outboxState.Version); + FilterDefinition updateFilter = + builder.Eq(x => x.OutboxId, outboxId) & builder.Lt(x => x.Version, outboxState.Version); - await stateCollection.FindOneAndReplace(updateFilter, outboxState, cancellationToken).ConfigureAwait(false); + await stateCollection.FindOneAndReplace(updateFilter, outboxState, cancellationToken).ConfigureAwait(false); + } } await dbContext.CommitTransaction(cancellationToken).ConfigureAwait(false); - return continueProcessing; } catch (OperationCanceledException) @@ -177,6 +183,13 @@ async Task Execute() while (continueProcessing) continueProcessing = await Execute().ConfigureAwait(false); } + catch (OperationCanceledException) + { + } + catch (Exception ex) + { + LogContext.Warning?.Log(ex, "Outbox Delivery Fault: {OutboxId}", outboxId); + } finally { await scope.DisposeAsync().ConfigureAwait(false); @@ -239,6 +252,7 @@ async Task DeliverOutboxMessages(MongoDbCollectionContext m var endpoint = await _busControl.GetSendEndpoint(message.DestinationAddress).ConfigureAwait(false); StartedActivity? activity = LogContext.Current?.StartOutboxDeliverActivity(message); + StartedInstrument? instrument = LogContext.Current?.StartOutboxDeliveryInstrument(message); try { await endpoint.Send(new SerializedMessageBody(), pipe, token.Token).ConfigureAwait(false); @@ -246,12 +260,14 @@ async Task DeliverOutboxMessages(MongoDbCollectionContext m catch (Exception exception) { activity?.AddExceptionEvent(exception); + instrument?.AddException(exception); throw; } finally { activity?.Stop(); + instrument?.Stop(); } sentSequenceNumber = message.SequenceNumber; diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbConsumeContextScopedBusContext.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbConsumeContextScopedBusContext.cs new file mode 100644 index 00000000000..c17994493dc --- /dev/null +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbConsumeContextScopedBusContext.cs @@ -0,0 +1,43 @@ +#nullable enable +namespace MassTransit.MongoDbIntegration; + +using System; +using Clients; +using DependencyInjection; +using Middleware.Outbox; + + +public class MongoDbConsumeContextScopedBusContext : + MongoDbScopedBusContext + where TBus : class, IBus +{ + readonly TBus _bus; + readonly IClientFactory _clientFactory; + readonly ConsumeContext _consumeContext; + readonly IServiceProvider _provider; + + public MongoDbConsumeContextScopedBusContext(TBus bus, MongoDbContext dbContext, IBusOutboxNotification notification, IClientFactory clientFactory, + IServiceProvider provider, ConsumeContext consumeContext) + : base(bus, dbContext, notification, clientFactory, provider) + { + _bus = bus; + _clientFactory = clientFactory; + _provider = provider; + _consumeContext = consumeContext; + } + + protected override IPublishEndpointProvider GetPublishEndpointProvider() + { + return new ScopedConsumePublishEndpointProvider(_bus, _consumeContext, _provider); + } + + protected override ISendEndpointProvider GetSendEndpointProvider() + { + return new ScopedConsumeSendEndpointProvider(_bus, _consumeContext, _provider); + } + + protected override ScopedClientFactory GetClientFactory() + { + return new ScopedClientFactory(new ClientFactory(new ScopedClientFactoryContext(_clientFactory, _provider)), _consumeContext); + } +} diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbScopedBusContext.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbScopedBusContext.cs index 47f97ae9c4a..b79a245e815 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbScopedBusContext.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbScopedBusContext.cs @@ -88,24 +88,12 @@ public async Task AddSend(SendContext context) return _provider.GetService(serviceType); } - public ISendEndpointProvider SendEndpointProvider - { - get { return _sendEndpointProvider ??= new OutboxSendEndpointProvider(this, _bus); } - } + public ISendEndpointProvider SendEndpointProvider => _sendEndpointProvider ??= new OutboxSendEndpointProvider(this, GetSendEndpointProvider()); - public IPublishEndpoint PublishEndpoint - { - get { return _publishEndpoint ??= new PublishEndpoint(new OutboxPublishEndpointProvider(this, _bus)); } - } + public IPublishEndpoint PublishEndpoint => + _publishEndpoint ??= new PublishEndpoint(new OutboxPublishEndpointProvider(this, GetPublishEndpointProvider())); - public IScopedClientFactory ClientFactory - { - get - { - return _scopedClientFactory ??= - new ScopedClientFactory(new ClientFactory(new ScopedClientFactoryContext(_clientFactory, _provider)), null); - } - } + public IScopedClientFactory ClientFactory => _scopedClientFactory ??= GetClientFactory(); bool WasCommitted() { @@ -128,5 +116,20 @@ bool WasCommitted() return outboxId; } + + protected virtual ScopedClientFactory GetClientFactory() + { + return new ScopedClientFactory(new ClientFactory(new ScopedClientFactoryContext(_clientFactory, _provider)), null); + } + + protected virtual IPublishEndpointProvider GetPublishEndpointProvider() + { + return _bus; + } + + protected virtual ISendEndpointProvider GetSendEndpointProvider() + { + return _bus; + } } } diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbScopedBusContextProvider.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbScopedBusContextProvider.cs index b0a6ad609a1..c0c249369ee 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbScopedBusContextProvider.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/MongoDbScopedBusContextProvider.cs @@ -12,10 +12,16 @@ public class MongoDbScopedBusContextProvider : where TBus : class, IBus { public MongoDbScopedBusContextProvider(TBus bus, MongoDbContext dbContext, IBusOutboxNotification notification, - Bind clientFactory, ScopedConsumeContextProvider consumeContextProvider, IServiceProvider provider) + Bind clientFactory, Bind consumeContextProvider, + IScopedConsumeContextProvider globalConsumeContextProvider, IServiceProvider provider) { - if (consumeContextProvider.HasContext) - Context = new ConsumeContextScopedBusContext(consumeContextProvider.GetContext(), clientFactory.Value); + if (consumeContextProvider.Value.HasContext) + Context = new ConsumeContextScopedBusContext(consumeContextProvider.Value.GetContext(), clientFactory.Value); + else if (globalConsumeContextProvider.HasContext) + { + Context = new MongoDbConsumeContextScopedBusContext(bus, dbContext, notification, clientFactory.Value, provider, + globalConsumeContextProvider.GetContext()); + } else Context = new MongoDbScopedBusContext(bus, dbContext, notification, clientFactory.Value, provider); } diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Outbox/MongoDbOutboxExtensions.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Outbox/MongoDbOutboxExtensions.cs index 5dec32613a8..834ee911586 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Outbox/MongoDbOutboxExtensions.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Outbox/MongoDbOutboxExtensions.cs @@ -35,6 +35,7 @@ public static Task AddSend(this MongoDbCollectionContext colle FaultAddress = context.FaultAddress, SentTime = context.SentTime ?? now, ContentType = context.ContentType?.ToString() ?? context.Serialization.DefaultContentType.ToString(), + MessageType = string.Join(";", context.SupportedMessageTypes), Body = body.GetString(), InboxMessageId = inboxMessageId, InboxConsumerId = inboxConsumerId, diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Outbox/OutboxMessage.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Outbox/OutboxMessage.cs index 3be53d6cca4..804d1fd1223 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Outbox/OutboxMessage.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Outbox/OutboxMessage.cs @@ -50,6 +50,7 @@ public class OutboxMessage : public Guid MessageId { get; set; } public string ContentType { get; set; } = null!; + public string MessageType { get; set; } = null!; public string Body { get; set; } = null!; diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepository.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepository.cs index c65221550c8..482686d7283 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepository.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepository.cs @@ -35,7 +35,7 @@ public static ISagaRepository Create(IMongoDatabase database, ICollection var repositoryContextFactory = new MongoDbSagaRepositoryContextFactory(mongoDbContext, mongoDbSagaConsumeContextFactory); - return new SagaRepository(repositoryContextFactory); + return new SagaRepository(repositoryContextFactory, repositoryContextFactory, repositoryContextFactory); } } } diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepositoryContext.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepositoryContext.cs index b8f5c6e719c..d7c082d1dca 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepositoryContext.cs @@ -113,7 +113,8 @@ public Task Undo(SagaConsumeContext context) public class MongoDbSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext + QuerySagaRepositoryContext, + LoadSagaRepositoryContext where TSaga : class, ISagaVersion { readonly MongoDbCollectionContext _dbContext; @@ -124,6 +125,13 @@ public MongoDbSagaRepositoryContext(MongoDbCollectionContext dbContext, C _dbContext = dbContext; } + public Task Load(Guid correlationId) + { + FilterDefinition filter = Builders.Filter.Eq(x => x.CorrelationId, correlationId); + + return _dbContext.Find(filter).FirstOrDefaultAsync(); + } + public async Task> Query(ISagaQuery query, CancellationToken cancellationToken) { IList instances = await _dbContext.Find(query.FilterExpression) @@ -133,12 +141,5 @@ public async Task> Query(ISagaQuery que return new DefaultSagaRepositoryQueryContext(this, instances); } - - public Task Load(Guid correlationId) - { - FilterDefinition filter = Builders.Filter.Eq(x => x.CorrelationId, correlationId); - - return _dbContext.Find(filter).FirstOrDefaultAsync(); - } } } diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepositoryContextFactory.cs index 26a2e2b6af4..7f5362cb84c 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/Saga/MongoDbSagaRepositoryContextFactory.cs @@ -9,7 +9,9 @@ namespace MassTransit.MongoDbIntegration.Saga public class MongoDbSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + IQuerySagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISagaVersion { readonly MongoDbCollectionContext _dbContext; @@ -22,6 +24,18 @@ public MongoDbSagaRepositoryContextFactory(MongoDbCollectionContext dbCon _factory = factory; } + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + public void Probe(ProbeContext context) { context.Add("persistence", "mongodb"); @@ -49,12 +63,12 @@ public async Task SendQuery(ConsumeContext context, ISagaQuery quer await next.Send(queryContext).ConfigureAwait(false); } - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + Task ExecuteAsyncMethod(Func, Task> asyncMethod, CancellationToken cancellationToken) where T : class { var repositoryContext = new MongoDbSagaRepositoryContext(_dbContext, cancellationToken); - return await asyncMethod(repositoryContext).ConfigureAwait(false); + return asyncMethod(repositoryContext); } } } diff --git a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/SagaConvention.cs b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/SagaConvention.cs index fc43d777644..072e56bd6f4 100644 --- a/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/SagaConvention.cs +++ b/src/Persistence/MassTransit.MongoDbIntegration/MongoDbIntegration/SagaConvention.cs @@ -11,7 +11,7 @@ public class SagaConvention : { public void Apply(BsonClassMap classMap) { - if (classMap.ClassType.GetTypeInfo().IsClass && typeof(ISaga).IsAssignableFrom(classMap.ClassType)) + if (classMap.ClassType.IsClass && typeof(ISaga).IsAssignableFrom(classMap.ClassType)) classMap.MapIdProperty(nameof(ISaga.CorrelationId)); } } diff --git a/src/Persistence/MassTransit.NHibernateIntegration/Configuration/NHibernateSagaRegistrationExtensions.cs b/src/Persistence/MassTransit.NHibernateIntegration/Configuration/NHibernateSagaRegistrationExtensions.cs index 038901e979c..7dbbbf37ce9 100644 --- a/src/Persistence/MassTransit.NHibernateIntegration/Configuration/NHibernateSagaRegistrationExtensions.cs +++ b/src/Persistence/MassTransit.NHibernateIntegration/Configuration/NHibernateSagaRegistrationExtensions.cs @@ -18,7 +18,11 @@ public static ISagaRegistrationConfigurator NHibernateRepository(this ISag where T : class, ISaga { configurator.Repository(x => - x.RegisterSagaRepository, NHibernateSagaRepositoryContextFactory>()); + { + x.RegisterLoadSagaRepository>(); + x.RegisterQuerySagaRepository>(); + x.RegisterSagaRepository, NHibernateSagaRepositoryContextFactory>(); + }); return configurator; } diff --git a/src/Persistence/MassTransit.NHibernateIntegration/MassTransit.NHibernateIntegration.csproj b/src/Persistence/MassTransit.NHibernateIntegration/MassTransit.NHibernateIntegration.csproj index 055646cc5a8..c848bc9754e 100644 --- a/src/Persistence/MassTransit.NHibernateIntegration/MassTransit.NHibernateIntegration.csproj +++ b/src/Persistence/MassTransit.NHibernateIntegration/MassTransit.NHibernateIntegration.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -21,10 +21,9 @@ - - - - + + + diff --git a/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/NHibernateSagaRepository.cs b/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/NHibernateSagaRepository.cs index dd663dbbca8..e4f1b92e464 100644 --- a/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/NHibernateSagaRepository.cs +++ b/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/NHibernateSagaRepository.cs @@ -14,7 +14,7 @@ public static ISagaRepository Create(ISessionFactory sessionFactory) var repositoryContextFactory = new NHibernateSagaRepositoryContextFactory(sessionFactory, consumeContextFactory); - return new SagaRepository(repositoryContextFactory); + return new SagaRepository(repositoryContextFactory, repositoryContextFactory, repositoryContextFactory); } } } diff --git a/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/Saga/NHibernateSagaRepositoryContext.cs b/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/Saga/NHibernateSagaRepositoryContext.cs index a65625f2309..8d1c2385544 100644 --- a/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/Saga/NHibernateSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/Saga/NHibernateSagaRepositoryContext.cs @@ -101,7 +101,8 @@ public Task> CreateSagaConsumeContext(ConsumeCon public class NHibernateSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext + QuerySagaRepositoryContext, + LoadSagaRepositoryContext where TSaga : class, ISaga { readonly ISession _session; diff --git a/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/Saga/NHibernateSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/Saga/NHibernateSagaRepositoryContextFactory.cs index 09d0fea696f..2ebdf5a4149 100644 --- a/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/Saga/NHibernateSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.NHibernateIntegration/NHibernateIntegration/Saga/NHibernateSagaRepositoryContextFactory.cs @@ -10,7 +10,9 @@ namespace MassTransit.NHibernateIntegration.Saga public class NHibernateSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + IQuerySagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISaga { readonly ISagaConsumeContextFactory _factory; @@ -22,6 +24,18 @@ public NHibernateSagaRepositoryContextFactory(ISessionFactory sessionFactory, IS _factory = factory; } + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + + public Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken) + where T : class + { + return ExecuteAsyncMethod(asyncMethod, cancellationToken); + } + public async Task Send(ConsumeContext context, IPipe> next) where T : class { @@ -109,7 +123,13 @@ public async Task SendQuery(ConsumeContext context, ISagaQuery quer } } - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken) + public void Probe(ProbeContext context) + { + context.Add("persistence", "nhibernate"); + context.Add("entities", _sessionFactory.GetAllClassMetadata().Select(x => x.Value.EntityName).ToArray()); + } + + async Task ExecuteAsyncMethod(Func, Task> asyncMethod, CancellationToken cancellationToken) where T : class { var session = _sessionFactory.OpenSession(); @@ -124,11 +144,5 @@ public async Task Execute(Func, Task> asyn session.Dispose(); } } - - public void Probe(ProbeContext context) - { - context.Add("persistence", "nhibernate"); - context.Add("entities", _sessionFactory.GetAllClassMetadata().Select(x => x.Value.EntityName).ToArray()); - } } } diff --git a/src/Persistence/MassTransit.RedisIntegration/Configuration/Configuration/ConnectionMultiplexerFactory.cs b/src/Persistence/MassTransit.RedisIntegration/Configuration/Configuration/ConnectionMultiplexerFactory.cs new file mode 100644 index 00000000000..2d7fcfbb2f0 --- /dev/null +++ b/src/Persistence/MassTransit.RedisIntegration/Configuration/Configuration/ConnectionMultiplexerFactory.cs @@ -0,0 +1,44 @@ +namespace MassTransit.Configuration; + +using System; +using System.Collections.Concurrent; +using System.Threading.Tasks; +using StackExchange.Redis; + + +public class ConnectionMultiplexerFactory : + IConnectionMultiplexerFactory, + IAsyncDisposable +{ + readonly ConcurrentDictionary> _connectionMultiplexers; + + public ConnectionMultiplexerFactory() + { + _connectionMultiplexers = new ConcurrentDictionary>(); + } + + public async ValueTask DisposeAsync() + { + foreach (Lazy value in _connectionMultiplexers.Values) + { + if (value.IsValueCreated) + await value.Value.DisposeAsync().ConfigureAwait(false); + } + } + + public IConnectionMultiplexer GetConnectionMultiplexer(ConfigurationOptions configuration) + { + IConnectionMultiplexer MultiplexerFactory(ConfigurationOptions configurationOptions) + { + LogContext.Debug?.Log("Creating Redis Connection Multiplexer: {Options}", configurationOptions.ToString(false)); + return ConnectionMultiplexer.Connect(configurationOptions); + } + + Lazy ValueFactory(ConfigurationOptions x) + { + return new Lazy(() => MultiplexerFactory(x)); + } + + return _connectionMultiplexers.GetOrAdd(configuration.ToString(false), ValueFactory(configuration)).Value; + } +} \ No newline at end of file diff --git a/src/Persistence/MassTransit.RedisIntegration/Configuration/Configuration/RedisSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.RedisIntegration/Configuration/Configuration/RedisSagaRepositoryConfigurator.cs index 641a2b8dbd8..29f0d3340d1 100644 --- a/src/Persistence/MassTransit.RedisIntegration/Configuration/Configuration/RedisSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.RedisIntegration/Configuration/Configuration/RedisSagaRepositoryConfigurator.cs @@ -2,6 +2,7 @@ namespace MassTransit.Configuration { using System; using System.Collections.Generic; + using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using RedisIntegration.Saga; using Saga; @@ -13,8 +14,7 @@ public class RedisSagaRepositoryConfigurator : ISpecification where TSaga : class, ISagaVersion { - ConfigurationOptions _configurationOptions; - Func _connectionFactory; + RedisConnectionFactory _connectionFactory; SelectDatabase _databaseSelector; public RedisSagaRepositoryConfigurator() @@ -32,6 +32,7 @@ public RedisSagaRepositoryConfigurator() public string LockSuffix { get; set; } public TimeSpan LockTimeout { get; set; } public TimeSpan? Expiry { get; set; } + public IRetryPolicy RetryPolicy { get; set; } public void DatabaseConfiguration(string configuration) { @@ -40,19 +41,22 @@ public void DatabaseConfiguration(string configuration) public void DatabaseConfiguration(ConfigurationOptions configurationOptions) { - _configurationOptions = configurationOptions; + IConnectionMultiplexer Factory(IServiceProvider provider) + { + return provider.GetRequiredService().GetConnectionMultiplexer(configurationOptions); + } - _connectionFactory = provider => ConnectionMultiplexer.Connect(_configurationOptions); + _connectionFactory = Factory; } - public void ConnectionFactory(Func connectionFactory) + public void ConnectionFactory(Func connectionFactory) { - _connectionFactory = provider => connectionFactory(); + _connectionFactory = _ => connectionFactory(); } - public void ConnectionFactory(Func connectionFactory) + public void ConnectionFactory(Func connectionFactory) { - _connectionFactory = connectionFactory; + _connectionFactory = x => connectionFactory(x); } public void SelectDatabase(SelectDatabase databaseSelector) @@ -68,13 +72,14 @@ public IEnumerable Validate() yield return this.Failure("LockTimeout", "Must be > TimeSpan.Zero"); } - public void Register(ISagaRepositoryRegistrationConfigurator configurator) - where T : class, ISagaVersion + public void Register(ISagaRepositoryRegistrationConfigurator configurator) { - configurator.TryAddSingleton(_connectionFactory); - configurator.TryAddSingleton(new RedisSagaRepositoryOptions(ConcurrencyMode, LockTimeout, LockSuffix, KeyPrefix, _databaseSelector, Expiry)); - configurator.RegisterSagaRepository, SagaConsumeContextFactory, T>, - RedisSagaRepositoryContextFactory>(); + configurator.TryAddSingleton(); + configurator.TryAddSingleton(new RedisSagaRepositoryOptions(ConcurrencyMode, LockTimeout, LockSuffix, KeyPrefix, _connectionFactory, + _databaseSelector, Expiry, RetryPolicy)); + configurator.RegisterLoadSagaRepository>(); + configurator.RegisterSagaRepository, SagaConsumeContextFactory, TSaga>, + RedisSagaRepositoryContextFactory>(); } static IDatabase SelectDefaultDatabase(IConnectionMultiplexer multiplexer) diff --git a/src/Persistence/MassTransit.RedisIntegration/Configuration/IConnectionMultiplexerFactory.cs b/src/Persistence/MassTransit.RedisIntegration/Configuration/IConnectionMultiplexerFactory.cs new file mode 100644 index 00000000000..2b9796106de --- /dev/null +++ b/src/Persistence/MassTransit.RedisIntegration/Configuration/IConnectionMultiplexerFactory.cs @@ -0,0 +1,9 @@ +namespace MassTransit; + +using StackExchange.Redis; + + +public interface IConnectionMultiplexerFactory +{ + IConnectionMultiplexer GetConnectionMultiplexer(ConfigurationOptions configuration); +} diff --git a/src/Persistence/MassTransit.RedisIntegration/Configuration/IRedisSagaRepositoryConfigurator.cs b/src/Persistence/MassTransit.RedisIntegration/Configuration/IRedisSagaRepositoryConfigurator.cs index 7701055eef9..698315c62ae 100644 --- a/src/Persistence/MassTransit.RedisIntegration/Configuration/IRedisSagaRepositoryConfigurator.cs +++ b/src/Persistence/MassTransit.RedisIntegration/Configuration/IRedisSagaRepositoryConfigurator.cs @@ -16,6 +16,8 @@ public interface IRedisSagaRepositoryConfigurator TimeSpan? Expiry { set; } + IRetryPolicy RetryPolicy { set; } + /// /// Set the database factory using configuration, which caches a under the hood. /// @@ -32,13 +34,13 @@ public interface IRedisSagaRepositoryConfigurator /// Use a simple factory method to create the connection /// /// - void ConnectionFactory(Func connectionFactory); + void ConnectionFactory(Func connectionFactory); /// /// Use the configuration service provider to resolve the connection /// /// - void ConnectionFactory(Func connectionFactory); + void ConnectionFactory(Func connectionFactory); /// /// Select a database other than the default to be used (optional) diff --git a/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisConnectionFactory.cs b/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisConnectionFactory.cs new file mode 100644 index 00000000000..dd50cda73ee --- /dev/null +++ b/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisConnectionFactory.cs @@ -0,0 +1,7 @@ +namespace MassTransit; + +using System; +using StackExchange.Redis; + + +public delegate IConnectionMultiplexer RedisConnectionFactory(IServiceProvider provider); diff --git a/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisSagaRepositoryOptions.cs b/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisSagaRepositoryOptions.cs index de0ef8ed2e7..67eb3173b80 100644 --- a/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisSagaRepositoryOptions.cs +++ b/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisSagaRepositoryOptions.cs @@ -7,7 +7,7 @@ public class RedisSagaRepositoryOptions where TSaga : class, ISaga { public RedisSagaRepositoryOptions(ConcurrencyMode concurrencyMode, TimeSpan? lockTimeout, string lockSuffix, string keyPrefix, - SelectDatabase databaseSelector, TimeSpan? expiry) + RedisConnectionFactory connectionFactory, SelectDatabase databaseSelector, TimeSpan? expiry, IRetryPolicy retryPolicy) { ConcurrencyMode = concurrencyMode; @@ -17,8 +17,9 @@ public RedisSagaRepositoryOptions(ConcurrencyMode concurrencyMode, TimeSpan? loc KeyPrefix = string.IsNullOrWhiteSpace(keyPrefix) ? null : keyPrefix.EndsWith(":") ? keyPrefix : $"{keyPrefix}:"; - RetryPolicy = Retry.Exponential(10, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(918)); + RetryPolicy = retryPolicy ?? Retry.Exponential(10, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(10), TimeSpan.FromMilliseconds(918)); + ConnectionFactory = connectionFactory; DatabaseSelector = databaseSelector; Expiry = expiry; @@ -31,6 +32,7 @@ public RedisSagaRepositoryOptions(ConcurrencyMode concurrencyMode, TimeSpan? loc public string LockSuffix { get; } public ConcurrencyMode ConcurrencyMode { get; } public SelectDatabase DatabaseSelector { get; } + public RedisConnectionFactory ConnectionFactory { get; } public TimeSpan? Expiry { get; } public string FormatSagaKey(Guid correlationId) diff --git a/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisSagaRepositoryRegistrationExtensions.cs b/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisSagaRepositoryRegistrationExtensions.cs index 3eace1ea5d1..a6077549584 100644 --- a/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisSagaRepositoryRegistrationExtensions.cs +++ b/src/Persistence/MassTransit.RedisIntegration/Configuration/RedisSagaRepositoryRegistrationExtensions.cs @@ -53,12 +53,51 @@ public static ISagaRegistrationConfigurator RedisRepository(this ISagaRegi return configurator; } + /// + /// Configure the Job Service saga state machines to use Redis + /// + /// + /// + /// + public static IJobSagaRegistrationConfigurator RedisRepository(this IJobSagaRegistrationConfigurator configurator, + Action configure = null) + { + var registrationProvider = new RedisSagaRepositoryRegistrationProvider(configure); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + + /// + /// Configure the Job Service saga state machines to use Redis + /// + /// + /// The Redis configuration string + /// + /// + public static IJobSagaRegistrationConfigurator RedisRepository(this IJobSagaRegistrationConfigurator configurator, string configuration, + Action configure = null) + { + var registrationProvider = new RedisSagaRepositoryRegistrationProvider(r => + { + r.DatabaseConfiguration(configuration); + + configure?.Invoke(r); + }); + + configurator.UseRepositoryRegistrationProvider(registrationProvider); + + return configurator; + } + /// /// Use the Redis saga repository for sagas configured by type (without a specific generic call to AddSaga/AddSagaStateMachine) /// /// /// - public static void SetRedisSagaRepositoryProvider(this IRegistrationConfigurator configurator, Action configure) + public static void SetRedisSagaRepositoryProvider(this IRegistrationConfigurator configurator, + Action configure = null) { configurator.SetSagaRepositoryProvider(new RedisSagaRepositoryRegistrationProvider(configure)); } @@ -70,7 +109,7 @@ public static void SetRedisSagaRepositoryProvider(this IRegistrationConfigurator /// The Redis configuration string /// public static void SetRedisSagaRepositoryProvider(this IRegistrationConfigurator configurator, string configuration, - Action configure) + Action configure = null) { configurator.SetSagaRepositoryProvider(new RedisSagaRepositoryRegistrationProvider(r => { diff --git a/src/Persistence/MassTransit.RedisIntegration/MassTransit.RedisIntegration.csproj b/src/Persistence/MassTransit.RedisIntegration/MassTransit.RedisIntegration.csproj index aae64a10690..38e9f3bcf09 100644 --- a/src/Persistence/MassTransit.RedisIntegration/MassTransit.RedisIntegration.csproj +++ b/src/Persistence/MassTransit.RedisIntegration/MassTransit.RedisIntegration.csproj @@ -1,11 +1,11 @@  - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -21,7 +21,6 @@ - diff --git a/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepository.cs b/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepository.cs index 06aa855f19b..0e0bfb61ce9 100644 --- a/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepository.cs +++ b/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepository.cs @@ -8,17 +8,19 @@ public static class RedisSagaRepository where TSaga : class, ISagaVersion { - public static ISagaRepository Create(Func redisDbFactory, bool optimistic = true, TimeSpan? lockTimeout = null, TimeSpan? - lockRetryTimeout = null, string keyPrefix = "", TimeSpan? expiry = null) + public static ISagaRepository Create(RedisConnectionFactory connectionFactory, Func redisDbFactory, bool optimistic = true, + TimeSpan? lockTimeout + = null, TimeSpan? + lockRetryTimeout = null, string keyPrefix = "", TimeSpan? expiry = null, IRetryPolicy retryPolicy = null) { var options = new RedisSagaRepositoryOptions(optimistic ? ConcurrencyMode.Optimistic : ConcurrencyMode.Pessimistic, lockTimeout, null, - keyPrefix, SelectDefaultDatabase, expiry); + keyPrefix, connectionFactory, SelectDefaultDatabase, expiry, retryPolicy); var consumeContextFactory = new SagaConsumeContextFactory, TSaga>(); var repositoryContextFactory = new RedisSagaRepositoryContextFactory(redisDbFactory, consumeContextFactory, options); - return new SagaRepository(repositoryContextFactory); + return new SagaRepository(repositoryContextFactory, loadSagaRepositoryContextFactory: repositoryContextFactory); } static IDatabase SelectDefaultDatabase(IConnectionMultiplexer multiplexer) diff --git a/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepositoryContext.cs b/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepositoryContext.cs index d34546e0cde..0a078c10139 100644 --- a/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepositoryContext.cs +++ b/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepositoryContext.cs @@ -102,7 +102,7 @@ public Task> CreateSagaConsumeContext(ConsumeCon public class RedisSagaRepositoryContext : BasePipeContext, - SagaRepositoryContext, + LoadSagaRepositoryContext, IAsyncDisposable where TSaga : class, ISagaVersion { @@ -119,11 +119,6 @@ public ValueTask DisposeAsync() return _context.DisposeAsync(); } - public Task> Query(ISagaQuery query, CancellationToken cancellationToken) - { - throw new NotImplementedByDesignException("Redis saga repository does not support queries"); - } - public Task Load(Guid correlationId) { return _context.Load(SystemTextJsonMessageSerializer.Instance, correlationId); diff --git a/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepositoryContextFactory.cs b/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepositoryContextFactory.cs index ec1b3e310f5..bc9f32aea47 100644 --- a/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepositoryContextFactory.cs +++ b/src/Persistence/MassTransit.RedisIntegration/RedisIntegration/Saga/RedisSagaRepositoryContextFactory.cs @@ -8,19 +8,20 @@ namespace MassTransit.RedisIntegration.Saga public class RedisSagaRepositoryContextFactory : - ISagaRepositoryContextFactory + ISagaRepositoryContextFactory, + ILoadSagaRepositoryContextFactory where TSaga : class, ISagaVersion { readonly Func _databaseFactory; readonly ISagaConsumeContextFactory, TSaga> _factory; readonly RedisSagaRepositoryOptions _options; - public RedisSagaRepositoryContextFactory(ConnectionMultiplexer multiplexer, ISagaConsumeContextFactory, TSaga> factory, + public RedisSagaRepositoryContextFactory(IServiceProvider provider, ISagaConsumeContextFactory, TSaga> factory, RedisSagaRepositoryOptions options) { IDatabase DatabaseFactory() { - return options.DatabaseSelector(multiplexer); + return options.DatabaseSelector(options.ConnectionFactory(provider)); } _databaseFactory = DatabaseFactory; @@ -38,12 +39,7 @@ public RedisSagaRepositoryContextFactory(Func databaseFactory, ISagaC _options = options; } - public void Probe(ProbeContext context) - { - context.Add("persistence", "redis"); - } - - public async Task Send(ConsumeContext context, IPipe> next) + public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) where T : class { var database = _databaseFactory(); @@ -51,12 +47,9 @@ public async Task Send(ConsumeContext context, IPipe(database, _options); try { - if (_options.ConcurrencyMode == ConcurrencyMode.Pessimistic) - await databaseContext.Lock(context.CorrelationId.Value, context.CancellationToken).ConfigureAwait(false); - - var repositoryContext = new RedisSagaRepositoryContext(databaseContext, context, _factory); + var repositoryContext = new RedisSagaRepositoryContext(databaseContext, cancellationToken); - await next.Send(repositoryContext).ConfigureAwait(false); + return await asyncMethod(repositoryContext).ConfigureAwait(false); } finally { @@ -64,13 +57,12 @@ public async Task Send(ConsumeContext context, IPipe(ConsumeContext context, ISagaQuery query, IPipe> next) - where T : class + public void Probe(ProbeContext context) { - throw new NotImplementedByDesignException("Redis saga repository does not support queries"); + context.Add("persistence", "redis"); } - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) + public async Task Send(ConsumeContext context, IPipe> next) where T : class { var database = _databaseFactory(); @@ -78,14 +70,23 @@ public async Task Execute(Func, Task> asyn var databaseContext = new RedisDatabaseContext(database, _options); try { - var repositoryContext = new RedisSagaRepositoryContext(databaseContext, cancellationToken); + if (_options.ConcurrencyMode == ConcurrencyMode.Pessimistic) + await databaseContext.Lock(context.CorrelationId.Value, context.CancellationToken).ConfigureAwait(false); - return await asyncMethod(repositoryContext).ConfigureAwait(false); + var repositoryContext = new RedisSagaRepositoryContext(databaseContext, context, _factory); + + await next.Send(repositoryContext).ConfigureAwait(false); } finally { await databaseContext.DisposeAsync().ConfigureAwait(false); } } + + public async Task SendQuery(ConsumeContext context, ISagaQuery query, IPipe> next) + where T : class + { + throw new NotImplementedByDesignException("Redis saga repository does not support queries"); + } } } diff --git a/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/PauseScheduledRecurringMessageConsumerDefinition.cs b/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/PauseScheduledRecurringMessageConsumerDefinition.cs index 1422d8efc13..e7d6357c9b0 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/PauseScheduledRecurringMessageConsumerDefinition.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/PauseScheduledRecurringMessageConsumerDefinition.cs @@ -17,7 +17,7 @@ public PauseScheduledRecurringMessageConsumerDefinition(HangfireEndpointDefiniti } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { consumerConfigurator.Message(m => { diff --git a/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ResumeScheduledMessageConsumerDefinition.cs b/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ResumeScheduledMessageConsumerDefinition.cs index 46a641e19ca..073c050ea80 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ResumeScheduledMessageConsumerDefinition.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ResumeScheduledMessageConsumerDefinition.cs @@ -17,7 +17,7 @@ public ResumeScheduledRecurringMessageConsumerDefinition(HangfireEndpointDefinit } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { consumerConfigurator.Message(m => { diff --git a/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ScheduleMessageConsumerDefinition.cs b/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ScheduleMessageConsumerDefinition.cs index d3f6de0570f..8093ae9eed9 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ScheduleMessageConsumerDefinition.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ScheduleMessageConsumerDefinition.cs @@ -17,7 +17,7 @@ public ScheduleMessageConsumerDefinition(HangfireEndpointDefinition endpointDefi } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Interval(5, 250)); diff --git a/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ScheduleRecurringMessageConsumerDefinition.cs b/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ScheduleRecurringMessageConsumerDefinition.cs index 2181d68a420..98c9b23ca7d 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ScheduleRecurringMessageConsumerDefinition.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/Configuration/Configuration/ScheduleRecurringMessageConsumerDefinition.cs @@ -17,7 +17,7 @@ public ScheduleRecurringMessageConsumerDefinition(HangfireEndpointDefinition end } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { consumerConfigurator.Message(m => { diff --git a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HangfireRecurringSceduledMessageData.cs b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HangfireRecurringSceduledMessageData.cs index 98f06561a49..165674ecc3a 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HangfireRecurringSceduledMessageData.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HangfireRecurringSceduledMessageData.cs @@ -24,7 +24,9 @@ public static HangfireRecurringScheduledMessageData Create(ConsumeContext(context.Message)); - SetBaseProperties(data, context, context.Message.Destination, messageBody); + SetBaseProperties(data, context, context.Message.Destination, messageBody, context.Message.PayloadType); + + data.MessageId = null; return data; } diff --git a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HangfireScheduledMessageData.cs b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HangfireScheduledMessageData.cs index 5ddbf7d22e9..c9f0871e37f 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HangfireScheduledMessageData.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HangfireScheduledMessageData.cs @@ -4,6 +4,7 @@ namespace MassTransit.HangfireIntegration using System.Collections.Generic; using System.Linq; using System.Text.Json; + using Context; using Serialization; @@ -23,11 +24,13 @@ public class HangfireScheduledMessageData public string? InitiatorId { get; set; } public string? TokenId { get; set; } public string? HeadersAsJson { get; set; } + public string? TransportProperties { get; set; } + public string? MessageType { get; set; } public Uri Destination => new Uri(DestinationAddress!); protected static void SetBaseProperties(HangfireScheduledMessageData data, ConsumeContext context, Uri destination, MessageBody messageBody, - Guid? tokenId = default) + string[] supportedMessageTypes, Guid? tokenId = default) { data.DestinationAddress = destination?.ToString() ?? ""; data.Body = messageBody.GetString(); @@ -35,6 +38,8 @@ protected static void SetBaseProperties(HangfireScheduledMessageData data, Consu data.FaultAddress = context.FaultAddress?.ToString() ?? ""; data.ResponseAddress = context.ResponseAddress?.ToString() ?? ""; + data.MessageType = string.Join(";", supportedMessageTypes); + if (context.MessageId.HasValue) data.MessageId = context.MessageId.Value.ToString(); @@ -59,6 +64,13 @@ protected static void SetBaseProperties(HangfireScheduledMessageData data, Consu IEnumerable> headers = context.Headers.GetAll().ToList(); if (headers.Any()) data.HeadersAsJson = JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options); + + if (context.ReceiveContext.TryGetPayload(out var transportReceiveContext)) + { + IDictionary? properties = transportReceiveContext.GetTransportProperties(); + if (properties != null) + data.TransportProperties = JsonSerializer.Serialize(properties, SystemTextJsonMessageSerializer.Options); + } } } } diff --git a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HashedHangfireScheduledMessageData.cs b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HashedHangfireScheduledMessageData.cs index 6c6890ab90c..db710cc6711 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HashedHangfireScheduledMessageData.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/HashedHangfireScheduledMessageData.cs @@ -17,7 +17,7 @@ public static HashedHangfireScheduledMessageData Create(ConsumeContext(context.Message)); - SetBaseProperties(message, context, context.Message.Destination, messageBody, context.Message.CorrelationId); + SetBaseProperties(message, context, context.Message.Destination, messageBody, context.Message.PayloadType, context.Message.CorrelationId); return message; } diff --git a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/MessageDataMessageContext.cs b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/MessageDataMessageContext.cs index e00f496535d..116cffd8939 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/MessageDataMessageContext.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/MessageDataMessageContext.cs @@ -3,7 +3,9 @@ namespace MassTransit.HangfireIntegration using System; using System.Collections; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Globalization; + using System.Linq; using System.Text.Json; using Metadata; using Serialization; @@ -35,6 +37,17 @@ public MessageDataMessageContext(HangfireScheduledMessageData messageData, IObje _objectDeserializer = objectDeserializer; } + public IReadOnlyDictionary? TransportProperties + { + get + { + return !string.IsNullOrWhiteSpace(_messageData.TransportProperties) + ? JsonSerializer.Deserialize>(_messageData.TransportProperties!, + SystemTextJsonMessageSerializer.Options) + : null; + } + } + public IEnumerator GetEnumerator() { return Headers.GetEnumerator(); @@ -50,34 +63,34 @@ public IEnumerable> GetAll() return Headers.GetAll(); } - public bool TryGetHeader(string key, out object? value) + public bool TryGetHeader(string key, [NotNullWhen(true)] out object? value) { switch (key) { case MessageHeaders.MessageId: value = MessageId; - return true; + return value != null; case MessageHeaders.CorrelationId: value = CorrelationId; - return true; + return value != null; case MessageHeaders.ConversationId: value = ConversationId; - return true; + return value != null; case MessageHeaders.RequestId: value = RequestId; - return true; + return value != null; case MessageHeaders.InitiatorId: value = InitiatorId; - return true; + return value != null; case MessageHeaders.SourceAddress: value = SourceAddress; - return true; + return value != null; case MessageHeaders.ResponseAddress: value = ResponseAddress; - return true; + return value != null; case MessageHeaders.FaultAddress: value = FaultAddress; - return true; + return value != null; } return Headers.TryGetHeader(key, out value); @@ -109,6 +122,8 @@ public bool TryGetHeader(string key, out object? value) public Headers Headers => _headers ??= GetHeaders(); public HostInfo Host => _hostInfo ??= HostMetadataCache.Host; + public string[] SupportedMessageTypes => _messageData.MessageType?.Split(';').ToArray() ?? Array.Empty(); + Headers GetHeaders() { var headers = new DictionarySendHeaders(); diff --git a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/ScheduleJob.cs b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/ScheduleJob.cs index f9466495c25..856fef50048 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/ScheduleJob.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/ScheduleJob.cs @@ -4,6 +4,7 @@ namespace MassTransit.HangfireIntegration using System.Collections.Generic; using System.Net.Mime; using System.Threading.Tasks; + using Context; using Hangfire; using Hangfire.Server; using Serialization; @@ -20,6 +21,11 @@ public ScheduleJob(IBus bus) } [HashCleanup] + public Task SendMessage(HashedHangfireScheduledMessageData messageData, PerformContext performContext) + { + return SendMessage((HangfireScheduledMessageData)messageData, performContext); + } + public async Task SendMessage(HangfireScheduledMessageData messageData, PerformContext performContext) { try @@ -98,7 +104,9 @@ public Task Send(SendContext context) var serializerContext = deserializer.Deserialize(body, _messageContext, _destinationAddress); - context.MessageId = _messageContext.MessageId; + if (_messageContext.MessageId.HasValue) + context.MessageId = _messageContext.MessageId; + context.RequestId = _messageContext.RequestId; context.ConversationId = _messageContext.ConversationId; context.CorrelationId = _messageContext.CorrelationId; @@ -106,6 +114,7 @@ public Task Send(SendContext context) context.SourceAddress = _messageContext.SourceAddress; context.ResponseAddress = _messageContext.ResponseAddress; context.FaultAddress = _messageContext.FaultAddress; + context.SupportedMessageTypes = _messageContext.SupportedMessageTypes; if (_messageContext.ExpirationTime.HasValue) context.TimeToLive = _messageContext.ExpirationTime.Value.ToUniversalTime() - DateTime.UtcNow; @@ -113,6 +122,10 @@ public Task Send(SendContext context) foreach (KeyValuePair header in _messageContext.Headers.GetAll()) context.Headers.Set(header.Key, header.Value); + IReadOnlyDictionary? transportProperties = _messageContext.TransportProperties; + if (transportProperties != null && context is TransportSendContext transportSendContext) + transportSendContext.ReadPropertiesFrom(transportProperties); + context.Serializer = serializerContext.GetMessageSerializer(); return Task.CompletedTask; diff --git a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/ScheduleRecurringMessageConsumer.cs b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/ScheduleRecurringMessageConsumer.cs index 4e0deaec1ce..487991ea35e 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/ScheduleRecurringMessageConsumer.cs +++ b/src/Scheduling/MassTransit.HangfireIntegration/HangfireIntegration/ScheduleRecurringMessageConsumer.cs @@ -41,7 +41,7 @@ public Task Consume(ConsumeContext context) jobKey, x => x.SendMessage(message, null!), context.Message.Schedule.CronExpression, - tz); + new RecurringJobOptions { TimeZone = tz }); LogContext.Debug?.Log("Scheduled: {Key}", jobKey); return Task.CompletedTask; diff --git a/src/Scheduling/MassTransit.HangfireIntegration/MassTransit.HangfireIntegration.csproj b/src/Scheduling/MassTransit.HangfireIntegration/MassTransit.HangfireIntegration.csproj index 6b8ca5e4388..9aef74dcfe8 100644 --- a/src/Scheduling/MassTransit.HangfireIntegration/MassTransit.HangfireIntegration.csproj +++ b/src/Scheduling/MassTransit.HangfireIntegration/MassTransit.HangfireIntegration.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -27,7 +27,6 @@ - diff --git a/src/Scheduling/MassTransit.HangfireIntegration/NullableAttributes.cs b/src/Scheduling/MassTransit.HangfireIntegration/NullableAttributes.cs new file mode 100644 index 00000000000..3f38561b675 --- /dev/null +++ b/src/Scheduling/MassTransit.HangfireIntegration/NullableAttributes.cs @@ -0,0 +1,24 @@ +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using System; + + + [AttributeUsage(AttributeTargets.Parameter)] + sealed class NotNullWhenAttribute : + Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +#endif diff --git a/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/CancelScheduledMessageConsumerDefinition.cs b/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/CancelScheduledMessageConsumerDefinition.cs index 0b07677b272..e3adc5875a6 100644 --- a/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/CancelScheduledMessageConsumerDefinition.cs +++ b/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/CancelScheduledMessageConsumerDefinition.cs @@ -17,7 +17,7 @@ public CancelScheduledMessageConsumerDefinition(QuartzEndpointDefinition endpoin } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { consumerConfigurator.Message(m => m.UsePartitioner(_endpointDefinition.Partition, p => p.Message.TokenId)); diff --git a/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/PauseScheduledMessageConsumerDefinition.cs b/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/PauseScheduledMessageConsumerDefinition.cs index 63a838f7331..452688c0dbc 100644 --- a/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/PauseScheduledMessageConsumerDefinition.cs +++ b/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/PauseScheduledMessageConsumerDefinition.cs @@ -17,7 +17,7 @@ public PauseScheduledMessageConsumerDefinition(QuartzEndpointDefinition endpoint } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { consumerConfigurator.Message(m => { diff --git a/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/ResumeScheduledMessageConsumerDefinition.cs b/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/ResumeScheduledMessageConsumerDefinition.cs index cd16eb906b6..7a5269d0cbb 100644 --- a/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/ResumeScheduledMessageConsumerDefinition.cs +++ b/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/ResumeScheduledMessageConsumerDefinition.cs @@ -17,7 +17,7 @@ public ResumeScheduledMessageConsumerDefinition(QuartzEndpointDefinition endpoin } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { consumerConfigurator.Message(m => { diff --git a/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/ScheduleMessageConsumerDefinition.cs b/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/ScheduleMessageConsumerDefinition.cs index 31026be1cb0..0cacb8095e0 100644 --- a/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/ScheduleMessageConsumerDefinition.cs +++ b/src/Scheduling/MassTransit.QuartzIntegration/Configuration/Configuration/ScheduleMessageConsumerDefinition.cs @@ -17,7 +17,7 @@ public ScheduleMessageConsumerDefinition(QuartzEndpointDefinition endpointDefini } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Interval(5, 250)); diff --git a/src/Scheduling/MassTransit.QuartzIntegration/MassTransit.QuartzIntegration.csproj b/src/Scheduling/MassTransit.QuartzIntegration/MassTransit.QuartzIntegration.csproj index d09c1e5cae7..1328b553c89 100644 --- a/src/Scheduling/MassTransit.QuartzIntegration/MassTransit.QuartzIntegration.csproj +++ b/src/Scheduling/MassTransit.QuartzIntegration/MassTransit.QuartzIntegration.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462;net472 + $(TargetFrameworks);net472 @@ -24,9 +24,6 @@ - - - diff --git a/src/Scheduling/MassTransit.QuartzIntegration/NullableAttributes.cs b/src/Scheduling/MassTransit.QuartzIntegration/NullableAttributes.cs new file mode 100644 index 00000000000..3f38561b675 --- /dev/null +++ b/src/Scheduling/MassTransit.QuartzIntegration/NullableAttributes.cs @@ -0,0 +1,24 @@ +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using System; + + + [AttributeUsage(AttributeTargets.Parameter)] + sealed class NotNullWhenAttribute : + Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +#endif diff --git a/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/JobDataMessageContext.cs b/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/JobDataMessageContext.cs index a7a93d2d36a..c380f2aa990 100644 --- a/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/JobDataMessageContext.cs +++ b/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/JobDataMessageContext.cs @@ -3,6 +3,7 @@ namespace MassTransit.QuartzIntegration using System; using System.Collections; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using System.Globalization; using Metadata; using Quartz; @@ -37,7 +38,7 @@ public JobDataMessageContext(IJobExecutionContext executionContext, IObjectDeser _jobDataMap = executionContext.MergedJobDataMap; _objectDeserializer = objectDeserializer; - Guid? messageId = ConvertIdToGuid(_jobDataMap.GetString(nameof(MessageId))); + Guid? messageId = _jobDataMap.TryGetString(nameof(MessageId), out var text) ? ConvertIdToGuid(text) : default; if (messageId.HasValue) _messageId = messageId; @@ -65,34 +66,34 @@ public IEnumerable> GetAll() return Headers.GetAll(); } - public bool TryGetHeader(string key, out object? value) + public bool TryGetHeader(string key, [NotNullWhen(true)] out object? value) { switch (key) { case MessageHeaders.MessageId: value = MessageId; - return true; + return value != null; case MessageHeaders.CorrelationId: value = CorrelationId; - return true; + return value != null; case MessageHeaders.ConversationId: value = ConversationId; - return true; + return value != null; case MessageHeaders.RequestId: value = RequestId; - return true; + return value != null; case MessageHeaders.InitiatorId: value = InitiatorId; - return true; + return value != null; case MessageHeaders.SourceAddress: value = SourceAddress; - return true; + return value != null; case MessageHeaders.ResponseAddress: value = ResponseAddress; - return true; + return value != null; case MessageHeaders.FaultAddress: value = FaultAddress; - return true; + return value != null; } return _jobDataMap.TryGetValue(key, out value); @@ -130,6 +131,9 @@ public bool TryGetHeader(string key, out object? value) public Headers Headers => _headers ??= GetHeaders(); public HostInfo Host => _hostInfo ??= _jobDataMap.TryGetValue(nameof(Host), out HostInfo? value) ? value! : HostMetadataCache.Empty; + public IReadOnlyDictionary? TransportProperties => + _jobDataMap.TryGetValue>("TransportProperties", out var properties) ? properties : default; + Headers GetHeaders() { var headers = new DictionarySendHeaders(); diff --git a/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/ScheduleMessageConsumer.cs b/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/ScheduleMessageConsumer.cs index 16a5b2577f6..bbec668f5e8 100644 --- a/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/ScheduleMessageConsumer.cs +++ b/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/ScheduleMessageConsumer.cs @@ -43,7 +43,8 @@ public async Task Consume(ConsumeContext context) .WithSchedule(SimpleScheduleBuilder.Create().WithMisfireHandlingInstructionFireNow()) .WithIdentity(triggerKey); - var trigger = PopulateTrigger(context, builder, messageBody, context.Message.Destination, context.MessageId, context.Message.CorrelationId); + var trigger = PopulateTrigger(context, builder, messageBody, context.Message.Destination, context.Message.PayloadType, messageId: context.MessageId, + tokenId: context.Message.CorrelationId); var scheduler = await _schedulerFactory.GetScheduler(context.CancellationToken).ConfigureAwait(false); @@ -92,8 +93,8 @@ public async Task Consume(ConsumeContext context) if (schedule.EndTime.HasValue) triggerBuilder.EndAt(schedule.EndTime); - var trigger = PopulateTrigger(context, triggerBuilder, messageBody, context.Message.Destination, context.MessageId, - context.Message.CorrelationId); + var trigger = PopulateTrigger(context, triggerBuilder, messageBody, context.Message.Destination, context.Message.PayloadType, + messageId: default, tokenId: context.Message.CorrelationId); var scheduler = await _schedulerFactory.GetScheduler(context.CancellationToken).ConfigureAwait(false); @@ -106,12 +107,13 @@ public async Task Consume(ConsumeContext context) } static ITrigger PopulateTrigger(ConsumeContext context, TriggerBuilder builder, MessageBody messageBody, Uri destination, - Guid? messageId = default, Guid? tokenId = default) + string[] messageTypes, Guid? messageId = default, Guid? tokenId = default) { builder = builder .UsingJobData("Destination", ToString(destination)) .UsingJobData("Body", messageBody.GetString()) - .UsingJobData("ContentType", context.ReceiveContext.ContentType.ToString()); + .UsingJobData("ContentType", context.ReceiveContext.ContentType.ToString()) + .UsingJobData("MessageType", string.Join(";", messageTypes)); if (messageId.HasValue) builder = builder.UsingJobData("MessageId", messageId.Value.ToString()); @@ -147,6 +149,13 @@ static ITrigger PopulateTrigger(ConsumeContext context, TriggerBuilder builder, if (headers.Any()) builder = builder.UsingJobData("HeadersAsJson", JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options)); + if (context.ReceiveContext.TryGetPayload(out var transportReceiveContext)) + { + IDictionary? properties = transportReceiveContext.GetTransportProperties(); + if (properties != null) + builder = builder.UsingJobData("TransportProperties", JsonSerializer.Serialize(properties, SystemTextJsonMessageSerializer.Options)); + } + var trigger = builder .Build(); diff --git a/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/ScheduledMessageJob.cs b/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/ScheduledMessageJob.cs index f16842845e6..71d0cca2a1e 100644 --- a/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/ScheduledMessageJob.cs +++ b/src/Scheduling/MassTransit.QuartzIntegration/QuartzIntegration/ScheduledMessageJob.cs @@ -6,6 +6,7 @@ namespace MassTransit.QuartzIntegration using System.Linq; using System.Net.Mime; using System.Threading.Tasks; + using Context; using Quartz; using Serialization; @@ -28,11 +29,14 @@ public async Task Execute(IJobExecutionContext context) var contentType = new ContentType(jobData.GetString("ContentType")!); var destinationAddress = new Uri(jobData.GetString("Destination")!); var body = jobData.GetString("Body") ?? string.Empty; - var messageType = jobData.GetString("MessageType")?.Split(';')?.ToArray() ?? Array.Empty(); + + var supportedMessageTypes = (jobData.TryGetString("MessageType", out var text) + ? text?.Split(';').ToArray() + : default) ?? Array.Empty(); try { - var pipe = new ForwardScheduledMessagePipe(contentType, messageContext, body, destinationAddress); + var pipe = new ForwardScheduledMessagePipe(contentType, messageContext, body, destinationAddress, supportedMessageTypes); var endpoint = await _bus.GetSendEndpoint(destinationAddress).ConfigureAwait(false); @@ -42,7 +46,7 @@ public async Task Execute(IJobExecutionContext context) } catch (Exception ex) { - LogContext.Error?.Log(ex, "Failed to send scheduled message: {MessageType} {DestinationAddress}", messageType, destinationAddress); + LogContext.Error?.Log(ex, "Failed to send scheduled message: {MessageType} {DestinationAddress}", supportedMessageTypes, destinationAddress); throw new JobExecutionException(ex, context.RefireCount < 5); } @@ -55,14 +59,17 @@ class ForwardScheduledMessagePipe : readonly string _body; readonly ContentType? _contentType; readonly Uri? _destinationAddress; + readonly string[] _supportedMessageTypes; readonly JobDataMessageContext _messageContext; - public ForwardScheduledMessagePipe(ContentType? contentType, JobDataMessageContext messageContext, string body, Uri? destinationAddress) + public ForwardScheduledMessagePipe(ContentType? contentType, JobDataMessageContext messageContext, string body, Uri? destinationAddress, + string[] supportedMessageTypes) { _contentType = contentType; _messageContext = messageContext; _body = body; _destinationAddress = destinationAddress; + _supportedMessageTypes = supportedMessageTypes; } public Task Send(SendContext context) @@ -84,12 +91,19 @@ public Task Send(SendContext context) context.ResponseAddress = _messageContext.ResponseAddress; context.FaultAddress = _messageContext.FaultAddress; + if (_supportedMessageTypes.Any()) + context.SupportedMessageTypes = _supportedMessageTypes; + if (_messageContext.ExpirationTime.HasValue) context.TimeToLive = _messageContext.ExpirationTime.Value.ToUniversalTime() - DateTime.UtcNow; foreach (KeyValuePair header in _messageContext.Headers.GetAll()) context.Headers.Set(header.Key, header.Value); + IReadOnlyDictionary? transportProperties = _messageContext.TransportProperties; + if (transportProperties != null && context is TransportSendContext transportSendContext) + transportSendContext.ReadPropertiesFrom(transportProperties); + context.Serializer = serializerContext.GetMessageSerializer(); return Task.CompletedTask; diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqBusFactory.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqBusFactory.cs index b04c9dca75d..91de5c53cb9 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqBusFactory.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqBusFactory.cs @@ -1,7 +1,6 @@ namespace MassTransit { using System; - using System.Threading; using ActiveMqTransport; using ActiveMqTransport.Configuration; using Configuration; @@ -10,8 +9,6 @@ public static class ActiveMqBusFactory { - public static IMessageTopologyConfigurator MessageTopology => Cached.MessageTopologyValue.Value; - /// /// Configure and create a bus for ActiveMQ /// @@ -19,7 +16,7 @@ public static class ActiveMqBusFactory /// public static IBusControl Create(Action configure) { - var topologyConfiguration = new ActiveMqTopologyConfiguration(MessageTopology); + var topologyConfiguration = new ActiveMqTopologyConfiguration(CreateMessageTopology()); var busConfiguration = new ActiveMqBusConfiguration(topologyConfiguration); var configurator = new ActiveMqBusFactoryConfigurator(busConfiguration); @@ -29,18 +26,19 @@ public static IBusControl Create(Action configu return configurator.Build(busConfiguration); } + public static IMessageTopologyConfigurator CreateMessageTopology() + { + return new MessageTopology(Cached.EntityNameFormatter); + } + static class Cached { - internal static readonly Lazy MessageTopologyValue = - new Lazy(() => new MessageTopology(_entityNameFormatter), - LazyThreadSafetyMode.PublicationOnly); - - static readonly IEntityNameFormatter _entityNameFormatter; + internal static readonly IEntityNameFormatter EntityNameFormatter; static Cached() { - _entityNameFormatter = new MessageNameFormatterEntityNameFormatter(new ActiveMqMessageNameFormatter()); + EntityNameFormatter = new MessageNameFormatterEntityNameFormatter(new ActiveMqMessageNameFormatter()); } } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqConnectionContext.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqConnectionContext.cs index b5c88058b2f..679eba9675b 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqConnectionContext.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqConnectionContext.cs @@ -19,7 +19,7 @@ public class ActiveMqConnectionContext : IAsyncDisposable { readonly IConnection _connection; - readonly ChannelExecutor _executor; + readonly TaskExecutor _executor; readonly ConcurrentDictionary _temporaryEntities; /// @@ -40,7 +40,7 @@ public ActiveMqConnectionContext(IConnection connection, IActiveMqHostConfigurat Topology = hostConfiguration.Topology; - _executor = new ChannelExecutor(1); + _executor = new TaskExecutor(); _temporaryEntities = new ConcurrentDictionary(); _virtualTopicConsumerPattern = new Regex(hostConfiguration.Topology.PublishTopology.VirtualTopicConsumerPattern, RegexOptions.Compiled); @@ -55,7 +55,8 @@ public async Task CreateSession(CancellationToken cancellationToken) { using var tokenSource = CancellationTokenSource.CreateLinkedTokenSource(CancellationToken, cancellationToken); - return await _executor.Run(() => _connection.CreateSession(AcknowledgementMode.IndividualAcknowledge), tokenSource.Token).ConfigureAwait(false); + return await _executor.Run(() => _connection.CreateSessionAsync(AcknowledgementMode.IndividualAcknowledge), tokenSource.Token) + .ConfigureAwait(false); } public bool IsVirtualTopicConsumer(string name) @@ -65,12 +66,12 @@ public bool IsVirtualTopicConsumer(string name) public IQueue GetTemporaryQueue(ISession session, string topicName) { - return (IQueue)_temporaryEntities.GetOrAdd(topicName, x => (IQueue)SessionUtil.GetDestination(session, topicName, DestinationType.TemporaryQueue)); + return (IQueue)_temporaryEntities.GetOrAdd(topicName, _ => (IQueue)SessionUtil.GetDestination(session, topicName, DestinationType.TemporaryQueue)); } public ITopic GetTemporaryTopic(ISession session, string topicName) { - return (ITopic)_temporaryEntities.GetOrAdd(topicName, x => (ITopic)SessionUtil.GetDestination(session, topicName, DestinationType.TemporaryTopic)); + return (ITopic)_temporaryEntities.GetOrAdd(topicName, _ => (ITopic)SessionUtil.GetDestination(session, topicName, DestinationType.TemporaryTopic)); } public bool TryGetTemporaryEntity(string name, out IDestination destination) @@ -95,7 +96,7 @@ public async ValueTask DisposeAsync() try { - _connection.Close(); + await _connection.CloseAsync().ConfigureAwait(false); TransportLogMessages.DisconnectedHost(Description); diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqDeadLetterTransport.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqDeadLetterTransport.cs index c626e0dd3ba..d186b0ef205 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqDeadLetterTransport.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqDeadLetterTransport.cs @@ -2,15 +2,16 @@ { using System.Threading.Tasks; using Apache.NMS; + using Middleware; using Topology; using Transports; public class ActiveMqDeadLetterTransport : - ActiveMqMoveTransport, + ActiveMqMoveTransport, IDeadLetterTransport { - public ActiveMqDeadLetterTransport(Queue destination, IFilter topologyFilter) + public ActiveMqDeadLetterTransport(Queue destination, ConfigureActiveMqTopologyFilter topologyFilter) : base(destination, topologyFilter) { } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqErrorTransport.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqErrorTransport.cs index f9e316b3079..ad88fc4bda4 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqErrorTransport.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqErrorTransport.cs @@ -2,15 +2,16 @@ { using System.Threading.Tasks; using Apache.NMS; + using Middleware; using Topology; using Transports; public class ActiveMqErrorTransport : - ActiveMqMoveTransport, + ActiveMqMoveTransport, IErrorTransport { - public ActiveMqErrorTransport(Queue destination, IFilter topologyFilter) + public ActiveMqErrorTransport(Queue destination, ConfigureActiveMqTopologyFilter topologyFilter) : base(destination, topologyFilter) { } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqHeaderProvider.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqHeaderProvider.cs index 140331214c2..bc59dfe7250 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqHeaderProvider.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqHeaderProvider.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using Apache.NMS; + using Internals; using Transports; @@ -43,6 +44,15 @@ public bool TryGetHeader(string key, out object value) return true; } + if (MessageHeaders.TransportSentTime.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + if (_message.NMSTimestamp > DateTimeConstants.Epoch) + { + value = _message.NMSTimestamp; + return true; + } + } + var found = _message.Properties.Contains(key); if (found) { diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqMessageNameFormatter.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqMessageNameFormatter.cs index c42f0c86dbe..35c85436882 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqMessageNameFormatter.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqMessageNameFormatter.cs @@ -14,7 +14,7 @@ public ActiveMqMessageNameFormatter() _formatter = new DefaultMessageNameFormatter("::", "--", ".", "-"); } - public MessageName GetMessageName(Type type) + public string GetMessageName(Type type) { return _formatter.GetMessageName(type); } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqMoveTransport.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqMoveTransport.cs index 71c474a6459..d5f07397da0 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqMoveTransport.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqMoveTransport.cs @@ -3,15 +3,17 @@ using System; using System.Threading.Tasks; using Apache.NMS; + using Middleware; using Topology; - public class ActiveMqMoveTransport + public class ActiveMqMoveTransport + where TSettings : class { readonly Queue _destination; - readonly IFilter _topologyFilter; + readonly ConfigureActiveMqTopologyFilter _topologyFilter; - protected ActiveMqMoveTransport(Queue destination, IFilter topologyFilter) + protected ActiveMqMoveTransport(Queue destination, ConfigureActiveMqTopologyFilter topologyFilter) { _topologyFilter = topologyFilter; _destination = destination; @@ -25,23 +27,29 @@ protected async Task Move(ReceiveContext context, Action if (!context.TryGetPayload(out ActiveMqMessageContext messageContext)) throw new ArgumentException("The ActiveMqMessageContext was not present", nameof(context)); - await _topologyFilter.Send(sessionContext, Pipe.Empty()).ConfigureAwait(false); + OneTimeContext> oneTimeContext = await _topologyFilter.Configure(sessionContext).ConfigureAwait(false); var queue = await sessionContext.GetQueue(_destination).ConfigureAwait(false); - var producer = await sessionContext.CreateMessageProducer(queue).ConfigureAwait(false); - var message = messageContext.TransportMessage switch { - IBytesMessage _ => producer.CreateBytesMessage(context.Body.GetBytes()), - ITextMessage _ => producer.CreateTextMessage(context.Body.GetString()), - _ => producer.CreateMessage(), + // ReSharper disable MethodHasAsyncOverload + IBytesMessage _ => sessionContext.CreateBytesMessage(context.Body.GetBytes()), + ITextMessage _ => sessionContext.CreateTextMessage(context.Body.GetString()), + _ => sessionContext.CreateMessage(), }; CloneMessage(message, messageContext.TransportMessage, preSend); - var task = Task.Run(() => producer.Send(message)); - context.AddReceiveTask(task); + try + { + await sessionContext.SendAsync(queue, message, context.CancellationToken).ConfigureAwait(false); + } + catch (Exception) + { + oneTimeContext.Evict(); + throw; + } } static void CloneMessage(IMessage message, IMessage source, Action preSend) diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqReceiveContext.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqReceiveContext.cs index dbf2ca082b1..19d931d54c8 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqReceiveContext.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqReceiveContext.cs @@ -1,17 +1,18 @@ namespace MassTransit.ActiveMqTransport { using System; + using System.Collections.Generic; using System.Threading.Tasks; using Apache.NMS; using Apache.NMS.ActiveMQ.Commands; + using Context; using Transports; public sealed class ActiveMqReceiveContext : BaseReceiveContext, ActiveMqMessageContext, - ReceiveContext, - ReceiveLockContext + TransportReceiveContext { public ActiveMqReceiveContext(IMessage transportMessage, ActiveMqReceiveEndpointContext context, params object[] payloads) : base(transportMessage.NMSRedelivered, context, payloads) @@ -23,6 +24,8 @@ public ActiveMqReceiveContext(IMessage transportMessage, ActiveMqReceiveEndpoint protected override IHeaderProvider HeaderProvider => new ActiveMqHeaderProvider(TransportMessage); + public override MessageBody Body { get; } + public IMessage TransportMessage { get; } public IPrimitiveMap Properties => TransportMessage.Properties; @@ -31,23 +34,18 @@ public ActiveMqReceiveContext(IMessage transportMessage, ActiveMqReceiveEndpoint public int GroupSequence => TransportMessage is Message message ? message.GroupSequence : default; - public override MessageBody Body { get; } - - public Task Complete() + public IDictionary GetTransportProperties() { - TransportMessage.Acknowledge(); + var properties = new Lazy>(() => new Dictionary()); - return Task.CompletedTask; - } + if (TransportMessage.NMSPriority != MsgPriority.Normal) + properties.Value[ActiveMqTransportPropertyNames.Priority] = TransportMessage.NMSPriority.ToString(); + if (GroupId != null) + properties.Value[ActiveMqTransportPropertyNames.GroupId] = GroupId; + if (GroupSequence != default) + properties.Value[ActiveMqTransportPropertyNames.GroupSequence] = GroupSequence; - public Task Faulted(Exception exception) - { - return Task.CompletedTask; - } - - public Task ValidateLockStatus() - { - return Task.CompletedTask; + return properties.IsValueCreated ? properties.Value : null; } protected override ISendEndpointProvider GetSendEndpointProvider() diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqReceiveLockContext.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqReceiveLockContext.cs new file mode 100644 index 00000000000..bc1e94e8f05 --- /dev/null +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqReceiveLockContext.cs @@ -0,0 +1,34 @@ +namespace MassTransit.ActiveMqTransport +{ + using System; + using System.Threading.Tasks; + using Apache.NMS; + using Transports; + + + public class ActiveMqReceiveLockContext : + ReceiveLockContext + { + readonly IMessage _message; + + public ActiveMqReceiveLockContext(IMessage message) + { + _message = message; + } + + public Task Complete() + { + return _message.AcknowledgeAsync(); + } + + public Task Faulted(Exception exception) + { + return Task.CompletedTask; + } + + public Task ValidateLockStatus() + { + return Task.CompletedTask; + } + } +} diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqSendTransportContext.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqSendTransportContext.cs index 3801809ce64..e535778f5ed 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqSendTransportContext.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqSendTransportContext.cs @@ -79,31 +79,29 @@ public async Task Send(SessionContext sessionContext, SendContext sendCont sendContext.CancellationToken.ThrowIfCancellationRequested(); var destination = context.ReplyDestination ?? await sessionContext.GetDestination(EntityName, _destinationType).ConfigureAwait(false); - var producer = await sessionContext.CreateMessageProducer(destination).ConfigureAwait(false); - var transportMessage = sessionContext.CreateBytesMessage(); + var transportMessage = sessionContext.CreateBytesMessage(context.Body.GetBytes()); SetResponseTo(transportMessage, context, sessionContext); - transportMessage.Content = context.Body.GetBytes(); - transportMessage.Properties.SetHeaders(context.Headers); transportMessage.Properties[MessageHeaders.ContentType] = context.ContentType.ToString(); - transportMessage.NMSDeliveryMode = context.Durable ? MsgDeliveryMode.Persistent : MsgDeliveryMode.NonPersistent; - if (context.MessageId.HasValue) transportMessage.NMSMessageId = context.MessageId.ToString(); if (context.CorrelationId.HasValue) transportMessage.NMSCorrelationID = context.CorrelationId.ToString(); + transportMessage.NMSDeliveryMode = context.Durable ? MsgDeliveryMode.Persistent : MsgDeliveryMode.NonPersistent; + if (context.TimeToLive.HasValue) transportMessage.NMSTimeToLive = context.TimeToLive > TimeSpan.Zero ? context.TimeToLive.Value : TimeSpan.FromSeconds(1); + else + transportMessage.NMSTimeToLive = NMSConstants.defaultTimeToLive; - if (context.Priority.HasValue) - transportMessage.NMSPriority = context.Priority.Value; + transportMessage.NMSPriority = context.Priority ?? NMSConstants.defaultPriority; if (transportMessage is Message message) { @@ -123,9 +121,7 @@ public async Task Send(SessionContext sessionContext, SendContext sendCont transportMessage.Properties["AMQ_SCHEDULED_DELAY"] = (long)delay.Value; } - var publishTask = Task.Run(() => producer.Send(transportMessage), context.CancellationToken); - - await publishTask.OrCanceled(context.CancellationToken).ConfigureAwait(false); + await sessionContext.SendAsync(destination, transportMessage, context.CancellationToken).ConfigureAwait(false); } static void SetResponseTo(IMessage transportMessage, SendContext context, SessionContext sessionContext) @@ -137,7 +133,7 @@ static void SetResponseTo(IMessage transportMessage, SendContext context, Sessio transportMessage.NMSReplyTo = sessionContext.GetTemporaryDestination(endpointName) ?? (context.ResponseAddress.TryGetValueFromQueryString("temporary", out _) - ? (IDestination)new ActiveMQTempQueue(endpointName) + ? new ActiveMQTempQueue(endpointName) : new ActiveMQQueue(endpointName)); } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqSessionContext.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqSessionContext.cs index cba0a9332fd..363c72a50f8 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqSessionContext.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqSessionContext.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Apache.NMS; using Apache.NMS.Util; + using Internals; using MassTransit.Middleware; using Topology; using Transports; @@ -16,7 +17,7 @@ public class ActiveMqSessionContext : SessionContext, IAsyncDisposable { - readonly ChannelExecutor _executor; + readonly TaskExecutor _executor; readonly MessageProducerCache _messageProducerCache; readonly ISession _session; @@ -27,7 +28,7 @@ public ActiveMqSessionContext(ConnectionContext connectionContext, ISession sess _session = session; CancellationToken = cancellationToken; - _executor = new ChannelExecutor(1); + _executor = new TaskExecutor(); _messageProducerCache = new MessageProducerCache(); } @@ -40,7 +41,7 @@ public async ValueTask DisposeAsync() { await _messageProducerCache.Stop(CancellationToken.None).ConfigureAwait(false); - _session.Close(); + await _session.CloseAsync().ConfigureAwait(false); } catch (Exception ex) { @@ -91,19 +92,33 @@ public Task GetDestination(string destinationName, DestinationType return _executor.Run(() => SessionUtil.GetDestination(_session, destinationName, destinationType), CancellationToken); } - public Task CreateMessageProducer(IDestination destination) + public Task CreateMessageConsumer(IDestination destination, string selector, bool noLocal) { - return _messageProducerCache.GetMessageProducer(destination, x => _executor.Run(() => _session.CreateProducer(x), CancellationToken)); + return _executor.Run(() => _session.CreateConsumerAsync(destination, selector, noLocal), CancellationToken); } - public Task CreateMessageConsumer(IDestination destination, string selector, bool noLocal) + public async Task SendAsync(IDestination destination, IMessage message, CancellationToken cancellationToken) + { + var producer = await _messageProducerCache.GetMessageProducer(destination, + x => _executor.Run(() => _session.CreateProducerAsync(x), cancellationToken)).ConfigureAwait(false); + + await _executor.Run(() => producer.SendAsync(message, message.NMSDeliveryMode, message.NMSPriority, message.NMSTimeToLive) + .OrCanceled(cancellationToken), cancellationToken).ConfigureAwait(false); + } + + public IBytesMessage CreateBytesMessage(byte[] content) + { + return _session.CreateBytesMessage(content); + } + + public ITextMessage CreateTextMessage(string content) { - return _executor.Run(() => _session.CreateConsumer(destination, selector, noLocal), CancellationToken); + return _session.CreateTextMessage(content); } - public IBytesMessage CreateBytesMessage() + public IMessage CreateMessage() { - return _session.CreateBytesMessage(); + return _session.CreateMessage(); } public Task DeleteTopic(string topicName) diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqTransportPropertyNames.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqTransportPropertyNames.cs new file mode 100644 index 00000000000..36b9f71a506 --- /dev/null +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ActiveMqTransportPropertyNames.cs @@ -0,0 +1,9 @@ +namespace MassTransit.ActiveMqTransport +{ + static class ActiveMqTransportPropertyNames + { + public const string Priority = "AMQ-Priority"; + public const string GroupId = "AMQ-GroupId"; + public const string GroupSequence = "AMQ-GroupSequence"; + } +} diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/CachedMessageProducer.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/CachedMessageProducer.cs index 225aa89985e..edac1f6b36c 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/CachedMessageProducer.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/CachedMessageProducer.cs @@ -1,6 +1,7 @@ namespace MassTransit.ActiveMqTransport { using System; + using System.Threading.Tasks; using Apache.NMS; using Caching; @@ -101,6 +102,84 @@ public IStreamMessage CreateStreamMessage() return _producer.CreateStreamMessage(); } + public Task SendAsync(IMessage message) + { + Used?.Invoke(); + return _producer.SendAsync(message); + } + + public Task SendAsync(IMessage message, MsgDeliveryMode deliveryMode, MsgPriority priority, TimeSpan timeToLive) + { + Used?.Invoke(); + return _producer.SendAsync(message, deliveryMode, priority, timeToLive); + } + + public Task SendAsync(IDestination destination, IMessage message) + { + Used?.Invoke(); + return _producer.SendAsync(destination, message); + } + + public Task SendAsync(IDestination destination, IMessage message, MsgDeliveryMode deliveryMode, MsgPriority priority, TimeSpan timeToLive) + { + Used?.Invoke(); + return _producer.SendAsync(destination, message, deliveryMode, priority, timeToLive); + } + + public Task CloseAsync() + { + Used?.Invoke(); + return _producer.CloseAsync(); + } + + public Task CreateMessageAsync() + { + Used?.Invoke(); + return _producer.CreateMessageAsync(); + } + + public Task CreateTextMessageAsync() + { + Used?.Invoke(); + return _producer.CreateTextMessageAsync(); + } + + public Task CreateTextMessageAsync(string text) + { + Used?.Invoke(); + return _producer.CreateTextMessageAsync(text); + } + + public Task CreateMapMessageAsync() + { + Used?.Invoke(); + return _producer.CreateMapMessageAsync(); + } + + public Task CreateObjectMessageAsync(object body) + { + Used?.Invoke(); + return _producer.CreateObjectMessageAsync(body); + } + + public Task CreateBytesMessageAsync() + { + Used?.Invoke(); + return _producer.CreateBytesMessageAsync(); + } + + public Task CreateBytesMessageAsync(byte[] body) + { + Used?.Invoke(); + return _producer.CreateBytesMessageAsync(body); + } + + public Task CreateStreamMessageAsync() + { + Used?.Invoke(); + return _producer.CreateStreamMessageAsync(); + } + public ProducerTransformerDelegate ProducerTransformer { get => _producer.ProducerTransformer; @@ -143,6 +222,12 @@ public bool DisableMessageTimestamp set => _producer.DisableMessageTimestamp = value; } + public TimeSpan DeliveryDelay + { + get => _producer.DeliveryDelay; + set => _producer.DeliveryDelay = value; + } + public event Action Used; } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqHostConfiguration.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqHostConfiguration.cs index 3288cee53b8..ad82c7f8ca1 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqHostConfiguration.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqHostConfiguration.cs @@ -1,6 +1,8 @@ namespace MassTransit.ActiveMqTransport.Configuration { using System; + using System.Threading.Channels; + using Apache.NMS.ActiveMQ; using MassTransit.Configuration; using Topology; using Transports; @@ -27,6 +29,8 @@ public ActiveMqHostConfiguration(IActiveMqBusConfiguration busConfiguration, IAc ReceiveTransportRetryPolicy = Retry.CreatePolicy(x => { x.Handle(); + x.Handle(); + x.Handle(); x.Exponential(1000, TimeSpan.FromSeconds(3), TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(3)); }); diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqHostConfigurator.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqHostConfigurator.cs index 49608595a49..ba11a2650c5 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqHostConfigurator.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqHostConfigurator.cs @@ -29,10 +29,17 @@ public void Password(string password) _settings.Password = password; } - public void UseSsl() + public void UseSsl(bool enabled = true) { - _settings.UseSsl = true; - if (_settings.Port == 61616) + _settings.UseSsl = enabled; + if (enabled && _settings.Port == 61616) + _settings.Port = 61617; + } + + public void UseSsl(bool enabled, bool updatePort) + { + _settings.UseSsl = enabled; + if (enabled && updatePort && _settings.Port == 61616) _settings.Port = 61617; } @@ -47,6 +54,11 @@ public void TransportOptions(IEnumerable> options) _settings.TransportOptions[option.Key] = option.Value; } + public void EnableAsyncSend() + { + _settings.TransportOptions["nms.AsyncSend"] = "true"; + } + public void EnableOptimizeAcknowledge() { _settings.TransportOptions["jms.optimizeAcknowledge"] = "true"; diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqReceiveEndpointBuilder.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqReceiveEndpointBuilder.cs index 2b2c0f9168d..9388edb5294 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqReceiveEndpointBuilder.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqReceiveEndpointBuilder.cs @@ -35,11 +35,12 @@ public ActiveMqReceiveEndpointContext CreateReceiveEndpointContext() { var brokerTopology = BuildTopology(_configuration.Settings); - var deadLetterTransport = CreateDeadLetterTransport(); - var errorTransport = CreateErrorTransport(); - var context = new ActiveMqConsumerReceiveEndpointContext(_hostConfiguration, _configuration, brokerTopology); + var deadLetterTransport = CreateDeadLetterTransport(context); + var errorTransport = CreateErrorTransport(context); + + context.GetOrAddPayload(() => deadLetterTransport); context.GetOrAddPayload(() => errorTransport); context.GetOrAddPayload(() => _hostConfiguration.Topology); @@ -47,21 +48,20 @@ public ActiveMqReceiveEndpointContext CreateReceiveEndpointContext() return context; } - IErrorTransport CreateErrorTransport() + IErrorTransport CreateErrorTransport(ActiveMqReceiveEndpointContext context) { - var errorSettings = _configuration.Topology.Send.GetErrorSettings(_configuration.Settings); - var filter = new ConfigureActiveMqTopologyFilter(errorSettings, errorSettings.GetBrokerTopology()); + var settings = _configuration.Topology.Send.GetErrorSettings(_configuration.Settings); + var filter = new ConfigureActiveMqTopologyFilter(settings, settings.GetBrokerTopology(), context); - return new ActiveMqErrorTransport(new QueueEntity(0, errorSettings.EntityName, errorSettings.Durable, errorSettings.AutoDelete), filter); + return new ActiveMqErrorTransport(new QueueEntity(0, settings.EntityName, settings.Durable, settings.AutoDelete), filter); } - IDeadLetterTransport CreateDeadLetterTransport() + IDeadLetterTransport CreateDeadLetterTransport(ActiveMqReceiveEndpointContext context) { - var deadLetterSettings = _configuration.Topology.Send.GetDeadLetterSettings(_configuration.Settings); - var filter = new ConfigureActiveMqTopologyFilter(deadLetterSettings, deadLetterSettings.GetBrokerTopology()); + var settings = _configuration.Topology.Send.GetDeadLetterSettings(_configuration.Settings); + var filter = new ConfigureActiveMqTopologyFilter(settings, settings.GetBrokerTopology(), context); - return new ActiveMqDeadLetterTransport(new QueueEntity(0, deadLetterSettings.EntityName, deadLetterSettings.Durable, deadLetterSettings.AutoDelete), - filter); + return new ActiveMqDeadLetterTransport(new QueueEntity(0, settings.EntityName, settings.Durable, settings.AutoDelete), filter); } BrokerTopology BuildTopology(ReceiveSettings settings) diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqReceiveEndpointConfiguration.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqReceiveEndpointConfiguration.cs index 7e3acfd93ba..56f6bb2816f 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqReceiveEndpointConfiguration.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqReceiveEndpointConfiguration.cs @@ -51,12 +51,15 @@ public void Build(IHost host) { var context = CreateActiveMqReceiveEndpointContext(); - _sessionConfigurator.UseFilter(new ConfigureActiveMqTopologyFilter(_settings, context.BrokerTopology)); + _sessionConfigurator.UseFilter(new ConfigureActiveMqTopologyFilter(_settings, context.BrokerTopology, context)); if (_hostConfiguration.DeployTopologyOnly) _sessionConfigurator.UseFilter(new TransportReadyFilter(context)); else + { + _sessionConfigurator.UseFilter(new ReceiveEndpointDependencyFilter(context)); _sessionConfigurator.UseFilter(new ActiveMqConsumerFilter(context)); + } IPipe sessionPipe = _sessionConfigurator.Build(); @@ -69,7 +72,7 @@ public void Build(IHost host) var brokerTopology = publishTopology.GetPublishBrokerTopology(); - transport.PreStartPipe = new ConfigureActiveMqTopologyFilter(publishTopology, brokerTopology).ToPipe(); + transport.PreStartPipe = new ConfigureActiveMqTopologyFilter(publishTopology, brokerTopology, context).ToPipe(); } var receiveEndpoint = new ReceiveEndpoint(transport, context); diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqRegistrationBusFactory.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqRegistrationBusFactory.cs index bfecdb9c612..da255800374 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqRegistrationBusFactory.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ActiveMqRegistrationBusFactory.cs @@ -15,7 +15,7 @@ public class ActiveMqRegistrationBusFactory : readonly Action _configure; public ActiveMqRegistrationBusFactory(Action configure) - : this(new ActiveMqBusConfiguration(new ActiveMqTopologyConfiguration(ActiveMqBusFactory.MessageTopology)), configure) + : this(new ActiveMqBusConfiguration(new ActiveMqTopologyConfiguration(ActiveMqBusFactory.CreateMessageTopology())), configure) { } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ConfigurationHostSettings.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ConfigurationHostSettings.cs index 0054b6d6c08..358e11e93e0 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ConfigurationHostSettings.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ConfigurationHostSettings.cs @@ -9,6 +9,31 @@ namespace MassTransit.ActiveMqTransport.Configuration public class ConfigurationHostSettings : ActiveMqHostSettings { + // ActiveMQ Failover connection parameters https://activemq.apache.org/components/classic/documentation/failover-transport-reference + static readonly HashSet _failoverArguments = + new HashSet(StringComparer.OrdinalIgnoreCase) + { + "backup", + "initialReconnectDelay", + "maxCacheSize", + "maxReconnectAttempts", + "maxReconnectDelay", + "randomize", + "reconnectDelayExponent", + "reconnectSupported", + "startupMaxReconnectAttempts", + "timeout", + "trackMessages", + "updateURIsSupported", + "updateURIsURL", + "useExponentialBackOff", + "warnAfterReconnectAttempts", + "ha", + "reconnectAttempts", + "priorityBackup", + "priorityURIs" + }; + readonly Lazy _brokerAddress; readonly Lazy _hostAddress; @@ -31,11 +56,7 @@ public ConfigurationHostSettings(Uri address) Password = parts[1]; } - TransportOptions = new Dictionary - { - { "wireFormat.tightEncodingEnabled", "true" }, - { "nms.AsyncSend", "true" } - }; + TransportOptions = new Dictionary { { "wireFormat.tightEncodingEnabled", "true" } }; _hostAddress = new Lazy(FormatHostAddress); _brokerAddress = new Lazy(FormatBrokerAddress); @@ -68,33 +89,37 @@ Uri FormatHostAddress() Uri FormatBrokerAddress() { var scheme = UseSsl ? "ssl" : "tcp"; - var queryPart = GetQueryString(); // create broker URI: http://activemq.apache.org/nms/activemq-uri-configuration.html if (FailoverHosts?.Length > 0) { + //filter only parameters which are not failover parameters + var failoverServerPart = GetQueryString(kv => !IsFailoverArgument(kv.Key)); var failoverPart = string.Join(",", FailoverHosts .Select(failoverHost => new UriBuilder { Scheme = scheme, Host = failoverHost, - Port = Port + Port = Port, + Query = failoverServerPart }.Uri.ToString() )); - - return new Uri($"activemq:failover:({failoverPart}){queryPart}"); + //filter failover parameters only. Apache.NMS.ActiveMQ requires prefix "transport." for failover parameters + var failoverQueryPart = GetQueryString(kv => IsFailoverArgument(kv.Key), "transport."); + return new Uri($"activemq:failover:({failoverPart}){failoverQueryPart}"); } + var queryPart = GetQueryString(_ => true); var uri = new Uri($"activemq:{scheme}://{Host}:{Port}{queryPart}"); return uri; } - string GetQueryString() + string GetQueryString(Func, bool> predicate, string prefix = "") { if (TransportOptions.Count == 0) return ""; - var queryString = string.Join("&", TransportOptions.Select(pair => $"{pair.Key}={pair.Value}")); + var queryString = string.Join("&", TransportOptions.Where(predicate).Select(pair => $"{prefix}{pair.Key}={pair.Value}")); return $"?{queryString}"; } @@ -108,5 +133,10 @@ public override string ToString() Port = Port }.Uri.ToString(); } + + static bool IsFailoverArgument(string key) + { + return key.StartsWith("nested.", StringComparison.OrdinalIgnoreCase) || _failoverArguments.Contains(key); + } } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ConsumerConsumeTopicTopologySpecification.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ConsumerConsumeTopicTopologySpecification.cs new file mode 100644 index 00000000000..640499f474d --- /dev/null +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Configuration/ConsumerConsumeTopicTopologySpecification.cs @@ -0,0 +1,39 @@ +namespace MassTransit.ActiveMqTransport.Configuration +{ + using System.Collections.Generic; + using System.Linq; + using Topology; + + + public class ConsumerConsumeTopicTopologySpecification : + ActiveMqTopicBindingConfigurator, + IActiveMqConsumeTopologySpecification + { + readonly IActiveMqConsumerEndpointQueueNameFormatter _consumerEndpointQueueNameFormatter; + + public ConsumerConsumeTopicTopologySpecification(string topicName, IActiveMqConsumerEndpointQueueNameFormatter consumerEndpointQueueNameFormatter, + bool durable = true, bool autoDelete = false) + : base(topicName, durable, autoDelete) + { + _consumerEndpointQueueNameFormatter = consumerEndpointQueueNameFormatter; + } + + public ConsumerConsumeTopicTopologySpecification(Topic topic, IActiveMqConsumerEndpointQueueNameFormatter consumerEndpointQueueNameFormatter) + : base(topic) + { + _consumerEndpointQueueNameFormatter = consumerEndpointQueueNameFormatter; + } + + public IEnumerable Validate() + { + return Enumerable.Empty(); + } + + public void Apply(IReceiveEndpointBrokerTopologyBuilder builder) + { + var topic = builder.CreateTopic(EntityName, Durable, AutoDelete); + + _ = builder.BindConsumer(topic, null, Selector); + } + } +} diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ConnectionContextSupervisor.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ConnectionContextSupervisor.cs index 7aa1ff4b4c5..76cb09cbf91 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ConnectionContextSupervisor.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ConnectionContextSupervisor.cs @@ -37,7 +37,8 @@ public Task CreateSendTransport(ActiveMqReceiveEndpointContext c var settings = _topologyConfiguration.Send.GetSendSettings(endpointAddress); - IPipe configureTopology = new ConfigureActiveMqTopologyFilter(settings, settings.GetBrokerTopology()).ToPipe(); + IPipe configureTopology = new ConfigureActiveMqTopologyFilter(settings, settings.GetBrokerTopology(), context) + .ToPipe(); return CreateSendTransport(context, sessionContextSupervisor, configureTopology, settings.EntityName, endpointAddress.Type == ActiveMqEndpointAddress.AddressType.Queue ? DestinationType.Queue : DestinationType.Topic); @@ -52,7 +53,8 @@ public Task CreatePublishTransport(ActiveMqReceiveEndpointCon var settings = publishTopology.GetSendSettings(_hostConfiguration.HostAddress); - IPipe configureTopology = new ConfigureActiveMqTopologyFilter(settings, publishTopology.GetBrokerTopology()).ToPipe(); + IPipe configureTopology = new ConfigureActiveMqTopologyFilter(settings, publishTopology.GetBrokerTopology(), context) + .ToPipe(); return CreateSendTransport(context, sessionContextSupervisor, configureTopology, settings.EntityName, DestinationType.Topic); } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/MessageProducerCache.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/MessageProducerCache.cs index d48dd6f984d..5f28ef94ddd 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/MessageProducerCache.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/MessageProducerCache.cs @@ -30,7 +30,7 @@ public async Task GetMessageProducer(IDestination key, Message return messageProducer; } - async Task GetMessageProducerFromFactory(IDestination destination, MessageProducerFactory factory) + static async Task GetMessageProducerFromFactory(IDestination destination, MessageProducerFactory factory) { var messageProducer = await factory(destination).ConfigureAwait(false); diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ActiveMqConsumer.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ActiveMqConsumer.cs index b758c5a1c1a..9f489ead0e9 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ActiveMqConsumer.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ActiveMqConsumer.cs @@ -4,8 +4,6 @@ namespace MassTransit.ActiveMqTransport.Middleware using System.Threading.Tasks; using Apache.NMS; using Apache.NMS.ActiveMQ; - using Internals; - using MassTransit.Middleware; using Transports; using Util; @@ -14,12 +12,9 @@ namespace MassTransit.ActiveMqTransport.Middleware /// Receives messages from ActiveMQ, pushing them to the InboundPipe of the service endpoint. /// public sealed class ActiveMqConsumer : - Agent, - DeliveryMetrics + ConsumerAgent { readonly ActiveMqReceiveEndpointContext _context; - readonly TaskCompletionSource _deliveryComplete; - readonly IReceivePipeDispatcher _dispatcher; readonly ChannelExecutor _executor; readonly MessageConsumer _messageConsumer; readonly ReceiveSettings _receiveSettings; @@ -33,6 +28,7 @@ public sealed class ActiveMqConsumer : /// The topology /// public ActiveMqConsumer(SessionContext session, MessageConsumer messageConsumer, ActiveMqReceiveEndpointContext context, ChannelExecutor executor) + : base(context, StringComparer.Ordinal) { _session = session; _messageConsumer = messageConsumer; @@ -41,30 +37,27 @@ public ActiveMqConsumer(SessionContext session, MessageConsumer messageConsumer, _receiveSettings = session.GetPayload(); - _deliveryComplete = TaskUtil.GetTask(); - - _dispatcher = context.CreateReceivePipeDispatcher(); - _dispatcher.ZeroActivity += HandleDeliveryComplete; - messageConsumer.Listener += HandleMessage; + TrySetManualConsumeTask(); + SetReady(); } - long DeliveryMetrics.DeliveryCount => _dispatcher.DispatchCount; - int DeliveryMetrics.ConcurrentDeliveryCount => _dispatcher.MaxConcurrentDispatchCount; - void HandleMessage(IMessage message) { _executor.PushWithWait(async () => { + if (IsStopping) + return; + LogContext.Current = _context.LogContext; var context = new ActiveMqReceiveContext(message, _context, _receiveSettings, _session, _session.ConnectionContext); try { - await _dispatcher.Dispatch(context, context).ConfigureAwait(false); + await Dispatch(message.NMSMessageId, context, new ActiveMqReceiveLockContext(message)).ConfigureAwait(false); } catch (Exception exception) { @@ -77,42 +70,17 @@ void HandleMessage(IMessage message) }, Stopping); } - Task HandleDeliveryComplete() - { - if (IsStopping) - _deliveryComplete.TrySetResult(true); - - return Task.CompletedTask; - } - - protected override Task StopAgent(StopContext context) + protected override async Task ActiveAndActualAgentsCompleted(StopContext context) { _messageConsumer.Stop(); _messageConsumer.Listener -= HandleMessage; _messageConsumer.Start(); - SetCompleted(ActiveAndActualAgentsCompleted(context)); - - return Completed; - } - - async Task ActiveAndActualAgentsCompleted(StopContext context) - { - if (_dispatcher.ActiveDispatchCount > 0) - { - try - { - await _deliveryComplete.Task.OrCanceled(context.CancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - LogContext.Warning?.Log("Stop canceled waiting for message consumers to complete: {InputAddress}", _context.InputAddress); - } - } + await base.ActiveAndActualAgentsCompleted(context).ConfigureAwait(false); try { - _messageConsumer.Close(); + await _messageConsumer.CloseAsync().ConfigureAwait(false); _messageConsumer.Dispose(); } catch (OperationCanceledException) diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ActiveMqConsumerFilter.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ActiveMqConsumerFilter.cs index a2907cd2616..88996c834a2 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ActiveMqConsumerFilter.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ActiveMqConsumerFilter.cs @@ -36,19 +36,28 @@ async Task IFilter.Send(SessionContext context, IPipe> { - CreateConsumer(context, new QueueEntity(0, receiveSettings.EntityName, receiveSettings.Durable, receiveSettings.AutoDelete), - receiveSettings.Selector, executor) + CreateConsumer(context, new QueueEntity(0, GetReceiveEntityName(receiveSettings), receiveSettings.Durable, + receiveSettings.AutoDelete), receiveSettings.Selector, executor) }; - consumers.AddRange(_context.BrokerTopology.Consumers.Select(x => - CreateConsumer(context, x.Destination, x.Selector, executor))); + consumers.AddRange(_context.BrokerTopology.Consumers.Where(x => x.Destination == null).Select(x => + CreateConsumer(context, new TopicEntity(0, GetReceiveEntityName(receiveSettings, x.Source.EntityName), x.Source.Durable, + x.Source.AutoDelete), x.Selector, executor))); + + consumers.AddRange(_context.BrokerTopology.Consumers.Where(x => x.Destination != null).Select(x => + CreateConsumer(context, new QueueEntity(0, GetReceiveEntityName(receiveSettings, x.Destination.EntityName), x.Destination.Durable, + x.Destination.AutoDelete), x.Selector, executor))); ActiveMqConsumer[] actualConsumers = await Task.WhenAll(consumers).ConfigureAwait(false); var supervisor = CreateConsumerSupervisor(context, actualConsumers); + await supervisor.Ready.ConfigureAwait(false); + LogContext.Debug?.Log("Consumers Ready: {InputAddress}", _context.InputAddress); + _context.AddConsumeAgent(supervisor); + await _context.TransportObservers.NotifyReady(_context.InputAddress).ConfigureAwait(false); try @@ -70,14 +79,16 @@ async Task IFilter.Send(SessionContext context, IPipe context.ConnectionContext.Connection.ExceptionListener -= HandleException, + supervisor.Completed.ContinueWith(_ => context.ConnectionContext.Connection.ExceptionListener -= HandleException, TaskContinuationOptions.ExecuteSynchronously); return supervisor; } - async Task CreateConsumer(SessionContext context, Queue entity, string selector, ChannelExecutor executor) + async Task CreateConsumer(SessionContext context, Queue entity, string selector, + ChannelExecutor executor) { var queue = await context.GetQueue(entity).ConfigureAwait(false); @@ -104,12 +116,53 @@ async Task CreateConsumer(SessionContext context, Queue entity var consumer = new ActiveMqConsumer(context, (MessageConsumer)messageConsumer, _context, executor); - await consumer.Ready.ConfigureAwait(false); + return consumer; + } + + async Task CreateConsumer(SessionContext context, Topic entity, string selector, + ChannelExecutor executor) + { + var topic = await context.GetTopic(entity).ConfigureAwait(false); + + var messageConsumer = await context.CreateMessageConsumer(topic, selector, false).ConfigureAwait(false); + + LogContext.Debug?.Log("Created consumer for {InputAddress}: {Topic}", _context.InputAddress, entity.EntityName); + + var consumer = new ActiveMqConsumer(context, (MessageConsumer)messageConsumer, _context, executor); return consumer; } + class ConsumerSupervisor : + Supervisor + { + public ConsumerSupervisor(ActiveMqConsumer[] consumers) + { + foreach (var consumer in consumers) + { + if (IsStopping) + return; + + consumer.Completed.ContinueWith(async _ => + { + try + { + if (!IsStopping) + await this.Stop("Consumer stopped, stopping supervisor").ConfigureAwait(false); + } + catch (Exception exception) + { + LogContext.Warning?.Log(exception, "Stop Faulted"); + } + }, TaskContinuationOptions.RunContinuationsAsynchronously); + + Add(consumer); + } + } + } + + class CombinedDeliveryMetrics : DeliveryMetrics { diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ConfigureActiveMqTopologyFilter.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ConfigureActiveMqTopologyFilter.cs index 3cdd1ea4684..68029b55dc2 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ConfigureActiveMqTopologyFilter.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/ConfigureActiveMqTopologyFilter.cs @@ -1,107 +1,84 @@ -namespace MassTransit.ActiveMqTransport.Middleware +namespace MassTransit.ActiveMqTransport.Middleware; + +using System; +using System.Linq; +using System.Threading.Tasks; +using Topology; + + +/// +/// Configures the broker with the supplied topology once the model is created, to ensure +/// that the exchanges, queues, and bindings for the model are properly configured in ActiveMQ. +/// +public class ConfigureActiveMqTopologyFilter : + IFilter + where TSettings : class { - using System; - using System.Linq; - using System.Threading.Tasks; - using Apache.NMS.ActiveMQ; - using Topology; - - - /// - /// Configures the broker with the supplied topology once the model is created, to ensure - /// that the exchanges, queues, and bindings for the model are properly configured in ActiveMQ. - /// - public class ConfigureActiveMqTopologyFilter : - IFilter - where TSettings : class + readonly BrokerTopology _brokerTopology; + readonly ActiveMqReceiveEndpointContext _context; + readonly TSettings _settings; + + public ConfigureActiveMqTopologyFilter(TSettings settings, BrokerTopology brokerTopology, ActiveMqReceiveEndpointContext context) { - readonly BrokerTopology _brokerTopology; - readonly TSettings _settings; + _settings = settings; + _brokerTopology = brokerTopology; + _context = context; + } - public ConfigureActiveMqTopologyFilter(TSettings settings, BrokerTopology brokerTopology) - { - _settings = settings; - _brokerTopology = brokerTopology; - } + public async Task Send(SessionContext context, IPipe next) + { + OneTimeContext> oneTimeContext = await Configure(context); - async Task IFilter.Send(SessionContext context, IPipe next) + try { - await context.OneTimeSetup>(async payload => - { - await ConfigureTopology(context).ConfigureAwait(false); - - context.GetOrAddPayload(() => _settings); - }, () => new Context()).ConfigureAwait(false); - await next.Send(context).ConfigureAwait(false); if (_settings is ReceiveSettings) - await DeleteAutoDelete(context).ConfigureAwait(false); + _context.AddSendAgent(new RemoveAutoDeleteAgent(context, _brokerTopology)); } - - void IProbeSite.Probe(ProbeContext context) + catch (Exception) { - var scope = context.CreateFilterScope("configureTopology"); + oneTimeContext.Evict(); - _brokerTopology.Probe(scope); + throw; } + } - async Task ConfigureTopology(SessionContext context) - { - await Task.WhenAll(_brokerTopology.Topics.Select(topic => Declare(context, topic))).ConfigureAwait(false); - - await Task.WhenAll(_brokerTopology.Queues.Select(queue => Declare(context, queue))).ConfigureAwait(false); - } + public void Probe(ProbeContext context) + { + var scope = context.CreateFilterScope("configureTopology"); - async Task DeleteAutoDelete(SessionContext context) - { - try - { - await Task.WhenAll(_brokerTopology.Consumers.Where(x => x.Destination.AutoDelete).Select(consumer => Delete(context, consumer.Destination))) - .ConfigureAwait(false); - - await Task.WhenAll(_brokerTopology.Topics.Where(x => x.AutoDelete).Select(topic => Delete(context, topic))).ConfigureAwait(false); - - await Task.WhenAll(_brokerTopology.Queues.Where(x => x.AutoDelete).Select(queue => Delete(context, queue))).ConfigureAwait(false); - } - catch (ConnectionClosedException exception) - { - LogContext.Debug?.Log(exception, "Connection was closed, auto-delete queues/topics/consumers could not be deleted"); - } - catch (Exception exception) - { - LogContext.Error?.Log(exception, "Failure removing auto-delete queues/topics"); - } - } + _brokerTopology.Probe(scope); + } - Task Declare(SessionContext context, Topic topic) + public async Task>> Configure(SessionContext context) + { + return await context.OneTimeSetup>(() => { - LogContext.Debug?.Log("Get topic {Topic}", topic); + context.GetOrAddPayload(() => _settings); - return context.GetTopic(topic); - } + return ConfigureTopology(context); + }).ConfigureAwait(false); + } - Task Declare(SessionContext context, Queue queue) - { - LogContext.Debug?.Log("Get queue {Queue}", queue); + async Task ConfigureTopology(SessionContext context) + { + await Task.WhenAll(_brokerTopology.Topics.Select(topic => Declare(context, topic))).ConfigureAwait(false); - return context.GetQueue(queue); - } + await Task.WhenAll(_brokerTopology.Queues.Select(queue => Declare(context, queue))).ConfigureAwait(false); + } - Task Delete(SessionContext context, Topic topic) - { - return context.DeleteTopic(topic.EntityName); - } + Task Declare(SessionContext context, Topic topic) + { + LogContext.Debug?.Log("Get topic {Topic}", topic); - Task Delete(SessionContext context, Queue queue) - { - return context.DeleteQueue(queue.EntityName); - } + return context.GetTopic(topic); + } + Task Declare(SessionContext context, Queue queue) + { + LogContext.Debug?.Log("Get queue {Queue}", queue); - class Context : - ConfigureTopologyContext - { - } + return context.GetQueue(queue); } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/RemoveAutoDeleteAgent.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/RemoveAutoDeleteAgent.cs new file mode 100644 index 00000000000..344bb3e4866 --- /dev/null +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Middleware/RemoveAutoDeleteAgent.cs @@ -0,0 +1,69 @@ +namespace MassTransit.ActiveMqTransport.Middleware; + +using System; +using System.Linq; +using System.Threading.Tasks; +using Apache.NMS.ActiveMQ; +using MassTransit.Middleware; +using Topology; + + +public sealed class RemoveAutoDeleteAgent : + Agent +{ + readonly BrokerTopology _brokerTopology; + readonly SessionContext _context; + + public RemoveAutoDeleteAgent(SessionContext context, BrokerTopology brokerTopology) + { + _brokerTopology = brokerTopology; + _context = context; + + SetReady(); + } + + protected override async Task StopAgent(StopContext context) + { + try + { + await DeleteAutoDelete(_context).ConfigureAwait(false); + } + catch (Exception ex) + { + LogContext.Warning?.Log(ex, "Failed to remove one or more subscriptions from the endpoint."); + } + + await base.StopAgent(context); + } + + async Task DeleteAutoDelete(SessionContext context) + { + try + { + await Task.WhenAll(_brokerTopology.Consumers.Where(x => x.Destination.AutoDelete).Select(consumer => Delete(context, consumer.Destination))) + .ConfigureAwait(false); + + await Task.WhenAll(_brokerTopology.Topics.Where(x => x.AutoDelete).Select(topic => Delete(context, topic))).ConfigureAwait(false); + + await Task.WhenAll(_brokerTopology.Queues.Where(x => x.AutoDelete).Select(queue => Delete(context, queue))).ConfigureAwait(false); + } + catch (ConnectionClosedException exception) + { + LogContext.Debug?.Log(exception, "Connection was closed, auto-delete queues/topics/consumers could not be deleted"); + } + catch (Exception exception) + { + LogContext.Error?.Log(exception, "Failure removing auto-delete queues/topics"); + } + } + + Task Delete(SessionContext context, Topic topic) + { + return context.DeleteTopic(topic.EntityName); + } + + Task Delete(SessionContext context, Queue queue) + { + return context.DeleteQueue(queue.EntityName); + } +} diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ReplyToSendEndpoint.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ReplyToSendEndpoint.cs index 1056f5e5aee..dacc25abe03 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ReplyToSendEndpoint.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ReplyToSendEndpoint.cs @@ -37,10 +37,14 @@ public ReplyToPipe(IDestination destination, IPipe>? pipe) protected override void Send(SendContext context) { + if (!context.TryGetPayload(out ConsumeContext? consumeContext)) + return; + if (!context.TryGetPayload(out ActiveMqSendContext? sendContext)) throw new ArgumentException("The ActiveMqSendContext was not available"); - sendContext!.ReplyDestination = _destination; + if (string.Equals(context.DestinationAddress?.AbsolutePath, consumeContext.ResponseAddress?.AbsolutePath)) + sendContext.ReplyDestination = _destination; } protected override void Send(SendContext context) diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ScopeSessionContext.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ScopeSessionContext.cs index c18f770507d..e2f47b772bc 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ScopeSessionContext.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/ScopeSessionContext.cs @@ -22,45 +22,45 @@ public ScopeSessionContext(SessionContext context, CancellationToken cancellatio public override CancellationToken CancellationToken { get; } - ISession SessionContext.Session => _context.Session; - ConnectionContext SessionContext.ConnectionContext => _context.ConnectionContext; + public ISession Session => _context.Session; + public ConnectionContext ConnectionContext => _context.ConnectionContext; - Task SessionContext.GetTopic(Topic topic) + public Task GetTopic(Topic topic) { return _context.GetTopic(topic); } - Task SessionContext.GetQueue(Queue queue) + public Task GetQueue(Queue queue) { return _context.GetQueue(queue); } - Task SessionContext.GetDestination(string destinationName, DestinationType destinationType) + public Task GetDestination(string destinationName, DestinationType destinationType) { return _context.GetDestination(destinationName, destinationType); } - Task SessionContext.CreateMessageProducer(IDestination destination) + public Task CreateMessageConsumer(IDestination destination, string selector, bool noLocal) { - return _context.CreateMessageProducer(destination); + return _context.CreateMessageConsumer(destination, selector, noLocal); } - Task SessionContext.CreateMessageConsumer(IDestination destination, string selector, bool noLocal) + public Task SendAsync(IDestination destination, IMessage message, CancellationToken cancellationToken) { - return _context.CreateMessageConsumer(destination, selector, noLocal); + return _context.SendAsync(destination, message, cancellationToken); } - public IBytesMessage CreateBytesMessage() + public IBytesMessage CreateBytesMessage(byte[] content) { - return _context.CreateBytesMessage(); + return _context.CreateBytesMessage(content); } - Task SessionContext.DeleteTopic(string topicName) + public Task DeleteTopic(string topicName) { return _context.DeleteTopic(topicName); } - Task SessionContext.DeleteQueue(string queueName) + public Task DeleteQueue(string queueName) { return _context.DeleteQueue(queueName); } @@ -69,5 +69,15 @@ public IDestination GetTemporaryDestination(string name) { return _context.GetTemporaryDestination(name); } + + public ITextMessage CreateTextMessage(string content) + { + return _context.CreateTextMessage(content); + } + + public IMessage CreateMessage() + { + return _context.CreateMessage(); + } } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SessionContext.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SessionContext.cs index ce30a6e50d5..087ceb8930c 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SessionContext.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SessionContext.cs @@ -1,5 +1,6 @@ namespace MassTransit.ActiveMqTransport { + using System.Threading; using System.Threading.Tasks; using Apache.NMS; using Topology; @@ -18,11 +19,15 @@ public interface SessionContext : Task GetDestination(string destinationName, DestinationType destinationType); - Task CreateMessageProducer(IDestination destination); - Task CreateMessageConsumer(IDestination destination, string selector, bool noLocal); - IBytesMessage CreateBytesMessage(); + Task SendAsync(IDestination destination, IMessage message, CancellationToken cancellationToken); + + IBytesMessage CreateBytesMessage(byte[] content); + + ITextMessage CreateTextMessage(string content); + + IMessage CreateMessage(); Task DeleteTopic(string topicName); diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SessionContextFactory.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SessionContextFactory.cs index 630685b3adc..249d5473817 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SessionContextFactory.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SessionContextFactory.cs @@ -17,7 +17,7 @@ public SessionContextFactory(IConnectionContextSupervisor connectionContextSuper _connectionContextSupervisor = connectionContextSupervisor; } - IPipeContextAgent IPipeContextFactory.CreateContext(ISupervisor supervisor) + public IPipeContextAgent CreateContext(ISupervisor supervisor) { IAsyncPipeContextAgent asyncContext = supervisor.AddAsyncContext(); @@ -26,7 +26,7 @@ IPipeContextAgent IPipeContextFactory.CreateCont return asyncContext; } - IActivePipeContextAgent IPipeContextFactory.CreateActiveContext(ISupervisor supervisor, + public IActivePipeContextAgent CreateActiveContext(ISupervisor supervisor, PipeContextHandle context, CancellationToken cancellationToken) { return supervisor.AddActiveContext(context, CreateSharedSession(context.Context, cancellationToken)); @@ -53,18 +53,18 @@ void HandleConnectionException(Exception exception) connectionContext.Connection.ExceptionListener += HandleConnectionException; - #pragma warning disable 4014 + #pragma warning disable 4014 // ReSharper disable once MethodSupportsCancellation asyncContext.Completed.ContinueWith(_ => connectionContext.Connection.ExceptionListener -= HandleConnectionException, TaskContinuationOptions.ExecuteSynchronously); - #pragma warning restore 4014 + #pragma warning restore 4014 return new ActiveMqSessionContext(connectionContext, session, createCancellationToken); } - #pragma warning disable CS4014 + #pragma warning disable CS4014 _connectionContextSupervisor.CreateAgent(asyncContext, CreateSessionContext, cancellationToken); - #pragma warning restore CS4014 + #pragma warning restore CS4014 } } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SharedSessionContext.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SharedSessionContext.cs index f656ccabc95..d546fe13c77 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SharedSessionContext.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/SharedSessionContext.cs @@ -22,45 +22,45 @@ public SharedSessionContext(SessionContext context, CancellationToken cancellati public override CancellationToken CancellationToken { get; } - ISession SessionContext.Session => _context.Session; - ConnectionContext SessionContext.ConnectionContext => _context.ConnectionContext; + public ISession Session => _context.Session; + public ConnectionContext ConnectionContext => _context.ConnectionContext; - Task SessionContext.GetTopic(Topic topic) + public Task GetTopic(Topic topic) { return _context.GetTopic(topic); } - Task SessionContext.GetQueue(Queue queue) + public Task GetQueue(Queue queue) { return _context.GetQueue(queue); } - Task SessionContext.GetDestination(string destinationName, DestinationType destinationType) + public Task GetDestination(string destinationName, DestinationType destinationType) { return _context.GetDestination(destinationName, destinationType); } - Task SessionContext.CreateMessageProducer(IDestination destination) + public Task CreateMessageConsumer(IDestination destination, string selector, bool noLocal) { - return _context.CreateMessageProducer(destination); + return _context.CreateMessageConsumer(destination, selector, noLocal); } - Task SessionContext.CreateMessageConsumer(IDestination destination, string selector, bool noLocal) + public Task SendAsync(IDestination destination, IMessage message, CancellationToken cancellationToken) { - return _context.CreateMessageConsumer(destination, selector, noLocal); + return _context.SendAsync(destination, message, cancellationToken); } - public IBytesMessage CreateBytesMessage() + public IBytesMessage CreateBytesMessage(byte[] content) { - return _context.CreateBytesMessage(); + return _context.CreateBytesMessage(content); } - Task SessionContext.DeleteTopic(string topicName) + public Task DeleteTopic(string topicName) { return _context.DeleteTopic(topicName); } - Task SessionContext.DeleteQueue(string queueName) + public Task DeleteQueue(string queueName) { return _context.DeleteQueue(queueName); } @@ -69,5 +69,15 @@ public IDestination GetTemporaryDestination(string name) { return _context.GetTemporaryDestination(name); } + + public ITextMessage CreateTextMessage(string content) + { + return _context.CreateTextMessage(content); + } + + public IMessage CreateMessage() + { + return _context.CreateMessage(); + } } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqConsumeTopology.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqConsumeTopology.cs index 9d0bd4e281f..dcec22743bc 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqConsumeTopology.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqConsumeTopology.cs @@ -58,16 +58,14 @@ public void Apply(IReceiveEndpointBrokerTopologyBuilder builder) public void Bind(string topicName, Action? configure = null) { - if (string.IsNullOrEmpty(_publishTopology.VirtualTopicPrefix) || topicName.StartsWith(_publishTopology.VirtualTopicPrefix)) - { - var specification = new ConsumerConsumeTopologySpecification(topicName, ConsumerEndpointQueueNameFormatter); + IActiveMqTopicBindingConfigurator specification = + string.IsNullOrEmpty(_publishTopology.VirtualTopicPrefix) || topicName.StartsWith(_publishTopology.VirtualTopicPrefix) + ? new ConsumerConsumeTopologySpecification(topicName, ConsumerEndpointQueueNameFormatter) + : new ConsumerConsumeTopicTopologySpecification(topicName, ConsumerEndpointQueueNameFormatter); - configure?.Invoke(specification); + configure?.Invoke(specification); - _specifications.Add(specification); - } - else - _specifications.Add(new InvalidActiveMqConsumeTopologySpecification("Bind", $"Only virtual topics can be bound: {topicName}")); + _specifications.Add((IActiveMqConsumeTopologySpecification)specification); } public override string CreateTemporaryQueueName(string tag) @@ -85,7 +83,7 @@ public override IEnumerable Validate() return base.Validate().Concat(_specifications.SelectMany(x => x.Validate())); } - protected override IMessageConsumeTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessageConsumeTopologyConfigurator CreateMessageTopology() { var messageTopology = new ActiveMqMessageConsumeTopology(_publishTopology.GetMessageTopology(), ConsumerEndpointQueueNameFormatter); diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqMessagePublishTopology.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqMessagePublishTopology.cs index 1db648fcaa6..5c96eea8d74 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqMessagePublishTopology.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqMessagePublishTopology.cs @@ -2,6 +2,7 @@ namespace MassTransit.ActiveMqTransport.Topology { using System; + using System.Diagnostics.CodeAnalysis; using Configuration; using MassTransit.Topology; @@ -38,7 +39,7 @@ bool IActiveMqTopicConfigurator.AutoDelete set => _topic.AutoDelete = value; } - public override bool TryGetPublishAddress(Uri baseAddress, out Uri? publishAddress) + public override bool TryGetPublishAddress(Uri baseAddress, [NotNullWhen(true)] out Uri? publishAddress) { publishAddress = _topic.GetEndpointAddress(baseAddress); return true; diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqPublishTopology.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqPublishTopology.cs index 9a2bc0e8c4a..c506a29956e 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqPublishTopology.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/ActiveMqPublishTopology.cs @@ -52,7 +52,7 @@ IActiveMqMessagePublishTopologyConfigurator IActiveMqPublishTopologyConfigura return GetMessageTopology() as IActiveMqMessagePublishTopologyConfigurator; } - protected override IMessagePublishTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessagePublishTopologyConfigurator CreateMessageTopology() { var messageTopology = new ActiveMqMessagePublishTopology(this, _messageTopology.GetMessageTopology()); diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/BrokerTopologyBuilder.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/BrokerTopologyBuilder.cs index a7950bd22d3..e123740ae1c 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/BrokerTopologyBuilder.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/BrokerTopologyBuilder.cs @@ -48,7 +48,7 @@ public ConsumerHandle BindConsumer(TopicHandle topic, QueueHandle queue, string var exchangeEntity = Topics.Get(topic); - var queueEntity = Queues.Get(queue); + var queueEntity = queue != null ? Queues.Get(queue) : null; var binding = new ConsumerEntity(id, exchangeEntity, queueEntity, selector); diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/Entities/ConsumerEntity.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/Entities/ConsumerEntity.cs index 50a4331113a..dcb2cc84db4 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/Entities/ConsumerEntity.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/Entities/ConsumerEntity.cs @@ -23,7 +23,7 @@ public ConsumerEntity(long id, TopicEntity topic, QueueEntity queue, string sele public static IEqualityComparer EntityComparer { get; } = new ConsumerEntityEqualityComparer(); public Topic Source => _topic.Topic; - public Queue Destination => _queue.Queue; + public Queue Destination => _queue?.Queue; public string Selector { get; } public long Id { get; } @@ -35,7 +35,7 @@ public override string ToString() new[] { $"source: {Source.EntityName}", - $"destination: {Destination.EntityName}", + $"destination: {Destination?.EntityName}", string.IsNullOrWhiteSpace(Selector) ? "" : $"selector: {Selector}" }.Where(x => !string.IsNullOrWhiteSpace(x))); } @@ -57,7 +57,9 @@ public bool Equals(ConsumerEntity x, ConsumerEntity y) if (x.GetType() != y.GetType()) return false; - return x._topic.Equals(y._topic) && x._queue.Equals(y._queue) && string.Equals(x.Selector, y.Selector); + return x._topic.Equals(y._topic) + && ((x._queue != null && x._queue.Equals(y._queue)) || (x._queue == null && y._queue == null)) + && string.Equals(x.Selector, y.Selector); } public int GetHashCode(ConsumerEntity obj) @@ -65,7 +67,8 @@ public int GetHashCode(ConsumerEntity obj) unchecked { var hashCode = obj._topic.GetHashCode(); - hashCode = (hashCode * 397) ^ obj._queue.GetHashCode(); + if (obj._queue != null) + hashCode = (hashCode * 397) ^ obj._queue.GetHashCode(); if (obj.Selector != null) hashCode = (hashCode * 397) ^ obj.Selector.GetHashCode(); @@ -91,12 +94,16 @@ public bool Equals(ConsumerEntity x, ConsumerEntity y) if (x.GetType() != y.GetType()) return false; - return string.Equals(x._queue.EntityName, y._queue.EntityName); + return x._queue == null && y._queue == null + ? string.Equals(x._topic.EntityName, y._topic.EntityName) + : string.Equals(x._queue?.EntityName, y._queue?.EntityName); } public int GetHashCode(ConsumerEntity obj) { - return obj._queue.EntityName.GetHashCode(); + return obj._queue == null + ? obj._topic.EntityName.GetHashCode() + : obj._queue.EntityName.GetHashCode(); } } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/Entities/QueueEntity.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/Entities/QueueEntity.cs index c03e49532a3..f1bb15d8eb3 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/Entities/QueueEntity.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/Topology/Entities/QueueEntity.cs @@ -8,19 +8,17 @@ public class QueueEntity : Queue, QueueHandle { - public QueueEntity(long id, string name, bool durable, bool autoDelete, bool lazy = false) + public QueueEntity(long id, string name, bool durable, bool autoDelete) { Id = id; EntityName = name; AutoDelete = autoDelete; Durable = durable; - Lazy = lazy; } public static IEqualityComparer NameComparer { get; } = new NameEqualityComparer(); public static IEqualityComparer QueueComparer { get; } = new QueueEntityEqualityComparer(); - public bool Lazy { get; } public string EntityName { get; } public bool AutoDelete { get; } diff --git a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/TransportActiveMqSendContext.cs b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/TransportActiveMqSendContext.cs index b2cd567b7d0..a31406d3828 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/TransportActiveMqSendContext.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/ActiveMqTransport/TransportActiveMqSendContext.cs @@ -25,9 +25,9 @@ public override void ReadPropertiesFrom(IReadOnlyDictionary prop { base.ReadPropertiesFrom(properties); - Priority = ReadEnum(properties, PropertyNames.Priority); - GroupId = ReadString(properties, PropertyNames.GroupId); - GroupSequence = ReadInt(properties, PropertyNames.GroupSequence); + Priority = ReadEnum(properties, ActiveMqTransportPropertyNames.Priority); + GroupId = ReadString(properties, ActiveMqTransportPropertyNames.GroupId); + GroupSequence = ReadInt(properties, ActiveMqTransportPropertyNames.GroupSequence); } public override void WritePropertiesTo(IDictionary properties) @@ -35,19 +35,11 @@ public override void WritePropertiesTo(IDictionary properties) base.WritePropertiesTo(properties); if (Priority != null) - properties[PropertyNames.Priority] = Priority.ToString(); + properties[ActiveMqTransportPropertyNames.Priority] = Priority.ToString(); if (GroupId != null) - properties[PropertyNames.GroupId] = GroupId; + properties[ActiveMqTransportPropertyNames.GroupId] = GroupId; if (GroupSequence != null) - properties[PropertyNames.GroupSequence] = GroupSequence.Value; - } - - - static class PropertyNames - { - public const string Priority = "AMQ-Priority"; - public const string GroupId = "AMQ-GroupId"; - public const string GroupSequence = "AMQ-GroupSequence"; + properties[ActiveMqTransportPropertyNames.GroupSequence] = GroupSequence.Value; } } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/Configuration/ActiveMqHostConfigurationExtensions.cs b/src/Transports/MassTransit.ActiveMqTransport/Configuration/ActiveMqHostConfigurationExtensions.cs index 713a3b91e82..3e044b6569f 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/Configuration/ActiveMqHostConfigurationExtensions.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/Configuration/ActiveMqHostConfigurationExtensions.cs @@ -65,9 +65,7 @@ public static void ReceiveEndpoint(this IActiveMqBusFactoryConfigurator configur } /// - /// Declare a ReceiveEndpoint using a unique generated queue name. This queue defaults to auto-delete - /// and non-durable. By default all services bus instances include a default receiveEndpoint that is - /// of this type (created automatically upon the first receiver binding). + /// Declare a receive endpoint using the endpoint . /// /// /// diff --git a/src/Transports/MassTransit.ActiveMqTransport/Configuration/IActiveMqHostConfigurator.cs b/src/Transports/MassTransit.ActiveMqTransport/Configuration/IActiveMqHostConfigurator.cs index 715cceb1e7d..a1184d89cb3 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/Configuration/IActiveMqHostConfigurator.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/Configuration/IActiveMqHostConfigurator.cs @@ -17,7 +17,14 @@ public interface IActiveMqHostConfigurator /// void Password(string password); - void UseSsl(); + void UseSsl(bool enabled = true); + + /// + /// Specify if SSL should be used, and if the port should be updated automatically to the default SSL port. + /// + /// + /// + void UseSsl(bool enabled, bool updatePort); /// /// Sets a list of hosts to enable the failover transport @@ -38,5 +45,11 @@ public interface IActiveMqHostConfigurator void SetPrefetchPolicy(int limit); void SetQueuePrefetchPolicy(int limit); + + /// + /// Previous versions has nms.AsyncSend enabled by default. This can result in message loss, + /// so now it's disabled by default. It can be enabled using this method, or by adding . + /// + void EnableAsyncSend(); } } diff --git a/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqConnectException.cs b/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqConnectException.cs index d476309b910..13b9f9f717b 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqConnectException.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqConnectException.cs @@ -23,6 +23,9 @@ public ActiveMqConnectException(string message, Exception innerException) { } + #if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] + #endif protected ActiveMqConnectException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqConnectionException.cs b/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqConnectionException.cs index ded881516f2..17467cb867e 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqConnectionException.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqConnectionException.cs @@ -22,6 +22,9 @@ public ActiveMqConnectionException(string message, Exception innerException) { } + #if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] + #endif protected ActiveMqConnectionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqTransportConfigurationException.cs b/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqTransportConfigurationException.cs index 2531ac389d8..5c4e499306a 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqTransportConfigurationException.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqTransportConfigurationException.cs @@ -22,6 +22,9 @@ public ActiveMqTransportConfigurationException(string message, Exception innerEx { } + #if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] + #endif protected ActiveMqTransportConfigurationException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqTransportException.cs b/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqTransportException.cs index 4b6f2e087a9..0e0adb8c65b 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqTransportException.cs +++ b/src/Transports/MassTransit.ActiveMqTransport/Exceptions/ActiveMqTransportException.cs @@ -22,6 +22,9 @@ public ActiveMqTransportException(string message, Exception innerException) { } + #if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] + #endif protected ActiveMqTransportException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.ActiveMqTransport/MassTransit.ActiveMqTransport.csproj b/src/Transports/MassTransit.ActiveMqTransport/MassTransit.ActiveMqTransport.csproj index c1a953047be..482c744234c 100644 --- a/src/Transports/MassTransit.ActiveMqTransport/MassTransit.ActiveMqTransport.csproj +++ b/src/Transports/MassTransit.ActiveMqTransport/MassTransit.ActiveMqTransport.csproj @@ -1,11 +1,12 @@  + - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -20,8 +21,8 @@ - - + + diff --git a/src/Transports/MassTransit.ActiveMqTransport/NullableAttributes.cs b/src/Transports/MassTransit.ActiveMqTransport/NullableAttributes.cs new file mode 100644 index 00000000000..3f38561b675 --- /dev/null +++ b/src/Transports/MassTransit.ActiveMqTransport/NullableAttributes.cs @@ -0,0 +1,24 @@ +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using System; + + + [AttributeUsage(AttributeTargets.Parameter)] + sealed class NotNullWhenAttribute : + Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +#endif diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsBusFactory.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsBusFactory.cs index c0a8636ef18..2881e695c01 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsBusFactory.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsBusFactory.cs @@ -1,7 +1,6 @@ namespace MassTransit { using System; - using System.Threading; using AmazonSqsTransport; using AmazonSqsTransport.Configuration; using Configuration; @@ -10,8 +9,6 @@ public static class AmazonSqsBusFactory { - public static IMessageTopologyConfigurator MessageTopology => Cached.MessageTopologyValue.Value; - /// /// Configure and create a bus for AmazonSQS /// @@ -19,7 +16,7 @@ public static class AmazonSqsBusFactory /// public static IBusControl Create(Action configure) { - var topologyConfiguration = new AmazonSqsTopologyConfiguration(MessageTopology); + var topologyConfiguration = new AmazonSqsTopologyConfiguration(CreateMessageTopology()); var busConfiguration = new AmazonSqsBusConfiguration(topologyConfiguration); var configurator = new AmazonSqsBusFactoryConfigurator(busConfiguration); @@ -29,18 +26,19 @@ public static IBusControl Create(Action config return configurator.Build(busConfiguration); } + public static IMessageTopologyConfigurator CreateMessageTopology() + { + return new MessageTopology(Cached.EntityNameFormatter); + } + static class Cached { - internal static readonly Lazy MessageTopologyValue = - new Lazy(() => new MessageTopology(_entityNameFormatter), - LazyThreadSafetyMode.PublicationOnly); - - static readonly IEntityNameFormatter _entityNameFormatter; + internal static readonly IEntityNameFormatter EntityNameFormatter; static Cached() { - _entityNameFormatter = new MessageNameFormatterEntityNameFormatter(new AmazonSqsMessageNameFormatter()); + EntityNameFormatter = new MessageNameFormatterEntityNameFormatter(new AmazonSqsMessageNameFormatter()); } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsEndpointAddress.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsEndpointAddress.cs index 179f4ce7f97..6301684b755 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsEndpointAddress.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsEndpointAddress.cs @@ -74,8 +74,6 @@ public AmazonSqsEndpointAddress(Uri hostAddress, Uri address, AddressType type = throw new ArgumentException($"The address scheme is not supported: {address.Scheme}", nameof(address)); } - AmazonSqsEntityNameValidator.Validator.ThrowIfInvalidEntityName(Name); - foreach (var (key, value) in address.SplitQueryString()) { switch (key) @@ -98,6 +96,11 @@ public AmazonSqsEndpointAddress(Uri hostAddress, Uri address, AddressType type = break; } } + + if (Type == AddressType.Queue) + AmazonSqsEntityNameValidator.Validator.ThrowIfInvalidEntityName(Name); + else + AmazonSnsTopicNameValidator.Validator.ThrowIfInvalidEntityName(Name); } public AmazonSqsEndpointAddress(Uri hostAddress, string name, bool durable = true, bool autoDelete = false, AddressType type = AddressType.Queue) diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsClientContext.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsClientContext.cs index 9c744e37feb..4fbe8bd6e3a 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsClientContext.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsClientContext.cs @@ -1,12 +1,9 @@ namespace MassTransit.AmazonSqsTransport { - using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; - using Amazon.Auth.AccessControlPolicy; - using Amazon.Auth.AccessControlPolicy.ActionIdentifiers; using Amazon.SimpleNotificationService; using Amazon.SimpleNotificationService.Model; using Amazon.SQS; @@ -23,7 +20,9 @@ public class AmazonSqsClientContext : readonly IAmazonSimpleNotificationService _snsClient; readonly IAmazonSQS _sqsClient; - public AmazonSqsClientContext(ConnectionContext connectionContext, IAmazonSQS sqsClient, IAmazonSimpleNotificationService snsClient, + public AmazonSqsClientContext(ConnectionContext connectionContext, + IAmazonSQS sqsClient, + IAmazonSimpleNotificationService snsClient, CancellationToken cancellationToken) : base(connectionContext) { @@ -48,7 +47,7 @@ public Task CreateQueue(Queue queue) return ConnectionContext.GetQueue(queue); } - public async Task CreateQueueSubscription(Topology.Topic topic, Queue queue) + public async Task CreateQueueSubscription(Topology.Topic topic, Queue queue) { var topicInfo = await ConnectionContext.GetTopic(topic).ConfigureAwait(false); var queueInfo = await ConnectionContext.GetQueue(queue).ConfigureAwait(false); @@ -65,40 +64,23 @@ public async Task CreateQueueSubscription(Topology.Topic topic, Queue queue) Attributes = subscriptionAttributes }; - var response = await _snsClient.SubscribeAsync(subscribeRequest, CancellationToken).ConfigureAwait(false); + SubscribeResponse response; + try + { + response = await _snsClient.SubscribeAsync(subscribeRequest, CancellationToken).ConfigureAwait(false); - response.EnsureSuccessfulResponse(); + response.EnsureSuccessfulResponse(); + } + catch (InvalidParameterException exception) when (exception.Message.Contains("exists")) + { + return false; + } queueInfo.SubscriptionArns.Add(response.SubscriptionArn); var sqsQueueArn = queueInfo.Arn; - var topicArnPattern = topicInfo.Arn.Substring(0, topicInfo.Arn.LastIndexOf(':') + 1) + "*"; - - queueInfo.Attributes.TryGetValue(QueueAttributeName.Policy, out var policyValue); - var policy = string.IsNullOrEmpty(policyValue) - ? new Policy() - : Policy.FromJson(policyValue); - - if (!QueueHasTopicPermission(policy, topicArnPattern, sqsQueueArn)) - { - var statement = new Statement(Statement.StatementEffect.Allow); - #pragma warning disable 618 - statement.Actions.Add(SQSActionIdentifiers.SendMessage); - #pragma warning restore 618 - statement.Resources.Add(new Resource(sqsQueueArn)); - statement.Conditions.Add(ConditionFactory.NewSourceArnCondition(topicArnPattern)); - statement.Principals.Add(new Principal("*")); - policy.Statements.Add(statement); - var jsonPolicy = policy.ToJson(); - - var setAttributes = new Dictionary { { QueueAttributeName.Policy, jsonPolicy } }; - var setAttributesResponse = await _sqsClient.SetQueueAttributesAsync(queueInfo.Url, setAttributes, CancellationToken).ConfigureAwait(false); - - setAttributesResponse.EnsureSuccessfulResponse(); - - queueInfo.Attributes[QueueAttributeName.Policy] = jsonPolicy; - } + return await queueInfo.UpdatePolicy(sqsQueueArn, topicInfo.Arn, CancellationToken).ConfigureAwait(false); } public async Task DeleteTopic(Topology.Topic topic) @@ -134,18 +116,11 @@ public async Task DeleteQueue(Queue queue) await ConnectionContext.RemoveQueueByName(queue.EntityName).ConfigureAwait(false); } - public async Task CreatePublishRequest(string topicName, string body) + public async Task Publish(string topicName, PublishBatchRequestEntry request, CancellationToken cancellationToken) { var topicInfo = await ConnectionContext.GetTopicByName(topicName).ConfigureAwait(false); - return new PublishRequest(topicInfo.Arn, body); - } - - public async Task Publish(PublishRequest request, CancellationToken cancellationToken) - { - var response = await _snsClient.PublishAsync(request, cancellationToken).ConfigureAwait(false); - - response.EnsureSuccessfulResponse(); + await topicInfo.Publish(request, cancellationToken).ConfigureAwait(false); } public async Task SendMessage(string queueName, SendMessageBatchRequestEntry request, CancellationToken cancellationToken) @@ -179,8 +154,10 @@ public async Task> ReceiveMessages(string queueName, int messageL { MaxNumberOfMessages = messageLimit, WaitTimeSeconds = waitTime, - AttributeNames = new List { "All" }, - MessageAttributeNames = new List { "All" } + #pragma warning disable CS0618 // Type or member is obsolete + AttributeNames = ["All"], + #pragma warning restore CS0618 // Type or member is obsolete + MessageAttributeNames = ["All"] }; var response = await _sqsClient.ReceiveMessageAsync(request, cancellationToken).ConfigureAwait(false); @@ -207,6 +184,13 @@ public async Task ChangeMessageVisibility(string queueUrl, string receiptHandle, response.EnsureSuccessfulResponse(); } + public async Task CreatePublishRequest(string topicName, string body) + { + var topicInfo = await ConnectionContext.GetTopicByName(topicName).ConfigureAwait(false); + + return new PublishRequest(topicInfo.Arn, body); + } + async Task DeleteQueueSubscription(string subscriptionArn) { var unsubscribeRequest = new UnsubscribeRequest { SubscriptionArn = subscriptionArn }; @@ -215,17 +199,5 @@ async Task DeleteQueueSubscription(string subscriptionArn) response.EnsureSuccessfulResponse(); } - - static bool QueueHasTopicPermission(Policy policy, string topicArnPattern, string sqsQueueArn) - { - IEnumerable conditions = policy.Statements - .Where(s => s.Resources.Any(r => r.Id.Equals(sqsQueueArn))) - .SelectMany(s => s.Conditions); - - return conditions.Any(c => - string.Equals(c.Type, ConditionFactory.ArnComparisonType.ArnLike.ToString(), StringComparison.OrdinalIgnoreCase) && - string.Equals(c.ConditionKey, ConditionFactory.SOURCE_ARN_CONDITION_KEY, StringComparison.OrdinalIgnoreCase) && - c.Values.Contains(topicArnPattern)); - } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsHeaderProvider.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsHeaderProvider.cs index 3a922a34181..bce17beef35 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsHeaderProvider.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsHeaderProvider.cs @@ -3,18 +3,22 @@ using System; using System.Collections.Generic; using System.Linq; + using Amazon.SQS; using Amazon.SQS.Model; + using Internals; using Transports; public class AmazonSqsHeaderProvider : IHeaderProvider { + readonly SqsMessageBody _body; readonly Message _message; - public AmazonSqsHeaderProvider(Message message) + public AmazonSqsHeaderProvider(Message message, SqsMessageBody body) { _message = message; + _body = body; } public bool TryGetHeader(string key, out object value) @@ -31,6 +35,24 @@ public bool TryGetHeader(string key, out object value) return true; } + if ("TopicArn".Equals(key, StringComparison.OrdinalIgnoreCase)) + { + value = _body.TopicArn; + return value != null; + } + + if (MessageHeaders.TransportSentTime.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + if (_message.Attributes.TryGetValue(MessageSystemAttributeName.SentTimestamp, out var sentTimestamp)) + { + if (long.TryParse(sentTimestamp, out var milliseconds)) + { + value = DateTimeConstants.Epoch + TimeSpan.FromMilliseconds(milliseconds); + return true; + } + } + } + value = null; return false; } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsMessageNameFormatter.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsMessageNameFormatter.cs index c98d01eb201..dcc9b9d7465 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsMessageNameFormatter.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsMessageNameFormatter.cs @@ -2,7 +2,6 @@ namespace MassTransit.AmazonSqsTransport { using System; using System.Collections.Concurrent; - using System.Reflection; using System.Text; using Transports; @@ -27,14 +26,14 @@ public AmazonSqsMessageNameFormatter(string genericArgumentSeparator = null, str _cache = new ConcurrentDictionary(); } - public MessageName GetMessageName(Type type) + public string GetMessageName(Type type) { - return new MessageName(_cache.GetOrAdd(type, CreateMessageName)); + return _cache.GetOrAdd(type, CreateMessageName); } string CreateMessageName(Type type) { - if (type.GetTypeInfo().IsGenericTypeDefinition) + if (type.IsGenericTypeDefinition) throw new ArgumentException("An open generic type cannot be used as a message name"); var sb = new StringBuilder(""); @@ -44,26 +43,25 @@ string CreateMessageName(Type type) string GetMessageName(StringBuilder sb, Type type, string scope) { - var typeInfo = type.GetTypeInfo(); - if (typeInfo.IsGenericParameter) + if (type.IsGenericParameter) return ""; - var ns = typeInfo.Namespace?.Replace(".", _nestedTypeSeparator); + var ns = type.Namespace?.Replace(".", _nestedTypeSeparator); if (ns != null && !ns.Equals(scope)) { sb.Append(ns); sb.Append(_namespaceSeparator); } - if (typeInfo.IsNested) + if (type.IsNested) { - GetMessageName(sb, typeInfo.DeclaringType, ns); + GetMessageName(sb, type.DeclaringType, ns); sb.Append(_nestedTypeSeparator); } - if (typeInfo.IsGenericType) + if (type.IsGenericType) { - var name = typeInfo.GetGenericTypeDefinition().Name; + var name = type.GetGenericTypeDefinition().Name; //remove `1 var index = name.IndexOf('`'); @@ -73,7 +71,7 @@ string GetMessageName(StringBuilder sb, Type type, string scope) sb.Append(name); sb.Append(_genericTypeSeparator); - Type[] arguments = typeInfo.GetGenericArguments(); + Type[] arguments = type.GetGenericArguments(); for (var i = 0; i < arguments.Length; i++) { if (i > 0) @@ -85,7 +83,7 @@ string GetMessageName(StringBuilder sb, Type type, string scope) sb.Append(_genericTypeSeparator); } else - sb.Append(typeInfo.Name); + sb.Append(type.Name); return sb.ToString(); } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsMessageSendContext.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsMessageSendContext.cs index 05ed0751f97..27e080a828c 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsMessageSendContext.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsMessageSendContext.cs @@ -28,8 +28,8 @@ public override void ReadPropertiesFrom(IReadOnlyDictionary prop { base.ReadPropertiesFrom(properties); - GroupId = ReadString(properties, PropertyNames.GroupId); - DeduplicationId = ReadString(properties, PropertyNames.DeduplicationId); + GroupId = ReadString(properties, AmazonSqsTransportPropertyNames.GroupId); + DeduplicationId = ReadString(properties, AmazonSqsTransportPropertyNames.DeduplicationId); } public override void WritePropertiesTo(IDictionary properties) @@ -37,16 +37,9 @@ public override void WritePropertiesTo(IDictionary properties) base.WritePropertiesTo(properties); if (!string.IsNullOrWhiteSpace(GroupId)) - properties[PropertyNames.GroupId] = GroupId; + properties[AmazonSqsTransportPropertyNames.GroupId] = GroupId; if (!string.IsNullOrWhiteSpace(DeduplicationId)) - properties[PropertyNames.DeduplicationId] = DeduplicationId; - } - - - static class PropertyNames - { - public const string GroupId = "SQS-GroupId"; - public const string DeduplicationId = "SQS-DeduplicationId"; + properties[AmazonSqsTransportPropertyNames.DeduplicationId] = DeduplicationId; } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsReceiveContext.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsReceiveContext.cs index 9b69124386c..bb2553766d7 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsReceiveContext.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsReceiveContext.cs @@ -2,169 +2,53 @@ { using System; using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; using Amazon.SQS; using Amazon.SQS.Model; - using Internals; + using Context; using Transports; public sealed class AmazonSqsReceiveContext : BaseReceiveContext, AmazonSqsMessageContext, - ReceiveLockContext + TransportReceiveContext { - static readonly TimeSpan MaxVisibilityTimeout = TimeSpan.FromHours(12); - - readonly CancellationTokenSource _activeTokenSource; - readonly ClientContext _clientContext; - readonly SqsReceiveEndpointContext _context; - readonly Message _message; - readonly ReceiveSettings _settings; - bool _locked; + readonly AmazonSqsHeaderProvider _headerProvider; public AmazonSqsReceiveContext(Message message, bool redelivered, SqsReceiveEndpointContext context, ClientContext clientContext, ReceiveSettings settings, ConnectionContext connectionContext) : base(redelivered, context, settings, clientContext, connectionContext) { - _context = context; - _clientContext = clientContext; - _message = message; - _settings = settings; + TransportMessage = message; - Body = new StringMessageBody(message?.Body); + var messageBody = new SqsMessageBody(message); - _activeTokenSource = new CancellationTokenSource(); - _locked = true; + Body = messageBody; - Task.Factory.StartNew(RenewMessageVisibility, _activeTokenSource.Token, TaskCreationOptions.None, TaskScheduler.Default); + _headerProvider = new AmazonSqsHeaderProvider(TransportMessage, messageBody); } - protected override IHeaderProvider HeaderProvider => new AmazonSqsHeaderProvider(TransportMessage); + protected override IHeaderProvider HeaderProvider => _headerProvider; public override MessageBody Body { get; } - public Message TransportMessage => _message; - - public Dictionary Attributes => _message.MessageAttributes; - - public Task Complete() - { - _activeTokenSource.Cancel(); - - return _clientContext.DeleteMessage(_settings.EntityName, _message.ReceiptHandle); - } - - public async Task Faulted(Exception exception) - { - _activeTokenSource.Cancel(); - - try - { - if (!_clientContext.CancellationToken.IsCancellationRequested) - { - await _clientContext.ChangeMessageVisibility(_settings.QueueUrl, _message.ReceiptHandle, _settings.RedeliverVisibilityTimeout) - .ConfigureAwait(false); - } - - _locked = false; - } - catch (OperationCanceledException) - { - } - catch (Exception ex) - { - LogContext.Error?.Log(ex, "ChangeMessageVisibility failed: {ReceiptHandle}, Original Exception: {Exception}", TransportMessage.ReceiptHandle, - exception); - } - } - - public Task ValidateLockStatus() - { - if (_locked) - return Task.CompletedTask; - - throw new TransportException(_context.InputAddress, $"Message Lock Lost: {TransportMessage.ReceiptHandle}"); - } - - public override void Dispose() - { - _activeTokenSource.Dispose(); + public Message TransportMessage { get; } - base.Dispose(); - } + public Dictionary Attributes => TransportMessage.MessageAttributes; - async Task RenewMessageVisibility() + public IDictionary GetTransportProperties() { - TimeSpan CalculateDelay(int timeout) - { - return TimeSpan.FromSeconds(timeout * 0.7); - } - - var visibilityTimeout = _settings.VisibilityTimeout; - - var delay = CalculateDelay(visibilityTimeout); - - visibilityTimeout = Math.Min(60, visibilityTimeout); - - while (_activeTokenSource.Token.IsCancellationRequested == false) - { - try - { - if (delay > TimeSpan.Zero) - await Task.Delay(delay, _activeTokenSource.Token).ConfigureAwait(false); - - if (_activeTokenSource.Token.IsCancellationRequested) - break; - - await _clientContext.ChangeMessageVisibility(_settings.QueueUrl, TransportMessage.ReceiptHandle, visibilityTimeout) - .ConfigureAwait(false); - - // Max 12 hours, https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html - if (ElapsedTime + TimeSpan.FromSeconds(visibilityTimeout) >= MaxVisibilityTimeout) - break; - - delay = CalculateDelay(visibilityTimeout); - } - catch (MessageNotInflightException exception) - { - LogContext.Warning?.Log(exception, "Message no longer in flight: {ReceiptHandle}", TransportMessage.ReceiptHandle); - - _locked = false; - - Cancel(); - break; - } - catch (ReceiptHandleIsInvalidException exception) - { - LogContext.Warning?.Log(exception, "Message receipt handle is invalid: {ReceiptHandle}", TransportMessage.ReceiptHandle); + var properties = new Lazy>(() => new Dictionary()); - _locked = false; + if (TransportMessage.Attributes.TryGetValue(MessageSystemAttributeName.MessageGroupId, out var messageGroupId) + && !string.IsNullOrWhiteSpace(messageGroupId)) + properties.Value[AmazonSqsTransportPropertyNames.GroupId] = messageGroupId; - Cancel(); - break; - } - catch (AmazonSQSException exception) - { - LogContext.Error?.Log(exception, "Failed to extend message {ReceiptHandle} visibility to {VisibilityTimeout} ({ElapsedTime})", - TransportMessage.ReceiptHandle, TimeSpan.FromSeconds(visibilityTimeout).ToFriendlyString(), ElapsedTime); + if (TransportMessage.Attributes.TryGetValue(MessageSystemAttributeName.MessageDeduplicationId, out var messageDeduplicationId) + && !string.IsNullOrWhiteSpace(messageDeduplicationId)) + properties.Value[AmazonSqsTransportPropertyNames.DeduplicationId] = messageDeduplicationId; - break; - } - catch (TimeoutException) - { - delay = TimeSpan.Zero; - } - catch (OperationCanceledException) - { - _activeTokenSource.Cancel(); - } - catch (Exception) - { - _activeTokenSource.Cancel(); - } - } + return properties.IsValueCreated ? properties.Value : null; } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsReceiveLockContext.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsReceiveLockContext.cs new file mode 100644 index 00000000000..7db5a7ac62a --- /dev/null +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsReceiveLockContext.cs @@ -0,0 +1,186 @@ +namespace MassTransit.AmazonSqsTransport +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Amazon.SQS; + using Amazon.SQS.Model; + using Internals; + using Transports; + + + public class AmazonSqsReceiveLockContext : + ReceiveLockContext + { + static readonly TimeSpan MaxVisibilityTimeout = TimeSpan.FromHours(12); + readonly CancellationTokenSource _activeTokenSource; + readonly ClientContext _clientContext; + readonly Uri _inputAddress; + readonly Message _message; + + readonly ReceiveSettings _settings; + readonly DateTime _startedAt; + readonly Task _visibilityTask; + bool _locked; + + public AmazonSqsReceiveLockContext(Uri inputAddress, Message message, ReceiveSettings settings, ClientContext clientContext) + { + _startedAt = DateTime.UtcNow; + _inputAddress = inputAddress; + _message = message; + _settings = settings; + _clientContext = clientContext; + _activeTokenSource = new CancellationTokenSource(); + _locked = true; + + _visibilityTask = Task.Run(() => RenewMessageVisibility()); + } + + public async Task Complete() + { + _activeTokenSource.Cancel(); + + try + { + await _clientContext.DeleteMessage(_settings.EntityName, _message.ReceiptHandle).ConfigureAwait(false); + + await _visibilityTask.ConfigureAwait(false); + } + catch (MessageNotInflightException) + { + _locked = false; + throw; + } + catch (ReceiptHandleIsInvalidException) + { + _locked = false; + throw; + } + finally + { + _activeTokenSource.Dispose(); + } + } + + public async Task Faulted(Exception exception) + { + _activeTokenSource.Cancel(); + + try + { + await _visibilityTask.ConfigureAwait(false); + + if (!_clientContext.CancellationToken.IsCancellationRequested) + { + await _clientContext.ChangeMessageVisibility(_settings.QueueUrl, _message.ReceiptHandle, _settings.RedeliverVisibilityTimeout) + .ConfigureAwait(false); + } + + _locked = false; + } + catch (OperationCanceledException) + { + } + catch (MessageNotInflightException) + { + _locked = false; + throw; + } + catch (ReceiptHandleIsInvalidException) + { + _locked = false; + throw; + } + catch (Exception ex) + { + LogContext.Error?.Log(ex, "ChangeMessageVisibility failed: {ReceiptHandle}, Original Exception: {Exception}", + _message.ReceiptHandle, exception); + } + finally + { + _activeTokenSource.Dispose(); + } + } + + public Task ValidateLockStatus() + { + if (_locked) + return Task.CompletedTask; + + throw new TransportException(_inputAddress, $"Message Lock Lost: {_message.ReceiptHandle}"); + } + + async Task RenewMessageVisibility() + { + TimeSpan CalculateDelay(int timeout) + { + return TimeSpan.FromSeconds(timeout * 0.7); + } + + var visibilityTimeout = _settings.VisibilityTimeout; + + var delay = CalculateDelay(visibilityTimeout); + + visibilityTimeout = Math.Min(60, visibilityTimeout); + + while (_locked && !_activeTokenSource.IsCancellationRequested) + { + try + { + if (delay > TimeSpan.Zero) + { + await Task.Delay(delay, _activeTokenSource.Token) + .ContinueWith(t => t, CancellationToken.None, TaskContinuationOptions.ExecuteSynchronously, TaskScheduler.Default) + .ConfigureAwait(false); + } + + if (_activeTokenSource.IsCancellationRequested) + break; + + await _clientContext.ChangeMessageVisibility(_settings.QueueUrl, _message.ReceiptHandle, visibilityTimeout).ConfigureAwait(false); + + // Max 12 hours, https://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/sqs-visibility-timeout.html + if (DateTime.UtcNow - _startedAt.AddSeconds(visibilityTimeout) >= MaxVisibilityTimeout) + break; + + delay = CalculateDelay(visibilityTimeout); + } + catch (MessageNotInflightException exception) + { + LogContext.Warning?.Log(exception, "Message no longer in flight: {ReceiptHandle}", _message.ReceiptHandle); + + _locked = false; + + break; + } + catch (ReceiptHandleIsInvalidException exception) + { + LogContext.Warning?.Log(exception, "Message receipt handle is invalid: {ReceiptHandle}", _message.ReceiptHandle); + + _locked = false; + + break; + } + catch (AmazonSQSException exception) + { + LogContext.Error?.Log(exception, "Failed to extend message {ReceiptHandle} visibility to {VisibilityTimeout} ({ElapsedTime})", + _message.ReceiptHandle, TimeSpan.FromSeconds(visibilityTimeout).ToFriendlyString(), DateTime.UtcNow - _startedAt); + + break; + } + catch (TimeoutException) + { + delay = TimeSpan.Zero; + } + catch (OperationCanceledException) + { + break; + } + catch (Exception) + { + break; + } + } + } + } +} diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsTransportPropertyNames.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsTransportPropertyNames.cs new file mode 100644 index 00000000000..435aa7f82f2 --- /dev/null +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonSqsTransportPropertyNames.cs @@ -0,0 +1,8 @@ +namespace MassTransit.AmazonSqsTransport +{ + static class AmazonSqsTransportPropertyNames + { + public const string GroupId = "SQS-GroupId"; + public const string DeduplicationId = "SQS-DeduplicationId"; + } +} diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonWebServiceResponseExtensions.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonWebServiceResponseExtensions.cs index 79ff38a3772..17573871c9e 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonWebServiceResponseExtensions.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/AmazonWebServiceResponseExtensions.cs @@ -11,11 +11,12 @@ public static void EnsureSuccessfulResponse(this AmazonWebServiceResponse respon const string documentationUri = "https://aws.amazon.com/blogs/developer/logging-with-the-aws-sdk-for-net/"; var statusCode = response.HttpStatusCode; - var requestId = response.ResponseMetadata.RequestId; if (statusCode >= HttpStatusCode.OK && statusCode < HttpStatusCode.MultipleChoices) return; + var requestId = response.ResponseMetadata?.RequestId ?? "[Missing RequestId]"; + throw new AmazonSqsTransportException( $"Received unsuccessful response ({statusCode}) from AWS endpoint. See AWS SDK logs ({requestId}) for more details: {documentationUri}"); } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Batcher.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Batcher.cs index c35a12c94a9..4e00e556447 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Batcher.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Batcher.cs @@ -13,12 +13,12 @@ public abstract class Batcher : { readonly Task _batchTask; readonly Channel> _channel; - readonly ChannelExecutor _executor; + readonly TaskExecutor _executor; readonly BatchSettings _settings; - protected Batcher() + protected Batcher(BatchSettings settings = null) { - _settings = ClientContextBatchSettings.GetBatchSettings(); + _settings = settings ?? ClientContextBatchSettings.GetBatchSettings(); var channelOptions = new BoundedChannelOptions(_settings.MessageLimit) { @@ -29,7 +29,7 @@ protected Batcher() }; _channel = Channel.CreateBounded>(channelOptions); - _executor = new ChannelExecutor(2, _settings.BatchLimit); + _executor = new TaskExecutor(2, _settings.BatchLimit); _batchTask = Task.Run(WaitForBatch); } @@ -44,7 +44,7 @@ public async Task Execute(TEntry entry, CancellationToken cancellationToken) public async ValueTask DisposeAsync() { - _channel.Writer.Complete(); + _channel.Writer.TryComplete(); await _batchTask.ConfigureAwait(false); @@ -70,23 +70,26 @@ async Task WaitForBatch() async Task ReadBatch() { var batchToken = new CancellationTokenSource(_settings.Timeout); - var batch = new List>(); + var batch = new List>(_settings.MessageLimit); try { try { - for (int entryId = 0, - batchLength = 0; - entryId < _settings.MessageLimit && batchLength < _settings.SizeLimit; - entryId++) - { - BatchEntry entry = await _channel.Reader.ReadAsync(batchToken.Token).ConfigureAwait(false); - - batchLength += AddingEntry(entry.Entry, entryId.ToString()); - batch.Add(entry); + var entryId = 0; + var batchLength = 0; - if (await _channel.Reader.WaitToReadAsync(batchToken.Token).ConfigureAwait(false) == false) + while (entryId < _settings.MessageLimit && batchLength < _settings.SizeLimit) + { + if (_channel.Reader.TryRead(out var entry)) + { + batchLength += AddingEntry(entry.Entry, entryId.ToString()); + batch.Add(entry); + entryId++; + } + else if (await _channel.Reader.WaitToReadAsync(batchToken.Token).ConfigureAwait(false) == false) + { break; + } } } catch (OperationCanceledException exception) when (exception.CancellationToken == batchToken.Token && batch.Count > 0) diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ClientContext.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ClientContext.cs index 5d50ce6fbf5..44487d5678a 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ClientContext.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ClientContext.cs @@ -17,15 +17,13 @@ public interface ClientContext : Task CreateQueue(Queue queue); - Task CreateQueueSubscription(Topology.Topic topic, Queue queue); + Task CreateQueueSubscription(Topology.Topic topic, Queue queue); Task DeleteTopic(Topology.Topic topic); Task DeleteQueue(Queue queue); - Task CreatePublishRequest(string topicName, string body); - - Task Publish(PublishRequest request, CancellationToken cancellationToken = default); + Task Publish(string topicName, PublishBatchRequestEntry request, CancellationToken cancellationToken = default); Task SendMessage(string queueName, SendMessageBatchRequestEntry request, CancellationToken cancellationToken); diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ClientContextCacheDefaults.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ClientContextCacheDefaults.cs index 77c91762d46..ce48ae661a1 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ClientContextCacheDefaults.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ClientContextCacheDefaults.cs @@ -1,7 +1,7 @@ namespace MassTransit.AmazonSqsTransport { using System; - using Caching; + using Internals.Caching; public static class ClientContextCacheDefaults @@ -17,9 +17,13 @@ static ClientContextCacheDefaults() public static TimeSpan MinAge { get; set; } public static TimeSpan MaxAge { get; set; } - public static CacheSettings GetCacheSettings() + public static ICache> CreateCache() + where TValue : class { - return new CacheSettings(Capacity, MinAge, MaxAge); + var options = new CacheOptions { Capacity = Capacity }; + var policy = new TimeToLiveCachePolicy(MaxAge); + + return new MassTransitCache>(policy, options); } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsReceiveEndpointBuilder.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsReceiveEndpointBuilder.cs index f4cd979eb17..a07f5022d10 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsReceiveEndpointBuilder.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsReceiveEndpointBuilder.cs @@ -66,10 +66,10 @@ BrokerTopology BuildTopology(ReceiveSettings settings) IErrorTransport CreateErrorTransport(TransportSetHeaderAdapter headerAdapter) { - var errorSettings = _configuration.Topology.Send.GetErrorSettings(_configuration.Settings); - var filter = new ConfigureAmazonSqsTopologyFilter(errorSettings, errorSettings.GetBrokerTopology()); + var settings = _configuration.Topology.Send.GetErrorSettings(_configuration.Settings); + var filter = new ConfigureAmazonSqsTopologyFilter(settings, settings.GetBrokerTopology()); - return new SqsErrorTransport(errorSettings.EntityName, headerAdapter, filter); + return new SqsErrorTransport(settings.EntityName, headerAdapter, filter); } IDeadLetterTransport CreateDeadLetterTransport(TransportSetHeaderAdapter headerAdapter) diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsReceiveEndpointConfiguration.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsReceiveEndpointConfiguration.cs index 8f770f8c1c5..b7da7d06d20 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsReceiveEndpointConfiguration.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsReceiveEndpointConfiguration.cs @@ -61,6 +61,7 @@ public void Build(IHost host) if (_settings.PurgeOnStartup) _clientConfigurator.UseFilter(new PurgeOnStartupFilter(_settings.EntityName)); + _clientConfigurator.UseFilter(new ReceiveEndpointDependencyFilter(context)); _clientConfigurator.UseFilter(new AmazonSqsConsumerFilter(context)); } @@ -124,6 +125,11 @@ public bool AutoDelete } } + public int ConcurrentDeliveryLimit + { + set => _settings.ConcurrentDeliveryLimit = value; + } + public ushort WaitTimeSeconds { set => _settings.WaitTimeSeconds = value; diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsRegistrationBusFactory.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsRegistrationBusFactory.cs index 0a3c9747d94..d9f535d775e 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsRegistrationBusFactory.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Configuration/AmazonSqsRegistrationBusFactory.cs @@ -2,6 +2,8 @@ namespace MassTransit.AmazonSqsTransport.Configuration { using System; using System.Collections.Generic; + using Amazon; + using Amazon.Runtime; using MassTransit.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; @@ -15,7 +17,7 @@ public class AmazonSqsRegistrationBusFactory : readonly Action _configure; public AmazonSqsRegistrationBusFactory(Action configure) - : this(new AmazonSqsBusConfiguration(new AmazonSqsTopologyConfiguration(AmazonSqsBusFactory.MessageTopology)), configure) + : this(new AmazonSqsBusConfiguration(new AmazonSqsTopologyConfiguration(AmazonSqsBusFactory.CreateMessageTopology())), configure) { } @@ -35,15 +37,20 @@ public override IBusInstance CreateBus(IBusRegistrationContext context, IEnumera var options = context.GetRequiredService>().Get(busName); if (!string.IsNullOrWhiteSpace(options.Region)) { - configurator.Host(new UriBuilder - { - Scheme = "amazonsqs", - Host = options.Region, - Path = options.Scope - }.Uri, h => + var regionEndpoint = RegionEndpoint.GetBySystemName(options.Region); + + configurator.Host(regionEndpoint.SystemName, h => { - h.AccessKey(options.AccessKey); - h.SecretKey(options.SecretKey); + if (!string.IsNullOrWhiteSpace(options.Scope)) + h.Scope(options.Scope); + + if (!string.IsNullOrWhiteSpace(options.AccessKey) && !string.IsNullOrWhiteSpace(options.SecretKey)) + { + h.AccessKey(options.AccessKey); + h.SecretKey(options.SecretKey); + } + else + h.Credentials(FallbackCredentialsFactory.GetCredentials()); }); } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Middleware/AmazonSqsMessageReceiver.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Middleware/AmazonSqsMessageReceiver.cs index 4632790cafc..22016ee707f 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Middleware/AmazonSqsMessageReceiver.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Middleware/AmazonSqsMessageReceiver.cs @@ -2,7 +2,7 @@ namespace MassTransit.AmazonSqsTransport.Middleware { using System; using System.Collections.Generic; - using System.Linq; + using System.Text; using System.Threading; using System.Threading.Tasks; using Amazon.SQS; @@ -17,13 +17,11 @@ namespace MassTransit.AmazonSqsTransport.Middleware /// Receives messages from AmazonSQS, pushing them to the InboundPipe of the service endpoint. /// public sealed class AmazonSqsMessageReceiver : - Agent, - DeliveryMetrics + ConsumerAgent { readonly ClientContext _client; readonly SqsReceiveEndpointContext _context; - readonly TaskCompletionSource _deliveryComplete; - readonly IReceivePipeDispatcher _dispatcher; + readonly IChannelExecutorPool _executorPool; readonly ReceiveSettings _receiveSettings; /// @@ -32,34 +30,24 @@ public sealed class AmazonSqsMessageReceiver : /// The model context for the consumer /// The topology public AmazonSqsMessageReceiver(ClientContext client, SqsReceiveEndpointContext context) + : base(context, StringComparer.Ordinal) { _client = client; _context = context; _receiveSettings = client.GetPayload(); - _deliveryComplete = TaskUtil.GetTask(); + _executorPool = new FifoChannelExecutorPool(_receiveSettings); - _dispatcher = context.CreateReceivePipeDispatcher(); - _dispatcher.ZeroActivity += HandleDeliveryComplete; - - var consumeTask = Task.Run(() => Consume()); - consumeTask.ContinueWith(async _ => - { - try - { - if (!IsStopping) - await this.Stop("Consume Loop Exited").ConfigureAwait(false); - } - catch (Exception exception) - { - LogContext.Warning?.Log(exception, "Stop Faulted"); - } - }); + TrySetConsumeTask(Task.Run(() => Consume())); } - long DeliveryMetrics.DeliveryCount => _dispatcher.DispatchCount; - int DeliveryMetrics.ConcurrentDeliveryCount => _dispatcher.MaxConcurrentDispatchCount; + protected override async Task ActiveAndActualAgentsCompleted(StopContext context) + { + await base.ActiveAndActualAgentsCompleted(context).ConfigureAwait(false); + + await _executorPool.DisposeAsync().ConfigureAwait(false); + } async Task Consume() { @@ -74,18 +62,19 @@ async Task Consume() SetReady(); + Task Handle(Message message, CancellationToken cancellationToken) + { + var lockContext = new AmazonSqsReceiveLockContext(_context.InputAddress, message, _receiveSettings, _client); + + return _receiveSettings.IsOrdered + ? _executorPool.Run(message, () => HandleMessage(message, lockContext), cancellationToken) + : HandleMessage(message, lockContext); + } + try { while (!IsStopping) - { - if (_receiveSettings.IsOrdered) - { - await algorithm.Run(ReceiveMessages, (m, _) => HandleMessage(m), GroupMessages, OrderMessages, Stopping) - .ConfigureAwait(false); - } - else - await algorithm.Run(ReceiveMessages, (m, _) => HandleMessage(m), Stopping).ConfigureAwait(false); - } + await algorithm.Run(ReceiveMessages, (m, c) => Handle(m, c), Stopping).ConfigureAwait(false); } catch (OperationCanceledException exception) when (exception.CancellationToken == Stopping) { @@ -96,15 +85,6 @@ await algorithm.Run(ReceiveMessages, (m, _) => HandleMessage(m), GroupMessages, } } - protected override Task StopAgent(StopContext context) - { - LogContext.Debug?.Log("Stopping consumer: {InputAddress}", _context.InputAddress); - - SetCompleted(ActiveAndActualAgentsCompleted(context)); - - return Completed; - } - async Task GetQueueAttributes() { var queueInfo = await _client.GetQueueInfo(_receiveSettings.EntityName).ConfigureAwait(false); @@ -121,7 +101,7 @@ async Task GetQueueAttributes() } } - async Task HandleMessage(Message message) + async Task HandleMessage(Message message, ReceiveLockContext lockContext) { if (IsStopping) return; @@ -131,7 +111,7 @@ async Task HandleMessage(Message message) var context = new AmazonSqsReceiveContext(message, redelivered, _context, _client, _receiveSettings, _client.ConnectionContext); try { - await _dispatcher.Dispatch(context, context).ConfigureAwait(false); + await Dispatch(message.MessageId, context, lockContext).ConfigureAwait(false); } catch (Exception exception) { @@ -143,23 +123,11 @@ async Task HandleMessage(Message message) } } - static IEnumerable> GroupMessages(IEnumerable messages) - { - return messages.GroupBy(x => x.Attributes.TryGetValue(MessageSystemAttributeName.MessageGroupId, out var groupId) ? groupId : ""); - } - - static IEnumerable OrderMessages(IEnumerable messages) - { - return messages.OrderBy(x => x.Attributes.TryGetValue("SequenceNumber", out var sequenceNumber) ? sequenceNumber : "", - SequenceNumberComparer.Instance); - } - async Task> ReceiveMessages(int messageLimit, CancellationToken cancellationToken) { try { - return await _client - .ReceiveMessages(_receiveSettings.EntityName, messageLimit, _receiveSettings.WaitTimeSeconds, cancellationToken) + return await _client.ReceiveMessages(_receiveSettings.EntityName, messageLimit, _receiveSettings.WaitTimeSeconds, cancellationToken) .ConfigureAwait(false); } catch (OperationCanceledException) @@ -168,47 +136,39 @@ async Task> ReceiveMessages(int messageLimit, CancellationT } } - Task HandleDeliveryComplete() - { - if (IsStopping) - _deliveryComplete.TrySetResult(true); - return Task.CompletedTask; - } - - async Task ActiveAndActualAgentsCompleted(StopContext context) + class FifoChannelExecutorPool : + IChannelExecutorPool { - if (_dispatcher.ActiveDispatchCount > 0) - { - try - { - await _deliveryComplete.Task.OrCanceled(context.CancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - LogContext.Warning?.Log("Stop canceled waiting for message consumers to complete: {InputAddress}", _context.InputAddress); - } - } - } - + readonly IChannelExecutorPool _keyExecutorPool; - class SequenceNumberComparer : - IComparer - { - public static readonly SequenceNumberComparer Instance = new SequenceNumberComparer(); + public FifoChannelExecutorPool(ReceiveSettings receiveSettings) + { + IHashGenerator hashGenerator = new Murmur3UnsafeHashGenerator(); + _keyExecutorPool = new PartitionChannelExecutorPool(MessageGroupIdProvider, hashGenerator, + receiveSettings.ConcurrentMessageLimit, receiveSettings.ConcurrentDeliveryLimit); + } - public int Compare(string x, string y) + public Task Push(Message result, Func handle, CancellationToken cancellationToken) { - if (string.IsNullOrWhiteSpace(x)) - throw new ArgumentNullException(nameof(x)); + return _keyExecutorPool.Push(result, handle, cancellationToken); + } - if (string.IsNullOrWhiteSpace(y)) - throw new ArgumentNullException(nameof(y)); + public Task Run(Message result, Func method, CancellationToken cancellationToken = default) + { + return _keyExecutorPool.Run(result, method, cancellationToken); + } - if (x.Length != y.Length) - return x.Length > y.Length ? 1 : -1; + public ValueTask DisposeAsync() + { + return _keyExecutorPool.DisposeAsync(); + } - return string.Compare(x, y, StringComparison.Ordinal); + static byte[] MessageGroupIdProvider(Message message) + { + return message.Attributes.TryGetValue(MessageSystemAttributeName.MessageGroupId, out var groupId) && !string.IsNullOrEmpty(groupId) + ? Encoding.UTF8.GetBytes(groupId) + : Array.Empty(); } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Middleware/ConfigureAmazonSqsTopologyFilter.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Middleware/ConfigureAmazonSqsTopologyFilter.cs index eecbb1a4feb..658f384f998 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Middleware/ConfigureAmazonSqsTopologyFilter.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Middleware/ConfigureAmazonSqsTopologyFilter.cs @@ -1,103 +1,124 @@ -namespace MassTransit.AmazonSqsTransport.Middleware +namespace MassTransit.AmazonSqsTransport.Middleware; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Topology; + + +/// +/// Configures the broker with the supplied topology once the model is created, to ensure +/// that the exchanges, queues, and bindings for the model are properly configured in AmazonSQS. +/// +public class ConfigureAmazonSqsTopologyFilter : + IFilter + where TSettings : class { - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Topology; - - - /// - /// Configures the broker with the supplied topology once the model is created, to ensure - /// that the exchanges, queues, and bindings for the model are properly configured in AmazonSQS. - /// - public class ConfigureAmazonSqsTopologyFilter : - IFilter - where TSettings : class + readonly BrokerTopology _brokerTopology; + readonly SqsReceiveEndpointContext _context; + readonly TSettings _settings; + + public ConfigureAmazonSqsTopologyFilter(TSettings settings, BrokerTopology brokerTopology, SqsReceiveEndpointContext context = null) { - readonly BrokerTopology _brokerTopology; - readonly SqsReceiveEndpointContext _context; - readonly TSettings _settings; + _settings = settings; + _brokerTopology = brokerTopology; + _context = context; + } - public ConfigureAmazonSqsTopologyFilter(TSettings settings, BrokerTopology brokerTopology, SqsReceiveEndpointContext context = null) + public async Task Send(ClientContext context, IPipe next) + { + OneTimeContext> oneTimeContext = await Configure(context); + + try { - _settings = settings; - _brokerTopology = brokerTopology; - _context = context; + await next.Send(context).ConfigureAwait(false); } - - async Task IFilter.Send(ClientContext context, IPipe next) + catch (Exception) { - await context.OneTimeSetup>(async payload => - { - await ConfigureTopology(context).ConfigureAwait(false); + oneTimeContext.Evict(); - context.GetOrAddPayload(() => _settings); + throw; + } + } - if (_context != null && AnyAutoDelete()) - _context.AddSendAgent(new RemoveAmazonSqsTopologyAgent(context, _brokerTopology)); - }, () => new Context()).ConfigureAwait(false); + public void Probe(ProbeContext context) + { + var scope = context.CreateFilterScope("configureTopology"); - await next.Send(context).ConfigureAwait(false); - } + _brokerTopology.Probe(scope); + } - void IProbeSite.Probe(ProbeContext context) + public async Task>> Configure(ClientContext context) + { + return await context.OneTimeSetup>(() => { - var scope = context.CreateFilterScope("configureTopology"); + context.GetOrAddPayload(() => _settings); - _brokerTopology.Probe(scope); - } + if (_context != null && AnyAutoDelete()) + _context.AddSendAgent(new RemoveAmazonSqsTopologyAgent(context, _brokerTopology)); - async Task ConfigureTopology(ClientContext context) - { - IEnumerable> topics = _brokerTopology.Topics.Select(topic => Declare(context, topic)); + return ConfigureTopology(context); + }).ConfigureAwait(false); + } - IEnumerable> queues = _brokerTopology.Queues.Select(queue => Declare(context, queue)); + async Task ConfigureTopology(ClientContext context) + { + IEnumerable> topics = _brokerTopology.Topics.Select(topic => Declare(context, topic)); - await Task.WhenAll(topics).ConfigureAwait(false); - await Task.WhenAll(queues).ConfigureAwait(false); + IEnumerable> queues = _brokerTopology.Queues.Select(queue => Declare(context, queue)); - IEnumerable subscriptions = _brokerTopology.QueueSubscriptions.Select(subscription => Declare(context, subscription)); - await Task.WhenAll(subscriptions).ConfigureAwait(false); - } + await Task.WhenAll(topics).ConfigureAwait(false); + await Task.WhenAll(queues).ConfigureAwait(false); - bool AnyAutoDelete() - { - return _brokerTopology.Topics.Any(x => x.AutoDelete) || _brokerTopology.Queues.Any(x => x.AutoDelete); - } + IEnumerable subscriptions = _brokerTopology.QueueSubscriptions.Select(subscription => Declare(context, subscription)); + await Task.WhenAll(subscriptions).ConfigureAwait(false); + } - async Task Declare(ClientContext context, Topic topic) - { - var topicInfo = await context.CreateTopic(topic).ConfigureAwait(false); - topicInfo = await context.CreateTopic(topic).ConfigureAwait(false); + bool AnyAutoDelete() + { + return _brokerTopology.Topics.Any(x => x.AutoDelete) || _brokerTopology.Queues.Any(x => x.AutoDelete); + } - LogContext.Debug?.Log("Created topic {Topic} {TopicArn}", topicInfo.EntityName, topicInfo.Arn); + static async Task Declare(ClientContext context, Topic topic) + { + var topicInfo = await context.CreateTopic(topic).ConfigureAwait(false); + if (topicInfo.Existing) + { + LogContext.Debug?.Log("Existing topic {Topic} {TopicArn}", topicInfo.EntityName, topicInfo.Arn); return topicInfo; } - Task Declare(ClientContext context, QueueSubscription subscription) - { - LogContext.Debug?.Log("Binding topic {Topic} to {Queue}", subscription.Source, subscription.Destination); + // Why? I don't know, but damn, it takes two times, or it doesn't catch properly + topicInfo = await context.CreateTopic(topic).ConfigureAwait(false); - return context.CreateQueueSubscription(subscription.Source, subscription.Destination); - } + LogContext.Debug?.Log("Created topic {Topic} {TopicArn}", topicInfo.EntityName, topicInfo.Arn); - async Task Declare(ClientContext context, Queue queue) - { - // Why? I don't know, but damn, it takes two times, or it doesn't catch properly - var queueInfo = await context.CreateQueue(queue).ConfigureAwait(false); - - queueInfo = await context.CreateQueue(queue).ConfigureAwait(false); + return topicInfo; + } - LogContext.Debug?.Log("Created queue {Queue} {QueueArn} {QueueUrl}", queueInfo.EntityName, queueInfo.Arn, queueInfo.Url); + static async Task Declare(ClientContext context, QueueSubscription subscription) + { + var created = await context.CreateQueueSubscription(subscription.Source, subscription.Destination).ConfigureAwait(false); + LogContext.Debug?.Log(created ? "Created subscription {Topic} to {Queue}" : "Existing subscription {Topic} to {Queue}", + subscription.Source, subscription.Destination); + } + static async Task Declare(ClientContext context, Queue queue) + { + var queueInfo = await context.CreateQueue(queue).ConfigureAwait(false); + if (queueInfo.Existing) + { + LogContext.Debug?.Log("Existing queue {Queue} {QueueArn} {QueueUrl}", queueInfo.EntityName, queueInfo.Arn, queueInfo.Url); return queueInfo; } + // Why? I don't know, but damn, it takes two times, or it doesn't catch properly + queueInfo = await context.CreateQueue(queue).ConfigureAwait(false); - class Context : - ConfigureTopologyContext - { - } + LogContext.Debug?.Log("Created queue {Queue} {QueueArn} {QueueUrl}", queueInfo.EntityName, queueInfo.Arn, queueInfo.Url); + + return queueInfo; } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/PublishBatchSettings.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/PublishBatchSettings.cs new file mode 100644 index 00000000000..10e5f0bc20d --- /dev/null +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/PublishBatchSettings.cs @@ -0,0 +1,46 @@ +namespace MassTransit.AmazonSqsTransport +{ + using System; + + + public static class PublishBatchSettings + { + static PublishBatchSettings() + { + MessageLimit = 5; + BatchLimit = 5; + SizeLimit = 128 * 1024; + Timeout = TimeSpan.FromMilliseconds(1); + } + + public static int MessageLimit { get; set; } + public static int BatchLimit { get; set; } + public static int SizeLimit { get; set; } + public static TimeSpan Timeout { get; set; } + + public static BatchSettings GetBatchSettings() + { + return new SnsPublishBatchSettings(Math.Min(5, MessageLimit), BatchLimit, Math.Min(256 * 1024, SizeLimit), Timeout); + } + + + class SnsPublishBatchSettings : + BatchSettings + { + public SnsPublishBatchSettings(int messageLimit, int batchLimit, int sizeLimit, TimeSpan timeout) + { + Enabled = true; + MessageLimit = messageLimit; + BatchLimit = batchLimit; + SizeLimit = sizeLimit; + Timeout = timeout; + } + + public bool Enabled { get; set; } + public int MessageLimit { get; set; } + public int BatchLimit { get; set; } + public int SizeLimit { get; set; } + public TimeSpan Timeout { get; set; } + } + } +} diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/PublishBatcher.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/PublishBatcher.cs new file mode 100644 index 00000000000..f8a7e903e7b --- /dev/null +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/PublishBatcher.cs @@ -0,0 +1,52 @@ +namespace MassTransit.AmazonSqsTransport +{ + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Amazon.SimpleNotificationService; + using Amazon.SimpleNotificationService.Model; + + + public class PublishBatcher : + Batcher + { + readonly CancellationToken _cancellationToken; + readonly IAmazonSimpleNotificationService _client; + readonly string _topicArn; + + public PublishBatcher(IAmazonSimpleNotificationService client, string topicArn, CancellationToken cancellationToken) + : base(PublishBatchSettings.GetBatchSettings()) + { + _client = client; + _topicArn = topicArn; + _cancellationToken = cancellationToken; + } + + protected override int AddingEntry(PublishBatchRequestEntry entry, string entryId) + { + entry.Id = entryId; + + return entry.Message.Length + + entry.MessageAttributes.Where(x => x.Value.DataType == "String").Sum(x => x.Key.Length + x.Value.StringValue.Length); + } + + protected override async Task SendBatch(IList> batch) + { + var batchRequest = new PublishBatchRequest + { + TopicArn = _topicArn, + PublishBatchRequestEntries = batch.Select(x => x.Entry).ToList() + }; + + var response = await _client.PublishBatchAsync(batchRequest, _cancellationToken).ConfigureAwait(false); + + response.EnsureSuccessfulResponse(); + + Complete(batch, response.Successful.Select(x => x.Id)); + + foreach (var error in response.Failed) + Fail(batch, error.Id, error.Code, error.Message); + } + } +} diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueCache.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueCache.cs index 02b9828165a..c0659201621 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueCache.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueCache.cs @@ -26,10 +26,7 @@ public QueueCache(IAmazonSQS client, CancellationToken cancellationToken) _client = client; _cancellationToken = cancellationToken; - var options = new CacheOptions { Capacity = ClientContextCacheDefaults.Capacity }; - var policy = new TimeToLiveCachePolicy(ClientContextCacheDefaults.MaxAge); - - _cache = new MassTransitCache>(policy, options); + _cache = ClientContextCacheDefaults.CreateCache(); _durableQueues = new Dictionary(); } @@ -115,7 +112,7 @@ async Task CreateMissingQueue(Queue queue) attributesResponse.EnsureSuccessfulResponse(); - var missingQueue = new QueueInfo(queue.EntityName, createResponse.QueueUrl, attributesResponse.Attributes, _client, _cancellationToken); + var missingQueue = new QueueInfo(queue.EntityName, createResponse.QueueUrl, attributesResponse.Attributes, _client, _cancellationToken, false); if (queue.Durable && queue.AutoDelete == false) { @@ -136,7 +133,7 @@ async Task GetExistingQueue(string queueName) attributesResponse.EnsureSuccessfulResponse(); - return new QueueInfo(queueName, urlResponse.QueueUrl, attributesResponse.Attributes, _client, _cancellationToken); + return new QueueInfo(queueName, urlResponse.QueueUrl, attributesResponse.Attributes, _client, _cancellationToken, true); } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueInfo.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueInfo.cs index 7c5596735a7..151ccd7cdee 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueInfo.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueInfo.cs @@ -2,8 +2,12 @@ namespace MassTransit.AmazonSqsTransport { using System; using System.Collections.Generic; + using System.Linq; using System.Threading; using System.Threading.Tasks; + using System.Xml.Schema; + using Amazon.Auth.AccessControlPolicy; + using Amazon.Auth.AccessControlPolicy.ActionIdentifiers; using Amazon.SQS; using Amazon.SQS.Model; @@ -11,13 +15,18 @@ namespace MassTransit.AmazonSqsTransport public class QueueInfo : IAsyncDisposable { - readonly IBatcher _batchDeleter; - readonly IBatcher _batchSender; + readonly Lazy> _batchDeleter; + readonly Lazy> _batchSender; + readonly IAmazonSQS _client; + readonly SemaphoreSlim _updateSemaphore; bool _disposed; - public QueueInfo(string entityName, string url, IDictionary attributes, IAmazonSQS client, CancellationToken cancellationToken) + public QueueInfo(string entityName, string url, IDictionary attributes, IAmazonSQS client, CancellationToken cancellationToken, + bool existing) { + _client = client; Attributes = attributes; + Existing = existing; EntityName = entityName; Url = url; @@ -25,8 +34,10 @@ public QueueInfo(string entityName, string url, IDictionary attr ? queueArn : throw new ArgumentException($"The queueArn was not found: {url}", nameof(attributes)); - _batchSender = new SendBatcher(client, url, cancellationToken); - _batchDeleter = new DeleteBatcher(client, url, cancellationToken); + _updateSemaphore = new SemaphoreSlim(1); + + _batchSender = new Lazy>(() => new SendBatcher(client, url, cancellationToken)); + _batchDeleter = new Lazy>(() => new DeleteBatcher(client, url, cancellationToken)); SubscriptionArns = new List(); } @@ -36,6 +47,7 @@ public QueueInfo(string entityName, string url, IDictionary attr public string Arn { get; } public IDictionary Attributes { get; } public IList SubscriptionArns { get; } + public bool Existing { get; } public async ValueTask DisposeAsync() { @@ -44,20 +56,105 @@ public async ValueTask DisposeAsync() _disposed = true; - await _batchSender.DisposeAsync().ConfigureAwait(false); - await _batchDeleter.DisposeAsync().ConfigureAwait(false); + _updateSemaphore.Dispose(); + + if (_batchSender.IsValueCreated) + await _batchSender.Value.DisposeAsync().ConfigureAwait(false); + if (_batchDeleter.IsValueCreated) + await _batchDeleter.Value.DisposeAsync().ConfigureAwait(false); } public Task Send(SendMessageBatchRequestEntry entry, CancellationToken cancellationToken) { - return _batchSender.Execute(entry, cancellationToken); + return _batchSender.Value.Execute(entry, cancellationToken); } public Task Delete(string receiptHandle, CancellationToken cancellationToken) { var entry = new DeleteMessageBatchRequestEntry("", receiptHandle); - return _batchDeleter.Execute(entry, cancellationToken); + return _batchDeleter.Value.Execute(entry, cancellationToken); + } + + public async Task UpdatePolicy(string sqsQueueArn, string topicArn, CancellationToken cancellationToken) + { + await _updateSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); + try + { + Attributes.TryGetValue(QueueAttributeName.Policy, out var policyValue); + var policy = string.IsNullOrEmpty(policyValue) + ? new Policy() + : Policy.FromJson(policyValue); + + if (QueueHasTopicPermission(policy, topicArn, sqsQueueArn)) + return false; + + #pragma warning disable 618 + var statement = policy.Statements.FirstOrDefault(x => x.Effect == Statement.StatementEffect.Allow + && x.Actions.Any(a => a.ActionName.Equals(SQSActionIdentifiers.SendMessage.ActionName, StringComparison.Ordinal)) + && x.Resources.Any(a => a.Id.Equals(sqsQueueArn, StringComparison.OrdinalIgnoreCase)) + && x.Principals.Any(a => string.Equals(a.Provider, "Service", StringComparison.OrdinalIgnoreCase) + && a.Id.Equals("sns.amazonaws.com", StringComparison.OrdinalIgnoreCase))); + + if (statement is null) + { + statement = new Statement(Statement.StatementEffect.Allow); + statement.Actions.Add(SQSActionIdentifiers.SendMessage); + statement.Resources.Add(new Resource(sqsQueueArn)); + statement.Principals.Add(new Principal("Service", "sns.amazonaws.com")); + policy.Statements.Add(statement); + } + #pragma warning restore 618 + + var condition = statement.Conditions.FirstOrDefault(x => + string.Equals(ConditionFactory.SOURCE_ARN_CONDITION_KEY, x.ConditionKey, StringComparison.Ordinal) && + x.Type.Equals(ConditionFactory.ArnComparisonType.ArnLike.ToString(), StringComparison.Ordinal)); + + if (condition is not null && condition.Values.Any(x => x.Equals(topicArn, StringComparison.OrdinalIgnoreCase))) + return false; + + if (condition is null) + { + statement.Conditions.Add(ConditionFactory.NewSourceArnCondition(topicArn)); + } + else + { + condition.Values = condition + .Values + .Append(topicArn) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToArray(); + } + + var jsonPolicy = policy.ToJson(); + + var setAttributes = new Dictionary { { QueueAttributeName.Policy, jsonPolicy } }; + var setAttributesResponse = await _client.SetQueueAttributesAsync(Url, setAttributes, cancellationToken).ConfigureAwait(false); + + setAttributesResponse.EnsureSuccessfulResponse(); + + Attributes[QueueAttributeName.Policy] = jsonPolicy; + + return true; + } + finally + { + _updateSemaphore.Release(); + } + } + + static bool QueueHasTopicPermission(Policy policy, string topicArn, string sqsQueueArn) + { + var topicArnPattern = topicArn.Substring(0, topicArn.LastIndexOf(':') + 1) + "*"; + + IEnumerable conditions = policy.Statements + .Where(s => s.Resources.Any(r => r.Id.Equals(sqsQueueArn))) + .SelectMany(s => s.Conditions); + + return conditions.Any(c => + string.Equals(c.Type, ConditionFactory.ArnComparisonType.ArnLike.ToString(), StringComparison.OrdinalIgnoreCase) && + string.Equals(c.ConditionKey, ConditionFactory.SOURCE_ARN_CONDITION_KEY, StringComparison.OrdinalIgnoreCase) && + c.Values.Any(v => v == topicArnPattern || v == topicArn)); } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueSendTransportContext.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueSendTransportContext.cs index f62be6f430f..9b1caa054be 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueSendTransportContext.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/QueueSendTransportContext.cs @@ -33,7 +33,7 @@ public QueueSendTransportContext(IAmazonSqsHostConfiguration hostConfiguration, } public override string EntityName { get; } - public override string ActivitySystem => "sqs"; + public override string ActivitySystem => "aws_sqs"; public Task Send(IPipe pipe, CancellationToken cancellationToken = default) { diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ReceiveSettings.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ReceiveSettings.cs index 005c550019b..15d5ddd2323 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ReceiveSettings.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ReceiveSettings.cs @@ -17,6 +17,8 @@ public interface ReceiveSettings : int ConcurrentMessageLimit { get; } + int ConcurrentDeliveryLimit { get; } + int WaitTimeSeconds { get; } /// @@ -35,11 +37,6 @@ public interface ReceiveSettings : /// IDictionary QueueSubscriptionAttributes { get; } - /// - /// Get the input address for the transport on the specified host - /// - Uri GetInputAddress(Uri hostAddress); - /// /// If the queue is ordered, enables grouping by MessageGroupId and process messages in ordered way by SequenceNumber /// @@ -53,5 +50,10 @@ public interface ReceiveSettings : int RedeliverVisibilityTimeout { get; set; } string QueueUrl { get; set; } + + /// + /// Get the input address for the transport on the specified host + /// + Uri GetInputAddress(Uri hostAddress); } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ScopeClientContext.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ScopeClientContext.cs index ca2c3d68f8b..ef053dc845b 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ScopeClientContext.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ScopeClientContext.cs @@ -36,7 +36,7 @@ public Task CreateQueue(Queue queue) return _context.CreateQueue(queue); } - public Task CreateQueueSubscription(Topology.Topic topic, Queue queue) + public Task CreateQueueSubscription(Topology.Topic topic, Queue queue) { return _context.CreateQueueSubscription(topic, queue); } @@ -51,14 +51,9 @@ public Task DeleteQueue(Queue queue) return _context.DeleteQueue(queue); } - public Task CreatePublishRequest(string topicName, string body) + public Task Publish(string topicName, PublishBatchRequestEntry request, CancellationToken cancellationToken = default) { - return _context.CreatePublishRequest(topicName, body); - } - - public Task Publish(PublishRequest request, CancellationToken cancellationToken) - { - return _context.Publish(request, cancellationToken); + return _context.Publish(topicName, request, cancellationToken); } public Task SendMessage(string queueName, SendMessageBatchRequestEntry request, CancellationToken cancellationToken) diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ScopeClientContextFactory.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ScopeClientContextFactory.cs index c8822bad7e6..d013a28dbfa 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ScopeClientContextFactory.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/ScopeClientContextFactory.cs @@ -45,7 +45,9 @@ static Task Create(ClientContext context, CancellationToken creat return Task.FromResult(new SharedClientContext(context, createCancellationToken)); } + #pragma warning disable CS4014 _supervisor.CreateAgent(asyncContext, Create, cancellationToken); + #pragma warning restore CS4014 } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SharedClientContext.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SharedClientContext.cs index f08d12405bf..66f7ba73789 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SharedClientContext.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SharedClientContext.cs @@ -36,7 +36,7 @@ public Task CreateQueue(Queue queue) return _context.CreateQueue(queue); } - public Task CreateQueueSubscription(Topology.Topic topic, Queue queue) + public Task CreateQueueSubscription(Topology.Topic topic, Queue queue) { return _context.CreateQueueSubscription(topic, queue); } @@ -51,14 +51,9 @@ public Task DeleteQueue(Queue queue) return _context.DeleteQueue(queue); } - public Task CreatePublishRequest(string topicName, string body) + public Task Publish(string topicName, PublishBatchRequestEntry request, CancellationToken cancellationToken = default) { - return _context.CreatePublishRequest(topicName, body); - } - - public Task Publish(PublishRequest request, CancellationToken cancellationToken) - { - return _context.Publish(request, cancellationToken); + return _context.Publish(topicName, request, cancellationToken); } public Task SendMessage(string queueName, SendMessageBatchRequestEntry request, CancellationToken cancellationToken) diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsDeadLetterTransport.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsDeadLetterTransport.cs index fd044b414c1..74f8ba698ed 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsDeadLetterTransport.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsDeadLetterTransport.cs @@ -3,16 +3,18 @@ using System.Collections.Generic; using System.Threading.Tasks; using Amazon.SQS.Model; + using Middleware; using Transports; public class SqsDeadLetterTransport : - SqsMoveTransport, + SqsMoveTransport, IDeadLetterTransport { readonly TransportSetHeaderAdapter _headerAdapter; - public SqsDeadLetterTransport(string destination, TransportSetHeaderAdapter headerAdapter, IFilter topologyFilter) + public SqsDeadLetterTransport(string destination, TransportSetHeaderAdapter headerAdapter, + ConfigureAmazonSqsTopologyFilter topologyFilter) : base(destination, topologyFilter) { _headerAdapter = headerAdapter; diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsErrorTransport.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsErrorTransport.cs index c01535a0eb6..62492e61947 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsErrorTransport.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsErrorTransport.cs @@ -3,16 +3,18 @@ using System.Collections.Generic; using System.Threading.Tasks; using Amazon.SQS.Model; + using Middleware; using Transports; public class SqsErrorTransport : - SqsMoveTransport, + SqsMoveTransport, IErrorTransport { readonly ITransportSetHeaderAdapter _headerAdapter; - public SqsErrorTransport(string destination, ITransportSetHeaderAdapter headerAdapter, IFilter topologyFilter) + public SqsErrorTransport(string destination, ITransportSetHeaderAdapter headerAdapter, + ConfigureAmazonSqsTopologyFilter topologyFilter) : base(destination, topologyFilter) { _headerAdapter = headerAdapter; diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsMessageBody.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsMessageBody.cs new file mode 100644 index 00000000000..cbac6225ab1 --- /dev/null +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsMessageBody.cs @@ -0,0 +1,40 @@ +#nullable enable +namespace MassTransit.AmazonSqsTransport; + +using System.Text.Json; +using Amazon.SQS.Model; + + +public class SqsMessageBody : + StringMessageBody, + JsonMessageBody +{ + readonly Message _message; + JsonElement? _topicArn; + + public SqsMessageBody(Message message) + : base(message.Body) + { + _message = message; + } + + public string? TopicArn => _topicArn?.GetString(); + + public JsonElement? GetJsonElement(JsonSerializerOptions options) + { + if (_message.Body == null) + return null; + + var jsonElement = JsonSerializer.Deserialize(_message.Body, options); + + if (jsonElement.TryGetProperty("TopicArn", out var topicElement) || jsonElement.TryGetProperty("topicArn", out topicElement)) + { + _topicArn = topicElement; + + if (jsonElement.TryGetProperty("Message", out var messageElement) || jsonElement.TryGetProperty("message", out messageElement)) + return JsonSerializer.Deserialize(messageElement.GetString() ?? string.Empty, options); + } + + return jsonElement; + } +} diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsMoveTransport.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsMoveTransport.cs index ef0881505b2..4748636ac43 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsMoveTransport.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/SqsMoveTransport.cs @@ -7,15 +7,17 @@ using System.Threading.Tasks; using Amazon.SQS; using Amazon.SQS.Model; + using Middleware; - public class SqsMoveTransport + public class SqsMoveTransport + where TSettings : class { readonly string _destination; readonly bool _isFifo; - readonly IFilter _topologyFilter; + readonly ConfigureAmazonSqsTopologyFilter _topologyFilter; - protected SqsMoveTransport(string destination, IFilter topologyFilter) + protected SqsMoveTransport(string destination, ConfigureAmazonSqsTopologyFilter topologyFilter) { _destination = destination; _topologyFilter = topologyFilter; @@ -28,7 +30,7 @@ protected async Task Move(ReceiveContext context, Action()).ConfigureAwait(false); + OneTimeContext> oneTimeContext = await _topologyFilter.Configure(clientContext).ConfigureAwait(false); var message = new SendMessageBatchRequestEntry("", Encoding.UTF8.GetString(context.GetBody())); @@ -50,9 +52,15 @@ protected async Task Move(ReceiveContext context, Action attributes) diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicCache.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicCache.cs index a22918a7962..b8b3725ba97 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicCache.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicCache.cs @@ -17,7 +17,7 @@ public class TopicCache : readonly CancellationToken _cancellationToken; readonly IAmazonSimpleNotificationService _client; readonly IDictionary _durableTopics; - readonly Lazy _loadExistingTopics; + Lazy _loadExistingTopics; bool _topicsLoaded; public TopicCache(IAmazonSimpleNotificationService client, CancellationToken cancellationToken) @@ -25,20 +25,25 @@ public TopicCache(IAmazonSimpleNotificationService client, CancellationToken can _client = client; _cancellationToken = cancellationToken; - var options = new CacheOptions { Capacity = ClientContextCacheDefaults.Capacity }; - var policy = new TimeToLiveCachePolicy(ClientContextCacheDefaults.MaxAge); + _cache = ClientContextCacheDefaults.CreateCache(); - _cache = new MassTransitCache>(policy, options); - - _loadExistingTopics = new Lazy(() => LoadExistingTopics(cancellationToken)); + ResetLoadExistingTopics(); _durableTopics = new Dictionary(); } public async ValueTask DisposeAsync() { + TopicInfo[] topicInfos; lock (_durableTopics) + { + topicInfos = _durableTopics.Values.ToArray(); + _durableTopics.Clear(); + } + + foreach (var topicInfo in topicInfos) + await topicInfo.DisposeAsync().ConfigureAwait(false); await _cache.Clear().ConfigureAwait(false); } @@ -46,7 +51,7 @@ public async ValueTask DisposeAsync() public async Task Get(Topology.Topic topic) { if (!_topicsLoaded) - await _loadExistingTopics.Value.ConfigureAwait(false); + await LoadExistingTopics().ConfigureAwait(false); lock (_durableTopics) { @@ -60,7 +65,7 @@ public async Task Get(Topology.Topic topic) public async Task GetByName(string entityName) { if (!_topicsLoaded) - await _loadExistingTopics.Value.ConfigureAwait(false); + await LoadExistingTopics().ConfigureAwait(false); lock (_durableTopics) { @@ -101,7 +106,7 @@ async Task CreateMissingTopic(Topology.Topic topic) attributesResponse.EnsureSuccessfulResponse(); - var missingTopic = new TopicInfo(topic.EntityName, createResponse.TopicArn); + var missingTopic = new TopicInfo(topic.EntityName, createResponse.TopicArn, _client, _cancellationToken, false); if (topic.Durable && topic.AutoDelete == false) { @@ -112,7 +117,28 @@ async Task CreateMissingTopic(Topology.Topic topic) return missingTopic; } - async Task LoadExistingTopics(CancellationToken token) + Lazy ResetLoadExistingTopics() + { + return _loadExistingTopics = new Lazy(() => LoadExistingTopicsLazy(_cancellationToken)); + } + + Task LoadExistingTopics() + { + var result = _loadExistingTopics.Value; + if (result.IsFaulted || result.IsCanceled) + { + lock (this) + { + result = _loadExistingTopics.Value; + if (result.IsFaulted || result.IsCanceled) + result = ResetLoadExistingTopics().Value; + } + } + + return result; + } + + async Task LoadExistingTopicsLazy(CancellationToken token) { var cursor = string.Empty; do @@ -131,7 +157,7 @@ async Task LoadExistingTopics(CancellationToken token) await _cache.GetOrAdd(topicName, async key => { - var topicInfo = new TopicInfo(topicName, topic.TopicArn); + var topicInfo = new TopicInfo(topicName, topic.TopicArn, _client, _cancellationToken, true); lock (_durableTopics) _durableTopics[topicInfo.EntityName] = topicInfo; diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicInfo.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicInfo.cs index 424cafff7ec..987d8395c16 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicInfo.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicInfo.cs @@ -1,14 +1,45 @@ namespace MassTransit.AmazonSqsTransport { - public class TopicInfo + using System; + using System.Threading; + using System.Threading.Tasks; + using Amazon.SimpleNotificationService; + using Amazon.SimpleNotificationService.Model; + + + public class TopicInfo : + IAsyncDisposable { - public TopicInfo(string entityName, string arn) + readonly Lazy> _batchPublisher; + bool _disposed; + + public TopicInfo(string entityName, string arn, IAmazonSimpleNotificationService client, CancellationToken cancellationToken, bool existing) { EntityName = entityName; Arn = arn; + Existing = existing; + + _batchPublisher = new Lazy>(() => new PublishBatcher(client, arn, cancellationToken)); } public string EntityName { get; } public string Arn { get; } + public bool Existing { get; } + + public async ValueTask DisposeAsync() + { + if (_disposed) + return; + + _disposed = true; + + if (_batchPublisher.IsValueCreated) + await _batchPublisher.Value.DisposeAsync().ConfigureAwait(false); + } + + public Task Publish(PublishBatchRequestEntry entry, CancellationToken cancellationToken) + { + return _batchPublisher.Value.Execute(entry, cancellationToken); + } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicSendTransportContext.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicSendTransportContext.cs index 6b71234d1e2..ce09fe025b8 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicSendTransportContext.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/TopicSendTransportContext.cs @@ -33,7 +33,7 @@ public TopicSendTransportContext(IAmazonSqsHostConfiguration hostConfiguration, } public override string EntityName { get; } - public override string ActivitySystem => "sqs"; + public override string ActivitySystem => "aws_sqs"; public Task Send(IPipe pipe, CancellationToken cancellationToken = default) { @@ -78,7 +78,7 @@ public async Task Send(ClientContext transportContext, SendContext sendCon sendContext.CancellationToken.ThrowIfCancellationRequested(); - var request = await transportContext.CreatePublishRequest(EntityName, context.Body.GetString()).ConfigureAwait(false); + var request = new PublishBatchRequestEntry { Message = context.Body.GetString() }; _headerAdapter.Set(request.MessageAttributes, context.Headers); _headerAdapter.Set(request.MessageAttributes, MessageHeaders.ContentType, context.ContentType.ToString()); @@ -90,7 +90,7 @@ public async Task Send(ClientContext transportContext, SendContext sendCon if (!string.IsNullOrEmpty(context.GroupId)) request.MessageGroupId = context.GroupId; - await transportContext.Publish(request, context.CancellationToken).ConfigureAwait(false); + await transportContext.Publish(EntityName, request, context.CancellationToken).ConfigureAwait(false); } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSnsTopicNameValidator.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSnsTopicNameValidator.cs new file mode 100644 index 00000000000..968539f0b59 --- /dev/null +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSnsTopicNameValidator.cs @@ -0,0 +1,37 @@ +namespace MassTransit.AmazonSqsTransport.Topology +{ + using System.Text.RegularExpressions; + + + public class AmazonSnsTopicNameValidator : + IEntityNameValidator + { + static readonly Regex _regex = new Regex(@"^[A-Za-z0-9\-_\.:]+$", RegexOptions.Compiled); + + public static IEntityNameValidator Validator => Cached.EntityNameValidator; + + public void ThrowIfInvalidEntityName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new AmazonSqsTransportConfigurationException("The entity name must not be null or empty"); + + var success = IsValidEntityName(name); + if (!success) + { + throw new AmazonSqsTransportConfigurationException( + "The entity name length must be <= 256 and a sequence of these characters: letters, digits, hyphen, underscore, period, or colon."); + } + } + + public bool IsValidEntityName(string name) + { + return _regex.Match(name).Success && name.Length <= 256; + } + + + static class Cached + { + internal static readonly IEntityNameValidator EntityNameValidator = new AmazonSnsTopicNameValidator(); + } + } +} diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsConsumeTopology.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsConsumeTopology.cs index 3f82d02fec3..681b7446d82 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsConsumeTopology.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsConsumeTopology.cs @@ -63,7 +63,7 @@ public override IEnumerable Validate() return base.Validate().Concat(_specifications.SelectMany(x => x.Validate())); } - protected override IMessageConsumeTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessageConsumeTopologyConfigurator CreateMessageTopology() { var messageTopology = new AmazonSqsMessageConsumeTopology(_messageTopology.GetMessageTopology(), _publishTopology); diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsEntityNameValidator.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsEntityNameValidator.cs index e05bb398d45..805af4f9a94 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsEntityNameValidator.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsEntityNameValidator.cs @@ -17,13 +17,15 @@ public void ThrowIfInvalidEntityName(string name) var success = IsValidEntityName(name); if (!success) + { throw new AmazonSqsTransportConfigurationException( - "The entity name must be a sequence of these characters: letters, digits, hyphen, underscore, period, or colon."); + "The entity name length must be <= 80 and a sequence of these characters: letters, digits, hyphen, underscore, period, or colon."); + } } public bool IsValidEntityName(string name) { - return _regex.Match(name).Success; + return _regex.Match(name).Success && name.Length <= 80; } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsMessagePublishTopology.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsMessagePublishTopology.cs index caacd230d1d..a44c58d77e3 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsMessagePublishTopology.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsMessagePublishTopology.cs @@ -3,6 +3,7 @@ namespace MassTransit.AmazonSqsTransport.Topology { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using Configuration; using Internals; using MassTransit.Topology; @@ -52,7 +53,7 @@ public AmazonSqsEndpointAddress GetEndpointAddress(Uri hostAddress) return _amazonSqsTopic.GetEndpointAddress(hostAddress); } - public override bool TryGetPublishAddress(Uri baseAddress, out Uri? publishAddress) + public override bool TryGetPublishAddress(Uri baseAddress, [NotNullWhen(true)] out Uri? publishAddress) { publishAddress = _amazonSqsTopic.GetEndpointAddress(baseAddress); return true; diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsPublishTopology.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsPublishTopology.cs index 9d571606a12..a8714bab7dd 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsPublishTopology.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/AmazonSqsPublishTopology.cs @@ -53,7 +53,7 @@ IAmazonSqsMessagePublishTopologyConfigurator IAmazonSqsPublishTopologyConfigu return GetMessageTopology() as IAmazonSqsMessagePublishTopologyConfigurator; } - protected override IMessagePublishTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessagePublishTopologyConfigurator CreateMessageTopology() { var messageTopology = new AmazonSqsMessagePublishTopology(this, _messageTopology.GetMessageTopology()); diff --git a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/QueueReceiveSettings.cs b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/QueueReceiveSettings.cs index 1c90826fda0..3e3eba8ef8d 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/QueueReceiveSettings.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/AmazonSqsTransport/Topology/QueueReceiveSettings.cs @@ -19,6 +19,8 @@ public QueueReceiveSettings(IAmazonSqsEndpointConfiguration configuration, strin VisibilityTimeout = 30; RedeliverVisibilityTimeout = 0; + ConcurrentDeliveryLimit = 1; + if (AmazonSqsEndpointAddress.IsFifo(queueName)) IsOrdered = true; } @@ -26,6 +28,8 @@ public QueueReceiveSettings(IAmazonSqsEndpointConfiguration configuration, strin public int PrefetchCount => _configuration.Transport.PrefetchCount; public int ConcurrentMessageLimit => _configuration.Transport.GetConcurrentMessageLimit(); + public int ConcurrentDeliveryLimit { get; set; } + public int WaitTimeSeconds { get; set; } public bool PurgeOnStartup { get; set; } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsBusFactoryConfiguratorExtensions.cs b/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsBusFactoryConfiguratorExtensions.cs index d2962461cf0..9dffb3d0a48 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsBusFactoryConfiguratorExtensions.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsBusFactoryConfiguratorExtensions.cs @@ -1,6 +1,9 @@ -namespace MassTransit +#nullable enable +namespace MassTransit { using System; + using Amazon; + using Amazon.Runtime; using Amazon.SimpleNotificationService; using Amazon.SQS; using AmazonSqsTransport.Configuration; @@ -22,14 +25,18 @@ public static IBusControl CreateUsingAmazonSqs(this IBusFactorySelector selector /// The registration configurator (configured via AddMassTransit) /// The configuration callback for the bus factory public static void UsingAmazonSqs(this IBusRegistrationConfigurator configurator, - Action configure = null) + Action? configure = null) { configurator.SetBusFactory(new AmazonSqsRegistrationBusFactory(configure)); } + /// + /// Configure the transport to use Localstack (hosted in Docker, on the default port of 4566 + /// + /// public static void LocalstackHost(this IAmazonSqsBusFactoryConfigurator configurator) { - configurator.Host(new Uri("amazonsqs://localhost:4576"), h => + configurator.Host(new Uri("amazonsqs://localhost:4566"), h => { h.AccessKey("admin"); h.SecretKey("admin"); @@ -38,5 +45,32 @@ public static void LocalstackHost(this IAmazonSqsBusFactoryConfigurator configur h.Config(new AmazonSimpleNotificationServiceConfig { ServiceURL = "http://localhost:4566" }); }); } + + /// + /// Configure the default Amazon SQS Host, using the FallbackRegionFactory and FallbackCredentialsFactory + /// + /// + /// + public static void UseDefaultHost(this IAmazonSqsBusFactoryConfigurator configurator, Action? configure = null) + { + configurator.UseDefaultHost(FallbackRegionFactory.GetRegionEndpoint(), configure); + } + + /// + /// Configure the default Amazon SQS Host, using the FallbackCredentialsFactory with the specified region + /// + /// + /// The region for the host + /// + public static void UseDefaultHost(this IAmazonSqsBusFactoryConfigurator configurator, RegionEndpoint endpoint, + Action? configure = null) + { + configurator.Host(endpoint.SystemName, h => + { + h.Credentials(FallbackCredentialsFactory.GetCredentials()); + + configure?.Invoke(h); + }); + } } } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsHostConfigurationExtensions.cs b/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsHostConfigurationExtensions.cs index 23820825a9b..ad3d33a3dcc 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsHostConfigurationExtensions.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsHostConfigurationExtensions.cs @@ -48,9 +48,7 @@ public static void ReceiveEndpoint(this IAmazonSqsBusFactoryConfigurator configu } /// - /// Declare a ReceiveEndpoint using a unique generated queue name. This queue defaults to auto-delete - /// and non-durable. By default all services bus instances include a default receiveEndpoint that is - /// of this type (created automatically upon the first receiver binding). + /// Declare a receive endpoint using the endpoint . /// /// /// diff --git a/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsMessageSchedulerExtensions.cs b/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsMessageSchedulerExtensions.cs index 20531e298bb..fe02e791209 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsMessageSchedulerExtensions.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/Configuration/AmazonSqsMessageSchedulerExtensions.cs @@ -27,7 +27,7 @@ public static void UseAmazonSqsMessageScheduler(this IBusFactoryConfigurator con /// /// [Obsolete("Use the transport independent AddDelayedMessageScheduler")] - public static void AddAmazonSqsMessageScheduler(this IRegistrationConfigurator configurator) + public static void AddAmazonSqsMessageScheduler(this IBusRegistrationConfigurator configurator) { configurator.AddDelayedMessageScheduler(); } diff --git a/src/Transports/MassTransit.AmazonSqsTransport/Configuration/IAmazonSqsReceiveEndpointConfigurator.cs b/src/Transports/MassTransit.AmazonSqsTransport/Configuration/IAmazonSqsReceiveEndpointConfigurator.cs index a0d0c42fffb..e1d1efcd44e 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/Configuration/IAmazonSqsReceiveEndpointConfigurator.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/Configuration/IAmazonSqsReceiveEndpointConfigurator.cs @@ -17,6 +17,12 @@ public interface IAmazonSqsReceiveEndpointConfigurator : /// int RedeliverVisibilityTimeout { set; } + /// + /// Set number of concurrent messages per MessageGroupId, higher value will increase throughput but will break delivery order (default: 1). + /// This applies to FIFO queues only. + /// + int ConcurrentDeliveryLimit { set; } + /// /// Bind an existing exchange for the message type to the receive endpoint by name /// diff --git a/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsConnectException.cs b/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsConnectException.cs index 6e9088e1026..07fe9a632fb 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsConnectException.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsConnectException.cs @@ -22,6 +22,9 @@ public AmazonSqsConnectException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected AmazonSqsConnectException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsConnectionException.cs b/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsConnectionException.cs index 837485936dc..6e56c8c9e36 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsConnectionException.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsConnectionException.cs @@ -22,6 +22,9 @@ public AmazonSqsConnectionException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected AmazonSqsConnectionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsTransportConfigurationException.cs b/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsTransportConfigurationException.cs index 5b164c77d49..d1df06d9e69 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsTransportConfigurationException.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsTransportConfigurationException.cs @@ -22,6 +22,9 @@ public AmazonSqsTransportConfigurationException(string message, Exception innerE { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected AmazonSqsTransportConfigurationException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsTransportException.cs b/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsTransportException.cs index b21613f8a30..321a097c4b5 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsTransportException.cs +++ b/src/Transports/MassTransit.AmazonSqsTransport/Exceptions/AmazonSqsTransportException.cs @@ -22,6 +22,9 @@ public AmazonSqsTransportException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected AmazonSqsTransportException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.AmazonSqsTransport/MassTransit.AmazonSqsTransport.csproj b/src/Transports/MassTransit.AmazonSqsTransport/MassTransit.AmazonSqsTransport.csproj index d4a73ee1b8f..417fcc0009f 100644 --- a/src/Transports/MassTransit.AmazonSqsTransport/MassTransit.AmazonSqsTransport.csproj +++ b/src/Transports/MassTransit.AmazonSqsTransport/MassTransit.AmazonSqsTransport.csproj @@ -2,11 +2,11 @@ - netstandard2.0;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -23,7 +23,6 @@ - diff --git a/src/Transports/MassTransit.AmazonSqsTransport/NullableAttributes.cs b/src/Transports/MassTransit.AmazonSqsTransport/NullableAttributes.cs new file mode 100644 index 00000000000..3f38561b675 --- /dev/null +++ b/src/Transports/MassTransit.AmazonSqsTransport/NullableAttributes.cs @@ -0,0 +1,24 @@ +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using System; + + + [AttributeUsage(AttributeTargets.Parameter)] + sealed class NotNullWhenAttribute : + Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +#endif diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureBusFactory.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureBusFactory.cs index 6ed3cc8b88a..84f7e6d5bd6 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureBusFactory.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureBusFactory.cs @@ -1,7 +1,6 @@ namespace MassTransit { using System; - using System.Threading; using AzureServiceBusTransport; using AzureServiceBusTransport.Configuration; using Configuration; @@ -10,8 +9,6 @@ public static class AzureBusFactory { - public static IMessageTopologyConfigurator MessageTopology => Cached.MessageTopologyValue.Value; - /// /// Configure and create a bus for Azure Service Bus (later, we'll use Event Hubs instead) /// @@ -19,7 +16,7 @@ public static class AzureBusFactory /// public static IBusControl CreateUsingServiceBus(Action configure) { - var topologyConfiguration = new ServiceBusTopologyConfiguration(MessageTopology); + var topologyConfiguration = new ServiceBusTopologyConfiguration(CreateMessageTopology()); var busConfiguration = new ServiceBusBusConfiguration(topologyConfiguration); var configurator = new ServiceBusBusFactoryConfigurator(busConfiguration); @@ -29,17 +26,19 @@ public static IBusControl CreateUsingServiceBus(Action MessageTopologyValue = - new Lazy(() => new MessageTopology(_entityNameFormatter), LazyThreadSafetyMode.PublicationOnly); - - static readonly IEntityNameFormatter _entityNameFormatter; + internal static readonly IEntityNameFormatter EntityNameFormatter; static Cached() { - _entityNameFormatter = new MessageNameFormatterEntityNameFormatter(new ServiceBusMessageNameFormatter()); + EntityNameFormatter = new MessageNameFormatterEntityNameFormatter(new ServiceBusMessageNameFormatter()); } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ClientSettings.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ClientSettings.cs index cf92edc1531..e813e404738 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ClientSettings.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ClientSettings.cs @@ -22,8 +22,18 @@ public interface ClientSettings /// /// The timeout before a message session is abandoned + /// - if unset the SDK will use + /// + /// ServiceBusRetryOptions.TryTimeout + /// /// - TimeSpan SessionIdleTimeout { get; } + TimeSpan? SessionIdleTimeout { get; } + + /// + /// The maximum number of concurrent sessions + /// + int MaxConcurrentSessions { get; } /// /// The maximum number of concurrent calls per session diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusEndpointEntityConfigurator.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusEndpointEntityConfigurator.cs index 910b6600a5c..34254687981 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusEndpointEntityConfigurator.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusEndpointEntityConfigurator.cs @@ -17,6 +17,7 @@ public class ServiceBusEndpointEntityConfigurator : public bool? RequiresSession { get; set; } + public int? MaxConcurrentSessions { get; set; } public int? MaxConcurrentCallsPerSession { get; set; } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusEntityReceiveEndpointConfiguration.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusEntityReceiveEndpointConfiguration.cs index b8721aeb994..b634de6779e 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusEntityReceiveEndpointConfiguration.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusEntityReceiveEndpointConfiguration.cs @@ -80,6 +80,11 @@ public bool RequiresSession set => _configurator.RequiresSession = value; } + public int MaxConcurrentSessions + { + set => _configurator.MaxConcurrentSessions = value; + } + public int MaxConcurrentCallsPerSession { set => _configurator.MaxConcurrentCallsPerSession = value; @@ -95,7 +100,7 @@ public TimeSpan MessageWaitTimeout set => _settings.SessionIdleTimeout = value; } - public TimeSpan SessionIdleTimeout + public TimeSpan? SessionIdleTimeout { set => _settings.SessionIdleTimeout = value; } @@ -127,11 +132,10 @@ protected void CreateReceiveEndpoint(IHost host, ServiceBusReceiveEndpointContex ClientPipeConfigurator.UseFilter(new TransportReadyFilter(receiveEndpointContext)); else { - var messageReceiver = new ServiceBusMessageReceiver(receiveEndpointContext); - + ClientPipeConfigurator.UseFilter(new ReceiveEndpointDependencyFilter(receiveEndpointContext)); ClientPipeConfigurator.UseFilter(_settings.RequiresSession - ? new MessageSessionReceiverFilter(messageReceiver, receiveEndpointContext) - : new MessageReceiverFilter(messageReceiver, receiveEndpointContext)); + ? new MessageSessionReceiverFilter(receiveEndpointContext) + : new MessageReceiverFilter(receiveEndpointContext)); } IPipe clientPipe = ClientPipeConfigurator.Build(); diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusQueueConfigurator.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusQueueConfigurator.cs index d3eaa502433..745e29fd021 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusQueueConfigurator.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/ServiceBusQueueConfigurator.cs @@ -32,6 +32,7 @@ public ServiceBusQueueConfigurator(string path) public bool? RequiresSession { get; set; } + public int? MaxConcurrentSessions { get; set; } public int? MaxConcurrentCallsPerSession { get; set; } public IEnumerable Validate() diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/IPartitionKeySendTopologyConvention.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/IPartitionKeySendTopologyConvention.cs deleted file mode 100644 index 170b225803a..00000000000 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/IPartitionKeySendTopologyConvention.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit.AzureServiceBusTransport.Configuration -{ - using MassTransit.Configuration; - - - public interface IPartitionKeySendTopologyConvention : - ISendTopologyConvention - { - /// - /// The default, non-message specific routing key formatter used by messages - /// when no specific convention has been specified. - /// - IPartitionKeyFormatter DefaultFormatter { get; set; } - } -} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SessionIdMessageSendTopologyConvention.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SessionIdMessageSendTopologyConvention.cs index 3a6ae42cac6..856d104acb5 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SessionIdMessageSendTopologyConvention.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SessionIdMessageSendTopologyConvention.cs @@ -1,6 +1,7 @@ namespace MassTransit.AzureServiceBusTransport.Configuration { using MassTransit.Configuration; + using Topology; public class SessionIdMessageSendTopologyConvention : diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SetPartitionKeyMessageSendTopology.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SetPartitionKeyMessageSendTopology.cs deleted file mode 100644 index d4d30d818b2..00000000000 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SetPartitionKeyMessageSendTopology.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace MassTransit.AzureServiceBusTransport.Configuration -{ - using System; - using System.Threading.Tasks; - using MassTransit.Configuration; - using Middleware; - - - public class SetPartitionKeyMessageSendTopology : - IMessageSendTopology - where T : class - { - readonly IFilter> _filter; - - public SetPartitionKeyMessageSendTopology(IMessagePartitionKeyFormatter partitionKeyFormatter) - { - if (partitionKeyFormatter == null) - throw new ArgumentNullException(nameof(partitionKeyFormatter)); - - _filter = new Proxy(new SetPartitionKeyFilter(partitionKeyFormatter)); - } - - public void Apply(ITopologyPipeBuilder> builder) - { - builder.AddFilter(_filter); - } - - - class Proxy : - IFilter> - { - readonly IFilter> _filter; - - public Proxy(IFilter> filter) - { - _filter = filter; - } - - public Task Send(SendContext context, IPipe> next) - { - var serviceBusSendContext = context.GetPayload>(); - - return _filter.Send(serviceBusSendContext, next); - } - - public void Probe(ProbeContext context) - { - } - } - } -} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SetSessionIdMessageSendTopology.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SetSessionIdMessageSendTopology.cs deleted file mode 100644 index 7e9bf5028d3..00000000000 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Configuration/Topology/SetSessionIdMessageSendTopology.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace MassTransit.AzureServiceBusTransport.Configuration -{ - using System; - using System.Threading.Tasks; - using MassTransit.Configuration; - using Middleware; - - - public class SetSessionIdMessageSendTopology : - IMessageSendTopology - where T : class - { - readonly IFilter> _filter; - - public SetSessionIdMessageSendTopology(IMessageSessionIdFormatter sessionIdFormatter) - { - if (sessionIdFormatter == null) - throw new ArgumentNullException(nameof(sessionIdFormatter)); - - _filter = new Proxy(new SetSessionIdFilter(sessionIdFormatter)); - } - - public void Apply(ITopologyPipeBuilder> builder) - { - builder.AddFilter(_filter); - } - - - class Proxy : - IFilter> - { - readonly IFilter> _filter; - - public Proxy(IFilter> filter) - { - _filter = filter; - } - - public Task Send(SendContext context, IPipe> next) - { - var sendContext = context.GetPayload>(); - - return _filter.Send(sendContext, next); - } - - public void Probe(ProbeContext context) - { - } - } - } -} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ConnectionContextFactory.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ConnectionContextFactory.cs index e019d0ea160..d618e981982 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ConnectionContextFactory.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ConnectionContextFactory.cs @@ -67,34 +67,44 @@ async Task CreateConnection(ISupervisor supervisor) EnableCrossEntityTransactions = false, }; + var managementOptions = new ServiceBusAdministrationClientOptions + { + Retry = + { + MaxRetries = settings.RetryLimit, + Mode = Azure.Core.RetryMode.Exponential, + MaxDelay = settings.RetryMaxBackoff + } + }; + if (settings.TokenCredential != null) { client ??= new ServiceBusClient(endpoint, settings.TokenCredential, clientOptions); - managementClient ??= new ServiceBusAdministrationClient(endpoint, settings.TokenCredential); + managementClient ??= new ServiceBusAdministrationClient(endpoint, settings.TokenCredential, managementOptions); } else if (settings.NamedKeyCredential != null) { client ??= new ServiceBusClient(endpoint, settings.NamedKeyCredential, clientOptions); - managementClient ??= new ServiceBusAdministrationClient(endpoint, settings.NamedKeyCredential); + managementClient ??= new ServiceBusAdministrationClient(endpoint, settings.NamedKeyCredential, managementOptions); } else if (settings.SasCredential != null) { client ??= new ServiceBusClient(endpoint, settings.SasCredential, clientOptions); - managementClient ??= new ServiceBusAdministrationClient(endpoint, settings.SasCredential); + managementClient ??= new ServiceBusAdministrationClient(endpoint, settings.SasCredential, managementOptions); } else { if (settings.ConnectionString != null && HasSharedAccess(settings.ConnectionString)) { client ??= new ServiceBusClient(settings.ConnectionString, clientOptions); - managementClient ??= new ServiceBusAdministrationClient(settings.ConnectionString); + managementClient ??= new ServiceBusAdministrationClient(settings.ConnectionString, managementOptions); } else { var defaultAzureCredential = new DefaultAzureCredential(); client ??= new ServiceBusClient(endpoint, defaultAzureCredential, clientOptions); - managementClient ??= new ServiceBusAdministrationClient(endpoint, defaultAzureCredential); + managementClient ??= new ServiceBusAdministrationClient(endpoint, defaultAzureCredential, managementOptions); } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/AzureServiceBusSendContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/AzureServiceBusSendContext.cs index 1e2a8bcebbe..42fc8151254 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/AzureServiceBusSendContext.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/AzureServiceBusSendContext.cs @@ -42,6 +42,7 @@ public override TimeSpan? Delay } public string ReplyToSessionId { get; set; } + public string ReplyTo { get; set; } public DateTime? ScheduledEnqueueTimeUtc { get; set; } @@ -110,19 +111,29 @@ public bool TryGetSequenceNumber(Guid id, out long sequenceNumber) return false; } - public long GetSequenceNumber(Guid scheduledMessageId) - { - return BitConverter.ToInt64(scheduledMessageId.ToByteArray(), 0); - } - public override void ReadPropertiesFrom(IReadOnlyDictionary properties) { base.ReadPropertiesFrom(properties); - PartitionKey = ReadString(properties, PropertyNames.PartitionKey); - SessionId = ReadString(properties, PropertyNames.SessionId); - ReplyToSessionId = ReadString(properties, PropertyNames.ReplyToSessionId); - Label = ReadString(properties, PropertyNames.Label); + var partitionKey = ReadString(properties, AzureServiceBusTransportPropertyNames.PartitionKey); + if (!string.IsNullOrWhiteSpace(partitionKey)) + PartitionKey = partitionKey; + + var sessionId = ReadString(properties, AzureServiceBusTransportPropertyNames.SessionId); + if (!string.IsNullOrWhiteSpace(sessionId)) + SessionId = sessionId; + + var replyToSessionId = ReadString(properties, AzureServiceBusTransportPropertyNames.ReplyToSessionId); + if (!string.IsNullOrWhiteSpace(replyToSessionId)) + ReplyToSessionId = replyToSessionId; + + var replyTo = ReadString(properties, AzureServiceBusTransportPropertyNames.ReplyTo); + if (!string.IsNullOrWhiteSpace(replyTo)) + ReplyTo = replyTo; + + var label = ReadString(properties, AzureServiceBusTransportPropertyNames.Label); + if (!string.IsNullOrWhiteSpace(label)) + Label = label; } public override void WritePropertiesTo(IDictionary properties) @@ -130,22 +141,15 @@ public override void WritePropertiesTo(IDictionary properties) base.WritePropertiesTo(properties); if (!string.IsNullOrWhiteSpace(PartitionKey)) - properties[PropertyNames.PartitionKey] = PartitionKey; + properties[AzureServiceBusTransportPropertyNames.PartitionKey] = PartitionKey; if (!string.IsNullOrWhiteSpace(SessionId)) - properties[PropertyNames.SessionId] = SessionId; + properties[AzureServiceBusTransportPropertyNames.SessionId] = SessionId; if (!string.IsNullOrWhiteSpace(ReplyToSessionId)) - properties[PropertyNames.ReplyToSessionId] = ReplyToSessionId; + properties[AzureServiceBusTransportPropertyNames.ReplyToSessionId] = ReplyToSessionId; + if (!string.IsNullOrWhiteSpace(ReplyTo)) + properties[AzureServiceBusTransportPropertyNames.ReplyTo] = ReplyTo; if (!string.IsNullOrWhiteSpace(Label)) - properties[PropertyNames.Label] = Label; - } - - - static class PropertyNames - { - public const string PartitionKey = "ASB-PartitionKey"; - public const string SessionId = "ASB-SessionId"; - public const string ReplyToSessionId = "ASB-ReplyToSessionId"; - public const string Label = "ASB-Label"; + properties[AzureServiceBusTransportPropertyNames.Label] = Label; } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/AzureServiceBusTransportPropertyNames.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/AzureServiceBusTransportPropertyNames.cs new file mode 100644 index 00000000000..78d282322e6 --- /dev/null +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/AzureServiceBusTransportPropertyNames.cs @@ -0,0 +1,11 @@ +namespace MassTransit.AzureServiceBusTransport +{ + static class AzureServiceBusTransportPropertyNames + { + public const string PartitionKey = "ASB-PartitionKey"; + public const string SessionId = "ASB-SessionId"; + public const string ReplyToSessionId = "ASB-ReplyToSessionId"; + public const string ReplyTo = "ASB-ReplyTo"; + public const string Label = "ASB-Label"; + } +} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/QueueClientContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/QueueClientContext.cs index 966c847847c..84203339fff 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/QueueClientContext.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/QueueClientContext.cs @@ -4,6 +4,7 @@ namespace MassTransit.AzureServiceBusTransport using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; + using Internals; using MassTransit.Middleware; @@ -104,7 +105,10 @@ public async Task CloseAsync() public Task NotifyFaulted(Exception exception, string entityPath) { - return _agent.Stop($"Unrecoverable exception on {entityPath}"); + Task.Run(() => _agent.Stop($"Unrecoverable exception on {entityPath}")) + .IgnoreUnobservedExceptions(); + + return Task.CompletedTask; } public async ValueTask DisposeAsync() diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusConnectionContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusConnectionContext.cs index 8b753c94622..39cf4459491 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusConnectionContext.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusConnectionContext.cs @@ -148,16 +148,28 @@ string NormalizeForwardTo(string forwardTo) { return string.IsNullOrEmpty(forwardTo) ? string.Empty - : forwardTo.Replace(Endpoint.ToString(), string.Empty).Trim('/'); + : Uri.IsWellFormedUriString(forwardTo, UriKind.Absolute) + ? new Uri(forwardTo).AbsolutePath.TrimStart('/') + : forwardTo.Replace(Endpoint.ToString(), string.Empty).Trim('/'); } var targetForwardTo = NormalizeForwardTo(createSubscriptionOptions.ForwardTo); var currentForwardTo = NormalizeForwardTo(subscriptionProperties.ForwardTo); - if (!targetForwardTo.Equals(currentForwardTo)) + if (!targetForwardTo.Equals(currentForwardTo) + || createSubscriptionOptions.LockDuration != subscriptionProperties.LockDuration + || createSubscriptionOptions.MaxDeliveryCount != subscriptionProperties.MaxDeliveryCount + || createSubscriptionOptions.EnableBatchedOperations != subscriptionProperties.EnableBatchedOperations + || createSubscriptionOptions.DeadLetteringOnMessageExpiration != subscriptionProperties.DeadLetteringOnMessageExpiration) { LogContext.Debug?.Log("Updating subscription: {Subscription} ({Topic} -> {ForwardTo})", subscriptionProperties.SubscriptionName, - subscriptionProperties.TopicName, subscriptionProperties.ForwardTo); + createSubscriptionOptions.TopicName, createSubscriptionOptions.ForwardTo); + + subscriptionProperties.ForwardTo = createSubscriptionOptions.ForwardTo; + subscriptionProperties.LockDuration = createSubscriptionOptions.LockDuration; + subscriptionProperties.MaxDeliveryCount = createSubscriptionOptions.MaxDeliveryCount; + subscriptionProperties.EnableBatchedOperations = createSubscriptionOptions.EnableBatchedOperations; + subscriptionProperties.DeadLetteringOnMessageExpiration = createSubscriptionOptions.DeadLetteringOnMessageExpiration; await UpdateSubscriptionAsync(subscriptionProperties).ConfigureAwait(false); } @@ -277,7 +289,7 @@ static ServiceBusSessionProcessorOptions GetSessionProcessorOptions(ClientSettin PrefetchCount = settings.PrefetchCount, MaxAutoLockRenewalDuration = settings.MaxAutoRenewDuration, ReceiveMode = ServiceBusReceiveMode.PeekLock, - MaxConcurrentSessions = settings.MaxConcurrentCalls, + MaxConcurrentSessions = settings.MaxConcurrentSessions, MaxConcurrentCallsPerSession = settings.MaxConcurrentCallsPerSession, SessionIdleTimeout = settings.SessionIdleTimeout }; diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusHeaderProvider.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusHeaderProvider.cs index acc1c33533e..55a66cfeaae 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusHeaderProvider.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusHeaderProvider.cs @@ -57,6 +57,12 @@ public bool TryGetHeader(string key, out object value) return !string.IsNullOrWhiteSpace(_message.CorrelationId); } + if (MessageHeaders.TransportSentTime.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + value = _message.EnqueuedTime.UtcDateTime; + return true; + } + if (MessageHeaders.ContentType.Equals(key, StringComparison.OrdinalIgnoreCase)) { value = _message.ContentType; diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusReceiveContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusReceiveContext.cs index f5b4c261507..76681c0c0f3 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusReceiveContext.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusReceiveContext.cs @@ -4,17 +4,20 @@ using System.Collections.Generic; using System.Net.Mime; using Azure.Messaging.ServiceBus; + using Context; using Transports; public sealed class ServiceBusReceiveContext : BaseReceiveContext, - ServiceBusMessageContext + ServiceBusMessageContext, + TransportReceiveContext, + ITransportSequenceNumber { readonly ServiceBusReceivedMessage _message; - public ServiceBusReceiveContext(ServiceBusReceivedMessage message, ReceiveEndpointContext receiveEndpointContext) - : base(message.DeliveryCount > 1, receiveEndpointContext) + public ServiceBusReceiveContext(ServiceBusReceivedMessage message, ReceiveEndpointContext receiveEndpointContext, params object[] payloads) + : base(message.DeliveryCount > 1, receiveEndpointContext, payloads) { _message = message; @@ -39,6 +42,7 @@ public ServiceBusReceiveContext(ServiceBusReceivedMessage message, ReceiveEndpoi public string Label => _message.Subject; + ulong? ITransportSequenceNumber.SequenceNumber => (ulong)SequenceNumber; public long SequenceNumber => _message.SequenceNumber; public long EnqueuedSequenceNumber => _message.EnqueuedSequenceNumber; @@ -63,6 +67,22 @@ public ServiceBusReceiveContext(ServiceBusReceivedMessage message, ReceiveEndpoi public DateTime ScheduledEnqueueTime => _message.ScheduledEnqueueTime.UtcDateTime; + public IDictionary GetTransportProperties() + { + var properties = new Lazy>(() => new Dictionary()); + + if (!string.IsNullOrWhiteSpace(PartitionKey)) + properties.Value[AzureServiceBusTransportPropertyNames.PartitionKey] = PartitionKey; + if (!string.IsNullOrWhiteSpace(SessionId)) + properties.Value[AzureServiceBusTransportPropertyNames.SessionId] = SessionId; + if (!string.IsNullOrWhiteSpace(ReplyToSessionId)) + properties.Value[AzureServiceBusTransportPropertyNames.ReplyToSessionId] = ReplyToSessionId; + if (!string.IsNullOrWhiteSpace(Label)) + properties.Value[AzureServiceBusTransportPropertyNames.Label] = Label; + + return properties.IsValueCreated ? properties.Value : null; + } + protected override ContentType GetContentType() { ContentType contentType = default; diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusSessionMessageLockContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusSessionMessageLockContext.cs index a04a79b081b..fd09b717ff0 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusSessionMessageLockContext.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/ServiceBusSessionMessageLockContext.cs @@ -36,16 +36,22 @@ public Task Abandon(Exception exception) public async Task DeadLetter() { - var headers = new Dictionary { { MessageHeaders.Reason, "dead-letter" } }; + const string reason = "dead-letter"; - await _session.DeadLetterMessageAsync(_message, headers).ConfigureAwait(false); + var headers = new Dictionary { { MessageHeaders.Reason, reason } }; + + await _session.DeadLetterMessageAsync(_message, headers, reason).ConfigureAwait(false); _deadLettered = true; } public async Task DeadLetter(Exception exception) { - await _session.DeadLetterMessageAsync(_message, ExceptionUtil.GetExceptionHeaderDictionary(exception)).ConfigureAwait(false); + const string reason = "fault"; + + (Dictionary dictionary, var message) = ExceptionUtil.GetExceptionHeaderDetail(exception); + + await _session.DeadLetterMessageAsync(_message, dictionary, reason, message).ConfigureAwait(false); _deadLettered = true; } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/SubscriptionClientContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/SubscriptionClientContext.cs index bd0d6e4a7c6..23834c0ffe6 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/SubscriptionClientContext.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Contexts/SubscriptionClientContext.cs @@ -4,6 +4,7 @@ namespace MassTransit.AzureServiceBusTransport using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; + using Internals; using MassTransit.Middleware; @@ -106,7 +107,10 @@ public async Task CloseAsync() public Task NotifyFaulted(Exception exception, string entityPath) { - return _agent.Stop($"Unrecoverable exception on {entityPath}"); + Task.Run(() => _agent.Stop($"Unrecoverable exception on {entityPath}")) + .IgnoreUnobservedExceptions(); + + return Task.CompletedTask; } public async ValueTask DisposeAsync() diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Defaults.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Defaults.cs index 769a3928885..bfd097a2ed6 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Defaults.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Defaults.cs @@ -16,16 +16,12 @@ public static class Defaults public static TimeSpan TemporaryAutoDeleteOnIdle { get; set; } = TimeSpan.FromMinutes(5); public static TimeSpan MaxAutoRenewDuration { get; set; } = TimeSpan.FromMinutes(5); - [Obsolete("use SessionIdleTimeout instead")] - public static TimeSpan MessageWaitTimeout - { - get => SessionIdleTimeout; - set => SessionIdleTimeout = value; - } - - public static TimeSpan SessionIdleTimeout { get; set; } = TimeSpan.FromSeconds(10); + public static TimeSpan? SessionIdleTimeout { get; set; } = null; // SDKs default is undefined - explicitly defined here for clarity public static TimeSpan ShutdownTimeout { get; set; } = TimeSpan.FromMilliseconds(100); + public static int MaxConcurrentSessions { get; set; } = 8; + public static int MaxConcurrentCallsPerSessions { get; set; } = 1; + public static CreateQueueOptions GetCreateQueueOptions(string queueName) { return new CreateQueueOptions(queueName) diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/EmptyPartitionKeyFormatter.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/EmptyPartitionKeyFormatter.cs deleted file mode 100644 index aebc4592894..00000000000 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/EmptyPartitionKeyFormatter.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MassTransit.AzureServiceBusTransport -{ - public class EmptyPartitionKeyFormatter : - IPartitionKeyFormatter - { - string IPartitionKeyFormatter.FormatPartitionKey(SendContext context) - { - return null; - } - } -} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IPartitionKeyFormatter.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IPartitionKeyFormatter.cs deleted file mode 100644 index f8020b842dc..00000000000 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IPartitionKeyFormatter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MassTransit.AzureServiceBusTransport -{ - public interface IPartitionKeyFormatter - { - string FormatPartitionKey(SendContext context) - where T : class; - } -} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IReceiver.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IReceiver.cs index 9e283825cba..2f1e814eca3 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IReceiver.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IReceiver.cs @@ -1,14 +1,12 @@ namespace MassTransit.AzureServiceBusTransport { - using System.Threading.Tasks; using Transports; public interface IReceiver : - IAgent + IAgent, + DeliveryMetrics { - DeliveryMetrics GetDeliveryMetrics(); - - Task Start(); + void Start(); } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IServiceBusMessageReceiver.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IServiceBusMessageReceiver.cs index ed347911e29..90864ddf542 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IServiceBusMessageReceiver.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/IServiceBusMessageReceiver.cs @@ -1,28 +1,18 @@ namespace MassTransit.AzureServiceBusTransport { - using System; using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; - using Transports; - public interface IServiceBusMessageReceiver : - IDispatchMetrics, - IReceiveObserverConnector, - IPublishObserverConnector, - ISendObserverConnector, - IConsumeMessageObserverConnector, - IConsumeObserverConnector, - IProbeSite + public interface IServiceBusMessageReceiver { /// /// Handles the /// /// /// Specify an optional cancellationToken - /// Callback to adjust the context /// - Task Handle(ServiceBusReceivedMessage message, CancellationToken cancellationToken = default, Action contextCallback = null); + Task Handle(ServiceBusReceivedMessage message, CancellationToken cancellationToken = default); } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessageSessionSagaConsumeContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessageSessionSagaConsumeContext.cs deleted file mode 100644 index 80ca08a42e9..00000000000 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessageSessionSagaConsumeContext.cs +++ /dev/null @@ -1,44 +0,0 @@ -namespace MassTransit.AzureServiceBusTransport -{ - using System; - using System.Threading.Tasks; - using Context; - - - public class MessageSessionSagaConsumeContext : - ConsumeContextScope, - SagaConsumeContext - where TMessage : class - where TSaga : class, ISaga - { - readonly MessageSessionContext _sessionContext; - - public MessageSessionSagaConsumeContext(ConsumeContext context, MessageSessionContext sessionContext, TSaga instance) - : base(context) - { - _sessionContext = sessionContext; - - Saga = instance; - } - - public override Guid? CorrelationId => Saga.CorrelationId; - - public async Task SetCompleted() - { - await RemoveState().ConfigureAwait(false); - - IsCompleted = true; - - LogContext.Debug?.Log("SAGA:{SagaType}:{CorrelationId} Removed {MessageType}", TypeCache.ShortName, Saga.CorrelationId, - TypeCache.ShortName); - } - - public bool IsCompleted { get; private set; } - public TSaga Saga { get; } - - Task RemoveState() - { - return _sessionContext.SetStateAsync(BinaryData.FromBytes(Array.Empty())); - } - } -} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessageSessionSagaRepositoryContextFactory.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessageSessionSagaRepositoryContextFactory.cs index bb9a26a5ec4..911203a05a3 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessageSessionSagaRepositoryContextFactory.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/MessageSessionSagaRepositoryContextFactory.cs @@ -1,7 +1,6 @@ namespace MassTransit.AzureServiceBusTransport { using System; - using System.Threading; using System.Threading.Tasks; using Saga; @@ -36,12 +35,5 @@ public async Task SendQuery(ConsumeContext context, ISagaQuery quer throw new NotImplementedException( $"Query-based saga correlation is not available when using the MessageSession-based saga repository: {TypeCache.ShortName}"); } - - public async Task Execute(Func, Task> asyncMethod, CancellationToken cancellationToken = default) - where T : class - { - throw new NotImplementedException( - $"Queries are not supported using the MessageSession-based saga repository: {TypeCache.ShortName}"); - } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/ConfigureServiceBusTopologyFilter.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/ConfigureServiceBusTopologyFilter.cs index 44e1038c51a..15b595b48c6 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/ConfigureServiceBusTopologyFilter.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/ConfigureServiceBusTopologyFilter.cs @@ -1,5 +1,6 @@ namespace MassTransit.AzureServiceBusTransport.Middleware { + using System; using System.Linq; using System.Threading.Tasks; using Logging; @@ -27,9 +28,18 @@ public ConfigureServiceBusTopologyFilter(TSettings settings, BrokerTopology brok public async Task Send(ClientContext context, IPipe next) { - await ConfigureTopology(context).ConfigureAwait(false); + OneTimeContext> oneTimeContext = await ConfigureTopology(context).ConfigureAwait(false); - await next.Send(context).ConfigureAwait(false); + try + { + await next.Send(context).ConfigureAwait(false); + } + catch (Exception) + { + oneTimeContext.Evict(); + + throw; + } } public void Probe(ProbeContext context) @@ -41,22 +51,33 @@ public void Probe(ProbeContext context) public async Task Send(SendEndpointContext context, IPipe next) { - await ConfigureTopology(context).ConfigureAwait(false); + OneTimeContext> oneTimeContext = await ConfigureTopology(context).ConfigureAwait(false); - await next.Send(context).ConfigureAwait(false); + try + { + await next.Send(context).ConfigureAwait(false); + } + catch (Exception) + { + oneTimeContext.Evict(); + + throw; + } } - async Task ConfigureTopology(NamespaceContext context) + async Task>> ConfigureTopology(NamespaceContext context) { - await context.OneTimeSetup>(async payload => + OneTimeContext> oneTimeContext = await context.OneTimeSetup>(() => { - await ConfigureTopology(context.ConnectionContext).ConfigureAwait(false); - context.GetOrAddPayload(() => _settings); if (_context != null && _removeSubscriptions) _context.AddSendAgent(new RemoveServiceBusTopologyAgent(context.ConnectionContext, _brokerTopology)); - }, () => new Context()).ConfigureAwait(false); + + return ConfigureTopology(context.ConnectionContext); + }).ConfigureAwait(false); + + return oneTimeContext; } async Task ConfigureTopology(ConnectionContext context) diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/MessageReceiverFilter.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/MessageReceiverFilter.cs index e4182fb7f73..c125ecc3d52 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/MessageReceiverFilter.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/MessageReceiverFilter.cs @@ -10,21 +10,21 @@ namespace MassTransit.AzureServiceBusTransport.Middleware public class MessageReceiverFilter : IFilter { - readonly ServiceBusReceiveEndpointContext _context; - readonly IServiceBusMessageReceiver _messageReceiver; readonly IReceiveTransportObserver _transportObserver; + protected readonly ServiceBusReceiveEndpointContext Context; - public MessageReceiverFilter(IServiceBusMessageReceiver messageReceiver, ServiceBusReceiveEndpointContext context) + public MessageReceiverFilter(ServiceBusReceiveEndpointContext context) { - _messageReceiver = messageReceiver; _transportObserver = context.TransportObservers; - _context = context; + Context = context; } - void IProbeSite.Probe(ProbeContext context) + public virtual void Probe(ProbeContext context) { var scope = context.CreateFilterScope("messageReceiver"); - _messageReceiver.Probe(scope); + scope.Add("type", "brokeredMessage"); + + Context.ReceivePipe.Probe(scope); } async Task IFilter.Send(ClientContext context, IPipe next) @@ -32,15 +32,15 @@ async Task IFilter.Send(ClientContext context, IPipe.Send(ClientContext context, IPipe : + IFilter> + where T : class +{ + readonly IFilter> _filter; + + public ServiceBusSendContextFilter(IFilter> filter) + { + _filter = filter; + } + + public Task Send(SendContext context, IPipe> next) + { + return context.TryGetPayload(out ServiceBusSendContext serviceBusSendContext) + ? _filter.Send(serviceBusSendContext, next) + : next.Send(context); + } + + public void Probe(ProbeContext context) + { + } +} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/SetPartitionKeyFilter.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/SetPartitionKeyFilter.cs deleted file mode 100644 index 8534fa668d7..00000000000 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Middleware/SetPartitionKeyFilter.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace MassTransit.AzureServiceBusTransport.Middleware -{ - using System.Threading.Tasks; - - - public class SetPartitionKeyFilter : - IFilter> - where T : class - { - readonly IMessagePartitionKeyFormatter _partitionKeyFormatter; - - public SetPartitionKeyFilter(IMessagePartitionKeyFormatter partitionKeyFormatter) - { - _partitionKeyFormatter = partitionKeyFormatter; - } - - public Task Send(ServiceBusSendContext context, IPipe> next) - { - var partitionKey = _partitionKeyFormatter.FormatPartitionKey(context); - - if (!string.IsNullOrWhiteSpace(partitionKey)) - context.PartitionKey = partitionKey; - - return next.Send(context); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("setPartitionKey"); - } - } -} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Receiver.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Receiver.cs index ee7d373bfb2..c968e15ee89 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Receiver.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Receiver.cs @@ -5,43 +5,31 @@ using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; - using Internals; using Logging; - using MassTransit.Middleware; using Transports; - using Util; public class Receiver : - Agent, + ConsumerAgent, IReceiver { - readonly ClientContext _context; - readonly TaskCompletionSource _deliveryComplete; - readonly IServiceBusMessageReceiver _messageReceiver; + readonly ClientContext _clientContext; + readonly ServiceBusReceiveEndpointContext _context; - public Receiver(ClientContext context, IServiceBusMessageReceiver messageReceiver) + public Receiver(ClientContext clientClientContext, ServiceBusReceiveEndpointContext context) + : base(context) { + _clientContext = clientClientContext; _context = context; - _messageReceiver = messageReceiver; - messageReceiver.ZeroActivity += HandleDeliveryComplete; - - _deliveryComplete = TaskUtil.GetTask(); - } - - public DeliveryMetrics GetDeliveryMetrics() - { - return _messageReceiver.GetMetrics(); + TrySetManualConsumeTask(); } - public virtual Task Start() + public virtual void Start() { - _context.OnMessageAsync(OnMessage, ExceptionHandler); + _clientContext.OnMessageAsync(OnMessage, ExceptionHandler); - SetReady(); - - return _context.StartAsync(); + SetReady(_clientContext.StartAsync()); } protected async Task ExceptionHandler(ProcessErrorEventArgs args) @@ -63,116 +51,141 @@ protected async Task ExceptionHandler(ProcessErrorEventArgs args) _ => true }; - if (args.Exception is ServiceBusException { IsTransient: true, Reason: ServiceBusFailureReason.ServiceCommunicationProblem }) - { - LogContext.Debug?.Log(args.Exception, - "ServiceBusException on Receiver {InputAddress} during {Action} ActiveDispatchCount({activeDispatch}) ErrorRequiresRecycle({requiresRecycle})", - _context.InputAddress, args.ErrorSource, _messageReceiver.ActiveDispatchCount, requiresRecycle); - } - else if (args.Exception is WebSocketException exception) - { - LogContext.Debug?.Log(exception, - "WebSocketException on Receiver {InputAddress} code {Code} ActiveDispatchCount({activeDispatch}) ErrorRequiresRecycle({requiresRecycle})", - _context.InputAddress, exception.WebSocketErrorCode, _messageReceiver.ActiveDispatchCount, requiresRecycle); - } - else if (args.Exception is ObjectDisposedException { ObjectName: "$cbs" }) - { - // don't log this one - } - else if (args.Exception is ServiceBusException { Reason: ServiceBusFailureReason.MessageLockLost }) - { - // don't log this one - } - else if (args.Exception is ServiceBusException { Reason: ServiceBusFailureReason.SessionLockLost }) - { - // don't log this one - } - else if (!(args.Exception is OperationCanceledException) && !(args.Exception.InnerException is TimeoutException)) + switch (args.Exception) { - EnabledLogger? logger = requiresRecycle ? LogContext.Error : LogContext.Warning; + case ServiceBusException { IsTransient: true, Reason: ServiceBusFailureReason.ServiceCommunicationProblem }: + LogContext.Debug?.Log(args.Exception, + "ServiceBusException on Receiver {InputAddress} during {Action} ActiveDispatchCount({activeDispatch}) ErrorRequiresRecycle({requiresRecycle})", + _clientContext.InputAddress, args.ErrorSource, ActiveDispatchCount, requiresRecycle); + break; + case WebSocketException exception: + LogContext.Debug?.Log(exception, + "WebSocketException on Receiver {InputAddress} code {Code} ActiveDispatchCount({activeDispatch}) ErrorRequiresRecycle({requiresRecycle})", + _clientContext.InputAddress, exception.WebSocketErrorCode, ActiveDispatchCount, requiresRecycle); + break; + case ObjectDisposedException { ObjectName: "$cbs" }: + case ServiceBusException { Reason: ServiceBusFailureReason.MessageLockLost }: + case ServiceBusException { Reason: ServiceBusFailureReason.SessionLockLost }: + // don't log those + break; + default: + { + if (!(args.Exception is OperationCanceledException) && !(args.Exception.InnerException is TimeoutException)) + { + EnabledLogger? logger = requiresRecycle ? LogContext.Error : LogContext.Warning; - logger?.Log(args.Exception, - "Exception on Receiver {InputAddress} during {Action} ActiveDispatchCount({activeDispatch}) ErrorRequiresRecycle({requiresRecycle})", - _context.InputAddress, args.ErrorSource, _messageReceiver.ActiveDispatchCount, requiresRecycle); + logger?.Log(args.Exception, + "Exception on Receiver {InputAddress} during {Action} ActiveDispatchCount({activeDispatch}) ErrorRequiresRecycle({requiresRecycle})", + _clientContext.InputAddress, args.ErrorSource, ActiveDispatchCount, requiresRecycle); + } + + break; + } } if (requiresRecycle) { - await _context.NotifyFaulted(args.Exception, args.EntityPath).ConfigureAwait(false); + await _clientContext.NotifyFaulted(args.Exception, args.EntityPath).ConfigureAwait(false); - _deliveryComplete.TrySetResult(false); - - #pragma warning disable 4014 - Task.Run(() => this.Stop($"Receiver Exception: {args.Exception.Message}")); - #pragma warning restore 4014 + TrySetConsumeException(args.Exception); } } - Task HandleDeliveryComplete() - { - if (IsStopping) - _deliveryComplete.TrySetResult(true); - return Task.CompletedTask; - } - - protected override async Task StopAgent(StopContext context) + protected override async Task ActiveAndActualAgentsCompleted(StopContext context) { - await _context.ShutdownAsync().ConfigureAwait(false); - - SetCompleted(ActiveAndActualAgentsCompleted(context) - .ContinueWith(_ => Close())); + await _clientContext.ShutdownAsync().ConfigureAwait(false); - await Completed.ConfigureAwait(false); + await base.ActiveAndActualAgentsCompleted(context).ConfigureAwait(false); - LogContext.Debug?.Log("Receiver stopped: {InputAddress}", _context.InputAddress); + try + { + await _clientContext.CloseAsync().ConfigureAwait(false); + } + catch (Exception exception) + { + LogContext.Warning?.Log(exception, "Failed to close the message receiver context: {InputAddress}", _clientContext.InputAddress); + } } - async Task ActiveAndActualAgentsCompleted(StopContext context) + async Task OnMessage(ProcessMessageEventArgs messageReceiver, ServiceBusReceivedMessage message, CancellationToken cancellationToken) { - if (_messageReceiver.ActiveDispatchCount > 0) + if (IsStopping) + return; + + MessageLockContext lockContext = new ServiceBusMessageLockContext(messageReceiver, message); + var context = new ServiceBusReceiveContext(message, _context, lockContext, _clientContext); + + CancellationTokenSource cancellationTokenSource = null; + CancellationTokenRegistration timeoutRegistration = default; + CancellationTokenRegistration registration = default; + if (cancellationToken.CanBeCanceled) { - try - { - await _deliveryComplete.Task.OrCanceled(context.CancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) + void Callback() { - LogContext.Warning?.Log("Stop canceled waiting for message consumers to complete: {InputAddress}", _context.InputAddress); + if (_context.ConsumerStopTimeout.HasValue) + { + cancellationTokenSource = new CancellationTokenSource(_context.ConsumerStopTimeout.Value); + timeoutRegistration = cancellationTokenSource.Token.Register(context.Cancel); + } + else + context.Cancel(); } + + registration = cancellationToken.Register(Callback); } - } - async Task Close() - { + try { - await _context.CloseAsync().ConfigureAwait(false); + await Dispatch(message, context, lockContext).ConfigureAwait(false); } - catch (Exception exception) + catch (Exception) { - LogContext.Warning?.Log(exception, "Failed to close the message receiver context: {InputAddress}", _context.InputAddress); + // do NOT let exceptions propagate to the Azure SDK + } + finally + { + timeoutRegistration.Dispose(); + registration.Dispose(); + + cancellationTokenSource?.Dispose(); + + context.Dispose(); } } - async Task OnMessage(ProcessMessageEventArgs messageReceiver, ServiceBusReceivedMessage message, CancellationToken cancellationToken) + protected async Task Dispatch(ServiceBusReceivedMessage message, ServiceBusReceiveContext context, MessageLockContext lockContext) { try { - await _messageReceiver.Handle(message, cancellationToken, context => AddReceiveContextPayloads(context, messageReceiver, message)) + await Dispatch(context.SequenceNumber, context, new ServiceBusReceiveLockContext(_context.InputAddress, lockContext, message)) .ConfigureAwait(false); } - catch (Exception) + catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.SessionLockLost) { - // do NOT let exceptions propagate to the Azure SDK - } - } + LogContext.Error?.Log("Session Lock Lost: {InputAddress} {MessageId} {SequenceNumber} ({SessionId})", _context.InputAddress, + message.MessageId, message.SequenceNumber, message.SessionId); - void AddReceiveContextPayloads(ReceiveContext receiveContext, ProcessMessageEventArgs receiverClient, ServiceBusReceivedMessage message) - { - MessageLockContext lockContext = new ServiceBusMessageLockContext(receiverClient, message); + await _context.ReceiveObservers.ReceiveFault(context, ex).ConfigureAwait(false); + throw; + } + catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.MessageLockLost) + { + LogContext.Error?.Log("Message Lock Lost: {InputAddress} {MessageId} {SequenceNumber}", _context.InputAddress, message.MessageId, + message.SequenceNumber); - receiveContext.GetOrAddPayload(() => lockContext); - receiveContext.GetOrAddPayload(() => _context); + await _context.ReceiveObservers.ReceiveFault(context, ex).ConfigureAwait(false); + throw; + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception exception) + { + context.LogTransportFaulted(exception); + throw; + } } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusMessageNameFormatter.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusMessageNameFormatter.cs index 38f5d06ceb6..e0dd4d60f3c 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusMessageNameFormatter.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusMessageNameFormatter.cs @@ -16,9 +16,9 @@ public ServiceBusMessageNameFormatter(string namespaceSeparator = default) : new DefaultMessageNameFormatter("---", "--", namespaceSeparator, "-"); } - public MessageName GetMessageName(Type type) + public string GetMessageName(Type type) { - return _formatter.GetMessageName(type); + return _formatter.GetMessageName(type).Replace("[]", "__"); } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusMessageReceiver.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusMessageReceiver.cs index 9058ffdb8eb..fe37680b70e 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusMessageReceiver.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusMessageReceiver.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Azure.Messaging.ServiceBus; + using Context; using Transports; @@ -20,46 +21,17 @@ public ServiceBusMessageReceiver(ReceiveEndpointContext context) _dispatcher = context.CreateReceivePipeDispatcher(); } - public void Probe(ProbeContext context) - { - var scope = context.CreateScope("receiver"); - scope.Add("type", "brokeredMessage"); - - _context.ReceivePipe.Probe(scope); - } - - public ConnectHandle ConnectReceiveObserver(IReceiveObserver observer) - { - return _context.ConnectReceiveObserver(observer); - } - - public ConnectHandle ConnectPublishObserver(IPublishObserver observer) - { - return _context.ConnectPublishObserver(observer); - } - - public ConnectHandle ConnectSendObserver(ISendObserver observer) - { - return _context.ConnectSendObserver(observer); - } - - public async Task Handle(ServiceBusReceivedMessage message, CancellationToken cancellationToken, - Action contextCallback) + public async Task Handle(ServiceBusReceivedMessage message, CancellationToken cancellationToken) { var context = new ServiceBusReceiveContext(message, _context); - contextCallback?.Invoke(context); CancellationTokenRegistration registration = default; if (cancellationToken.CanBeCanceled) registration = cancellationToken.Register(context.Cancel); - var receiveLock = context.TryGetPayload(out var lockContext) - ? new ServiceBusReceiveLockContext(lockContext, context) - : default; - try { - await _dispatcher.Dispatch(context, receiveLock).ConfigureAwait(false); + await _dispatcher.Dispatch(context, NoLockReceiveContext.Instance).ConfigureAwait(false); } catch (ServiceBusException ex) when (ex.Reason == ServiceBusFailureReason.SessionLockLost) { @@ -92,33 +64,5 @@ public async Task Handle(ServiceBusReceivedMessage message, CancellationToken ca context.Dispose(); } } - - public ConnectHandle ConnectConsumeMessageObserver(IConsumeMessageObserver observer) - where T : class - { - return _context.ReceivePipe.ConnectConsumeMessageObserver(observer); - } - - public ConnectHandle ConnectConsumeObserver(IConsumeObserver observer) - { - return _context.ReceivePipe.ConnectConsumeObserver(observer); - } - - public int ActiveDispatchCount => _dispatcher.ActiveDispatchCount; - - public long DispatchCount => _dispatcher.DispatchCount; - - public int MaxConcurrentDispatchCount => _dispatcher.MaxConcurrentDispatchCount; - - public event ZeroActiveDispatchHandler ZeroActivity - { - add => _dispatcher.ZeroActivity += value; - remove => _dispatcher.ZeroActivity -= value; - } - - public DeliveryMetrics GetMetrics() - { - return _dispatcher.GetMetrics(); - } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusQueueMoveTransport.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusQueueMoveTransport.cs index f9296540ce7..cb88f8c065b 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusQueueMoveTransport.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusQueueMoveTransport.cs @@ -51,12 +51,6 @@ protected Task Move(ReceiveContext context, Action _configure; public ServiceBusRegistrationBusFactory(Action configure) - : this(new ServiceBusBusConfiguration(new ServiceBusTopologyConfiguration(AzureBusFactory.MessageTopology)), configure) + : this(new ServiceBusBusConfiguration(new ServiceBusTopologyConfiguration(AzureBusFactory.CreateMessageTopology())), configure) { } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusSendTransportContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusSendTransportContext.cs index 187f1dfa174..c1cb4e137c0 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusSendTransportContext.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/ServiceBusSendTransportContext.cs @@ -31,7 +31,7 @@ public ServiceBusSendTransportContext(IServiceBusHostConfiguration hostConfigura } public override string EntityName { get; } - public override string ActivitySystem => "service-bus"; + public override string ActivitySystem => "servicebus"; public Task Send(IPipe pipe, CancellationToken cancellationToken = default) { @@ -148,6 +148,10 @@ async Task CancelScheduledSend(SendEndpointContext clientContext, Guid tokenId, { MassTransit.LogContext.Debug?.Log("CANCEL {DestinationAddress} {TokenId} message not found", EntityName, tokenId); } + catch (InvalidOperationException exception) when (exception.Message.Contains("already being cancelled")) + { + MassTransit.LogContext.Debug?.Log("CANCEL {DestinationAddress} {TokenId} message already being canceled", EntityName, tokenId); + } } static bool IsCancelScheduledSend(AzureServiceBusSendContext context, out Guid tokenId, out long sequenceNumber) @@ -197,6 +201,9 @@ static ServiceBusMessage CreateMessage(AzureServiceBusSendContext context) if (context.ReplyToSessionId != null) message.ReplyToSessionId = context.ReplyToSessionId; + if (context.ReplyTo != null) + message.ReplyTo = context.ReplyTo; + if (context.Label != null) message.Subject = context.Label; diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/SessionReceiver.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/SessionReceiver.cs index 6e22ca9dcc1..3ac877ecfe0 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/SessionReceiver.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/SessionReceiver.cs @@ -9,46 +9,49 @@ namespace MassTransit.AzureServiceBusTransport public class SessionReceiver : Receiver { - readonly ClientContext _context; - readonly IServiceBusMessageReceiver _messageReceiver; + readonly ClientContext _clientContext; + readonly ServiceBusReceiveEndpointContext _context; - public SessionReceiver(ClientContext context, IServiceBusMessageReceiver messageReceiver) - : base(context, messageReceiver) + public SessionReceiver(ClientContext clientContext, ServiceBusReceiveEndpointContext context) + : base(clientContext, context) { + _clientContext = clientContext; _context = context; - _messageReceiver = messageReceiver; } - public override Task Start() + public override void Start() { - _context.OnSessionAsync(OnSession, ExceptionHandler); + _clientContext.OnSessionAsync(OnSession, ExceptionHandler); - SetReady(); - - return _context.StartAsync(); + SetReady(_clientContext.StartAsync()); } async Task OnSession(ProcessSessionMessageEventArgs messageSession, ServiceBusReceivedMessage message, CancellationToken cancellationToken) { + if (IsStopping) + return; + + MessageLockContext lockContext = new ServiceBusSessionMessageLockContext(messageSession, message); + MessageSessionContext sessionContext = new ServiceBusMessageSessionContext(messageSession); + var context = new ServiceBusReceiveContext(message, _context, lockContext, _clientContext, sessionContext); + + CancellationTokenRegistration registration = default; + if (cancellationToken.CanBeCanceled) + registration = cancellationToken.Register(context.Cancel); + try { - await _messageReceiver.Handle(message, cancellationToken, context => AddReceiveContextPayloads(context, messageSession, message)) - .ConfigureAwait(false); + await Dispatch(message, context, lockContext).ConfigureAwait(false); } catch (Exception) { // do NOT let exceptions propagate to the Azure SDK } - } - - void AddReceiveContextPayloads(ReceiveContext receiveContext, ProcessSessionMessageEventArgs messageSession, ServiceBusReceivedMessage message) - { - MessageSessionContext sessionContext = new ServiceBusMessageSessionContext(messageSession); - MessageLockContext lockContext = new ServiceBusSessionMessageLockContext(messageSession, message); - - receiveContext.GetOrAddPayload(() => sessionContext); - receiveContext.GetOrAddPayload(() => lockContext); - receiveContext.GetOrAddPayload(() => _context); + finally + { + registration.Dispose(); + context.Dispose(); + } } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/BaseClientSettings.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/BaseClientSettings.cs index d866b8d5c19..b6db0adb0e4 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/BaseClientSettings.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/BaseClientSettings.cs @@ -22,9 +22,10 @@ protected BaseClientSettings(IServiceBusEndpointConfiguration configuration, ISe public IServiceBusEndpointEntityConfigurator Configurator { get; } public abstract bool RequiresSession { get; } + public abstract int MaxConcurrentSessions { get; } public abstract int MaxConcurrentCallsPerSession { get; } - public TimeSpan SessionIdleTimeout { get; set; } + public TimeSpan? SessionIdleTimeout { get; set; } public int MaxConcurrentCalls => Math.Max(_configuration.Transport.GetConcurrentMessageLimit(), 1); public int PrefetchCount => _configuration.Transport.PrefetchCount; diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ReceiveEndpointSettings.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ReceiveEndpointSettings.cs index 732597565d7..94d2594c78c 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ReceiveEndpointSettings.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ReceiveEndpointSettings.cs @@ -25,7 +25,8 @@ public ReceiveEndpointSettings(IServiceBusEndpointConfiguration endpointConfigur public override bool RequiresSession => _queueConfigurator.RequiresSession ?? false; public bool RemoveSubscriptions { get; set; } - public override int MaxConcurrentCallsPerSession => _queueConfigurator.MaxConcurrentCallsPerSession ?? 1; + public override int MaxConcurrentSessions => _queueConfigurator.MaxConcurrentSessions ?? Defaults.MaxConcurrentSessions; + public override int MaxConcurrentCallsPerSession => _queueConfigurator.MaxConcurrentCallsPerSession ?? Defaults.MaxConcurrentCallsPerSessions; public override string Path => _queueConfigurator.FullPath; diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusBusTopology.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusBusTopology.cs index faa2feeff59..da9ae4995e0 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusBusTopology.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusBusTopology.cs @@ -1,6 +1,5 @@ namespace MassTransit.AzureServiceBusTransport.Topology { - using System; using Configuration; using Transports; @@ -22,15 +21,6 @@ public ServiceBusBusTopology(IServiceBusHostConfiguration hostConfiguration, ISe IServiceBusPublishTopology IServiceBusBusTopology.PublishTopology => _configuration.Publish; IServiceBusSendTopology IServiceBusBusTopology.SendTopology => _configuration.Send; - public Uri GetDestinationAddress(string queueName, Action configure = null) - { - var configurator = new ServiceBusQueueConfigurator(queueName); - - configure?.Invoke(configurator); - - return configurator.GetQueueAddress(_hostConfiguration.HostAddress); - } - IServiceBusMessagePublishTopology IServiceBusBusTopology.Publish() { return _configuration.Publish.GetMessageTopology(); diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusConsumeTopology.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusConsumeTopology.cs index ddeacd40e04..541f9f39fe2 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusConsumeTopology.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusConsumeTopology.cs @@ -76,7 +76,7 @@ public override IEnumerable Validate() return base.Validate().Concat(_specifications.SelectMany(x => x.Validate())); } - protected override IMessageConsumeTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessageConsumeTopologyConfigurator CreateMessageTopology() { var messageTopology = new ServiceBusMessageConsumeTopology(_messageTopology.GetMessageTopology(), _publishTopology.GetMessageTopology()); diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusMessagePublishTopology.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusMessagePublishTopology.cs index e34fa99f2ee..e539589741a 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusMessagePublishTopology.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusMessagePublishTopology.cs @@ -3,6 +3,7 @@ namespace MassTransit.AzureServiceBusTransport.Topology { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using Azure.Messaging.ServiceBus.Administration; using Configuration; using MassTransit.Topology; @@ -29,7 +30,7 @@ public ServiceBusMessagePublishTopology(IServiceBusPublishTopology publishTopolo _createTopicOptions = new Lazy(() => _topicConfigurator.GetCreateTopicOptions()); } - public override bool TryGetPublishAddress(Uri baseAddress, out Uri? publishAddress) + public override bool TryGetPublishAddress(Uri baseAddress, [NotNullWhen(true)] out Uri? publishAddress) { publishAddress = new ServiceBusEndpointAddress(new Uri(baseAddress.GetLeftPart(UriPartial.Authority)), _topicConfigurator.FullPath, _topicConfigurator.AutoDeleteOnIdle, ServiceBusEndpointAddress.AddressType.Topic); diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusPublishTopology.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusPublishTopology.cs index 530d41d3aba..fb1783216ec 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusPublishTopology.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/ServiceBusPublishTopology.cs @@ -77,7 +77,7 @@ IServiceBusMessagePublishTopologyConfigurator IServiceBusPublishTopologyConfi return GetMessageTopology() as IServiceBusMessagePublishTopologyConfigurator; } - protected override IMessagePublishTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessagePublishTopologyConfigurator CreateMessageTopology() { var messageTopology = new ServiceBusMessagePublishTopology(this, _messageTopology.GetMessageTopology()); diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/SetSessionIdMessageSendTopology.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/SetSessionIdMessageSendTopology.cs new file mode 100644 index 00000000000..22c3a020c2b --- /dev/null +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/SetSessionIdMessageSendTopology.cs @@ -0,0 +1,27 @@ +namespace MassTransit.AzureServiceBusTransport.Topology +{ + using System; + using MassTransit.Configuration; + using Middleware; + + + public class SetSessionIdMessageSendTopology : + IMessageSendTopology + where T : class + { + readonly IFilter> _filter; + + public SetSessionIdMessageSendTopology(IMessageSessionIdFormatter sessionIdFormatter) + { + if (sessionIdFormatter == null) + throw new ArgumentNullException(nameof(sessionIdFormatter)); + + _filter = new ServiceBusSendContextFilter(new SetSessionIdFilter(sessionIdFormatter)); + } + + public void Apply(ITopologyPipeBuilder> builder) + { + builder.AddFilter(_filter); + } + } +} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/SubscriptionEndpointSettings.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/SubscriptionEndpointSettings.cs index 2d89824eecf..6aef2142f84 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/SubscriptionEndpointSettings.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/AzureServiceBusTransport/Topology/SubscriptionEndpointSettings.cs @@ -36,7 +36,10 @@ public SubscriptionEndpointSettings(IServiceBusEndpointConfiguration configurati public IServiceBusSubscriptionConfigurator SubscriptionConfigurator => _subscriptionConfigurator; public override bool RequiresSession => _subscriptionConfigurator.RequiresSession ?? false; - public override int MaxConcurrentCallsPerSession => _subscriptionConfigurator.MaxConcurrentCallsPerSession ?? 1; + + public bool RemoveSubscriptions { get; set; } + public override int MaxConcurrentSessions => _subscriptionConfigurator.MaxConcurrentSessions ?? Defaults.MaxConcurrentSessions; + public override int MaxConcurrentCallsPerSession => _subscriptionConfigurator.MaxConcurrentCallsPerSession ?? Defaults.MaxConcurrentCallsPerSessions; CreateTopicOptions SubscriptionSettings.CreateTopicOptions => _createTopicOptions; CreateSubscriptionOptions SubscriptionSettings.CreateSubscriptionOptions => _subscriptionConfigurator.GetCreateSubscriptionOptions(); @@ -46,8 +49,6 @@ public SubscriptionEndpointSettings(IServiceBusEndpointConfiguration configurati public override string Path { get; } - public bool RemoveSubscriptions { get; set; } - protected override IEnumerable GetQueryStringOptions() { if (_subscriptionConfigurator.AutoDeleteOnIdle.HasValue && _subscriptionConfigurator.AutoDeleteOnIdle.Value > TimeSpan.Zero diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Configuration/ServiceBusBusFactoryConfigurator.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Configuration/ServiceBusBusFactoryConfigurator.cs index 8580846b28e..f839665a063 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Configuration/ServiceBusBusFactoryConfigurator.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Configuration/ServiceBusBusFactoryConfigurator.cs @@ -189,6 +189,11 @@ public bool RequiresSession set => _queueConfigurator.RequiresSession = value; } + public int MaxConcurrentSessions + { + set => _queueConfigurator.MaxConcurrentSessions = value; + } + public int MaxConcurrentCallsPerSession { set => _queueConfigurator.MaxConcurrentCallsPerSession = value; @@ -199,12 +204,7 @@ public string UserMetadata set => _queueConfigurator.UserMetadata = value; } - public TimeSpan MessageWaitTimeout - { - set => _settings.SessionIdleTimeout = value; - } - - public TimeSpan SessionIdleTimeout + public TimeSpan? SessionIdleTimeout { set => _settings.SessionIdleTimeout = value; } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Configuration/ServiceBusHostConfigurator.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Configuration/ServiceBusHostConfigurator.cs index 6f2a52fd1e9..37866dcf924 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Configuration/ServiceBusHostConfigurator.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Configuration/ServiceBusHostConfigurator.cs @@ -1,4 +1,5 @@ -namespace MassTransit.Configuration +#nullable enable +namespace MassTransit.Configuration { using System; using Azure; @@ -41,7 +42,7 @@ public ServiceBusHostConfigurator(string connectionString) _settings = new HostSettings { ConnectionString = connectionString, - ServiceUri = properties.Endpoint, + ServiceUri = ParseEndpoint(connectionString), }; if (IsMissingCredentials(properties)) @@ -127,5 +128,59 @@ static bool IsMissingCredentials(ServiceBusConnectionStringProperties properties return string.IsNullOrWhiteSpace(properties.SharedAccessKeyName) && string.IsNullOrWhiteSpace(properties.SharedAccessKey) && string.IsNullOrWhiteSpace(properties.SharedAccessSignature); } + + public static Uri? ParseEndpoint(string connectionString) + { + var itemIndex = connectionString[0] == ';' ? 0 : 1; + var startIndex = 0; + var separatorIndex = 0; + while (separatorIndex != -1) + { + separatorIndex = connectionString.IndexOf(';', startIndex + 1); + var item = separatorIndex < 0 ? connectionString.Substring(startIndex) : connectionString.Substring(startIndex, separatorIndex - startIndex); + var index = item.IndexOf('='); + if (index >= 0) + { + var key = item.Substring(1 - itemIndex, index - 1 + itemIndex); + var value = item.Substring(index + 1); + if ((!string.IsNullOrEmpty(key) && char.IsWhiteSpace(key[0])) || char.IsWhiteSpace(key[key.Length - 1])) + key = key.Trim(); + if (!string.IsNullOrEmpty(value) && (char.IsWhiteSpace(value[0]) || char.IsWhiteSpace(value[value.Length - 1]))) + value = value.Trim(); + if (string.IsNullOrEmpty(value)) + throw new FormatException("Invalid connection string"); + + if (string.Compare("Endpoint", key, StringComparison.OrdinalIgnoreCase) == 0) + { + if (!Uri.TryCreate(value, UriKind.Absolute, out var result)) + result = null; + + if ((object?)result == null) + return null; + + var builder = new UriBuilder + { + Scheme = "sb", + Host = result.Host, + Path = result.AbsolutePath, + Port = result.IsDefaultPort ? -1 : result.Port + }; + + if (string.Compare(builder.Scheme, "sb", StringComparison.OrdinalIgnoreCase) != 0 + || Uri.CheckHostName(builder.Host) == UriHostNameType.Unknown) + throw new FormatException("Invalid connection string"); + + return builder.Uri; + } + } + else if (item.Length != 1 || item[0] != ';') + throw new FormatException("Invalid connection string"); + + itemIndex = 0; + startIndex = separatorIndex; + } + + return null; + } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/IServiceBusEndpointConfigurator.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/IServiceBusEndpointConfigurator.cs index c6e733d3b6e..730bae5622f 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/IServiceBusEndpointConfigurator.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/IServiceBusEndpointConfigurator.cs @@ -51,6 +51,11 @@ public interface IServiceBusEndpointConfigurator /// bool RequiresSession { set; } + /// + /// If session is required, sets the maximum concurrent sessions (defaults to 1) + /// + int MaxConcurrentSessions { set; } + /// /// If session is required, sets the maximum concurrent calls per session (defaults to 1) /// @@ -61,16 +66,10 @@ public interface IServiceBusEndpointConfigurator /// string UserMetadata { set; } - /// - /// Sets the message session timeout period - /// - [Obsolete("use SessionIdleTimeout, which this method calls through to for now")] - TimeSpan MessageWaitTimeout { set; } - /// /// Sets the message session idle timeout period /// - TimeSpan SessionIdleTimeout { set; } + TimeSpan? SessionIdleTimeout { set; } /// /// Sets the maximum time for locks/sessions to be automatically renewed diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/IServiceBusEndpointEntityConfigurator.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/IServiceBusEndpointEntityConfigurator.cs index e4fc5e59463..73dc8288d91 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/IServiceBusEndpointEntityConfigurator.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/IServiceBusEndpointEntityConfigurator.cs @@ -21,6 +21,11 @@ public interface IServiceBusEndpointEntityConfigurator : /// bool? RequiresSession { set; } + /// + /// Sets the maximum number of concurrent sessions + /// + int? MaxConcurrentSessions { set; } + /// /// Sets the maximum number of concurrent calls per session /// diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusBatchingExtensions.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusBatchingExtensions.cs new file mode 100644 index 00000000000..2138b12b1ee --- /dev/null +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusBatchingExtensions.cs @@ -0,0 +1,46 @@ +namespace MassTransit; + +using System; + + +public static class ServiceBusBatchingExtensions +{ + /// + /// Configures for batching with Azure Service Bus sessions to ensure in-order processing of sessions + /// - Sessions will be delivered as batches of up to + /// - Batches from up to sessions will be processed concurrently + /// Note: Consider max concurrency impacts of the SDK to avoid thread exhaustion + /// - total number of concurrent calls = * + /// + /// + public static void SetServiceBusSessionBatchOptions(this IConsumerConfigurator consumerConfigurator, + Action configure) + where TConsumer : class + { + ServiceBusSessionBatchOptions sessionOptions = new(); + configure(sessionOptions); + + consumerConfigurator.Options(o => + { + o.GroupBy(e => e.SessionId()) + .SetConcurrencyLimit(sessionOptions.MaxConcurrentSessions) + .SetMessageLimit(sessionOptions.MessageLimitPerSession) + .SetTimeLimit(sessionOptions.TimeLimit) + .SetTimeLimitStart(sessionOptions.TimeLimitStart) + .SetConfigurationCallback((name, configurator) => + { + if (configurator is not IServiceBusEndpointConfigurator sb) + throw new ArgumentException("Expecting IServiceBusReceiveEndpointConfigurator", nameof(configurator)); + + sb.RequiresSession = true; + + sb.MaxConcurrentSessions = sessionOptions.MaxConcurrentSessions; + sb.MaxConcurrentCallsPerSession = sessionOptions.MessageLimitPerSession; + sb.SessionIdleTimeout = sessionOptions.SessionIdleTimeout; + + if (configurator.PrefetchCount != 0 && configurator.PrefetchCount < sessionOptions.MessageLimitPerSession) + configurator.PrefetchCount = sessionOptions.MessageLimitPerSession; + }); + }); + } +} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusBusFactoryConfiguratorExtensions.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusBusFactoryConfiguratorExtensions.cs index 826f7d5aa67..a933b795f32 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusBusFactoryConfiguratorExtensions.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusBusFactoryConfiguratorExtensions.cs @@ -105,9 +105,7 @@ public static void ReceiveEndpoint(this IServiceBusBusFactoryConfigurator config } /// - /// Declare a ReceiveEndpoint using a unique generated queue name. This queue defaults to auto-delete - /// and non-durable. By default all services bus instances include a default receiveEndpoint that is - /// of this type (created automatically upon the first receiver binding). + /// Declare a receive endpoint using the endpoint . /// /// /// diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusMessageSchedulerBusExtensions.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusMessageSchedulerBusExtensions.cs index 152193099e9..5456428e054 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusMessageSchedulerBusExtensions.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusMessageSchedulerBusExtensions.cs @@ -1,5 +1,6 @@ namespace MassTransit { + using DependencyInjection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Scheduling; @@ -35,7 +36,7 @@ public static IMessageScheduler CreateServiceBusMessageScheduler(this ISendEndpo } /// - /// Add a to the container that uses the Azure message enqueue time to schedule messages. + /// Add an to the container that uses the Azure message enqueue time to schedule messages. /// /// public static void AddServiceBusMessageScheduler(this IRegistrationConfigurator configurator) @@ -47,5 +48,20 @@ public static void AddServiceBusMessageScheduler(this IRegistrationConfigurator return sendEndpointProvider.CreateServiceBusMessageScheduler(bus.Topology); }); } + + /// + /// Add an to the container that uses the Azure message enqueue time to schedule messages. + /// + /// + public static void AddServiceBusMessageScheduler(this IBusRegistrationConfigurator configurator) + where TBus : class, IBus + { + configurator.TryAddScoped(provider => + { + var bus = provider.GetRequiredService(); + var sendEndpointProvider = provider.GetRequiredService(); + return Bind.Create(sendEndpointProvider.CreateServiceBusMessageScheduler(bus.Topology)); + }); + } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusSessionBatchOptions.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusSessionBatchOptions.cs new file mode 100644 index 00000000000..8fd879cfe69 --- /dev/null +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/ServiceBusSessionBatchOptions.cs @@ -0,0 +1,83 @@ +namespace MassTransit; + +using System; +using AzureServiceBusTransport; + + +public class ServiceBusSessionBatchOptions +{ + /// + /// The maximum number of messages in a single batch + /// + public int MessageLimitPerSession { get; set; } = 10; + + /// + /// The maximum number of concurrent sessions + /// + public int MaxConcurrentSessions { get; set; } = 1; + + /// + /// The timeout before a message session is abandoned + /// + public TimeSpan? SessionIdleTimeout { get; set; } = Defaults.SessionIdleTimeout; + + /// + /// The maximum time to wait before delivering a partial batch + /// + public TimeSpan TimeLimit { get; set; } = TimeSpan.FromSeconds(1); + + /// + /// The starting point for the + /// + public BatchTimeLimitStart TimeLimitStart { get; set; } = BatchTimeLimitStart.FromFirst; + + /// + /// Sets the maximum number of messages in a single batch + /// + /// The message limit + public ServiceBusSessionBatchOptions SetMessageLimitPerSession(int limit) + { + MessageLimitPerSession = limit; + return this; + } + + /// + /// Sets the maximum number of concurrent sessions + /// + /// The maximum number of concurrent sessions + public ServiceBusSessionBatchOptions SetMaxConcurrentSessions(int limit) + { + MaxConcurrentSessions = limit; + return this; + } + + /// + /// Sets the maximum time to wait for messages within a session before abandoning the session for another + /// + /// The time limit + public ServiceBusSessionBatchOptions SetSessionIdleTimeout(TimeSpan limit) + { + SessionIdleTimeout = limit; + return this; + } + + /// + /// Sets the maximum time to wait before delivering a partial batch + /// + /// The time limit + public ServiceBusSessionBatchOptions SetTimeLimit(TimeSpan limit) + { + TimeLimit = limit; + return this; + } + + /// + /// Sets the starting point for the + /// + /// The starting point + public ServiceBusSessionBatchOptions SetTimeLimitStart(BatchTimeLimitStart timeLimitStart) + { + TimeLimitStart = timeLimitStart; + return this; + } +} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Topology/ServiceBusPartitionKeyConventionExtensions.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Topology/ServiceBusPartitionKeyConventionExtensions.cs deleted file mode 100644 index 4e087189087..00000000000 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Configuration/Topology/ServiceBusPartitionKeyConventionExtensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace MassTransit -{ - using System; - using AzureServiceBusTransport; - using AzureServiceBusTransport.Configuration; - - - public static class ServiceBusPartitionKeyConventionExtensions - { - public static void UsePartitionKeyFormatter(this IMessageSendTopologyConfigurator configurator, IMessagePartitionKeyFormatter formatter) - where T : class - { - configurator.UpdateConvention>( - update => - { - update.SetFormatter(formatter); - - return update; - }); - } - - /// - /// Use the partition key formatter for the specified message type - /// - /// - /// - /// - public static void UsePartitionKeyFormatter(this ISendTopologyConfigurator configurator, IMessagePartitionKeyFormatter formatter) - where T : class - { - configurator.GetMessageTopology().UsePartitionKeyFormatter(formatter); - } - - /// - /// Use the delegate to format the partition key, using Empty if the string is null upon return - /// - /// - /// - /// - public static void UsePartitionKeyFormatter(this ISendTopologyConfigurator configurator, Func, string> formatter) - where T : class - { - configurator.GetMessageTopology().UsePartitionKeyFormatter(new DelegatePartitionKeyFormatter(formatter)); - } - - /// - /// Use the delegate to format the partition key, using Empty if the string is null upon return - /// - /// - /// - /// - public static void UsePartitionKeyFormatter(this IMessageSendTopologyConfigurator configurator, Func, string> formatter) - where T : class - { - configurator.UsePartitionKeyFormatter(new DelegatePartitionKeyFormatter(formatter)); - } - } -} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/MessageLockExpiredException.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/MessageLockExpiredException.cs index 2bac53fd9fc..e02d542e05d 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/MessageLockExpiredException.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/MessageLockExpiredException.cs @@ -27,6 +27,9 @@ public MessageLockExpiredException(Uri uri, string message, Exception innerExcep { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MessageLockExpiredException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/MessageTimeToLiveExpiredException.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/MessageTimeToLiveExpiredException.cs index ef5f97e58c6..131684ca9fc 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/MessageTimeToLiveExpiredException.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/MessageTimeToLiveExpiredException.cs @@ -27,6 +27,9 @@ public MessageTimeToLiveExpiredException(Uri uri, string message, Exception inne { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MessageTimeToLiveExpiredException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/ServiceBusConnectionException.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/ServiceBusConnectionException.cs index f7ea3fb0c79..b59c5e05d01 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/ServiceBusConnectionException.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Exceptions/ServiceBusConnectionException.cs @@ -21,6 +21,9 @@ public ServiceBusConnectionException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected ServiceBusConnectionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/MassTransit.Azure.ServiceBus.Core.csproj b/src/Transports/MassTransit.Azure.ServiceBus.Core/MassTransit.Azure.ServiceBus.Core.csproj index 15f7c33e4a8..7327a45f405 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/MassTransit.Azure.ServiceBus.Core.csproj +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/MassTransit.Azure.ServiceBus.Core.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -23,7 +23,6 @@ - diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/MassTransit.Azure.ServiceBus.Core.csproj.DotSettings b/src/Transports/MassTransit.Azure.ServiceBus.Core/MassTransit.Azure.ServiceBus.Core.csproj.DotSettings index 82500b76fed..5a1b68daed2 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/MassTransit.Azure.ServiceBus.Core.csproj.DotSettings +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/MassTransit.Azure.ServiceBus.Core.csproj.DotSettings @@ -1,11 +1,13 @@ - + True True True + True True False True True True True - False \ No newline at end of file + False diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/NullableAttributes.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/NullableAttributes.cs new file mode 100644 index 00000000000..3f38561b675 --- /dev/null +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/NullableAttributes.cs @@ -0,0 +1,24 @@ +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using System; + + + [AttributeUsage(AttributeTargets.Parameter)] + sealed class NotNullWhenAttribute : + Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +#endif diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Scheduling/ServiceBusScheduleMessageProvider.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Scheduling/ServiceBusScheduleMessageProvider.cs index 728831f2211..9b3d6acc636 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Scheduling/ServiceBusScheduleMessageProvider.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Scheduling/ServiceBusScheduleMessageProvider.cs @@ -39,12 +39,12 @@ public async Task> ScheduleSend(Uri destinationAddress, D return new ScheduledMessageHandle(scheduleMessagePipe.ScheduledMessageId ?? NewId.NextGuid(), scheduledTime, destinationAddress, message); } - public Task CancelScheduledSend(Guid tokenId) + public Task CancelScheduledSend(Guid tokenId, CancellationToken cancellationToken) { return Task.CompletedTask; } - public async Task CancelScheduledSend(Uri destinationAddress, Guid tokenId) + public async Task CancelScheduledSend(Uri destinationAddress, Guid tokenId, CancellationToken cancellationToken) { var endpoint = await _sendEndpointProvider.GetSendEndpoint(destinationAddress).ConfigureAwait(false); @@ -53,7 +53,7 @@ await endpoint.Send(new InVar.CorrelationId, InVar.Timestamp, TokenId = tokenId - }).ConfigureAwait(false); + }, cancellationToken).ConfigureAwait(false); } } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusMessageContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusMessageContext.cs index e5b10570762..008f91e73c4 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusMessageContext.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusMessageContext.cs @@ -8,7 +8,8 @@ namespace MassTransit /// The context of a Message from AzureServiceBus - gives access to the transport /// message when requested. /// - public interface ServiceBusMessageContext + public interface ServiceBusMessageContext : + PartitionKeyConsumeContext { int DeliveryCount { get; } string Label { get; } @@ -20,7 +21,6 @@ public interface ServiceBusMessageContext long Size { get; } string To { get; } string ReplyToSessionId { get; } - string PartitionKey { get; } string ReplyTo { get; } DateTime EnqueuedTime { get; } DateTime ScheduledEnqueueTime { get; } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusMessageContextExtensions.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusMessageContextExtensions.cs index 5559a5eb546..b3bf13920ca 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusMessageContextExtensions.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusMessageContextExtensions.cs @@ -1,20 +1,14 @@ -namespace MassTransit +namespace MassTransit; + +public static class ServiceBusMessageContextExtensions { - public static class ServiceBusMessageContextExtensions + public static string SessionId(this ConsumeContext context) { - public static string SessionId(this ConsumeContext context) - { - return context.TryGetPayload(out var brokeredMessageContext) ? brokeredMessageContext.SessionId : string.Empty; - } - - public static string PartitionKey(this ConsumeContext context) - { - return context.TryGetPayload(out var brokeredMessageContext) ? brokeredMessageContext.PartitionKey : string.Empty; - } + return context.TryGetPayload(out var brokeredMessageContext) ? brokeredMessageContext.SessionId : string.Empty; + } - public static string ReplyToSessionId(this ConsumeContext context) - { - return context.TryGetPayload(out var brokeredMessageContext) ? brokeredMessageContext.ReplyToSessionId : string.Empty; - } + public static string ReplyToSessionId(this ConsumeContext context) + { + return context.TryGetPayload(out var brokeredMessageContext) ? brokeredMessageContext.ReplyToSessionId : string.Empty; } } diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusSendContext.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusSendContext.cs index ec18545bafd..32587f3053e 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusSendContext.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusSendContext.cs @@ -4,18 +4,14 @@ public interface ServiceBusSendContext : - SendContext + SendContext, + PartitionKeySendContext { /// /// Set the time at which the message should be enqueued, which is essentially scheduling the message for future delivery to the queue. /// DateTime? ScheduledEnqueueTimeUtc { set; } - /// - /// Set the partition key for the message, which is used to split load across nodes in Azure - /// - string PartitionKey { set; } - /// /// Set the sessionId of the message /// @@ -26,6 +22,11 @@ public interface ServiceBusSendContext : /// string ReplyToSessionId { set; } + /// + /// Sets the ReplyTo address of the message + /// + string ReplyTo { set; } + /// /// Set the application specific label of the message /// diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusSendContextExtensions.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusSendContextExtensions.cs index 9016bfe68a1..9f38f0b81d3 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusSendContextExtensions.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/ServiceBusSendContextExtensions.cs @@ -44,10 +44,10 @@ public static void SetReplyToSessionId(this SendContext context, string sessionI sendContext.ReplyToSessionId = sessionId; } - public static void SetPartitionKey(this SendContext context, string partitionKey) + public static void SetReplyTo(this SendContext context, string replyTo) { if (context.TryGetPayload(out ServiceBusSendContext sendContext)) - sendContext.PartitionKey = partitionKey; + sendContext.ReplyTo = replyTo; } public static void SetLabel(this SendContext context, string label) diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Testing/AzureFunctionsTestExtensions.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Testing/AzureFunctionsTestExtensions.cs new file mode 100644 index 00000000000..b0861e0afe2 --- /dev/null +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Testing/AzureFunctionsTestExtensions.cs @@ -0,0 +1,55 @@ +namespace MassTransit.Testing +{ + using System; + using System.Reflection; + using System.Threading.Tasks; + using Azure.Core.Amqp; + using Azure.Messaging.ServiceBus; + using AzureServiceBusTransport; + using Microsoft.Extensions.DependencyInjection; + using Serialization; + + + public static class AzureFunctionsTestExtensions + { + public static IBusRegistrationConfigurator AddAzureFunctionsTestComponents(this IBusRegistrationConfigurator configurator) + { + configurator.AddSingleton() + .AddSingleton(); + + return configurator; + } + + /// + /// Handle the Azure Service Bus message using the specified consumer + /// + /// + /// + /// + public static Task HandleConsumer(this ITestHarness harness, object message) + where TConsumer : class, IConsumer + { + var body = SystemTextJsonMessageSerializer.Instance.SerializeObject(message); + + var messageBody = new AmqpMessageBody(new[] { new BinaryData(body.GetBytes()).ToMemory() }); + var annotatedMessage = new AmqpAnnotatedMessage(messageBody) + { + Header = { DeliveryCount = 1 }, + Properties = + { + MessageId = new AmqpMessageId(NewId.NextGuid().ToString()), + ContentType = SystemTextJsonRawMessageSerializer.JsonContentType.MediaType + } + }; + + var receivedMessage = (ServiceBusReceivedMessage)typeof(ServiceBusReceivedMessage).GetConstructor( + BindingFlags.NonPublic | BindingFlags.Instance, + null, new[] { typeof(AmqpAnnotatedMessage) }, null).Invoke(new object[] { annotatedMessage }); + + var receiver = harness.Scope.ServiceProvider.GetRequiredService(); + var formatter = harness.Scope.ServiceProvider.GetService() ?? DefaultEndpointNameFormatter.Instance; + + return receiver.HandleConsumer(formatter.Consumer(), receivedMessage, harness.CancellationToken); + } + } +} diff --git a/src/Transports/MassTransit.Azure.ServiceBus.Core/Topology/IServiceBusBusTopology.cs b/src/Transports/MassTransit.Azure.ServiceBus.Core/Topology/IServiceBusBusTopology.cs index f0914275ce1..4270702c8f4 100644 --- a/src/Transports/MassTransit.Azure.ServiceBus.Core/Topology/IServiceBusBusTopology.cs +++ b/src/Transports/MassTransit.Azure.ServiceBus.Core/Topology/IServiceBusBusTopology.cs @@ -1,8 +1,5 @@ namespace MassTransit { - using System; - - public interface IServiceBusBusTopology : IBusTopology { @@ -15,13 +12,5 @@ public interface IServiceBusBusTopology : new IServiceBusMessageSendTopology Send() where T : class; - - /// - /// Returns the destination address for the specified queue - /// - /// - /// Callback to configure queue settings - /// - Uri GetDestinationAddress(string queueName, Action configure = null); } } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubConsumeContext.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubConsumeContext.cs index 91747b9bf66..a73f9817fe4 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubConsumeContext.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubConsumeContext.cs @@ -4,12 +4,12 @@ namespace MassTransit using System.Collections.Generic; - public interface EventHubConsumeContext + public interface EventHubConsumeContext : + PartitionKeyConsumeContext { DateTimeOffset EnqueuedTime { get; } long Offset { get; } string PartitionId { get; } - string PartitionKey { get; } long SequenceNumber { get; } IReadOnlyDictionary SystemProperties { get; } IDictionary Properties { get; } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubConsumeContextExtensions.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubConsumeContextExtensions.cs index 6829d1aa921..e8ff953a10a 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubConsumeContextExtensions.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubConsumeContextExtensions.cs @@ -1,20 +1,14 @@ -namespace MassTransit +namespace MassTransit; + +public static class EventHubConsumeContextExtensions { - public static class EventHubConsumeContextExtensions + public static long? Offset(this ConsumeContext context) { - public static string PartitionKey(this ConsumeContext context) - { - return context.TryGetPayload(out EventHubConsumeContext consumeContext) ? consumeContext.PartitionKey : string.Empty; - } - - public static long? Offset(this ConsumeContext context) - { - return context.TryGetPayload(out EventHubConsumeContext consumeContext) ? consumeContext.Offset : default; - } + return context.TryGetPayload(out EventHubConsumeContext consumeContext) ? consumeContext.Offset : default; + } - public static long? SequenceNumber(this ConsumeContext context) - { - return context.TryGetPayload(out EventHubConsumeContext consumeContext) ? consumeContext.SequenceNumber : default; - } + public static long? SequenceNumber(this ConsumeContext context) + { + return context.TryGetPayload(out EventHubConsumeContext consumeContext) ? consumeContext.SequenceNumber : default; } } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Activities/ProducerFactoryExtensions.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Activities/ProducerFactoryExtensions.cs index 75c817bfe1b..7d7b8dcdda9 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Activities/ProducerFactoryExtensions.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Activities/ProducerFactoryExtensions.cs @@ -6,7 +6,7 @@ namespace MassTransit.EventHubIntegration.Activities static class ProducerFactoryExtensions { internal static Task GetProducer(this BehaviorContext context, ConsumeContext consumeContext, string eventHubName) - where T : class, ISaga + where T : class, SagaStateMachineInstance { return context.GetServiceOrCreateInstance() .GetProducerProvider(consumeContext) diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/BatchCheckpointer.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/BatchCheckpointer.cs index bc43dd62ff0..cf0990cbf9a 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/BatchCheckpointer.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/BatchCheckpointer.cs @@ -39,7 +39,7 @@ public async Task Pending(IPendingConfirmation confirmation) public async ValueTask DisposeAsync() { - _channel.Writer.Complete(); + _channel.Writer.TryComplete(); await _checkpointTask.ConfigureAwait(false); } @@ -73,16 +73,17 @@ async Task ReadBatch() { try { - for (var i = 0; i < _settings.CheckpointMessageCount; i++) + while (batch.Count < _settings.CheckpointMessageCount) { - var confirmation = await _channel.Reader.ReadAsync(batchToken.Token).ConfigureAwait(false); - - await confirmation.Confirmed.OrCanceled(_cancellationToken).ConfigureAwait(false); - - batch.Add(confirmation); - - if (await _channel.Reader.WaitToReadAsync(batchToken.Token).ConfigureAwait(false) == false) + if (_channel.Reader.TryRead(out var confirmation)) + { + await confirmation.Confirmed.OrCanceled(_cancellationToken).ConfigureAwait(false); + batch.Add(confirmation); + } + else if (await _channel.Reader.WaitToReadAsync(batchToken.Token).ConfigureAwait(false) == false) + { break; + } } } catch (Exception) when (batch.Count > 0) diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PartitionOffset.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PartitionOffset.cs new file mode 100644 index 00000000000..b12d645d28f --- /dev/null +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PartitionOffset.cs @@ -0,0 +1,28 @@ +namespace MassTransit.EventHubIntegration.Checkpoints +{ + using Azure.Messaging.EventHubs.Processor; + + + // ReSharper disable NotAccessedField.Local + public readonly struct PartitionOffset + { + readonly string _partitionId; + readonly long _offset; + + PartitionOffset(string partitionId, long offset) + { + _partitionId = partitionId; + _offset = offset; + } + + public override string ToString() + { + return $"{_partitionId}/{_offset}"; + } + + public static implicit operator PartitionOffset(in ProcessEventArgs args) + { + return new PartitionOffset(args.Partition.PartitionId, args.Data.Offset); + } + } +} diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PendingConfirmation.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PendingConfirmation.cs index bb94254ae24..53706cc2b16 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PendingConfirmation.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PendingConfirmation.cs @@ -11,18 +11,16 @@ namespace MassTransit.EventHubIntegration.Checkpoints public class PendingConfirmation : IPendingConfirmation { - readonly string _eventHubName; readonly TaskCompletionSource _source; ProcessEventArgs _eventArgs; - public PendingConfirmation(string eventHubName, ProcessEventArgs eventArgs) + public PendingConfirmation(ProcessEventArgs eventArgs) { - _eventHubName = eventHubName; _eventArgs = eventArgs; _source = TaskUtil.GetTask(); } - Uri Topic => new Uri($"topic:{_eventHubName}"); + Uri Topic => new Uri($"topic:{Partition.EventHubName}"); public PartitionContext Partition => _eventArgs.Partition; public long Offset => _eventArgs.Data.Offset; diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PendingConfirmationCollection.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PendingConfirmationCollection.cs index 41cd000fdf7..614ee5a89ab 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PendingConfirmationCollection.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Checkpoints/PendingConfirmationCollection.cs @@ -9,17 +9,17 @@ namespace MassTransit.EventHubIntegration.Checkpoints public class PendingConfirmationCollection : IDisposable { - readonly ConcurrentDictionary _confirmations; - readonly string _eventHubName; + readonly CancellationToken _cancellationToken; + readonly ConcurrentDictionary _confirmations; readonly CancellationTokenRegistration? _registration; - public PendingConfirmationCollection(string eventHubName, CancellationToken cancellationToken) + public PendingConfirmationCollection(CancellationToken cancellationToken) { - _eventHubName = eventHubName; - _confirmations = new ConcurrentDictionary(); + _cancellationToken = cancellationToken; + _confirmations = new ConcurrentDictionary(); if (cancellationToken.CanBeCanceled) - _registration = cancellationToken.Register(() => Cancel(cancellationToken)); + _registration = cancellationToken.Register(Cancel); } public void Dispose() @@ -29,34 +29,39 @@ public void Dispose() public IPendingConfirmation Add(ProcessEventArgs eventArgs) { - var pendingConfirmation = new PendingConfirmation(_eventHubName, eventArgs); - return _confirmations.AddOrUpdate(pendingConfirmation.Offset, key => pendingConfirmation, (key, existing) => + _cancellationToken.ThrowIfCancellationRequested(); + + var pendingConfirmation = new PendingConfirmation(eventArgs); + return _confirmations.AddOrUpdate(eventArgs, key => pendingConfirmation, (key, existing) => { - existing.Faulted($"Duplicate key: {key}, partition: {eventArgs.Partition.PartitionId}"); + existing.Faulted($"Duplicate key: {key} on EventHub: {eventArgs.Partition.EventHubName}"); return pendingConfirmation; }); } - public void Faulted(long offset, Exception exception) + public void Faulted(PartitionOffset partitionOffset, Exception exception) { - if (_confirmations.TryRemove(offset, out var confirmation)) + if (_confirmations.TryRemove(partitionOffset, out var confirmation)) confirmation.Faulted(exception); } - public void Complete(long offset) + public void Complete(PartitionOffset partitionOffset) { - if (_confirmations.TryRemove(offset, out var confirmation)) + if (_confirmations.TryRemove(partitionOffset, out var confirmation)) confirmation.Complete(); } - void Cancel(CancellationToken cancellationToken) + public void Canceled(PartitionOffset partitionOffset, CancellationToken cancellationToken) { - foreach (var offset in _confirmations.Keys) - { - if (_confirmations.TryRemove(offset, out var confirmation)) - confirmation.Canceled(cancellationToken); - } + if (_confirmations.TryRemove(partitionOffset, out var confirmation)) + confirmation.Canceled(cancellationToken); + } + + void Cancel() + { + foreach (var partitionOffset in _confirmations.Keys) + Canceled(partitionOffset, _cancellationToken); } } } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Configuration/EventHubProducerSpecification.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Configuration/EventHubProducerSpecification.cs index fe3a61be90d..f09c2493b54 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Configuration/EventHubProducerSpecification.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Configuration/EventHubProducerSpecification.cs @@ -12,12 +12,12 @@ public class EventHubProducerSpecification : IEventHubProducerConfigurator, IEventHubProducerSpecification { + readonly List> _configureSend; readonly IEventHubHostConfiguration _hostConfiguration; readonly IHostSettings _hostSettings; readonly SendObservable _sendObservers; readonly ISerializationConfiguration _serializationConfiguration; Action _configureOptions; - Action _configureSend; public EventHubProducerSpecification(IEventHubHostConfiguration hostConfiguration, IHostSettings hostSettings) { @@ -25,6 +25,7 @@ public EventHubProducerSpecification(IEventHubHostConfiguration hostConfiguratio _hostSettings = hostSettings; _serializationConfiguration = new SerializationConfiguration(); _sendObservers = new SendObservable(); + _configureSend = new List>(); } public ConnectHandle ConnectSendObserver(ISendObserver observer) @@ -34,7 +35,7 @@ public ConnectHandle ConnectSendObserver(ISendObserver observer) public void ConfigureSend(Action callback) { - _configureSend = callback ?? throw new ArgumentNullException(nameof(callback)); + _configureSend.Add(callback ?? throw new ArgumentNullException(nameof(callback))); } public Action ConfigureOptions @@ -58,7 +59,9 @@ public IEnumerable Validate() public EventHubSendTransportContext CreateSendTransportContext(string eventHubName, IBusInstance busInstance) { var sendConfiguration = new SendPipeConfiguration(busInstance.HostConfiguration.Topology.SendTopology); - _configureSend?.Invoke(sendConfiguration.Configurator); + for (var i = 0; i < _configureSend.Count; i++) + _configureSend[i].Invoke(sendConfiguration.Configurator); + var sendPipe = sendConfiguration.CreatePipe(); var supervisor = new ProducerContextSupervisor(_hostConfiguration.ConnectionContextSupervisor, eventHubName); diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Configuration/EventHubReceiveEndpointConfigurator.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Configuration/EventHubReceiveEndpointConfigurator.cs index 040fd21548e..bdbe8bb408c 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Configuration/EventHubReceiveEndpointConfigurator.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Configuration/EventHubReceiveEndpointConfigurator.cs @@ -6,6 +6,7 @@ namespace MassTransit.EventHubIntegration.Configuration using Azure.Messaging.EventHubs.Processor; using Azure.Storage.Blobs; using MassTransit.Configuration; + using MassTransit.Middleware; using Middleware; using Transports; @@ -54,6 +55,9 @@ public EventHubReceiveEndpointConfigurator(IEventHubHostConfiguration hostConfig _blobClient = new Lazy(CreateBlobClient); PublishFaults = false; + + this.DiscardFaultedMessages(); + this.DiscardSkippedMessages(); } public override Uri HostAddress => _endpointConfiguration.HostAddress; @@ -110,6 +114,7 @@ public ReceiveEndpoint Build() var context = CreateEventHubReceiveContext(); _processorConfigurator.UseFilter(new EventHubBlobContainerFactoryFilter(_blobClient.Value)); + _processorConfigurator.UseFilter(new ReceiveEndpointDependencyFilter(context)); _processorConfigurator.UseFilter(new EventHubConsumerFilter(context)); IPipe processorPipe = _processorConfigurator.Build(); diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubDataReceiver.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubDataReceiver.cs index d4ebc39f83b..0131650d539 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubDataReceiver.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubDataReceiver.cs @@ -6,83 +6,72 @@ using System.Threading.Tasks; using Azure.Messaging.EventHubs; using Azure.Messaging.EventHubs.Processor; - using Internals; + using Checkpoints; using MassTransit.Middleware; using Transports; using Util; public class EventHubDataReceiver : - Agent, + ConsumerAgent, IEventHubDataReceiver { readonly CancellationTokenSource _checkpointTokenSource; readonly EventProcessorClient _client; readonly ReceiveEndpointContext _context; - readonly TaskCompletionSource _deliveryComplete; - readonly IReceivePipeDispatcher _dispatcher; readonly IChannelExecutorPool _executorPool; + readonly SemaphoreSlim _limit; readonly IProcessorLockContext _lockContext; public EventHubDataReceiver(ReceiveSettings receiveSettings, ReceiveEndpointContext context, ProcessorContext processorContext) + : base(context) { _context = context; _checkpointTokenSource = CancellationTokenSource.CreateLinkedTokenSource(Stopped); + _limit = new SemaphoreSlim(receiveSettings.PrefetchCount); var lockContext = new ProcessorLockContext(processorContext, receiveSettings, _checkpointTokenSource.Token); - _executorPool = new CombinedChannelExecutorPool(lockContext, receiveSettings); - _deliveryComplete = TaskUtil.GetTask(); - - _dispatcher = context.CreateReceivePipeDispatcher(); - _dispatcher.ZeroActivity += HandleDeliveryComplete; + IHashGenerator hashGenerator = new Murmur3UnsafeHashGenerator(); + _executorPool = new PartitionChannelExecutorPool(GetBytes, hashGenerator, + receiveSettings.ConcurrentMessageLimit, + receiveSettings.ConcurrentDeliveryLimit); _client = processorContext.GetClient(lockContext); + _lockContext = lockContext; _client.ProcessErrorAsync += HandleError; _client.ProcessEventAsync += HandleMessage; - _lockContext = lockContext; + + TrySetManualConsumeTask(); SetReady(_client.StartProcessingAsync(Stopping)); } - public long DeliveryCount => _dispatcher.DispatchCount; - - public int ConcurrentDeliveryCount => _dispatcher.MaxConcurrentDispatchCount; - async Task HandleError(ProcessErrorEventArgs eventArgs) { LogContext.SetCurrentIfNull(_context.LogContext); - var activeDispatchCount = _dispatcher.ActiveDispatchCount; - if (activeDispatchCount == 0) + if (IsIdle) { LogContext.Debug?.Log("Receiver shutdown completed: {InputAddress}, PartitionId: {PartitionId}", _context.InputAddress, eventArgs.PartitionId); - _deliveryComplete.TrySetResult(true); - - #pragma warning disable 4014 - Task.Run(async () => - { - try - { - if (!IsStopping) - await this.Stop($"Data Receiver Exception: {eventArgs.Exception.Message}").ConfigureAwait(false); - } - catch (Exception exception) - { - LogContext.Warning?.Log(exception, "Stop Faulted"); - } - }); - #pragma warning restore 4014 + TrySetConsumeException(eventArgs.Exception); } } + static byte[] GetBytes(ProcessEventArgs eventArgs) + { + var partitionKey = eventArgs.Data.PartitionKey; + return !string.IsNullOrEmpty(partitionKey) ? Encoding.UTF8.GetBytes(partitionKey) : Array.Empty(); + } + async Task HandleMessage(ProcessEventArgs eventArgs) { if (IsStopping || !eventArgs.HasEvent) return; + await _limit.WaitAsync(Stopping).ConfigureAwait(false); await _lockContext.Pending(eventArgs).ConfigureAwait(false); await _executorPool.Push(eventArgs, () => Handle(eventArgs), Stopping).ConfigureAwait(false); } @@ -92,54 +81,33 @@ async Task Handle(ProcessEventArgs eventArgs) if (IsStopping) return; - var context = new EventHubReceiveContext(eventArgs, _context, _lockContext); + var context = new EventHubReceiveContext(eventArgs, _context); + var cancellationToken = context.CancellationToken; + CancellationTokenRegistration? registration = null; + if (cancellationToken.CanBeCanceled) + registration = cancellationToken.Register(() => _lockContext.Canceled(eventArgs, cancellationToken)); try { - await _dispatcher.Dispatch(context, context).ConfigureAwait(false); + await Dispatch(eventArgs, context, new EventHubReceiveLockContext(eventArgs, _lockContext)).ConfigureAwait(false); } - finally + catch (Exception exception) { - context.Dispose(); + context.LogTransportFaulted(exception); } - } - - Task HandleDeliveryComplete() - { - if (IsStopping) + finally { - LogContext.Debug?.Log("Consumer shutdown completed: {InputAddress}", _context.InputAddress); - - _deliveryComplete.TrySetResult(true); + registration?.Dispose(); + context.Dispose(); + _limit.Release(); } - - return Task.CompletedTask; } - protected override Task StopAgent(StopContext context) - { - LogContext.Debug?.Log("Stopping consumer: {InputAddress}", _context.InputAddress); - - SetCompleted(ActiveAndActualAgentsCompleted(context)); - - return Completed; - } - - async Task ActiveAndActualAgentsCompleted(StopContext context) + protected override async Task ActiveAndActualAgentsCompleted(StopContext context) { var stopProcessing = _client.StopProcessingAsync(); - if (_dispatcher.ActiveDispatchCount > 0) - { - try - { - await _deliveryComplete.Task.OrCanceled(context.CancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - LogContext.Warning?.Log("Stop canceled waiting for message consumers to complete: {InputAddress}", _context.InputAddress); - } - } + await base.ActiveAndActualAgentsCompleted(context).ConfigureAwait(false); await _executorPool.DisposeAsync().ConfigureAwait(false); @@ -150,46 +118,9 @@ async Task ActiveAndActualAgentsCompleted(StopContext context) _client.ProcessEventAsync -= HandleMessage; _client.ProcessErrorAsync -= HandleError; + await _lockContext.DisposeAsync().ConfigureAwait(false); _checkpointTokenSource.Dispose(); - } - - - class CombinedChannelExecutorPool : - IChannelExecutorPool - { - readonly IChannelExecutorPool _keyExecutorPool; - readonly IChannelExecutorPool _partitionExecutorPool; - - public CombinedChannelExecutorPool(IChannelExecutorPool partitionExecutorPool, ReceiveSettings receiveSettings) - { - _partitionExecutorPool = partitionExecutorPool; - IHashGenerator hashGenerator = new Murmur3UnsafeHashGenerator(); - _keyExecutorPool = new PartitionChannelExecutorPool(GetBytes, hashGenerator, - receiveSettings.ConcurrentMessageLimit, - receiveSettings.ConcurrentDeliveryLimit); - } - - public Task Push(ProcessEventArgs args, Func handle, CancellationToken cancellationToken) - { - return _partitionExecutorPool.Push(args, () => _keyExecutorPool.Run(args, handle, cancellationToken), cancellationToken); - } - - public Task Run(ProcessEventArgs args, Func method, CancellationToken cancellationToken = default) - { - return _partitionExecutorPool.Run(args, () => _keyExecutorPool.Run(args, method, cancellationToken), cancellationToken); - } - - public async ValueTask DisposeAsync() - { - await _partitionExecutorPool.DisposeAsync().ConfigureAwait(false); - await _keyExecutorPool.DisposeAsync().ConfigureAwait(false); - } - - static byte[] GetBytes(ProcessEventArgs args) - { - var partitionKey = args.Data.PartitionKey; - return !string.IsNullOrEmpty(partitionKey) ? Encoding.UTF8.GetBytes(partitionKey) : Array.Empty(); - } + _limit.Dispose(); } } } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubProducer.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubProducer.cs index fff70be3ae8..f2378805801 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubProducer.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubProducer.cs @@ -123,6 +123,8 @@ public async Task Send(ProducerContext context) sendContext.CancellationToken.ThrowIfCancellationRequested(); StartedActivity? activity = LogContext.Current?.StartSendActivity(_context, sendContext); + StartedInstrument? instrument = LogContext.Current?.StartSendInstrument(_context, sendContext); + try { if (_context.SendObservers.Count > 0) @@ -144,12 +146,14 @@ public async Task Send(ProducerContext context) await _context.SendObservers.SendFault(sendContext, exception).ConfigureAwait(false); activity?.AddExceptionEvent(exception); + instrument?.AddException(exception); throw; } finally { activity?.Stop(); + instrument?.Stop(); } } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubProducerSendTransportContext.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubProducerSendTransportContext.cs index c35bc0a24f2..264149ceaeb 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubProducerSendTransportContext.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubProducerSendTransportContext.cs @@ -155,13 +155,14 @@ async Task FlushAsync(EventDataBatch batch) eventData.Properties.Set(context.Headers); - while (!eventDataBatch.TryAdd(eventData) && eventDataBatch.Count > 0) - { - await FlushAsync(eventDataBatch).ConfigureAwait(false); + if (eventDataBatch.TryAdd(eventData)) + continue; - if (sendContexts.Length - i > 1) - eventDataBatch = await producerContext.CreateBatch(options, context.CancellationToken).ConfigureAwait(false); - } + await FlushAsync(eventDataBatch).ConfigureAwait(false); + eventDataBatch = await producerContext.CreateBatch(options, context.CancellationToken).ConfigureAwait(false); + + if (!eventDataBatch.TryAdd(eventData)) + throw new ApplicationException("Message can not be added to the empty EventDataBatch"); } if (eventDataBatch.Count > 0) @@ -174,7 +175,7 @@ public Task Send(IPipe pipe, CancellationToken cancellationToke } public override string EntityName => _endpointAddress.EventHubName; - public override string ActivitySystem => EventHubEndpointAddress.PathPrefix; + public override string ActivitySystem => "eventhubs"; public override Task> CreateSendContext(T message, IPipe> pipe, CancellationToken cancellationToken) { diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubReceiveContext.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubReceiveContext.cs index 72bd036eac4..56155428b02 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubReceiveContext.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubReceiveContext.cs @@ -2,7 +2,6 @@ { using System; using System.Collections.Generic; - using System.Threading.Tasks; using Azure.Messaging.EventHubs; using Azure.Messaging.EventHubs.Processor; using Transports; @@ -10,19 +9,16 @@ public sealed class EventHubReceiveContext : BaseReceiveContext, - EventHubConsumeContext, - ReceiveLockContext + EventHubConsumeContext { readonly ProcessEventArgs _eventArgs; readonly EventData _eventData; - readonly IProcessorLockContext _lockContext; - public EventHubReceiveContext(ProcessEventArgs eventArgs, ReceiveEndpointContext receiveEndpointContext, IProcessorLockContext lockContext) + public EventHubReceiveContext(ProcessEventArgs eventArgs, ReceiveEndpointContext receiveEndpointContext) : base(false, receiveEndpointContext) { _eventArgs = eventArgs; _eventData = eventArgs.Data; - _lockContext = lockContext; Body = new BytesMessageBody(eventArgs.Data.Body.ToArray()); } @@ -38,20 +34,5 @@ public EventHubReceiveContext(ProcessEventArgs eventArgs, ReceiveEndpointContext public IDictionary Properties => _eventData.Properties; public long SequenceNumber => _eventData.SequenceNumber; public IReadOnlyDictionary SystemProperties => _eventData.SystemProperties; - - public Task Complete() - { - return _lockContext.Complete(_eventArgs); - } - - public Task Faulted(Exception exception) - { - return _lockContext.Faulted(_eventArgs, exception); - } - - public Task ValidateLockStatus() - { - return Task.CompletedTask; - } } } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubReceiveLockContext.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubReceiveLockContext.cs new file mode 100644 index 00000000000..157d8d7c33c --- /dev/null +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubReceiveLockContext.cs @@ -0,0 +1,36 @@ +namespace MassTransit.EventHubIntegration +{ + using System; + using System.Threading.Tasks; + using Azure.Messaging.EventHubs.Processor; + using Transports; + + + public class EventHubReceiveLockContext : + ReceiveLockContext + { + readonly ProcessEventArgs _eventArgs; + readonly IProcessorLockContext _lockContext; + + public EventHubReceiveLockContext(ProcessEventArgs eventArgs, IProcessorLockContext lockContext) + { + _eventArgs = eventArgs; + _lockContext = lockContext; + } + + public Task Complete() + { + return _lockContext.Complete(_eventArgs); + } + + public Task Faulted(Exception exception) + { + return _lockContext.Faulted(_eventArgs, exception); + } + + public Task ValidateLockStatus() + { + return Task.CompletedTask; + } + } +} diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubRider.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubRider.cs index 39207816864..56e1b6aa326 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubRider.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/EventHubRider.cs @@ -14,18 +14,18 @@ public class EventHubRider : IEventHubRider { readonly IBusInstance _busInstance; + readonly IRiderRegistrationContext _context; readonly IReceiveEndpointCollection _endpoints; readonly IEventHubHostConfiguration _hostConfiguration; - readonly IRiderRegistrationContext _registrationContext; Lazy _producerProvider; public EventHubRider(IEventHubHostConfiguration hostConfiguration, IBusInstance busInstance, IReceiveEndpointCollection endpoints, - IRiderRegistrationContext registrationContext) + IRiderRegistrationContext context) { _hostConfiguration = hostConfiguration; _busInstance = busInstance; _endpoints = endpoints; - _registrationContext = registrationContext; + _context = context; Reset(); } @@ -42,7 +42,7 @@ public HostReceiveEndpointHandle ConnectEventHubEndpoint(string eventHubName, st { var specification = _hostConfiguration.CreateSpecification(eventHubName, consumerGroup, configurator => { - configure?.Invoke(_registrationContext, configurator); + configure?.Invoke(_context, configurator); }); _endpoints.Add(specification.EndpointName, specification.CreateReceiveEndpoint(_busInstance)); @@ -91,7 +91,7 @@ public RiderAgent(IConnectionContextSupervisor supervisor, IReceiveEndpointColle protected override async Task StopAgent(StopContext context) { - await _endpoints.Stop(context.CancellationToken).ConfigureAwait(false); + await _endpoints.StopEndpoints(context.CancellationToken).ConfigureAwait(false); await _supervisor.Stop(context).ConfigureAwait(false); await base.StopAgent(context).ConfigureAwait(false); diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/IProcessorLockContext.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/IProcessorLockContext.cs index a729a3dcfbf..b5152c9fc69 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/IProcessorLockContext.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/IProcessorLockContext.cs @@ -1,16 +1,17 @@ namespace MassTransit.EventHubIntegration { using System; + using System.Threading; using System.Threading.Tasks; using Azure.Messaging.EventHubs.Processor; - using Util; public interface IProcessorLockContext : - IChannelExecutorPool + IAsyncDisposable { Task Pending(ProcessEventArgs eventArgs); - Task Faulted(ProcessEventArgs eventArgs, Exception exception); Task Complete(ProcessEventArgs eventArgs); + Task Faulted(ProcessEventArgs eventArgs, Exception exception); + void Canceled(ProcessEventArgs eventArgs, CancellationToken cancellationToken); } } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Middleware/EventHubBlobContainerFactoryFilter.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Middleware/EventHubBlobContainerFactoryFilter.cs index 7ca78e24e24..778157e7922 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Middleware/EventHubBlobContainerFactoryFilter.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/Middleware/EventHubBlobContainerFactoryFilter.cs @@ -1,5 +1,6 @@ namespace MassTransit.EventHubIntegration.Middleware { + using System; using System.Threading; using System.Threading.Tasks; using Azure; @@ -18,10 +19,19 @@ public EventHubBlobContainerFactoryFilter(BlobContainerClient blockClient) public async Task Send(ProcessorContext context, IPipe next) { - await context.OneTimeSetup(_ => CreateBlobIfNotExistsAsync(context.CancellationToken), () => new Context()) + OneTimeContext oneTimeContext = await context + .OneTimeSetup(() => CreateBlobIfNotExistsAsync(context.CancellationToken)) .ConfigureAwait(false); - await next.Send(context).ConfigureAwait(false); + try + { + await next.Send(context).ConfigureAwait(false); + } + catch (Exception) + { + oneTimeContext.Evict(); + throw; + } } public void Probe(ProbeContext context) @@ -48,11 +58,5 @@ async Task CreateBlobIfNotExistsAsync(CancellationToken cancellationToken return false; } } - - - class Context : - ConfigureTopologyContext - { - } } } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/PartitionCheckpointData.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/PartitionCheckpointData.cs new file mode 100644 index 00000000000..27980b09171 --- /dev/null +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/PartitionCheckpointData.cs @@ -0,0 +1,41 @@ +namespace MassTransit.EventHubIntegration +{ + using System.Threading; + using System.Threading.Tasks; + using Azure.Messaging.EventHubs.Processor; + using Checkpoints; + + + public class PartitionCheckpointData + { + readonly CancellationTokenSource _cancellationTokenSource; + readonly ICheckpointer _checkpointer; + readonly PendingConfirmationCollection _pending; + + public PartitionCheckpointData(ReceiveSettings settings, PendingConfirmationCollection pending) + { + _cancellationTokenSource = new CancellationTokenSource(); + _checkpointer = new BatchCheckpointer(settings, _cancellationTokenSource.Token); + _pending = pending; + } + + public Task Pending(ProcessEventArgs eventArgs) + { + var pendingConfirmation = _pending.Add(eventArgs); + return _checkpointer.Pending(pendingConfirmation); + } + + public async Task Close(PartitionClosingEventArgs args) + { + if (args.Reason != ProcessingStoppedReason.Shutdown) + _cancellationTokenSource.Cancel(); + + await _checkpointer.DisposeAsync().ConfigureAwait(false); + + LogContext.Info?.Log("Partition: {PartitionId} was closed, reason: {Reason}", args.PartitionId, args.Reason); + + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } + } +} diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/ProcessorLockContext.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/ProcessorLockContext.cs index 3380af353ef..1fcd699c79c 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/ProcessorLockContext.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegration/ProcessorLockContext.cs @@ -12,16 +12,23 @@ public class ProcessorLockContext : IProcessorLockContext, ProcessorClientBuilderContext { - readonly CancellationToken _cancellationToken; readonly ProcessorContext _context; - readonly SingleThreadedDictionary _data = new SingleThreadedDictionary(); + readonly SingleThreadedDictionary _data; + readonly PendingConfirmationCollection _pending; readonly ReceiveSettings _receiveSettings; public ProcessorLockContext(ProcessorContext context, ReceiveSettings receiveSettings, CancellationToken cancellationToken) { _context = context; _receiveSettings = receiveSettings; - _cancellationToken = cancellationToken; + _pending = new PendingConfirmationCollection(cancellationToken); + _data = new SingleThreadedDictionary(StringComparer.Ordinal); + } + + public ValueTask DisposeAsync() + { + _pending.Dispose(); + return default; } public Task Pending(ProcessEventArgs eventArgs) @@ -35,8 +42,7 @@ public Task Faulted(ProcessEventArgs eventArgs, Exception exception) { LogContext.SetCurrentIfNull(_context.LogContext); - if (_data.TryGetValue(eventArgs.Partition.PartitionId, out var data)) - data.Faulted(eventArgs, exception); + _pending.Faulted(eventArgs, exception); return Task.CompletedTask; } @@ -45,109 +51,33 @@ public Task Complete(ProcessEventArgs eventArgs) { LogContext.SetCurrentIfNull(_context.LogContext); - if (_data.TryGetValue(eventArgs.Partition.PartitionId, out var data)) - data.Complete(eventArgs); + _pending.Complete(eventArgs); return Task.CompletedTask; } - public ValueTask DisposeAsync() - { - return default; - } - - public Task Push(ProcessEventArgs partition, Func method, CancellationToken cancellationToken = default) + public void Canceled(ProcessEventArgs eventArgs, CancellationToken cancellationToken) { - return _data[partition.Partition.PartitionId].Push(method); - } + LogContext.SetCurrentIfNull(_context.LogContext); - public Task Run(ProcessEventArgs partition, Func method, CancellationToken cancellationToken = default) - { - return _data[partition.Partition.PartitionId].Run(method); + _pending.Canceled(eventArgs, cancellationToken); } public Task OnPartitionInitializing(PartitionInitializingEventArgs eventArgs) { LogContext.SetCurrentIfNull(_context.LogContext); - if (_data.TryAdd(eventArgs.PartitionId, _ => new PartitionCheckpointData(_receiveSettings, _cancellationToken))) + if (_data.TryAdd(eventArgs.PartitionId, _ => new PartitionCheckpointData(_receiveSettings, _pending))) LogContext.Info?.Log("Partition: {PartitionId} was initialized", eventArgs.PartitionId); return Task.CompletedTask; } - public async Task OnPartitionClosing(PartitionClosingEventArgs eventArgs) + public Task OnPartitionClosing(PartitionClosingEventArgs eventArgs) { LogContext.SetCurrentIfNull(_context.LogContext); - if (!_data.TryGetValue(eventArgs.PartitionId, out var data)) - return; - - await data.Close(eventArgs).ConfigureAwait(false); - _data.TryRemove(eventArgs.PartitionId, out _); - } - - - sealed class PartitionCheckpointData - { - readonly CancellationToken _cancellationToken; - readonly CancellationTokenSource _cancellationTokenSource; - readonly ICheckpointer _checkpointer; - readonly ChannelExecutor _executor; - readonly PendingConfirmationCollection _pending; - - public PartitionCheckpointData(ReceiveSettings settings, CancellationToken cancellationToken) - { - _cancellationToken = cancellationToken; - _cancellationTokenSource = new CancellationTokenSource(); - _executor = new ChannelExecutor(settings.PrefetchCount, settings.ConcurrentMessageLimit); - _checkpointer = new BatchCheckpointer(settings, _cancellationTokenSource.Token); - _pending = new PendingConfirmationCollection(settings.EventHubName, cancellationToken); - } - - public Task Pending(ProcessEventArgs eventArgs) - { - if (_cancellationToken.IsCancellationRequested) - return Task.CompletedTask; - - var pendingConfirmation = _pending.Add(eventArgs); - return _checkpointer.Pending(pendingConfirmation); - } - - public void Complete(ProcessEventArgs eventArgs) - { - _pending.Complete(eventArgs.Data.Offset); - } - - public void Faulted(ProcessEventArgs eventArgs, Exception exception) - { - _pending.Faulted(eventArgs.Data.Offset, exception); - } - - public async Task Close(PartitionClosingEventArgs args) - { - if (args.Reason != ProcessingStoppedReason.Shutdown) - _cancellationTokenSource.Cancel(); - - await _executor.DisposeAsync().ConfigureAwait(false); - await _checkpointer.DisposeAsync().ConfigureAwait(false); - - LogContext.Info?.Log("Partition: {PartitionId} was closed, reason: {Reason}", args.PartitionId, args.Reason); - - _pending.Dispose(); - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); - } - - public Task Push(Func method) - { - return _executor.Push(method, _cancellationTokenSource.Token); - } - - public Task Run(Func method) - { - return _executor.Run(method, _cancellationTokenSource.Token); - } + return _data.TryRemove(eventArgs.PartitionId, out var data) ? data.Close(eventArgs) : Task.CompletedTask; } } } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegrationExtensions.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegrationExtensions.cs index 39d3f1d37ba..b7f5a5f24b4 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubIntegrationExtensions.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubIntegrationExtensions.cs @@ -36,7 +36,7 @@ public static void UsingEventHub(this IRiderRegistrationConfigurator static IEventHubProducerProvider GetCurrentProducerProvider(IEventHubRider rider, IServiceProvider provider) { - var contextProvider = provider.GetService(); + var contextProvider = provider.GetService(); if (contextProvider != null) { return contextProvider.HasContext diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubNameProvider.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubNameProvider.cs index 434d4cae34b..9aa1a69f288 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubNameProvider.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubNameProvider.cs @@ -3,21 +3,21 @@ namespace MassTransit /// /// Return the name of the event hub /// - /// + /// /// /// - public delegate string EventHubNameProvider(BehaviorContext context) - where TSaga : class, ISaga; + public delegate string EventHubNameProvider(BehaviorContext context) + where TInstance : class, SagaStateMachineInstance; /// /// Return the name of the event hub /// - /// + /// /// /// /// - public delegate string EventHubNameProvider(BehaviorContext context) - where TSaga : class, ISaga + public delegate string EventHubNameProvider(BehaviorContext context) + where TInstance : class, SagaStateMachineInstance where TMessage : class; } diff --git a/src/Transports/MassTransit.EventHubIntegration/EventHubSendContext.cs b/src/Transports/MassTransit.EventHubIntegration/EventHubSendContext.cs index d18b59d9934..cf8fc04d355 100644 --- a/src/Transports/MassTransit.EventHubIntegration/EventHubSendContext.cs +++ b/src/Transports/MassTransit.EventHubIntegration/EventHubSendContext.cs @@ -1,10 +1,10 @@ namespace MassTransit { public interface EventHubSendContext : - SendContext + SendContext, + PartitionKeySendContext { string PartitionId { get; set; } - string PartitionKey { get; set; } } diff --git a/src/Transports/MassTransit.EventHubIntegration/ExceptionEventHubNameProvider.cs b/src/Transports/MassTransit.EventHubIntegration/ExceptionEventHubNameProvider.cs index bac010a79c3..8da5567504d 100644 --- a/src/Transports/MassTransit.EventHubIntegration/ExceptionEventHubNameProvider.cs +++ b/src/Transports/MassTransit.EventHubIntegration/ExceptionEventHubNameProvider.cs @@ -14,7 +14,7 @@ namespace MassTransit public delegate string ExceptionEventHubNameProvider(BehaviorExceptionContext context) where TException : Exception where TData : class - where TInstance : class, ISaga; + where TInstance : class, SagaStateMachineInstance; /// @@ -26,5 +26,5 @@ public delegate string ExceptionEventHubNameProvider public delegate string ExceptionEventHubNameProvider(BehaviorExceptionContext context) where TException : Exception - where TInstance : class, ISaga; + where TInstance : class, SagaStateMachineInstance; } diff --git a/src/Transports/MassTransit.EventHubIntegration/Exceptions/EventHubConnectionException.cs b/src/Transports/MassTransit.EventHubIntegration/Exceptions/EventHubConnectionException.cs index 94be00b4d3c..3978f126ad6 100644 --- a/src/Transports/MassTransit.EventHubIntegration/Exceptions/EventHubConnectionException.cs +++ b/src/Transports/MassTransit.EventHubIntegration/Exceptions/EventHubConnectionException.cs @@ -22,6 +22,9 @@ public EventHubConnectionException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected EventHubConnectionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.EventHubIntegration/MassTransit.EventHubIntegration.csproj b/src/Transports/MassTransit.EventHubIntegration/MassTransit.EventHubIntegration.csproj index 50e11bf54e5..17ffe0d4d1b 100644 --- a/src/Transports/MassTransit.EventHubIntegration/MassTransit.EventHubIntegration.csproj +++ b/src/Transports/MassTransit.EventHubIntegration/MassTransit.EventHubIntegration.csproj @@ -2,11 +2,12 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 + Denys Kozhevnikov, $(Authors) - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -26,7 +27,6 @@ - diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/GrpcConfigurationExtensions.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/GrpcConfigurationExtensions.cs deleted file mode 100644 index 18af5a1e80f..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/GrpcConfigurationExtensions.cs +++ /dev/null @@ -1,55 +0,0 @@ -namespace MassTransit -{ - using System; - using GrpcTransport.Configuration; - - - public static class GrpcConfigurationExtensions - { - /// - /// Configure and create a gRPC bus - /// - /// Hang off the selector interface for visibility - /// The configuration callback to configure the bus - /// - public static IBusControl CreateUsingGrpc(this IBusFactorySelector selector, Action configure) - { - return GrpcBus.Create(configure); - } - - /// - /// Configure and create a gRPC bus - /// - /// Hang off the selector interface for visibility - /// Override the default base address - /// The configuration callback to configure the bus - /// - public static IBusControl CreateUsingGrpc(this IBusFactorySelector selector, Uri baseAddress, Action configure) - { - return GrpcBus.Create(baseAddress, configure); - } - - /// - /// Configure MassTransit to use the gRPC transport - /// - /// The registration configurator (configured via AddMassTransit) - /// The configuration callback for the bus factory - public static void UsingGrpc(this IBusRegistrationConfigurator configurator, - Action configure = null) - { - UsingGrpc(configurator, null, configure); - } - - /// - /// Configure MassTransit to use the gRPC transport - /// - /// The registration configurator (configured via AddMassTransit) - /// The base Address of the transport - /// The configuration callback for the bus factory - public static void UsingGrpc(this IBusRegistrationConfigurator configurator, Uri baseAddress, - Action configure = null) - { - configurator.SetBusFactory(new GrpcRegistrationBusFactory(baseAddress, configure)); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/GrpcReceiveEndpointConfiguratorExtensions.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/GrpcReceiveEndpointConfiguratorExtensions.cs deleted file mode 100644 index 3477104f5cd..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/GrpcReceiveEndpointConfiguratorExtensions.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace MassTransit -{ - using Transports.Fabric; - - - public static class GrpcReceiveEndpointConfiguratorExtensions - { - /// - /// Bind an exchange to the receive endpoint queue - /// - /// - /// The exchange name (not case-sensitive) - /// The exchange type - /// Only valid for direct/topic exchanges - public static void Bind(this IReceiveEndpointConfigurator configurator, string exchangeName, ExchangeType exchangeType = ExchangeType.FanOut, - string routingKey = default) - { - if (configurator is IGrpcReceiveEndpointConfigurator grpc) - grpc.Bind(exchangeName, exchangeType, routingKey); - } - - /// - /// Bind an exchange to the receive endpoint queue - /// - /// - /// The exchange type - /// Only valid for direct/topic exchanges - public static void Bind(this IReceiveEndpointConfigurator configurator, ExchangeType? exchangeType = default, string routingKey = default) - where T : class - { - if (configurator is IGrpcReceiveEndpointConfigurator grpc) - grpc.Bind(exchangeType, routingKey); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/GrpcRoutingKeyConventionExtensions.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/GrpcRoutingKeyConventionExtensions.cs deleted file mode 100644 index e6ef6e84719..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/GrpcRoutingKeyConventionExtensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace MassTransit -{ - using System; - using GrpcTransport; - using GrpcTransport.Configuration; - - - public static class GrpcRoutingKeyConventionExtensions - { - public static void UseRoutingKeyFormatter(this IMessageSendTopologyConfigurator configurator, IMessageRoutingKeyFormatter formatter) - where T : class - { - configurator.UpdateConvention>( - update => - { - update.SetFormatter(formatter); - - return update; - }); - } - - /// - /// Use the routing key formatter for the specified message type - /// - /// - /// - /// - public static void UseRoutingKeyFormatter(this ISendTopologyConfigurator configurator, IMessageRoutingKeyFormatter formatter) - where T : class - { - configurator.GetMessageTopology().UseRoutingKeyFormatter(formatter); - } - - /// - /// Use the delegate to format the routing key, using Empty if the string is null upon return - /// - /// - /// - /// - public static void UseRoutingKeyFormatter(this ISendTopologyConfigurator configurator, Func, string> formatter) - where T : class - { - configurator.GetMessageTopology().UseRoutingKeyFormatter(new DelegateRoutingKeyFormatter(formatter)); - } - - /// - /// Use the delegate to format the routing key, using Empty if the string is null upon return - /// - /// - /// - /// - public static void UseRoutingKeyFormatter(this IMessageSendTopologyConfigurator configurator, Func, string> formatter) - where T : class - { - configurator.UseRoutingKeyFormatter(new DelegateRoutingKeyFormatter(formatter)); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcBusFactoryConfigurator.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcBusFactoryConfigurator.cs deleted file mode 100644 index 4a66247a5cf..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcBusFactoryConfigurator.cs +++ /dev/null @@ -1,37 +0,0 @@ -#nullable enable -namespace MassTransit -{ - using System; - - - public interface IGrpcBusFactoryConfigurator : - IBusFactoryConfigurator - { - new IGrpcPublishTopologyConfigurator PublishTopology { get; } - - /// - /// Configure the send topology of the message type - /// - /// - /// - void Publish(Action>? configureTopology) - where T : class; - - void Publish(Type messageType, Action? configure = null); - - /// - /// Configure the base address for the host - /// - /// - /// - void Host(Action? configure = null); - - /// - /// Configure the base address for the host - /// - /// The base address for the in-memory host - /// - /// - void Host(Uri baseAddress, Action? configure = null); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcConsumeTopologyConfigurator.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcConsumeTopologyConfigurator.cs deleted file mode 100644 index 4d1a69c3aa0..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcConsumeTopologyConfigurator.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace MassTransit -{ - using Transports.Fabric; - - - public interface IGrpcConsumeTopologyConfigurator : - IConsumeTopologyConfigurator, - IGrpcConsumeTopology - { - new IGrpcMessageConsumeTopologyConfigurator GetMessageTopology() - where T : class; - - void AddSpecification(IGrpcConsumeTopologySpecification specification); - - void Bind(string exchangeName, ExchangeType exchangeType = ExchangeType.FanOut, string routingKey = default); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcConsumeTopologySpecification.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcConsumeTopologySpecification.cs deleted file mode 100644 index 554aa1cd09b..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcConsumeTopologySpecification.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MassTransit -{ - using Configuration; - - - public interface IGrpcConsumeTopologySpecification : - ISpecification - { - void Apply(IMessageFabricConsumeTopologyBuilder builder); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcHostConfigurator.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcHostConfigurator.cs deleted file mode 100644 index 383c64cfa23..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcHostConfigurator.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit -{ - using System; - - - public interface IGrpcHostConfigurator - { - /// - /// Set the port for the http server - /// - int Port { set; } - - /// - /// Set the host name - /// - string Host { set; } - - /// - /// Add a server to connect to on startup as part of the message fabric - /// - /// - void AddServer(Uri address); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcMessageConsumeTopologyConfigurator.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcMessageConsumeTopologyConfigurator.cs deleted file mode 100644 index afbbfffa409..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcMessageConsumeTopologyConfigurator.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MassTransit -{ - using Configuration; - using Transports.Fabric; - - - public interface IGrpcMessageConsumeTopologyConfigurator : - IMessageConsumeTopologyConfigurator, - IGrpcMessageConsumeTopology - where TMessage : class - { - /// - /// Adds the exchange bindings for this message type - /// - void Bind(ExchangeType? exchangeType = ExchangeType.FanOut, string routingKey = default); - } - - - public interface IGrpcMessageConsumeTopologyConfigurator : - IMessageConsumeTopologyConfigurator - { - /// - /// Apply the message topology to the builder - /// - /// - void Apply(IMessageFabricConsumeTopologyBuilder builder); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcMessagePublishTopologyConfigurator.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcMessagePublishTopologyConfigurator.cs deleted file mode 100644 index 9dc19b07ee9..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcMessagePublishTopologyConfigurator.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MassTransit -{ - using Transports.Fabric; - - - public interface IGrpcMessagePublishTopologyConfigurator : - IMessagePublishTopologyConfigurator, - IGrpcMessagePublishTopology, - IGrpcMessagePublishTopologyConfigurator - where TMessage : class - { - new ExchangeType ExchangeType { set; } - } - - - public interface IGrpcMessagePublishTopologyConfigurator : - IMessagePublishTopologyConfigurator - { - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcPublishTopologyConfigurator.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcPublishTopologyConfigurator.cs deleted file mode 100644 index 1852d5a83da..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcPublishTopologyConfigurator.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit -{ - using System; - - - public interface IGrpcPublishTopologyConfigurator : - IPublishTopologyConfigurator, - IGrpcPublishTopology - { - new IGrpcMessagePublishTopologyConfigurator GetMessageTopology() - where T : class; - - new IGrpcMessagePublishTopologyConfigurator GetMessageTopology(Type messageType); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcReceiveEndpointConfigurator.cs b/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcReceiveEndpointConfigurator.cs deleted file mode 100644 index 2bf67f647b2..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Configuration/IGrpcReceiveEndpointConfigurator.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MassTransit -{ - using Transports.Fabric; - - - public interface IGrpcReceiveEndpointConfigurator : - IReceiveEndpointConfigurator - { - /// - /// Bind an exchange to the receive endpoint queue - /// - /// The exchange name (not case-sensitive) - /// The exchange type - /// Only valid for direct/topic exchanges - void Bind(string exchangeName, ExchangeType exchangeType = ExchangeType.FanOut, string routingKey = default); - - /// - /// Bind an exchange to the receive endpoint queue - /// - /// The exchange type - /// Only valid for direct/topic exchanges - void Bind(ExchangeType? exchangeType = default, string routingKey = default) - where T : class; - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcBus.cs b/src/Transports/MassTransit.GrpcTransport/GrpcBus.cs deleted file mode 100644 index 4e7a5759625..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcBus.cs +++ /dev/null @@ -1,56 +0,0 @@ -namespace MassTransit -{ - using System; - using System.Threading; - using Configuration; - using GrpcTransport.Configuration; - using Topology; - - - public static class GrpcBus - { - public static IMessageTopologyConfigurator MessageTopology => Cached.MessageTopologyValue.Value; - - /// - /// Configure and create an in-memory bus - /// - /// The configuration callback to configure the bus - /// - public static IBusControl Create(Action configure) - { - return Create(null, configure); - } - - /// - /// Configure and create an in-memory bus - /// - /// Override the default base address - /// The configuration callback to configure the bus - /// - public static IBusControl Create(Uri baseAddress, Action configure) - { - var topologyConfiguration = new GrpcTopologyConfiguration(MessageTopology); - var busConfiguration = new GrpcBusConfiguration(topologyConfiguration, baseAddress); - - var configurator = new GrpcBusFactoryConfigurator(busConfiguration); - - configure(configurator); - - return configurator.Build(busConfiguration); - } - - - static class Cached - { - internal static readonly Lazy MessageTopologyValue = - new Lazy(() => new MessageTopology(_entityNameFormatter), LazyThreadSafetyMode.PublicationOnly); - - static readonly IEntityNameFormatter _entityNameFormatter; - - static Cached() - { - _entityNameFormatter = new MessageUrnEntityNameFormatter(); - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcConsumeContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcConsumeContext.cs deleted file mode 100644 index 5367360326f..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcConsumeContext.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MassTransit -{ - public interface GrpcConsumeContext : - RoutingKeyConsumeContext - { - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcEndpointAddress.cs b/src/Transports/MassTransit.GrpcTransport/GrpcEndpointAddress.cs deleted file mode 100644 index cfcd93fca68..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcEndpointAddress.cs +++ /dev/null @@ -1,151 +0,0 @@ -namespace MassTransit -{ - using System; - using System.Collections.Generic; - using System.Diagnostics; - using Internals; - using Transports.Fabric; - - - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] - public readonly struct GrpcEndpointAddress - { - const string BindQueueKey = "bind"; - const string QueueNameKey = "queue"; - const string ExchangeTypeKey = "type"; - const string InstanceIdKey = "instance"; - - public readonly string Scheme; - public readonly string Host; - public readonly int Port; - public readonly string VirtualHost; - - public readonly string Name; - public readonly ExchangeType ExchangeType; - public readonly bool BindToQueue; - public readonly string QueueName; - public readonly string InstanceId; - - public GrpcEndpointAddress(Uri hostAddress, Uri address) - { - Scheme = default; - Host = default; - Port = default; - VirtualHost = default; - - ExchangeType = ExchangeType.FanOut; - - BindToQueue = false; - QueueName = default; - InstanceId = default; - - var scheme = address.Scheme.ToLowerInvariant(); - switch (scheme) - { - case "http": - case "https": - Scheme = scheme; - Host = address.Host; - Port = address.Port; - - address.ParseHostPathAndEntityName(out VirtualHost, out Name); - break; - - case "queue": - ParseLeft(hostAddress, out Scheme, out Host, out Port, out VirtualHost); - - Name = address.AbsolutePath; - BindToQueue = true; - break; - - case "exchange": - case "topic": - ParseLeft(hostAddress, out Scheme, out Host, out Port, out VirtualHost); - - Name = address.AbsolutePath; - break; - - default: - throw new ArgumentException($"The address scheme is not supported: {address.Scheme}", nameof(address)); - } - - if (Name == "*") - Name = NewId.Next().ToString("NS"); - - foreach (var (key, value) in address.SplitQueryString()) - { - switch (key) - { - case ExchangeTypeKey when Enum.TryParse(value, out var result): - ExchangeType = result; - break; - - case BindQueueKey when bool.TryParse(value, out var result): - BindToQueue = result; - break; - - case QueueNameKey when !string.IsNullOrWhiteSpace(value): - QueueName = Uri.UnescapeDataString(value); - break; - - case InstanceIdKey when !string.IsNullOrWhiteSpace(value): - InstanceId = value; - break; - } - } - } - - public GrpcEndpointAddress(Uri hostAddress, string exchangeName, bool bindToQueue = false, string queueName = default, - ExchangeType exchangeType = ExchangeType.FanOut, string instanceId = default) - { - ParseLeft(hostAddress, out Scheme, out Host, out Port, out VirtualHost); - - Name = exchangeName; - ExchangeType = exchangeType; - - BindToQueue = bindToQueue; - QueueName = queueName; - InstanceId = instanceId; - } - - static void ParseLeft(Uri address, out string scheme, out string host, out int port, out string virtualHost) - { - var hostAddress = new GrpcHostAddress(address); - scheme = hostAddress.Scheme; - host = hostAddress.Host; - port = hostAddress.Port; - virtualHost = hostAddress.VirtualHost; - } - - public static implicit operator Uri(in GrpcEndpointAddress address) - { - var builder = new UriBuilder - { - Scheme = address.Scheme, - Host = address.Host, - Port = address.Port, - Path = address.VirtualHost == "/" - ? $"/{address.Name}" - : $"/{Uri.EscapeDataString(address.VirtualHost)}/{address.Name}" - }; - - builder.Query += string.Join("&", address.GetQueryStringOptions()); - - return builder.Uri; - } - - Uri DebuggerDisplay => this; - - IEnumerable GetQueryStringOptions() - { - if (BindToQueue) - yield return $"{BindQueueKey}=true"; - if (!string.IsNullOrWhiteSpace(QueueName)) - yield return $"{QueueNameKey}={Uri.EscapeDataString(QueueName)}"; - if (ExchangeType != ExchangeType.FanOut) - yield return $"{ExchangeTypeKey}={ExchangeType}"; - if (!string.IsNullOrWhiteSpace(InstanceId)) - yield return $"{InstanceIdKey}={InstanceId}"; - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcHostAddress.cs b/src/Transports/MassTransit.GrpcTransport/GrpcHostAddress.cs deleted file mode 100644 index bfc2253a378..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcHostAddress.cs +++ /dev/null @@ -1,61 +0,0 @@ -namespace MassTransit -{ - using System; - using System.Diagnostics; - using Internals; - - - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + "}")] - public readonly struct GrpcHostAddress - { - public readonly string Scheme; - public readonly string Host; - public readonly int Port; - public readonly string VirtualHost; - - public GrpcHostAddress(Uri address) - { - Scheme = default; - Host = default; - Port = default; - VirtualHost = default; - - var scheme = address.Scheme.ToLowerInvariant(); - switch (scheme) - { - case "http": - case "https": - ParseLeft(address, out Scheme, out Host, out Port, out VirtualHost); - break; - - default: - throw new ArgumentException($"The address scheme is not supported: {address.Scheme}", nameof(address)); - } - } - - static void ParseLeft(Uri address, out string scheme, out string host, out int port, out string virtualHost) - { - scheme = address.Scheme; - host = address.Host; - port = address.Port; - virtualHost = address.ParseHostPath(); - } - - public static implicit operator Uri(in GrpcHostAddress address) - { - var builder = new UriBuilder - { - Scheme = address.Scheme, - Host = address.Host, - Port = address.Port, - Path = address.VirtualHost == "/" - ? "/" - : $"/{Uri.EscapeDataString(address.VirtualHost)}" - }; - - return builder.Uri; - } - - Uri DebuggerDisplay => this; - } -} \ No newline at end of file diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcPublishTopologyConfigurationExtensions.cs b/src/Transports/MassTransit.GrpcTransport/GrpcPublishTopologyConfigurationExtensions.cs deleted file mode 100644 index 6ac950a896b..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcPublishTopologyConfigurationExtensions.cs +++ /dev/null @@ -1,73 +0,0 @@ -#nullable enable -namespace MassTransit -{ - using System; - using System.Collections.Generic; - using Util; - - - public static class GrpcPublishTopologyConfigurationExtensions - { - /// - /// Adds any valid message types found in the specified namespace to the publish topology - /// - /// - /// - /// - public static void AddPublishMessageTypesFromNamespaceContaining(this IGrpcBusFactoryConfigurator configurator, - Action? configure = null, Func? filter = null) - { - AddPublishMessageTypesFromNamespaceContaining(configurator, typeof(T), configure, filter); - } - - /// - /// Adds any valid message types found in the specified namespace to the publish topology - /// - /// - /// The type to use to identify the assembly and namespace to scan - /// - /// - public static void AddPublishMessageTypesFromNamespaceContaining(this IGrpcBusFactoryConfigurator configurator, Type type, - Action? configure = null, Func? filter = null) - { - if (type == null) - throw new ArgumentNullException(nameof(type)); - - if (type.Assembly == null || type.Namespace == null) - throw new ArgumentException($"The type {TypeCache.GetShortName(type)} is not in an assembly with a valid namespace", nameof(type)); - - IEnumerable types; - - const TypeClassification typeClassification = TypeClassification.Concrete | TypeClassification.Closed | TypeClassification.Abstract - | TypeClassification.Interface; - - if (filter != null) - { - bool IsAllowed(Type candidate) - { - return MessageTypeCache.IsValidMessageType(candidate) && filter(candidate); - } - - types = AssemblyTypeCache.FindTypesInNamespace(type, IsAllowed, typeClassification); - } - else - types = AssemblyTypeCache.FindTypesInNamespace(type, MessageTypeCache.IsValidMessageType, typeClassification); - - foreach (var messageType in types) - configurator.Publish(messageType, x => configure?.Invoke(x, messageType)); - } - - /// - /// Adds the specified message types to the publish topology - /// - /// - /// - /// - public static void AddPublishMessageTypes(this IGrpcBusFactoryConfigurator configurator, IEnumerable messageTypes, - Action? configure = null) - { - foreach (var messageType in messageTypes) - configurator.Publish(messageType, x => configure?.Invoke(x, messageType)); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcSendContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcSendContext.cs deleted file mode 100644 index 539ec80f1a4..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcSendContext.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MassTransit -{ - public interface GrpcSendContext : - SendContext, - GrpcSendContext - where T : class - { - } - - - public interface GrpcSendContext : - SendContext, - RoutingKeySendContext - { - /// - /// Specify that the published message must be delivered to a queue or it will be returned - /// - bool Mandatory { get; set; } - - /// - /// The destination exchange for the message - /// - string Exchange { get; } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/ClientNodeContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/ClientNodeContext.cs deleted file mode 100644 index a63afaf1021..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/ClientNodeContext.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - - - public class ClientNodeContext : - NodeContext - { - public ClientNodeContext(Uri nodeAddress) - { - NodeAddress = nodeAddress; - SessionId = NewId.NextGuid(); - } - - public NodeType NodeType => NodeType.Client; - public Uri NodeAddress { get; } - public Guid SessionId { get; } - public HostInfo Host { get; set; } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcBusConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcBusConfiguration.cs deleted file mode 100644 index ad250274915..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcBusConfiguration.cs +++ /dev/null @@ -1,48 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System; - using MassTransit.Configuration; - using Observables; - using Serialization; - - - public class GrpcBusConfiguration : - GrpcEndpointConfiguration, - IGrpcBusConfiguration - { - readonly BusObservable _busObservers; - - public GrpcBusConfiguration(IGrpcTopologyConfiguration topologyConfiguration, Uri baseAddress) - : base(topologyConfiguration) - { - HostConfiguration = new GrpcHostConfiguration(this, baseAddress, topologyConfiguration); - - var factory = new GrpcSerializerFactory(); - - Serialization.Clear(); - Serialization.AddSerializer(factory); - Serialization.AddDeserializer(factory, true); - - BusEndpointConfiguration = CreateEndpointConfiguration(true); - - _busObservers = new BusObservable(); - } - - IHostConfiguration IBusConfiguration.HostConfiguration => HostConfiguration; - IEndpointConfiguration IBusConfiguration.BusEndpointConfiguration => BusEndpointConfiguration; - IBusObserver IBusConfiguration.BusObservers => _busObservers; - - public IGrpcEndpointConfiguration BusEndpointConfiguration { get; } - public IGrpcHostConfiguration HostConfiguration { get; } - - public ConnectHandle ConnectBusObserver(IBusObserver observer) - { - return _busObservers.Connect(observer); - } - - public ConnectHandle ConnectEndpointConfigurationObserver(IEndpointConfigurationObserver observer) - { - return HostConfiguration.ConnectEndpointConfigurationObserver(observer); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcBusFactoryConfigurator.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcBusFactoryConfigurator.cs deleted file mode 100644 index a622a269765..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcBusFactoryConfigurator.cs +++ /dev/null @@ -1,90 +0,0 @@ -#nullable enable -namespace MassTransit.GrpcTransport.Configuration -{ - using System; - using MassTransit.Configuration; - - - public class GrpcBusFactoryConfigurator : - BusFactoryConfigurator, - IGrpcBusFactoryConfigurator, - IBusFactory - { - readonly IGrpcBusConfiguration _busConfiguration; - readonly IGrpcHostConfiguration _hostConfiguration; - - public GrpcBusFactoryConfigurator(IGrpcBusConfiguration busConfiguration) - : base(busConfiguration) - { - _busConfiguration = busConfiguration; - _hostConfiguration = busConfiguration.HostConfiguration; - - busConfiguration.BusEndpointConfiguration.Consume.Configurator.AutoStart = true; - } - - public IReceiveEndpointConfiguration CreateBusEndpointConfiguration(Action configure) - { - var hostAddress = _hostConfiguration.HostAddress; - - var queueName = $"bus-{hostAddress.Host}-{hostAddress.Port}"; - - return _hostConfiguration.CreateReceiveEndpointConfiguration(queueName, _busConfiguration.BusEndpointConfiguration, configure); - } - - public override bool AutoStart - { - set { } - } - - public void Publish(Action>? configureTopology) - where T : class - { - IGrpcMessagePublishTopologyConfigurator configurator = _busConfiguration.Topology.Publish.GetMessageTopology(); - - configureTopology?.Invoke(configurator); - } - - public void Publish(Type messageType, Action? configure = null) - { - var configurator = _busConfiguration.Topology.Publish.GetMessageTopology(messageType); - - configure?.Invoke(configurator); - } - - public void Host(Action? configure) - { - configure?.Invoke(_hostConfiguration.Configurator); - } - - public void Host(Uri baseAddress, Action? configure) - { - _hostConfiguration.BaseAddress = baseAddress; - - configure?.Invoke(_hostConfiguration.Configurator); - } - - public new IGrpcPublishTopologyConfigurator PublishTopology => _busConfiguration.Topology.Publish; - - public void ReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter? endpointNameFormatter, - Action? configureEndpoint) - { - _hostConfiguration.ReceiveEndpoint(definition, endpointNameFormatter, configureEndpoint); - } - - public void ReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter? endpointNameFormatter, - Action? configureEndpoint) - { - _hostConfiguration.ReceiveEndpoint(definition, endpointNameFormatter, configureEndpoint); - } - - public void ReceiveEndpoint(string queueName, Action configureEndpoint) - { - _hostConfiguration.ReceiveEndpoint(queueName, configureEndpoint); - } - - public void ReceiveEndpoint(string queueName, Action configureEndpoint) - { - _hostConfiguration.ReceiveEndpoint(queueName, configureEndpoint); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcEndpointConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcEndpointConfiguration.cs deleted file mode 100644 index d854e46c60c..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcEndpointConfiguration.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - - - public class GrpcEndpointConfiguration : - EndpointConfiguration, - IGrpcEndpointConfiguration - { - readonly IGrpcTopologyConfiguration _topologyConfiguration; - - protected GrpcEndpointConfiguration(IGrpcTopologyConfiguration topologyConfiguration) - : base(topologyConfiguration) - { - _topologyConfiguration = topologyConfiguration; - } - - GrpcEndpointConfiguration(IGrpcEndpointConfiguration parentConfiguration, IGrpcTopologyConfiguration topologyConfiguration, bool isBusEndpoint) - : base(parentConfiguration, topologyConfiguration, isBusEndpoint) - { - _topologyConfiguration = topologyConfiguration; - } - - IGrpcTopologyConfiguration IGrpcEndpointConfiguration.Topology => _topologyConfiguration; - - public IGrpcEndpointConfiguration CreateEndpointConfiguration(bool isBusEndpoint) - { - var topologyConfiguration = new GrpcTopologyConfiguration(_topologyConfiguration); - - return new GrpcEndpointConfiguration(this, topologyConfiguration, isBusEndpoint); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcHostConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcHostConfiguration.cs deleted file mode 100644 index ccd417c1e24..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcHostConfiguration.cs +++ /dev/null @@ -1,159 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System; - using System.Collections.Generic; - using Grpc.Core; - using MassTransit.Configuration; - using Topology; - using Transports; - using Util; - - - public class GrpcHostConfiguration : - BaseHostConfiguration, - IGrpcHostConfiguration, - IGrpcHostConfigurator - { - static readonly Uri _defaultHostAddress = new Uri("http://127.0.0.1:0/"); - - readonly IGrpcBusConfiguration _busConfiguration; - readonly IList _serverConfigurations; - readonly GrpcBusTopology _topology; - readonly Recycle _transportProvider; - bool _anyReceiveEndpointConfigured; - Uri _baseAddress; - Uri _hostAddress; - - public GrpcHostConfiguration(IGrpcBusConfiguration busConfiguration, Uri baseAddress, IGrpcTopologyConfiguration topologyConfiguration) - : base(busConfiguration) - { - _busConfiguration = busConfiguration; - - BaseAddress = baseAddress; - - _topology = new GrpcBusTopology(this, topologyConfiguration); - - _serverConfigurations = new List(); - - ReceiveTransportRetryPolicy = Retry.CreatePolicy(x => - { - x.Handle(); - x.Handle(); - - x.Exponential(1000, TimeSpan.FromSeconds(0.1), TimeSpan.FromSeconds(30), TimeSpan.FromSeconds(3)); - }); - - _transportProvider = new Recycle(() => new GrpcTransportProvider(this, topologyConfiguration)); - } - - public override Uri HostAddress => _hostAddress ??= _transportProvider.Supervisor.HostAddress; - public override IBusTopology Topology => _topology; - - public IEnumerable ServerConfigurations => _serverConfigurations; - - public override IRetryPolicy ReceiveTransportRetryPolicy { get; } - - public Uri BaseAddress - { - get => _baseAddress ?? _defaultHostAddress; - set => _baseAddress = value ?? _defaultHostAddress; - } - - IGrpcHostConfigurator IGrpcHostConfiguration.Configurator => this; - IGrpcTransportProvider IGrpcHostConfiguration.TransportProvider => _transportProvider.Supervisor; - IGrpcBusTopology IGrpcHostConfiguration.Topology => _topology; - - public void ApplyEndpointDefinition(IGrpcReceiveEndpointConfigurator configurator, IEndpointDefinition definition) - { - base.ApplyEndpointDefinition(configurator, definition); - } - - public IGrpcReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(string queueName, - Action configure) - { - var endpointConfiguration = _busConfiguration.CreateEndpointConfiguration(); - - return CreateReceiveEndpointConfiguration(queueName, endpointConfiguration, configure); - } - - public IGrpcReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(string queueName, - IGrpcEndpointConfiguration endpointConfiguration, Action configure) - { - if (endpointConfiguration == null) - throw new ArgumentNullException(nameof(endpointConfiguration)); - if (string.IsNullOrWhiteSpace(queueName)) - throw new ArgumentException("Value cannot be null or whitespace.", nameof(queueName)); - - var configuration = new GrpcReceiveEndpointConfiguration(this, queueName, endpointConfiguration); - - configure?.Invoke(configuration); - - Observers.EndpointConfigured(configuration); - Add(configuration); - - _anyReceiveEndpointConfigured = true; - - return configuration; - } - - public override void ReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter endpointNameFormatter, - Action configureEndpoint = null) - { - var queueName = definition.GetEndpointName(endpointNameFormatter ?? DefaultEndpointNameFormatter.Instance); - - ReceiveEndpoint(queueName, configurator => - { - ApplyEndpointDefinition(configurator, definition); - configureEndpoint?.Invoke(configurator); - }); - } - - public override void ReceiveEndpoint(string queueName, Action configureEndpoint) - { - CreateReceiveEndpointConfiguration(queueName, configureEndpoint); - } - - public override IReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(string queueName, - Action configure = null) - { - return CreateReceiveEndpointConfiguration(queueName, configure); - } - - public override IHost Build() - { - var host = new GrpcHost(this, _topology); - - foreach (var endpointConfiguration in GetConfiguredEndpoints()) - endpointConfiguration.Build(host); - - return host; - } - - public string Host - { - set - { - if (_anyReceiveEndpointConfigured) - throw new ConfigurationException("The host must be configured before any receive endpoints"); - - _baseAddress = new UriBuilder(_baseAddress) { Host = value }.Uri; - } - } - - public int Port - { - set - { - if (_anyReceiveEndpointConfigured) - throw new ConfigurationException("The host must be configured before any receive endpoints"); - - _baseAddress = new UriBuilder(_baseAddress) { Port = value }.Uri; - } - } - - public void AddServer(Uri address) - { - _serverConfigurations.Add(new GrpcServerConfiguration(address)); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcReceiveEndpointBuilder.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcReceiveEndpointBuilder.cs deleted file mode 100644 index 487a1b03ac1..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcReceiveEndpointBuilder.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - - - public class GrpcReceiveEndpointBuilder : - ReceiveEndpointBuilder - { - readonly IGrpcReceiveEndpointConfiguration _configuration; - readonly IGrpcHostConfiguration _hostConfiguration; - - public GrpcReceiveEndpointBuilder(IGrpcHostConfiguration hostConfiguration, IGrpcReceiveEndpointConfiguration configuration) - : base(configuration) - { - _hostConfiguration = hostConfiguration; - _configuration = configuration; - } - - public override ConnectHandle ConnectConsumePipe(IPipe> pipe, ConnectPipeOptions options) - { - if (_configuration.ConfigureConsumeTopology && options.HasFlag(ConnectPipeOptions.ConfigureConsumeTopology)) - { - IGrpcMessageConsumeTopologyConfigurator topology = _configuration.Topology.Consume.GetMessageTopology(); - if (topology.ConfigureConsumeTopology) - topology.Bind(); - } - - return base.ConnectConsumePipe(pipe, options); - } - - public GrpcReceiveEndpointContext CreateReceiveEndpointContext() - { - var context = new TransportGrpcReceiveEndpointContext(_hostConfiguration, _configuration); - - context.GetOrAddPayload(() => _hostConfiguration.Topology); - - return context; - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcReceiveEndpointConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcReceiveEndpointConfiguration.cs deleted file mode 100644 index cf03d883dc5..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcReceiveEndpointConfiguration.cs +++ /dev/null @@ -1,81 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System; - using MassTransit.Configuration; - using Transports; - using Transports.Fabric; - - - public class GrpcReceiveEndpointConfiguration : - ReceiveEndpointConfiguration, - IGrpcReceiveEndpointConfiguration, - IGrpcReceiveEndpointConfigurator - { - readonly IGrpcEndpointConfiguration _endpointConfiguration; - readonly IGrpcHostConfiguration _hostConfiguration; - readonly string _queueName; - - public GrpcReceiveEndpointConfiguration(IGrpcHostConfiguration hostConfiguration, string queueName, - IGrpcEndpointConfiguration endpointConfiguration) - : base(hostConfiguration, endpointConfiguration) - { - _hostConfiguration = hostConfiguration; - - _queueName = queueName ?? throw new ArgumentNullException(nameof(queueName)); - _endpointConfiguration = endpointConfiguration ?? throw new ArgumentNullException(nameof(endpointConfiguration)); - - HostAddress = hostConfiguration?.HostAddress ?? throw new ArgumentNullException(nameof(hostConfiguration.HostAddress)); - - InputAddress = new GrpcEndpointAddress(hostConfiguration.HostAddress, queueName); - } - - IGrpcReceiveEndpointConfigurator IGrpcReceiveEndpointConfiguration.Configurator => this; - - IGrpcTopologyConfiguration IGrpcEndpointConfiguration.Topology => _endpointConfiguration.Topology; - - public override Uri HostAddress { get; } - - public override Uri InputAddress { get; } - - public override ReceiveEndpointContext CreateReceiveEndpointContext() - { - return CreateGrpcReceiveEndpointContext(); - } - - public void Build(IHost host) - { - var context = CreateGrpcReceiveEndpointContext(); - - var transport = new GrpcReceiveTransport(context, _queueName); - - var receiveEndpoint = new ReceiveEndpoint(transport, context); - - host.AddReceiveEndpoint(_queueName, receiveEndpoint); - - ReceiveEndpoint = receiveEndpoint; - } - - public void Bind(string exchangeName, ExchangeType exchangeType = ExchangeType.FanOut, string routingKey = default) - { - if (exchangeName == null) - throw new ArgumentNullException(nameof(exchangeName)); - - _endpointConfiguration.Topology.Consume.Bind(exchangeName, exchangeType, routingKey); - } - - public void Bind(ExchangeType? exchangeType, string routingKey = default) - where T : class - { - _endpointConfiguration.Topology.Consume.GetMessageTopology().Bind(exchangeType, routingKey); - } - - GrpcReceiveEndpointContext CreateGrpcReceiveEndpointContext() - { - var builder = new GrpcReceiveEndpointBuilder(_hostConfiguration, this); - - ApplySpecifications(builder); - - return builder.CreateReceiveEndpointContext(); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcRegistrationBusFactory.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcRegistrationBusFactory.cs deleted file mode 100644 index a3e71dc9c87..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcRegistrationBusFactory.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System; - using System.Collections.Generic; - using MassTransit.Configuration; - using Transports; - - - public class GrpcRegistrationBusFactory : - TransportRegistrationBusFactory - { - readonly GrpcBusConfiguration _busConfiguration; - readonly Action _configure; - - public GrpcRegistrationBusFactory(Uri baseAddress, Action configure) - : this(new GrpcBusConfiguration(new GrpcTopologyConfiguration(GrpcBus.MessageTopology), baseAddress), configure) - { - } - - GrpcRegistrationBusFactory(GrpcBusConfiguration busConfiguration, - Action configure) - : base(busConfiguration.HostConfiguration) - { - _configure = configure; - - _busConfiguration = busConfiguration; - } - - public override IBusInstance CreateBus(IBusRegistrationContext context, IEnumerable specifications, string busName) - { - var configurator = new GrpcBusFactoryConfigurator(_busConfiguration); - - return CreateBus(configurator, context, _configure, specifications); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcServerConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcServerConfiguration.cs deleted file mode 100644 index 8c705ea9d7b..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcServerConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System; - - - public class GrpcServerConfiguration - { - public GrpcServerConfiguration(Uri address) - { - Address = address; - } - - public Uri Address { get; } - } -} \ No newline at end of file diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcTopologyConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcTopologyConfiguration.cs deleted file mode 100644 index 766f1b62ce2..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/GrpcTopologyConfiguration.cs +++ /dev/null @@ -1,59 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System.Collections.Generic; - using System.Linq; - using MassTransit.Configuration; - using MassTransit.Topology; - using Topology; - - - public class GrpcTopologyConfiguration : - IGrpcTopologyConfiguration - { - readonly GrpcConsumeTopology _consumeTopology; - readonly IMessageTopologyConfigurator _messageTopology; - readonly IGrpcPublishTopologyConfigurator _publishTopology; - readonly ISendTopologyConfigurator _sendTopology; - - public GrpcTopologyConfiguration(IMessageTopologyConfigurator messageTopology) - { - _messageTopology = messageTopology; - - _sendTopology = new SendTopology(); - _sendTopology.ConnectSendTopologyConfigurationObserver(new DelegateSendTopologyConfigurationObserver(GlobalTopology.Send)); - _sendTopology.TryAddConvention(new RoutingKeySendTopologyConvention()); - - _publishTopology = new GrpcPublishTopology(messageTopology); - _publishTopology.ConnectPublishTopologyConfigurationObserver(new DelegatePublishTopologyConfigurationObserver(GlobalTopology.Publish)); - - var observer = new PublishToSendTopologyConfigurationObserver(_sendTopology); - _publishTopology.ConnectPublishTopologyConfigurationObserver(observer); - - _consumeTopology = new GrpcConsumeTopology(messageTopology, _publishTopology); - } - - public GrpcTopologyConfiguration(IGrpcTopologyConfiguration topologyConfiguration) - { - _messageTopology = topologyConfiguration.Message; - _sendTopology = topologyConfiguration.Send; - _publishTopology = topologyConfiguration.Publish; - - _consumeTopology = new GrpcConsumeTopology(topologyConfiguration.Message, _publishTopology); - } - - IMessageTopologyConfigurator ITopologyConfiguration.Message => _messageTopology; - ISendTopologyConfigurator ITopologyConfiguration.Send => _sendTopology; - IPublishTopologyConfigurator ITopologyConfiguration.Publish => _publishTopology; - IConsumeTopologyConfigurator ITopologyConfiguration.Consume => _consumeTopology; - - IGrpcPublishTopologyConfigurator IGrpcTopologyConfiguration.Publish => _publishTopology; - IGrpcConsumeTopologyConfigurator IGrpcTopologyConfiguration.Consume => _consumeTopology; - - public IEnumerable Validate() - { - return _sendTopology.Validate() - .Concat(_publishTopology.Validate()) - .Concat(_consumeTopology.Validate()); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcBusConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcBusConfiguration.cs deleted file mode 100644 index 5e59aa0631f..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcBusConfiguration.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - - - public interface IGrpcBusConfiguration : - IBusConfiguration, - IGrpcEndpointConfiguration - { - new IGrpcHostConfiguration HostConfiguration { get; } - - new IGrpcEndpointConfiguration BusEndpointConfiguration { get; } - - /// - /// Create an endpoint configuration on the bus, which can later be turned into a receive endpoint - /// - /// - IGrpcEndpointConfiguration CreateEndpointConfiguration(bool isBusEndpoint = false); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcEndpointConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcEndpointConfiguration.cs deleted file mode 100644 index d65846d45c4..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcEndpointConfiguration.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - - - public interface IGrpcEndpointConfiguration : - IEndpointConfiguration - { - new IGrpcTopologyConfiguration Topology { get; } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcHostConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcHostConfiguration.cs deleted file mode 100644 index 0d933ec41fe..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcHostConfiguration.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System; - using System.Collections.Generic; - using MassTransit.Configuration; - - - public interface IGrpcHostConfiguration : - IHostConfiguration, - IReceiveConfigurator - { - /// - /// Set the host's base address - /// - Uri BaseAddress { get; set; } - - IGrpcHostConfigurator Configurator { get; } - - IGrpcTransportProvider TransportProvider { get; } - - new IGrpcBusTopology Topology { get; } - - IEnumerable ServerConfigurations { get; } - - void ApplyEndpointDefinition(IGrpcReceiveEndpointConfigurator configurator, IEndpointDefinition definition); - - IGrpcReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(string queueName, - Action configure = null); - - IGrpcReceiveEndpointConfiguration CreateReceiveEndpointConfiguration(string queueName, IGrpcEndpointConfiguration endpointConfiguration, - Action configure = null); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcReceiveEndpointConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcReceiveEndpointConfiguration.cs deleted file mode 100644 index 31b8529460a..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcReceiveEndpointConfiguration.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - using Transports; - - - public interface IGrpcReceiveEndpointConfiguration : - IReceiveEndpointConfiguration, - IGrpcEndpointConfiguration - { - IGrpcReceiveEndpointConfigurator Configurator { get; } - - void Build(IHost host); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcTopologyConfiguration.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcTopologyConfiguration.cs deleted file mode 100644 index c41e851ff15..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/IGrpcTopologyConfiguration.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - - - public interface IGrpcTopologyConfiguration : - ITopologyConfiguration - { - new IGrpcPublishTopologyConfigurator Publish { get; } - - new IGrpcConsumeTopologyConfigurator Consume { get; } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/ExchangeBindingConsumeTopologySpecification.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/ExchangeBindingConsumeTopologySpecification.cs deleted file mode 100644 index 4bab353ac02..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/ExchangeBindingConsumeTopologySpecification.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System.Collections.Generic; - using MassTransit.Configuration; - using Transports.Fabric; - - - /// - /// Used to bind an exchange to the consuming queue's exchange - /// - public class ExchangeBindingConsumeTopologySpecification : - IGrpcConsumeTopologySpecification - { - readonly string _exchange; - readonly ExchangeType _exchangeType; - readonly string _routingKey; - - public ExchangeBindingConsumeTopologySpecification(string exchange, ExchangeType exchangeType, string routingKey) - { - _exchange = exchange; - _routingKey = routingKey; - _exchangeType = exchangeType; - } - - public IEnumerable Validate() - { - yield break; - } - - public void Apply(IMessageFabricConsumeTopologyBuilder builder) - { - builder.ExchangeDeclare(_exchange, _exchangeType); - builder.ExchangeBind(_exchange, builder.Exchange, _routingKey); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/IRoutingKeyMessageSendTopologyConvention.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/IRoutingKeyMessageSendTopologyConvention.cs deleted file mode 100644 index 6a4b37e691b..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/IRoutingKeyMessageSendTopologyConvention.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - - - public interface IRoutingKeyMessageSendTopologyConvention : - IMessageSendTopologyConvention - where TMessage : class - { - void SetFormatter(IRoutingKeyFormatter formatter); - void SetFormatter(IMessageRoutingKeyFormatter formatter); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/IRoutingKeySendTopologyConvention.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/IRoutingKeySendTopologyConvention.cs deleted file mode 100644 index 1ffb58d91ef..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/IRoutingKeySendTopologyConvention.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - - - public interface IRoutingKeySendTopologyConvention : - ISendTopologyConvention - { - /// - /// The default, non-message specific routing key formatter used by messages - /// when no specific convention has been specified. - /// - IRoutingKeyFormatter DefaultFormatter { get; set; } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/InvalidGrpcConsumeTopologySpecification.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/InvalidGrpcConsumeTopologySpecification.cs deleted file mode 100644 index 79ec77b9d10..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/InvalidGrpcConsumeTopologySpecification.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System.Collections.Generic; - using MassTransit.Configuration; - - - public class InvalidGrpcConsumeTopologySpecification : - IGrpcConsumeTopologySpecification - { - readonly string _key; - readonly string _message; - - public InvalidGrpcConsumeTopologySpecification(string key, string message) - { - _key = key; - _message = message; - } - - public IEnumerable Validate() - { - yield return this.Failure(_key, _message); - } - - public void Apply(IMessageFabricConsumeTopologyBuilder builder) - { - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/RoutingKeyMessageSendTopologyConvention.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/RoutingKeyMessageSendTopologyConvention.cs deleted file mode 100644 index 7fbaffcf29e..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/RoutingKeyMessageSendTopologyConvention.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - - - public class RoutingKeyMessageSendTopologyConvention : - IRoutingKeyMessageSendTopologyConvention - where TMessage : class - { - IMessageRoutingKeyFormatter _formatter; - - public RoutingKeyMessageSendTopologyConvention(IRoutingKeyFormatter formatter) - { - if (formatter != null) - SetFormatter(formatter); - } - - bool IMessageSendTopologyConvention.TryGetMessageSendTopology(out IMessageSendTopology messageSendTopology) - { - if (_formatter != null) - { - messageSendTopology = new SetRoutingKeyMessageSendTopology(_formatter); - return true; - } - - messageSendTopology = null; - return false; - } - - bool IMessageSendTopologyConvention.TryGetMessageSendTopologyConvention(out IMessageSendTopologyConvention convention) - { - convention = this as IMessageSendTopologyConvention; - - return convention != null; - } - - public void SetFormatter(IRoutingKeyFormatter formatter) - { - _formatter = new MessageRoutingKeyFormatter(formatter); - } - - public void SetFormatter(IMessageRoutingKeyFormatter formatter) - { - _formatter = formatter; - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/RoutingKeySendTopologyConvention.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/RoutingKeySendTopologyConvention.cs deleted file mode 100644 index aa3248886e0..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/RoutingKeySendTopologyConvention.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using MassTransit.Configuration; - - - public class RoutingKeySendTopologyConvention : - IRoutingKeySendTopologyConvention - { - readonly ITopologyConventionCache _cache; - - public RoutingKeySendTopologyConvention() - { - DefaultFormatter = new EmptyRoutingKeyFormatter(); - - _cache = new TopologyConventionCache(typeof(IRoutingKeyMessageSendTopologyConvention<>), new Factory()); - } - - bool IMessageSendTopologyConvention.TryGetMessageSendTopologyConvention(out IMessageSendTopologyConvention convention) - { - return _cache.GetOrAdd>().TryGetMessageSendTopologyConvention(out convention); - } - - public IRoutingKeyFormatter DefaultFormatter { get; set; } - - - class Factory : - IConventionTypeFactory - { - IMessageSendTopologyConvention IConventionTypeFactory.Create() - { - return new RoutingKeyMessageSendTopologyConvention(null); - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/SetRoutingKeyMessageSendTopology.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/SetRoutingKeyMessageSendTopology.cs deleted file mode 100644 index 44f0cae6482..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Configuration/Topology/SetRoutingKeyMessageSendTopology.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace MassTransit.GrpcTransport.Configuration -{ - using System; - using System.Threading.Tasks; - using MassTransit.Configuration; - using Middleware; - - - public class SetRoutingKeyMessageSendTopology : - IMessageSendTopology - where T : class - { - readonly IFilter> _filter; - - public SetRoutingKeyMessageSendTopology(IMessageRoutingKeyFormatter routingKeyFormatter) - { - if (routingKeyFormatter == null) - throw new ArgumentNullException(nameof(routingKeyFormatter)); - - _filter = new Proxy(new SetRoutingKeyFilter(routingKeyFormatter)); - } - - public void Apply(ITopologyPipeBuilder> builder) - { - builder.AddFilter(_filter); - } - - - class Proxy : - IFilter> - { - readonly IFilter> _filter; - - public Proxy(IFilter> filter) - { - _filter = filter; - } - - public Task Send(SendContext context, IPipe> next) - { - var rabbitMqSendContext = context.GetPayload>(); - - return _filter.Send(rabbitMqSendContext, next); - } - - public void Probe(ProbeContext context) - { - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/DelegateRoutingKeyFormatter.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/DelegateRoutingKeyFormatter.cs deleted file mode 100644 index 605029318b0..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/DelegateRoutingKeyFormatter.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - - - public class DelegateRoutingKeyFormatter : - IMessageRoutingKeyFormatter - where TMessage : class - { - readonly Func, string> _formatter; - - public DelegateRoutingKeyFormatter(Func, string> formatter) - { - _formatter = formatter; - } - - public string FormatRoutingKey(GrpcSendContext context) - { - return _formatter(context) ?? ""; - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/DictionaryHostInfo.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/DictionaryHostInfo.cs deleted file mode 100644 index 4b774dc3cdc..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/DictionaryHostInfo.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Collections.Generic; - - - class DictionaryHostInfo : - HostInfo - { - public DictionaryHostInfo(IReadOnlyDictionary host) - { - if (host.ContainsKey(nameof(MachineName))) - MachineName = host[nameof(MachineName)]; - if (host.ContainsKey(nameof(ProcessName))) - ProcessName = host[nameof(ProcessName)]; - if (host.ContainsKey(nameof(ProcessId)) && int.TryParse(host[nameof(ProcessId)], out var processId)) - ProcessId = processId; - if (host.ContainsKey(nameof(Assembly))) - Assembly = host[nameof(Assembly)]; - if (host.ContainsKey(nameof(AssemblyVersion))) - AssemblyVersion = host[nameof(AssemblyVersion)]; - if (host.ContainsKey(nameof(FrameworkVersion))) - FrameworkVersion = host[nameof(FrameworkVersion)]; - if (host.ContainsKey(nameof(MassTransitVersion))) - MassTransitVersion = host[nameof(MassTransitVersion)]; - if (host.ContainsKey(nameof(OperatingSystemVersion))) - OperatingSystemVersion = host[nameof(OperatingSystemVersion)]; - } - - public string MachineName { get; } - public string ProcessName { get; } - public int ProcessId { get; } - public string Assembly { get; } - public string AssemblyVersion { get; } - public string FrameworkVersion { get; } - public string MassTransitVersion { get; } - public string OperatingSystemVersion { get; } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/EmptyRoutingKeyFormatter.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/EmptyRoutingKeyFormatter.cs deleted file mode 100644 index 0699e330e78..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/EmptyRoutingKeyFormatter.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - public class EmptyRoutingKeyFormatter : - IRoutingKeyFormatter - { - string IRoutingKeyFormatter.FormatRoutingKey(GrpcSendContext context) - { - return context.RoutingKey; - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Fabric/GrpcDeliveryContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Fabric/GrpcDeliveryContext.cs deleted file mode 100644 index 2fc1961bf70..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Fabric/GrpcDeliveryContext.cs +++ /dev/null @@ -1,45 +0,0 @@ -#nullable enable -namespace MassTransit.GrpcTransport.Fabric -{ - using System; - using System.Collections.Generic; - using System.Threading; - using Contracts; - using Transports.Fabric; - - - public class GrpcDeliveryContext : - DeliveryContext - { - readonly HashSet> _delivered; - - public GrpcDeliveryContext(GrpcTransportMessage message, CancellationToken cancellationToken) - { - Message = message; - CancellationToken = cancellationToken; - - _delivered = new HashSet>(); - } - - public CancellationToken CancellationToken { get; } - - public GrpcTransportMessage Message { get; } - public string? RoutingKey => Message.RoutingKey; - public DateTime? EnqueueTime => Message.EnqueueTime; - - public long? ReceiverId => - Message.Message.Deliver.DestinationCase == Deliver.DestinationOneofCase.Receiver - ? Message.Message.Deliver.Receiver.ReceiverId - : default(long?); - - public bool WasAlreadyDelivered(IMessageSink sink) - { - return _delivered.Contains(sink); - } - - public void Delivered(IMessageSink sink) - { - _delivered.Add(sink); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Fabric/GrpcTransportMessage.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Fabric/GrpcTransportMessage.cs deleted file mode 100644 index 3461dafbc79..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Fabric/GrpcTransportMessage.cs +++ /dev/null @@ -1,168 +0,0 @@ -#nullable enable -namespace MassTransit.GrpcTransport.Fabric -{ - using System; - using System.Collections; - using System.Collections.Generic; - using System.Linq; - using Contracts; - using Serialization; - - - public class GrpcTransportMessage : - MessageContext, - Headers, - GrpcConsumeContext - { - readonly Envelope _envelope; - Guid? _conversationId; - Guid? _correlationId; - Uri? _destinationAddress; - DateTime? _enqueueTime; - DateTime? _expirationTime; - Uri? _faultAddress; - Guid? _initiatorId; - Guid? _messageId; - string[]? _messageType; - Guid? _requestId; - Uri? _responseAddress; - DateTime? _sentTime; - Uri? _sourceAddress; - - public GrpcTransportMessage(TransportMessage message, HostInfo host) - { - Host = host; - Message = message; - _envelope = message.Deliver.Envelope; - - Body = message.Deliver.Envelope.Body.ToByteArray(); - - ContentType = message.Deliver.Envelope.ContentType; - - SendHeaders = new DictionarySendHeaders(); - - foreach (KeyValuePair header in message.Deliver.Envelope.Headers) - SendHeaders.Set(header.Key, header.Value); - } - - public string[] MessageType => _messageType ??= _envelope.MessageType.ToArray(); - - public string ContentType { get; } - public byte[] Body { get; } - - public DateTime? EnqueueTime => _enqueueTime ??= _envelope.EnqueueTime.ToDateTime(); - - public TransportMessage Message { get; } - - public SendHeaders SendHeaders { get; } - - public string? RoutingKey => Message.Deliver.Exchange?.RoutingKey; - - public IEnumerator GetEnumerator() - { - return Headers.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public IEnumerable> GetAll() - { - return Headers.GetAll(); - } - - public bool TryGetHeader(string key, out object? value) - { - switch (key) - { - case MessageHeaders.MessageId: - value = MessageId; - return true; - case MessageHeaders.CorrelationId: - value = CorrelationId; - return true; - case MessageHeaders.ConversationId: - value = ConversationId; - return true; - case MessageHeaders.RequestId: - value = RequestId; - return true; - case MessageHeaders.InitiatorId: - value = InitiatorId; - return true; - case MessageHeaders.SourceAddress: - value = SourceAddress; - return true; - case MessageHeaders.ResponseAddress: - value = ResponseAddress; - return true; - case MessageHeaders.FaultAddress: - value = FaultAddress; - return true; - } - - return Headers.TryGetHeader(key, out value); - } - - public T? Get(string key, T? defaultValue = default) - where T : class - { - return TryGetHeader(key, out var value) ? SystemTextJsonMessageSerializer.Instance.DeserializeObject(value, defaultValue) : default; - } - - public T? Get(string key, T? defaultValue = default) - where T : struct - { - return TryGetHeader(key, out var value) ? SystemTextJsonMessageSerializer.Instance.DeserializeObject(value, defaultValue) : default; - } - - public Headers Headers => SendHeaders; - - public Guid? MessageId => _messageId ??= ToGuid(_envelope.MessageId); - - public Guid? RequestId => _requestId ??= ToGuid(_envelope.RequestId); - - public Guid? CorrelationId => _correlationId ??= ToGuid(_envelope.CorrelationId); - - public Guid? ConversationId => _conversationId ??= ToGuid(_envelope.ConversationId); - - public Guid? InitiatorId => _initiatorId ??= ToGuid(_envelope.InitiatorId); - - public Uri? SourceAddress => _sourceAddress ??= ToUri(_envelope.SourceAddress); - - public Uri? DestinationAddress => _destinationAddress ??= ToUri(_envelope.DestinationAddress); - - public Uri? ResponseAddress => _responseAddress ??= ToUri(_envelope.ResponseAddress); - - public Uri? FaultAddress => _faultAddress ??= ToUri(_envelope.FaultAddress); - - public DateTime? ExpirationTime => _expirationTime ??= _envelope.ExpirationTime.ToDateTime(); - - public DateTime? SentTime => _sentTime ??= _envelope.SentTime.ToDateTime(); - - public HostInfo Host { get; } - - static Guid? ToGuid(string? value) - { - return Guid.TryParse(value, out var guid) - ? guid - : default; - } - - static Uri? ToUri(string? value) - { - try - { - return string.IsNullOrWhiteSpace(value) - ? default - : new Uri(value); - } - catch (FormatException) - { - return default; - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcClient.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcClient.cs deleted file mode 100644 index 8f681daf85e..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcClient.cs +++ /dev/null @@ -1,139 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Configuration; - using Contracts; - using Grpc.Core; - using MassTransit.Middleware; - - - public class GrpcClient : - Agent, - IGrpcClient - { - readonly TransportService.TransportServiceClient _client; - readonly IGrpcHostConfiguration _hostConfiguration; - readonly IGrpcHostNode _hostNode; - readonly IGrpcNode _node; - readonly Task _runTask; - - public GrpcClient(IGrpcHostConfiguration hostConfiguration, IGrpcHostNode hostNode, TransportService.TransportServiceClient client, IGrpcNode node) - { - _hostConfiguration = hostConfiguration; - _hostNode = hostNode; - _client = client; - _node = node; - - _runTask = Task.Run(() => RunAsync()); - } - - async Task RunAsync() - { - LogContext.SetCurrentIfNull(_hostConfiguration.LogContext); - - var stoppingContext = new ClientStoppingContext(Stopping); - - RetryPolicyContext policyContext = _hostConfiguration.ReceiveTransportRetryPolicy.CreatePolicyContext(stoppingContext); - - try - { - RetryContext retryContext = null; - - while (!IsStopping) - { - try - { - if (retryContext?.Delay != null) - await Task.Delay(retryContext.Delay.Value, Stopping).ConfigureAwait(false); - - LogContext.Info?.Log("gRPC Connect: {Server}", _node.NodeAddress); - - using AsyncDuplexStreamingCall eventStream = _client.EventStream(cancellationToken: Stopping); - - await SendJoin(eventStream.RequestStream).ConfigureAwait(false); - - LogContext.Info?.Log("gRPC Connected: {Server}", _node.NodeAddress); - - SetReady(_node.Ready); - - await _node.Connect(eventStream.RequestStream, eventStream.ResponseStream, Stopping).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - throw; - } - catch (RpcException exception) when (exception.StatusCode == StatusCode.Cancelled) - { - throw; - } - catch (Exception exception) - { - LogContext.Warning?.Log(exception, "gRPC Connection Faulted: {Server}", _node.NodeAddress); - - if (retryContext != null) - { - retryContext = retryContext.CanRetry(exception, out RetryContext nextRetryContext) - ? nextRetryContext - : null; - } - - if (retryContext == null && !policyContext.CanRetry(exception, out retryContext)) - break; - } - } - } - catch (OperationCanceledException exception) - { - if (exception.CancellationToken != Stopping) - LogContext.Debug?.Log(exception, "gRPC Client Canceled: {Server}", _node.NodeAddress); - } - catch (RpcException exception) when (exception.StatusCode == StatusCode.Cancelled) - { - } - catch (Exception exception) - { - LogContext.Error?.Log(exception, "gRPC Client Faulted: {Server}", _node.NodeAddress); - - SetNotReady(exception); - } - finally - { - LogContext.Debug?.Log("gRPC Client Exiting: {Server}", _node.NodeAddress); - - policyContext.Dispose(); - } - } - - protected override async Task StopAgent(StopContext context) - { - await _runTask.ConfigureAwait(false); - - await base.StopAgent(context); - } - - async Task SendJoin(IAsyncStreamWriter requestStream) - { - var message = new TransportMessage - { - MessageId = NewId.NextGuid().ToString(), - Join = new Join { Node = new Node().Initialize(_hostNode) } - }; - - await requestStream.WriteAsync(message).ConfigureAwait(false); - - _node.LogSent(message); - } - - - class ClientStoppingContext : - BasePipeContext - { - public ClientStoppingContext(CancellationToken cancellationToken) - : base(cancellationToken) - { - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcDeadLetterTransport.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcDeadLetterTransport.cs deleted file mode 100644 index 2cea3f4e577..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcDeadLetterTransport.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Threading.Tasks; - using Fabric; - using Transports; - using Transports.Fabric; - - - public class GrpcDeadLetterTransport : - GrpcMoveTransport, - IDeadLetterTransport - { - public GrpcDeadLetterTransport(IMessageExchange exchange) - : base(exchange) - { - } - - public Task Send(ReceiveContext context, string reason) - { - void PreSend(GrpcTransportMessage message, SendHeaders headers) - { - headers.Set(MessageHeaders.Reason, reason ?? "Unspecified"); - } - - return Move(context, PreSend); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcErrorTransport.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcErrorTransport.cs deleted file mode 100644 index 717e8d872c2..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcErrorTransport.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Threading.Tasks; - using Fabric; - using Transports; - using Transports.Fabric; - - - public class GrpcErrorTransport : - GrpcMoveTransport, - IErrorTransport - { - public GrpcErrorTransport(IMessageExchange exchange) - : base(exchange) - { - } - - public Task Send(ExceptionReceiveContext context) - { - void PreSend(GrpcTransportMessage message, SendHeaders headers) - { - headers.SetExceptionHeaders(context); - } - - return Move(context, PreSend); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcHeaderProvider.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcHeaderProvider.cs deleted file mode 100644 index c4fb7a8edae..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcHeaderProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Collections.Generic; - using Transports; - - - public class GrpcHeaderProvider : - IHeaderProvider - { - readonly Headers _headers; - - public GrpcHeaderProvider(Headers headers) - { - _headers = headers; - } - - public IEnumerable> GetAll() - { - return _headers.GetAll(); - } - - public bool TryGetHeader(string key, out object value) - { - return _headers.TryGetHeader(key, out value); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcHost.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcHost.cs deleted file mode 100644 index 895c5b516a0..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcHost.cs +++ /dev/null @@ -1,71 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using Configuration; - using Transports; - - - public class GrpcHost : - BaseHost, - IGrpcHost - { - readonly IGrpcHostConfiguration _hostConfiguration; - - public GrpcHost(IGrpcHostConfiguration hostConfiguration, IGrpcBusTopology busTopology) - : base(hostConfiguration, busTopology) - { - _hostConfiguration = hostConfiguration; - } - - public override HostReceiveEndpointHandle ConnectReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter endpointNameFormatter, - Action configureEndpoint = null) - { - return ConnectReceiveEndpoint(definition, endpointNameFormatter, configureEndpoint); - } - - public HostReceiveEndpointHandle ConnectReceiveEndpoint(IEndpointDefinition definition, IEndpointNameFormatter endpointNameFormatter, - Action configureEndpoint = null) - { - var queueName = definition.GetEndpointName(endpointNameFormatter ?? DefaultEndpointNameFormatter.Instance); - - return ConnectReceiveEndpoint(queueName, configurator => - { - _hostConfiguration.ApplyEndpointDefinition(configurator, definition); - configureEndpoint?.Invoke(configurator); - }); - } - - public override HostReceiveEndpointHandle ConnectReceiveEndpoint(string queueName, Action configureEndpoint = null) - { - return ConnectReceiveEndpoint(queueName, configureEndpoint); - } - - public HostReceiveEndpointHandle ConnectReceiveEndpoint(string queueName, Action configure = null) - { - LogContext.SetCurrentIfNull(_hostConfiguration.LogContext); - - var configuration = _hostConfiguration.CreateReceiveEndpointConfiguration(queueName, configure); - - TransportLogMessages.ConnectReceiveEndpoint(configuration.InputAddress); - - configuration.Validate().ThrowIfContainsFailure("The receive endpoint configuration is invalid:"); - - configuration.Build(this); - - return ReceiveEndpoints.Start(queueName); - } - - protected override void Probe(ProbeContext context) - { - context.Add("type", "gRPC"); - context.Add("hostAddress", _hostConfiguration.HostAddress); - - _hostConfiguration.TransportProvider.Probe(context); - } - - protected override IAgent[] GetAgentHandles() - { - return new IAgent[] { _hostConfiguration.TransportProvider }; - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcHostNode.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcHostNode.cs deleted file mode 100644 index ed73aa31e4c..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcHostNode.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Collections.Generic; - using Fabric; - using Transports.Fabric; - - - public sealed class GrpcHostNode : - GrpcNode, - IGrpcHostNode - { - readonly HostNodeTopology _hostTopology; - - public GrpcHostNode(IMessageFabric messageFabric, NodeContext context) - : base(messageFabric, context) - { - _hostTopology = new HostNodeTopology(); - - SetReady(); - } - - public TopologyHandle AddTopology(Contracts.Topology topology, TopologyHandle handle) - { - return _hostTopology.Add(topology, handle); - } - - public IEnumerable GetTopology() - { - return _hostTopology.GetTopology(); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcLogExtensions.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcLogExtensions.cs deleted file mode 100644 index 309fb6bf1f2..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcLogExtensions.cs +++ /dev/null @@ -1,84 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Runtime.CompilerServices; - using Contracts; - using Logging; - using Microsoft.Extensions.Logging; - - - public static class GrpcLogExtensions - { - static readonly LogMessage _logSent = - LogContext.Define(LogLevel.Debug, "GRPC-SEND {NodeAddress} {MessageId} {MessageType}"); - - static readonly LogMessage _logReceived = - LogContext.Define(LogLevel.Debug, "GRPC-RECV {NodeAddress} {MessageId} {MessageType}"); - - static readonly LogMessage _logDisconnect = - LogContext.Define(LogLevel.Debug, "GRPC-PART {NodeAddress}"); - - static readonly LogMessage _logExchange = - LogContext.Define(LogLevel.Debug, "TOPOLOGY Exchange {NodeAddress} {Exchange}"); - - static readonly LogMessage _logExchangeBind = - LogContext.Define(LogLevel.Debug, "TOPOLOGY ExchangeBind {NodeAddress} {Source} -> {Destination}"); - - static readonly LogMessage _logQueue = - LogContext.Define(LogLevel.Debug, "TOPOLOGY Queue {NodeAddress} {Queue}"); - - static readonly LogMessage _logQueueBind = - LogContext.Define(LogLevel.Debug, "TOPOLOGY QueueBind {NodeAddress} {Source} -> {Destination}"); - - static readonly LogMessage _logConsumer = - LogContext.Define(LogLevel.Debug, "TOPOLOGY Consumer {NodeAddress} {Queue}"); - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LogSent(this NodeContext context, TransportMessage message) - { - // _logSent(context.NodeAddress, message.MessageId, message.ContentCase); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LogReceived(this NodeContext context, TransportMessage message) - { - // _logReceived(context.NodeAddress, message.MessageId, message.ContentCase); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LogDisconnect(this NodeContext context) - { - _logDisconnect(context.NodeAddress); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LogTopology(this NodeContext context, Exchange exchange, ExchangeType exchangeType) - { - // _logExchange(context.NodeAddress, exchange.Name); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LogTopology(this NodeContext context, ExchangeBind exchangeBind) - { - // _logExchangeBind(context.NodeAddress, exchangeBind.Source, exchangeBind.Destination); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LogTopology(this NodeContext context, Queue queue) - { - // _logQueue(context.NodeAddress, queue.Name); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LogTopology(this NodeContext context, QueueBind queueBind) - { - // _logQueueBind(context.NodeAddress, queueBind.Source, queueBind.Destination); - } - - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void LogTopology(this NodeContext context, Receiver receiver) - { - // _logConsumer(context.NodeAddress, consumer.QueueName); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcMoveTransport.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcMoveTransport.cs deleted file mode 100644 index 643c1a3bb51..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcMoveTransport.cs +++ /dev/null @@ -1,40 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Contracts; - using Fabric; - using Transports.Fabric; - - - public class GrpcMoveTransport - { - readonly IMessageExchange _exchange; - - protected GrpcMoveTransport(IMessageExchange exchange) - { - _exchange = exchange; - } - - protected async Task Move(ReceiveContext context, Action preSend) - { - if (context.TryGetPayload(out GrpcTransportMessage receivedMessage)) - { - var message = new TransportMessage - { - MessageId = receivedMessage.Message.MessageId, - Deliver = new Deliver(receivedMessage.Message.Deliver) { Exchange = new ExchangeDestination { Name = _exchange.Name } } - }; - - var transportMessage = new GrpcTransportMessage(message, receivedMessage.Host); - - preSend(transportMessage, transportMessage.SendHeaders); - - var deliveryContext = new GrpcDeliveryContext(transportMessage, CancellationToken.None); - - await _exchange.Deliver(deliveryContext).ConfigureAwait(false); - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcNode.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcNode.cs deleted file mode 100644 index 1260ddf3f8b..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcNode.cs +++ /dev/null @@ -1,223 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Channels; - using System.Threading.Tasks; - using Contracts; - using Fabric; - using Grpc.Core; - using Internals; - using MassTransit.Middleware; - using Transports.Fabric; - - - public class GrpcNode : - Agent, - IGrpcNode - { - readonly Channel _channel; - readonly IMessageFabric _messageFabric; - readonly RemoteNodeTopology _remoteTopology; - NodeContext _context; - - public GrpcNode(IMessageFabric messageFabric, NodeContext context) - { - _messageFabric = messageFabric; - _context = context; - - _remoteTopology = new RemoteNodeTopology(this, messageFabric); - - var outputOptions = new UnboundedChannelOptions - { - SingleWriter = false, - SingleReader = true, - AllowSynchronousContinuations = true - }; - - _channel = System.Threading.Channels.Channel.CreateUnbounded(outputOptions); - - Writer = _channel.Writer; - } - - public ChannelWriter Writer { get; } - - public NodeType NodeType => _context.NodeType; - public Uri NodeAddress => _context.NodeAddress; - public Guid SessionId => _context.SessionId; - - public HostInfo Host - { - get => _context.Host; - set => _context.Host = value; - } - - public async Task Connect(IAsyncStreamWriter writer, IAsyncStreamReader reader, CancellationToken cancellationToken) - { - if (_context.NodeType == NodeType.Server) - SetReady(); - - using var source = CancellationTokenSource.CreateLinkedTokenSource(Stopping, cancellationToken); - - var writerTask = StartWriter(writer, source.Token); - var readerTask = StartReader(reader, source.Token); - - try - { - await Task.WhenAll(readerTask, writerTask).ConfigureAwait(false); - } - catch (Exception) - { - source.Cancel(); - - try - { - if (!readerTask.IsCompleted) - await readerTask.ConfigureAwait(false); - - if (!writerTask.IsCompleted) - await writerTask.ConfigureAwait(false); - } - catch (Exception ex) - { - LogContext.Debug?.Log(ex, "gRPC Cancel Faulted: {Server}", _context.NodeAddress); - } - - throw; - } - } - - public void Join(NodeContext context, IEnumerable topologies) - { - if (context.SessionId != SessionId) - _context = context; - - _remoteTopology.Join(context.SessionId, topologies); - } - - protected override async Task StopAgent(StopContext context) - { - _channel.Writer.Complete(); - - await _channel.Reader.Completion.ConfigureAwait(false); - - await base.StopAgent(context).ConfigureAwait(false); - } - - async Task StartWriter(IAsyncStreamWriter writer, CancellationToken cancellationToken) - { - try - { - while (await _channel.Reader.WaitToReadAsync(cancellationToken).ConfigureAwait(false)) - { - if (!_channel.Reader.TryPeek(out var message)) - continue; - - if (string.IsNullOrWhiteSpace(message.MessageId)) - message.MessageId = NewId.NextGuid().ToString(); - - await writer.WriteAsync(message).ConfigureAwait(false); - - await _channel.Reader.ReadAsync(cancellationToken).ConfigureAwait(false); - - _context.LogSent(message); - } - } - catch (OperationCanceledException) - { - } - } - - async Task StartReader(IAsyncStreamReader reader, CancellationToken cancellationToken) - { - try - { - while (await reader.MoveNext(CancellationToken.None).OrCanceled(cancellationToken).ConfigureAwait(false)) - { - var message = reader.Current; - - DispatchMessageAsync(message); - - _context.LogReceived(message); - } - } - catch (OperationCanceledException) - { - } - catch (RpcException exception) when (exception.Status.StatusCode == StatusCode.Cancelled) - { - } - } - - void DispatchMessageAsync(TransportMessage message) - { - Task.Run(() => ProcessMessage(message), Stopping); - } - - async Task ProcessMessage(TransportMessage message) - { - try - { - if (message.ContentCase == TransportMessage.ContentOneofCase.Join) - LogContext.Warning?.Log("Join is not allowed on a connected instance: {InstanceId}", NodeAddress); - else if (message.ContentCase == TransportMessage.ContentOneofCase.Welcome) - { - _context.Host = new DictionaryHostInfo(message.Welcome.Node.Host); - - Guid.TryParse(message.Welcome.Node.SessionId, out var sessionId); - - _remoteTopology.Join(sessionId, message.Welcome.Node.Topology); - - if (_context.NodeType == NodeType.Client) - SetReady(); - } - else if (message.ContentCase == TransportMessage.ContentOneofCase.Topology) - _remoteTopology.ProcessTopology(message.Topology); - else if (message.ContentCase == TransportMessage.ContentOneofCase.Deliver) - await DeliverMessage(message).ConfigureAwait(false); - else - LogContext.Warning?.Log("Unsupported message received: {MessageType} on {Instance}", message.ContentCase, NodeAddress); - } - catch (Exception exception) - { - LogContext.Error?.Log(exception, "Message Faulted: {MessageId}", message.MessageId); - } - } - - Task DeliverMessage(TransportMessage message) - { - var transportMessage = new GrpcTransportMessage(message, _context.Host); - - switch (message.Deliver.DestinationCase) - { - case Deliver.DestinationOneofCase.Exchange: - { - var destination = message.Deliver.Exchange; - - IMessageExchange exchange = _messageFabric.GetExchange(_context, destination.Name); - - return exchange.Send(transportMessage, Stopping); - } - case Deliver.DestinationOneofCase.Queue: - { - IMessageQueue queue = _messageFabric.GetQueue(_context, message.Deliver.Queue.Name); - - return queue.Send(transportMessage, Stopping); - } - case Deliver.DestinationOneofCase.Receiver: - { - var receiver = message.Deliver.Receiver; - - IMessageQueue queue = _messageFabric.GetQueue(_context, receiver.QueueName); - - message.Deliver.Receiver.ReceiverId = _remoteTopology.GetLocalConsumerId(receiver.QueueName, receiver.ReceiverId); - - return queue.Send(transportMessage, Stopping); - } - default: - return Task.CompletedTask; - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcNodeMessageExtensions.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcNodeMessageExtensions.cs deleted file mode 100644 index 2dddd57dc5e..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcNodeMessageExtensions.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Threading.Tasks; - using Contracts; - using Fabric; - - - public static class GrpcNodeMessageExtensions - { - public static ValueTask SendWelcome(this IGrpcNode node, IGrpcHostNode hostNode) - { - return node.Writer.WriteAsync(new TransportMessage {Welcome = new Welcome {Node = new Node().Initialize(hostNode)}}); - } - - public static ValueTask DeliverMessage(this IGrpcNode node, GrpcTransportMessage message) - { - return node.Writer.WriteAsync(message.Message); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcPublishTransportProvider.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcPublishTransportProvider.cs deleted file mode 100644 index 38c96680f2e..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcPublishTransportProvider.cs +++ /dev/null @@ -1,27 +0,0 @@ -#nullable enable -namespace MassTransit.GrpcTransport -{ - using System; - using System.Threading.Tasks; - using Transports; - - - public class GrpcPublishTransportProvider : - IPublishTransportProvider - { - readonly IGrpcTransportProvider _transportProvider; - readonly ReceiveEndpointContext _context; - - public GrpcPublishTransportProvider(IGrpcTransportProvider transportProvider, ReceiveEndpointContext context) - { - _transportProvider = transportProvider; - _context = context; - } - - public Task GetPublishTransport(Uri? publishAddress) - where T : class - { - return _transportProvider.CreatePublishTransport(_context, publishAddress); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcReceiveContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcReceiveContext.cs deleted file mode 100644 index 8f68534f56b..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcReceiveContext.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Net.Mime; - using Fabric; - using Transports; - - - public class GrpcReceiveContext : - BaseReceiveContext - { - public GrpcReceiveContext(GrpcTransportMessage message, GrpcReceiveEndpointContext receiveEndpointContext) - : base(false, receiveEndpointContext) - { - Message = message; - - Body = new BytesMessageBody(message.Body); - - HeaderProvider = new GrpcHeaderProvider(message.Headers); - } - - public GrpcTransportMessage Message { get; } - - protected override IHeaderProvider HeaderProvider { get; } - - public override MessageBody Body { get; } - - protected override ContentType GetContentType() - { - return ConvertToContentType(Message.ContentType); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcReceiveEndpointContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcReceiveEndpointContext.cs deleted file mode 100644 index 2da19f38fe1..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcReceiveEndpointContext.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using Fabric; - using Transports; - using Transports.Fabric; - - - public interface GrpcReceiveEndpointContext : - ReceiveEndpointContext - { - IMessageFabric MessageFabric { get; } - - IGrpcTransportProvider TransportProvider { get; } - - void ConfigureTopology(NodeContext nodeContext); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcReceiveTransport.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcReceiveTransport.cs deleted file mode 100644 index 74b3a307af7..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcReceiveTransport.cs +++ /dev/null @@ -1,224 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Threading; - using System.Threading.Channels; - using System.Threading.Tasks; - using Fabric; - using Internals; - using MassTransit.Middleware; - using Transports; - using Transports.Fabric; - - - /// - /// Support in-memory message queue that is not durable, but supports parallel delivery of messages - /// based on TPL usage. - /// - public class GrpcReceiveTransport : - IReceiveTransport - { - readonly GrpcReceiveEndpointContext _context; - readonly string _queueName; - - public GrpcReceiveTransport(GrpcReceiveEndpointContext context, string queueName) - { - _context = context; - _queueName = queueName; - } - - public void Probe(ProbeContext context) - { - var scope = context.CreateScope("receiveTransport"); - scope.Add("type", "gRPC"); - scope.Set(new - { - Address = _context.InputAddress, - _context.PrefetchCount, - _context.ConcurrentMessageLimit - }); - } - - ReceiveTransportHandle IReceiveTransport.Start() - { - return new ReceiveTransportAgent(_context, _queueName); - } - - ConnectHandle IReceiveObserverConnector.ConnectReceiveObserver(IReceiveObserver observer) - { - return _context.ConnectReceiveObserver(observer); - } - - ConnectHandle IReceiveTransportObserverConnector.ConnectReceiveTransportObserver(IReceiveTransportObserver observer) - { - return _context.ConnectReceiveTransportObserver(observer); - } - - ConnectHandle IPublishObserverConnector.ConnectPublishObserver(IPublishObserver observer) - { - return _context.ConnectPublishObserver(observer); - } - - ConnectHandle ISendObserverConnector.ConnectSendObserver(ISendObserver observer) - { - return _context.ConnectSendObserver(observer); - } - - - class ReceiveTransportAgent : - Agent, - ReceiveTransportHandle, - IMessageReceiver - { - readonly Channel _channel; - readonly Task _consumeDispatcher; - readonly GrpcReceiveEndpointContext _context; - readonly IReceivePipeDispatcher _dispatcher; - readonly IConcurrencyLimiter _limiter; - readonly string _queueName; - TopologyHandle _topologyHandle; - - public ReceiveTransportAgent(GrpcReceiveEndpointContext context, string queueName) - { - _context = context; - _queueName = queueName; - - _dispatcher = context.CreateReceivePipeDispatcher(); - - var outputOptions = new BoundedChannelOptions(context.PrefetchCount) - { - SingleWriter = true, - SingleReader = true, - AllowSynchronousContinuations = true - }; - - _channel = Channel.CreateBounded(outputOptions); - - if (context.ConcurrentMessageLimit.HasValue) - _limiter = new ConcurrencyLimiter(context.ConcurrentMessageLimit.Value); - - _consumeDispatcher = Task.Run(() => StartDispatcher()); - - var startup = Task.Run(() => Startup()); - - SetReady(startup); - } - - public async Task Deliver(GrpcTransportMessage message, CancellationToken cancellationToken) - { - if (IsStopped) - return; - - await _channel.Writer.WriteAsync(message, cancellationToken).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - var scope = context.CreateScope("local"); - scope.Add("nodeAddress", _context.TransportProvider.HostNodeContext.NodeAddress); - } - - Task ReceiveTransportHandle.Stop(CancellationToken cancellationToken) - { - return this.Stop("Stop Receive Transport", cancellationToken); - } - - async Task StartDispatcher() - { - LogContext.Current = _context.LogContext; - - try - { - await Ready.ConfigureAwait(false); - - while (await _channel.Reader.WaitToReadAsync(Stopping).ConfigureAwait(false)) - { - var message = await _channel.Reader.ReadAsync(Stopping).ConfigureAwait(false); - - if (_limiter != null) - await _limiter.Wait(Stopping).ConfigureAwait(false); - - _ = Task.Run(async () => - { - var context = new GrpcReceiveContext(message, _context); - try - { - await _dispatcher.Dispatch(context).ConfigureAwait(false); - } - catch (Exception exception) - { - context.LogTransportFaulted(exception); - } - finally - { - _limiter?.Release(); - context.Dispose(); - } - }, Stopping); - } - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - LogContext.Warning?.Log(exception, "Consumer dispatcher faulted: {Queue}", _queueName); - } - } - - async Task Startup() - { - try - { - await _context.TransportProvider.StartupTask.OrCanceled(Stopping).ConfigureAwait(false); - - await _context.DependenciesReady.OrCanceled(Stopping).ConfigureAwait(false); - - var hostNodeContext = _context.TransportProvider.HostNodeContext; - - var queue = _context.MessageFabric.GetQueue(hostNodeContext, _queueName); - - IDeadLetterTransport deadLetterTransport = - new GrpcDeadLetterTransport(_context.MessageFabric.GetExchange(hostNodeContext, $"{_queueName}_skipped")); - _context.AddOrUpdatePayload(() => deadLetterTransport, _ => deadLetterTransport); - - IErrorTransport errorTransport = - new GrpcErrorTransport(_context.MessageFabric.GetExchange(hostNodeContext, $"{_queueName}_error")); - _context.AddOrUpdatePayload(() => errorTransport, _ => errorTransport); - - _context.ConfigureTopology(hostNodeContext); - - _topologyHandle = queue.ConnectMessageReceiver(hostNodeContext, this); - - await _context.TransportObservers.NotifyReady(_context.InputAddress).ConfigureAwait(false); - } - catch (Exception exception) - { - SetNotReady(exception); - throw; - } - } - - protected override async Task StopAgent(StopContext context) - { - LogContext.SetCurrentIfNull(_context.LogContext); - - _channel.Writer.Complete(); - - await _channel.Reader.Completion.ConfigureAwait(false); - - await _consumeDispatcher.ConfigureAwait(false); - - var metrics = _dispatcher.GetMetrics(); - - await _context.TransportObservers.NotifyCompleted(_context.InputAddress, metrics).ConfigureAwait(false); - - _context.LogConsumerCompleted(metrics.DeliveryCount, metrics.ConcurrentDeliveryCount); - - _topologyHandle?.Disconnect(); - - await base.StopAgent(context).ConfigureAwait(false); - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcSendTransportContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcSendTransportContext.cs deleted file mode 100644 index f939c86b602..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcSendTransportContext.cs +++ /dev/null @@ -1,165 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Collections.Generic; - using System.Threading; - using System.Threading.Tasks; - using Contracts; - using Fabric; - using Google.Protobuf; - using Google.Protobuf.WellKnownTypes; - using Initializers.TypeConverters; - using MassTransit.Configuration; - using MassTransit.Middleware; - using Metadata; - using Transports; - using Transports.Fabric; - - - public class GrpcSendTransportContext : - BaseSendTransportContext, - SendTransportContext - { - static readonly DateTimeOffsetTypeConverter _dateTimeOffsetConverter = new DateTimeOffsetTypeConverter(); - static readonly DateTimeTypeConverter _dateTimeConverter = new DateTimeTypeConverter(); - - readonly IMessageExchange _exchange; - - public GrpcSendTransportContext(IHostConfiguration hostConfiguration, ReceiveEndpointContext receiveEndpointContext, - IMessageExchange exchange) - : base(hostConfiguration, receiveEndpointContext.Serialization) - { - _exchange = exchange; - } - - public override string EntityName => _exchange.Name; - public override string ActivitySystem => "grpc"; - - public override async Task> CreateSendContext(T message, IPipe> pipe, CancellationToken cancellationToken) - { - var sendContext = new TransportGrpcSendContext(_exchange.Name, message, cancellationToken); - - await pipe.Send(sendContext).ConfigureAwait(false); - - return sendContext; - } - - public void Probe(ProbeContext context) - { - } - - public Task> CreateSendContext(PipeContext context, T message, IPipe> pipe, - CancellationToken cancellationToken) - where T : class - { - return CreateSendContext(message, pipe, cancellationToken); - } - - public Task Send(PipeContext transportContext, SendContext sendContext) - where T : class - { - TransportGrpcSendContext context = sendContext as TransportGrpcSendContext - ?? throw new ArgumentException("Invalid SendContext type", nameof(sendContext)); - - sendContext.CancellationToken.ThrowIfCancellationRequested(); - - var messageId = context.MessageId ?? NewId.NextGuid(); - - var transportMessage = new TransportMessage - { - Deliver = new Deliver - { - Exchange = new ExchangeDestination - { - Name = _exchange.Name, - RoutingKey = context.RoutingKey ?? "" - }, - Envelope = new Envelope - { - MessageId = messageId.ToString("D"), - RequestId = context.RequestId?.ToString("D") ?? "", - ConversationId = context.ConversationId?.ToString("D") ?? "", - CorrelationId = context.CorrelationId?.ToString("D") ?? "", - InitiatorId = context.InitiatorId?.ToString("D") ?? "", - SourceAddress = context.SourceAddress?.ToString() ?? "", - DestinationAddress = context.DestinationAddress?.ToString() ?? "", - ResponseAddress = context.ResponseAddress?.ToString() ?? "", - FaultAddress = context.FaultAddress?.ToString() ?? "", - ContentType = context.ContentType?.ToString() ?? "", - Body = ByteString.CopyFrom(context.Body.GetBytes()), - EnqueueTime = context.Delay.ToFutureDateTime(), - ExpirationTime = context.TimeToLive.ToFutureDateTime(), - SentTime = Timestamp.FromDateTime(context.SentTime ?? DateTime.UtcNow), - } - } - }; - - transportMessage.Deliver.Envelope.MessageType.AddRange(MessageTypeCache.MessageTypeNames); - - SetHeaders(transportMessage.Deliver.Envelope.Headers, context.Headers); - - var grpcTransportMessage = new GrpcTransportMessage(transportMessage, HostMetadataCache.Host); - - return _exchange.Send(grpcTransportMessage, context.CancellationToken); - } - - public Task Send(IPipe pipe, CancellationToken cancellationToken = default) - { - var pipeContext = new Context(cancellationToken); - - return pipe.Send(pipeContext); - } - - static void SetHeaders(IDictionary dictionary, SendHeaders headers) - { - foreach (KeyValuePair header in headers.GetAll()) - { - if (header.Value == null) - { - if (dictionary.ContainsKey(header.Key)) - dictionary.Remove(header.Key); - - continue; - } - - if (dictionary.ContainsKey(header.Key)) - continue; - - switch (header.Value) - { - case DateTimeOffset value: - if (_dateTimeOffsetConverter.TryConvert(value, out string text)) - dictionary[header.Key] = text; - break; - - case DateTime value: - if (_dateTimeConverter.TryConvert(value, out text)) - dictionary[header.Key] = text; - break; - - case string value: - dictionary[header.Key] = value; - break; - - case bool value when value: - dictionary[header.Key] = bool.TrueString; - break; - - case IFormattable formatValue: - dictionary[header.Key] = formatValue.ToString(); - break; - } - } - } - - - class Context : - BasePipeContext - { - public Context(CancellationToken cancellationToken) - : base(cancellationToken) - { - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcSendTransportProvider.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcSendTransportProvider.cs deleted file mode 100644 index 5efb5e9a6bf..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcSendTransportProvider.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Threading.Tasks; - using Transports; - - - public class GrpcSendTransportProvider : - ISendTransportProvider - { - readonly ReceiveEndpointContext _context; - readonly IGrpcTransportProvider _transportProvider; - - public GrpcSendTransportProvider(IGrpcTransportProvider transportProvider, ReceiveEndpointContext context) - { - _transportProvider = transportProvider; - _context = context; - } - - public Uri NormalizeAddress(Uri address) - { - return _transportProvider.NormalizeAddress(address); - } - - Task ISendTransportProvider.GetSendTransport(Uri address) - { - return _transportProvider.CreateSendTransport(_context, address); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcTransportHelperExtensions.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcTransportHelperExtensions.cs deleted file mode 100644 index 8bdf6530c37..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcTransportHelperExtensions.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Fabric; - using Transports.Fabric; - - - static class GrpcTransportHelperExtensions - { - public static Contracts.ExchangeType ToGrpcExchangeType(this Transports.Fabric.ExchangeType exchangeType) - { - return exchangeType switch - { - ExchangeType.Direct => Contracts.ExchangeType.Direct, - ExchangeType.Topic => Contracts.ExchangeType.Topic, - ExchangeType.FanOut => Contracts.ExchangeType.FanOut, - _ => throw new ArgumentException(nameof(exchangeType)) - }; - } - - public static Transports.Fabric.ExchangeType ToExchangeType(this Contracts.ExchangeType exchangeType) - { - return exchangeType switch - { - Contracts.ExchangeType.Direct => ExchangeType.Direct, - Contracts.ExchangeType.Topic => ExchangeType.Topic, - Contracts.ExchangeType.FanOut => ExchangeType.FanOut, - _ => throw new ArgumentException(nameof(exchangeType)) - }; - } - - public static Task Send(this IMessageExchange exchange, GrpcTransportMessage message, CancellationToken cancellationToken = - default) - { - var deliveryContext = new GrpcDeliveryContext(message, cancellationToken); - - return exchange.Deliver(deliveryContext); - } - - public static Task Send(this IMessageQueue queue, GrpcTransportMessage message, - CancellationToken cancellationToken = default) - { - var deliveryContext = new GrpcDeliveryContext(message, cancellationToken); - - return queue.Deliver(deliveryContext); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcTransportProvider.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcTransportProvider.cs deleted file mode 100644 index 23174b8b218..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcTransportProvider.cs +++ /dev/null @@ -1,170 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Configuration; - using Contracts; - using Fabric; - using Grpc.Core; - using Grpc.Net.Client; - using Internals; - using MassTransit.Middleware; - using Metadata; - using Transports; - using Transports.Fabric; - - - public sealed class GrpcTransportProvider : - Supervisor, - IGrpcTransportProvider - { - const int MaxMessageLengthBytes = int.MaxValue; - readonly IList _clients; - readonly IGrpcHostConfiguration _hostConfiguration; - readonly GrpcHostNode _hostNode; - readonly IMessageFabric _messageFabric; - readonly NodeCollection _nodeCollection; - readonly Server _server; - readonly Lazy _startupTask; - readonly IGrpcTopologyConfiguration _topologyConfiguration; - - public GrpcTransportProvider(IGrpcHostConfiguration hostConfiguration, IGrpcTopologyConfiguration topologyConfiguration) - { - _hostConfiguration = hostConfiguration; - _topologyConfiguration = topologyConfiguration; - - _messageFabric = new MessageFabric(); - - _nodeCollection = new NodeCollection(this, _messageFabric); - _clients = new List(); - - var transport = new GrpcTransportService(this, _hostConfiguration, _nodeCollection); - - _server = new Server(GetChannelOptions()) - { - Services = { TransportService.BindService(transport) }, - Ports = { new ServerPort(hostConfiguration.BaseAddress.Host, hostConfiguration.BaseAddress.Port, ServerCredentials.Insecure) } - }; - - var serverPort = _server.Ports.First(); - - HostAddress = new UriBuilder(_hostConfiguration.BaseAddress) - { - Host = serverPort.Host, - Port = serverPort.BoundPort - }.Uri; - - HostNodeContext = new HostNodeContext(HostAddress); - - _hostNode = new GrpcHostNode(_messageFabric, HostNodeContext); - - var observer = new NodeMessageFabricObserver(_nodeCollection, _hostNode); - - _messageFabric.ConnectMessageFabricObserver(observer); - - _startupTask = new Lazy(() => Task.Run(() => Startup())); - } - - public IGrpcHostNode HostNode => _hostNode; - public Task StartupTask => _startupTask.Value; - - public Uri HostAddress { get; } - public NodeContext HostNodeContext { get; } - - public IMessageFabric MessageFabric => _messageFabric; - - public async Task CreateSendTransport(ReceiveEndpointContext receiveEndpointContext, Uri address) - { - LogContext.SetCurrentIfNull(_hostConfiguration.LogContext); - - var endpointAddress = new GrpcEndpointAddress(HostAddress, address); - - TransportLogMessages.CreateSendTransport(address); - - IMessageExchange exchange = _messageFabric.GetExchange(HostNodeContext, endpointAddress.Name, endpointAddress.ExchangeType); - - var transportContext = new GrpcSendTransportContext(_hostConfiguration, receiveEndpointContext, exchange); - - return new SendTransport(transportContext); - } - - public Uri NormalizeAddress(Uri address) - { - return new GrpcEndpointAddress(HostAddress, address); - } - - public Task CreatePublishTransport(ReceiveEndpointContext receiveEndpointContext, Uri publishAddress) - where T : class - { - IGrpcMessagePublishTopologyConfigurator publishTopology = _topologyConfiguration.Publish.GetMessageTopology(); - - publishTopology.Apply(new MessageFabricPublishTopologyBuilder(HostNodeContext, _messageFabric)); - - return CreateSendTransport(receiveEndpointContext, publishAddress); - } - - public void Probe(ProbeContext context) - { - _messageFabric.Probe(context); - } - - IGrpcClient GetClient(Uri address) - { - var channel = HostMetadataCache.IsNetFramework - ? (ChannelBase)new Channel(address.Host, address.Port, ChannelCredentials.Insecure) - : GrpcChannel.ForAddress(address.GetLeftPart(UriPartial.Authority), new GrpcChannelOptions - { - Credentials = ChannelCredentials.Insecure, - MaxReceiveMessageSize = MaxMessageLengthBytes, - }); - - var client = new TransportService.TransportServiceClient(channel); - - var clientNodeContext = new ClientNodeContext(address); - - var node = _nodeCollection.GetNode(clientNodeContext); - - return new GrpcClient(_hostConfiguration, _hostNode, client, node); - } - - Task Startup() - { - _server.Start(); - - foreach (var server in _hostConfiguration.ServerConfigurations) - { - var client = GetClient(server.Address); - - _clients.Add(client); - - Add(client); - } - - return Task.WhenAll(_clients.Select(x => x.Ready)).OrCanceled(Stopping); - } - - static IEnumerable GetChannelOptions() - { - return new[] - { - new ChannelOption(ChannelOptions.MaxReceiveMessageLength, MaxMessageLengthBytes), - new ChannelOption(ChannelOptions.MaxSendMessageLength, MaxMessageLengthBytes) - }; - } - - protected override async Task StopSupervisor(StopSupervisorContext context) - { - LogContext.Debug?.Log("gRPC Stopping: {HostAddress}", HostAddress); - - await base.StopSupervisor(context).ConfigureAwait(false); - - var shutdownAsync = _server.ShutdownAsync(); - - await shutdownAsync.ConfigureAwait(false); - - await _messageFabric.Stop(context).ConfigureAwait(false); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcTransportService.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcTransportService.cs deleted file mode 100644 index dbc32a0af9e..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/GrpcTransportService.cs +++ /dev/null @@ -1,72 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Configuration; - using Contracts; - using Grpc.Core; - using Internals; - - - public class GrpcTransportService : - TransportService.TransportServiceBase - { - readonly INodeCollection _collection; - readonly IGrpcHostConfiguration _hostConfiguration; - readonly IGrpcTransportProvider _transportProvider; - - public GrpcTransportService(IGrpcTransportProvider transportProvider, IGrpcHostConfiguration hostConfiguration, INodeCollection collection) - { - _transportProvider = transportProvider; - _hostConfiguration = hostConfiguration; - _collection = collection; - } - - public override async Task EventStream(IAsyncStreamReader requestStream, IServerStreamWriter responseStream, - ServerCallContext context) - { - LogContext.SetCurrentIfNull(_hostConfiguration.LogContext); - - try - { - var ready = await requestStream.MoveNext(CancellationToken.None).OrCanceled(context.CancellationToken).ConfigureAwait(false); - if (ready) - { - var message = requestStream.Current; - if (message.ContentCase == TransportMessage.ContentOneofCase.Join) - { - var joinNode = message.Join.Node; - - var nodeAddress = new Uri(joinNode.Address); - - Guid.TryParse(joinNode.SessionId, out var sessionId); - - var nodeContext = new ServerNodeContext(context, nodeAddress, sessionId, joinNode.Host); - - nodeContext.LogReceived(message); - - var node = _collection.GetNode(nodeContext); - - node.Join(nodeContext, joinNode.Topology); - - await node.SendWelcome(_transportProvider.HostNode).ConfigureAwait(false); - - await node.Connect(responseStream, requestStream, context.CancellationToken).ConfigureAwait(false); - - nodeContext.LogDisconnect(); - } - } - else - LogContext.Warning?.Log("GRPC no content received: {Address}", context.Peer); - } - catch (OperationCanceledException) - { - } - catch (Exception exception) - { - LogContext.Error?.Log(exception, "Connection {NodeAddress} faulted", context.Peer); - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/HostNodeContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/HostNodeContext.cs deleted file mode 100644 index 97ab8f7f385..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/HostNodeContext.cs +++ /dev/null @@ -1,28 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using Metadata; - - - public class HostNodeContext : - NodeContext - { - public HostNodeContext(Uri nodeAddress) - { - NodeAddress = nodeAddress; - SessionId = NewId.NextGuid(); - } - - public NodeType NodeType => NodeType.Host; - - public Uri NodeAddress { get; } - - public Guid SessionId { get; } - - public HostInfo Host - { - get => HostMetadataCache.Host; - set => throw new InvalidOperationException("The Host cannot be set on the Host"); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/HostNodeTopology.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/HostNodeTopology.cs deleted file mode 100644 index 4b8c5b245c0..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/HostNodeTopology.cs +++ /dev/null @@ -1,68 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Collections.Generic; - using System.Linq; - using Transports.Fabric; - - - public class HostNodeTopology - { - readonly Dictionary _entries; - long _nextSequenceNumber; - - public HostNodeTopology() - { - _entries = new Dictionary(); - } - - public TopologyHandle Add(Contracts.Topology topology, TopologyHandle handle) - { - if (topology == null) - throw new ArgumentNullException(nameof(topology)); - - lock (_entries) - { - var sequenceNumber = ++_nextSequenceNumber; - - topology.SequenceNumber = sequenceNumber; - - var entry = new TopologyEntry(topology, handle); - _entries.Add(sequenceNumber, entry); - - return entry; - } - } - - public IEnumerable GetTopology() - { - lock (_entries) - return _entries.Values.Select(x => x.Topology).ToList(); - } - - - class TopologyEntry : - TopologyHandle - { - readonly TopologyHandle _handle; - - public TopologyEntry(Contracts.Topology topology, TopologyHandle handle) - { - _handle = handle; - - Topology = topology; - } - - public Contracts.Topology Topology { get; } - - public long Id => _handle.Id; - - public void Disconnect() - { - _handle?.Disconnect(); - - Topology.Valid = false; - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcClient.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcClient.cs deleted file mode 100644 index c3f88039a11..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcClient.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - public interface IGrpcClient : - IAgent - { - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcHost.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcHost.cs deleted file mode 100644 index de49a8129bd..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcHost.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using Transports; - - - public interface IGrpcHost : - IHost - { - } -} \ No newline at end of file diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcHostNode.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcHostNode.cs deleted file mode 100644 index 300f51c3026..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcHostNode.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Collections.Generic; - using Transports.Fabric; - - - public interface IGrpcHostNode : - IGrpcNode - { - TopologyHandle AddTopology(Contracts.Topology topology, TopologyHandle handle = default); - - IEnumerable GetTopology(); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcNode.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcNode.cs deleted file mode 100644 index f3c25f44ab3..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcNode.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Collections.Generic; - using System.Threading; - using System.Threading.Channels; - using System.Threading.Tasks; - using Contracts; - using Grpc.Core; - - - public interface IGrpcNode : - IAgent, - NodeContext - { - ChannelWriter Writer { get; } - - Task Connect(IAsyncStreamWriter writer, IAsyncStreamReader reader, CancellationToken cancellationToken); - - void Join(NodeContext context, IEnumerable topologies); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcTransportProvider.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcTransportProvider.cs deleted file mode 100644 index f862d62d06f..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IGrpcTransportProvider.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Threading.Tasks; - using Fabric; - using Transports; - using Transports.Fabric; - - - public interface IGrpcTransportProvider : - IAgent, - IProbeSite - { - Uri HostAddress { get; } - NodeContext HostNodeContext { get; } - IMessageFabric MessageFabric { get; } - - IGrpcHostNode HostNode { get; } - - /// - /// Await this task to ensure the server and clients have started up successfully - /// - Task StartupTask { get; } - - Task CreateSendTransport(ReceiveEndpointContext context, Uri address); - - Task CreatePublishTransport(ReceiveEndpointContext context, Uri publishAddress) - where T : class; - - Uri NormalizeAddress(Uri address); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IMessageRoutingKeyFormatter.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IMessageRoutingKeyFormatter.cs deleted file mode 100644 index 865513a9576..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IMessageRoutingKeyFormatter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - public interface IMessageRoutingKeyFormatter - where TMessage : class - { - string FormatRoutingKey(GrpcSendContext context); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/INodeCollection.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/INodeCollection.cs deleted file mode 100644 index 8d22e1a3ab1..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/INodeCollection.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Collections.Generic; - - - public interface INodeCollection : - IEnumerable - { - IGrpcNode GetNode(NodeContext context); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IRoutingKeyFormatter.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IRoutingKeyFormatter.cs deleted file mode 100644 index a01c97fa0af..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/IRoutingKeyFormatter.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - public interface IRoutingKeyFormatter - { - /// - /// Format the routing key for the send context, so that it can be passed to RabbitMQ - /// - /// The message type - /// The message send context - /// The routing key to specify in the transport - string FormatRoutingKey(GrpcSendContext context) - where T : class; - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/MessageRoutingKeyFormatter.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/MessageRoutingKeyFormatter.cs deleted file mode 100644 index e9c8a38c6cc..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/MessageRoutingKeyFormatter.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - public class MessageRoutingKeyFormatter : - IMessageRoutingKeyFormatter - where TMessage : class - { - readonly IRoutingKeyFormatter _formatter; - - public MessageRoutingKeyFormatter(IRoutingKeyFormatter formatter) - { - _formatter = formatter; - } - - public string FormatRoutingKey(GrpcSendContext context) - { - return _formatter.FormatRoutingKey(context); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Middleware/SetRoutingKeyFilter.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Middleware/SetRoutingKeyFilter.cs deleted file mode 100644 index 01e762d9ce4..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Middleware/SetRoutingKeyFilter.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace MassTransit.GrpcTransport.Middleware -{ - using System.Threading.Tasks; - - - public class SetRoutingKeyFilter : - IFilter> - where T : class - { - readonly IMessageRoutingKeyFormatter _routingKeyFormatter; - - public SetRoutingKeyFilter(IMessageRoutingKeyFormatter routingKeyFormatter) - { - _routingKeyFormatter = routingKeyFormatter; - } - - public Task Send(GrpcSendContext context, IPipe> next) - { - var routingKey = _routingKeyFormatter.FormatRoutingKey(context); - - context.RoutingKey = routingKey; - - return next.Send(context); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("SetCorrelationId"); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeCollection.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeCollection.cs deleted file mode 100644 index 0009a51a073..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeCollection.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Collections; - using System.Collections.Concurrent; - using System.Collections.Generic; - using Fabric; - using Transports.Fabric; - - - public class NodeCollection : - INodeCollection - { - readonly IMessageFabric _messageFabric; - readonly ConcurrentDictionary _nodes; - readonly ISupervisor _supervisor; - - public NodeCollection(ISupervisor supervisor, IMessageFabric messageFabric) - { - _supervisor = supervisor; - _messageFabric = messageFabric; - _nodes = new ConcurrentDictionary(); - } - - public IEnumerator GetEnumerator() - { - return _nodes.Values.GetEnumerator(); - } - - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } - - public IGrpcNode GetNode(NodeContext context) - { - return _nodes.GetOrAdd(context.NodeAddress, _ => CreateNode(context)); - } - - IGrpcNode CreateNode(NodeContext context) - { - var instance = new GrpcNode(_messageFabric, context); - - _supervisor.Add(instance); - - return instance; - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeContext.cs deleted file mode 100644 index 34e7593ce74..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeContext.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - - - public interface NodeContext - { - NodeType NodeType { get; } - - Uri NodeAddress { get; } - - Guid SessionId { get; } - - HostInfo Host { get; set; } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeMessageFabricObserver.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeMessageFabricObserver.cs deleted file mode 100644 index 6bf4d067e1e..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeMessageFabricObserver.cs +++ /dev/null @@ -1,117 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Linq; - using Contracts; - using Transports.Fabric; - - - public class NodeMessageFabricObserver : - IMessageFabricObserver - { - readonly IGrpcHostNode _hostNode; - readonly INodeCollection _nodes; - - public NodeMessageFabricObserver(INodeCollection nodes, IGrpcHostNode hostNode) - { - _nodes = nodes; - _hostNode = hostNode; - } - - public void ExchangeDeclared(NodeContext context, string name, Transports.Fabric.ExchangeType exchangeType) - { - if (context.NodeType != NodeType.Host) - return; - - Send(context, new Contracts.Topology - { - Exchange = new Exchange - { - Name = name, - Type = exchangeType.ToGrpcExchangeType() - } - }); - } - - public void ExchangeBindingCreated(NodeContext context, string source, string destination, string routingKey) - { - if (context.NodeType != NodeType.Host) - return; - - Send(context, new Contracts.Topology - { - ExchangeBind = new ExchangeBind - { - Source = source, - Destination = destination, - RoutingKey = routingKey ?? "" - } - }); - } - - public void QueueDeclared(NodeContext context, string name) - { - if (context.NodeType != NodeType.Host) - return; - - Send(context, new Contracts.Topology { Queue = new Queue { Name = name } }); - } - - public void QueueBindingCreated(NodeContext context, string source, string destination) - { - if (context.NodeType != NodeType.Host) - return; - - Send(context, new Contracts.Topology - { - QueueBind = new QueueBind - { - Source = source, - Destination = destination, - } - }); - } - - public TopologyHandle ConsumerConnected(NodeContext context, TopologyHandle handle, string queueName) - { - if (context.NodeType != NodeType.Host) - return handle; - - return Send(context, new Contracts.Topology - { - Receiver = new Receiver - { - QueueName = queueName, - ReceiverId = handle.Id - } - }, handle); - } - - TopologyHandle Send(NodeContext context, Contracts.Topology topology, TopologyHandle handle = default) - { - try - { - handle = _hostNode.AddTopology(topology, handle); - - var transportMessage = new TransportMessage - { - MessageId = NewId.NextGuid().ToString(), - Topology = topology - }; - - foreach (var node in _nodes.Where(x => x.NodeAddress != context.NodeAddress)) - { - if (!node.Writer.TryWrite(transportMessage)) - LogContext.Error?.Log("Failed to Send Topology {Topology} to {Address}", topology.ChangeCase, node.NodeAddress); - } - - return handle; - } - catch (Exception exception) - { - LogContext.Error?.Log(exception, "Failed to send topology message"); - throw; - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeType.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeType.cs deleted file mode 100644 index f0f51a08f83..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/NodeType.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - public enum NodeType - { - Host = 0, - Server = 1, - Client = 2, - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/RemoteNodeMessageReceiver.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/RemoteNodeMessageReceiver.cs deleted file mode 100644 index 320acefcf72..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/RemoteNodeMessageReceiver.cs +++ /dev/null @@ -1,49 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Threading; - using System.Threading.Tasks; - using Contracts; - using Fabric; - using Transports.Fabric; - - - public class RemoteNodeMessageReceiver : - IMessageReceiver - { - readonly IGrpcNode _node; - readonly string _queueName; - readonly long _receiverId; - - public RemoteNodeMessageReceiver(IGrpcNode node, string queueName, long receiverId) - { - _node = node; - _queueName = queueName; - _receiverId = receiverId; - } - - public async Task Deliver(GrpcTransportMessage message, CancellationToken cancellationToken) - { - var transportMessage = new TransportMessage(message.Message) - { - Deliver = - { - Receiver = new ReceiverDestination - { - QueueName = _queueName, - ReceiverId = _receiverId - } - } - }; - - var grpcTransportMessage = new GrpcTransportMessage(transportMessage, message.Host); - - await _node.DeliverMessage(grpcTransportMessage).ConfigureAwait(false); - } - - public void Probe(ProbeContext context) - { - var scope = context.CreateScope("remote"); - scope.Add("nodeAddress", _node.NodeAddress); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/RemoteNodeTopology.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/RemoteNodeTopology.cs deleted file mode 100644 index 1cccd3a390b..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/RemoteNodeTopology.cs +++ /dev/null @@ -1,201 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Collections.Generic; - using System.Linq; - using Fabric; - using Transports.Fabric; - - - public class RemoteNodeTopology - { - readonly Dictionary _entries; - readonly IMessageFabric _messageFabric; - readonly IGrpcNode _node; - readonly Dictionary _receiverMap; - long _lastSequenceNumber; - Guid _sessionId; - - public RemoteNodeTopology(IGrpcNode node, IMessageFabric messageFabric) - { - _node = node; - _messageFabric = messageFabric; - - _entries = new Dictionary(); - _receiverMap = new Dictionary(); - } - - public void Join(Guid sessionId, IEnumerable topologies) - { - if (sessionId != _sessionId) - { - Reset(); - _sessionId = sessionId; - } - - foreach (var topology in topologies) - ProcessTopology(topology); - } - - public void ProcessTopology(Contracts.Topology topology) - { - if (topology == null) - throw new ArgumentNullException(nameof(topology)); - - var topologySequenceNumber = topology.SequenceNumber; - - lock (_entries) - { - if (_entries.TryGetValue(topologySequenceNumber, out var existingEntry)) - { - if (existingEntry.Topology.ChangeCase != topology.ChangeCase) - LogContext.Warning?.Log("Topology Mismatch, {Existing} != {New}", existingEntry.Topology.ChangeCase, topology.ChangeCase); - } - else - _entries.Add(topologySequenceNumber, new TopologyEntry(topology)); - - while (_entries.TryGetValue(_lastSequenceNumber + 1, out var nextEntry)) - { - if (nextEntry.Topology.ChangeCase == Contracts.Topology.ChangeOneofCase.Exchange) - { - var exchange = nextEntry.Topology.Exchange; - - _messageFabric.ExchangeDeclare(_node, exchange.Name, exchange.Type.ToExchangeType()); - - _node.LogTopology(exchange, exchange.Type); - } - else if (nextEntry.Topology.ChangeCase == Contracts.Topology.ChangeOneofCase.Queue) - { - var queue = nextEntry.Topology.Queue; - - _messageFabric.QueueDeclare(_node, queue.Name); - - _node.LogTopology(queue); - } - else if (nextEntry.Topology.ChangeCase == Contracts.Topology.ChangeOneofCase.ExchangeBind) - { - var exchangeBind = nextEntry.Topology.ExchangeBind; - - _messageFabric.ExchangeBind(_node, exchangeBind.Source, exchangeBind.Destination, exchangeBind.RoutingKey); - - _node.LogTopology(exchangeBind); - } - else if (nextEntry.Topology.ChangeCase == Contracts.Topology.ChangeOneofCase.QueueBind) - { - var queueBind = nextEntry.Topology.QueueBind; - - _messageFabric.QueueBind(_node, queueBind.Source, queueBind.Destination); - - _node.LogTopology(queueBind); - } - else if (nextEntry.Topology.ChangeCase == Contracts.Topology.ChangeOneofCase.Receiver) - { - var receiver = nextEntry.Topology.Receiver; - var queueName = receiver.QueueName; - - var queue = _messageFabric.GetQueue(_node, queueName); - - var messageReceiver = new RemoteNodeMessageReceiver(_node, queueName, receiver.ReceiverId); - - nextEntry.Handle = queue.ConnectMessageReceiver(_node, messageReceiver); - - var key = new ReceiverKey(queueName, receiver.ReceiverId); - - _receiverMap[key] = nextEntry.Handle.Id; - - _node.LogTopology(receiver); - } - - _lastSequenceNumber = nextEntry.Topology.SequenceNumber; - } - } - } - - public IEnumerable GetTopology() - { - lock (_entries) - return _entries.Values.Where(x => x.Topology.SequenceNumber <= _lastSequenceNumber).Select(x => x.Topology).ToList(); - } - - void Reset() - { - lock (_entries) - { - foreach (var entry in _entries.Values) - entry.Disconnect(); - - _lastSequenceNumber = 0; - - _entries.Clear(); - _receiverMap.Clear(); - } - } - - public long GetLocalConsumerId(string queueName, long consumerId) - { - return _receiverMap.TryGetValue(new ReceiverKey(queueName, consumerId), out var localConsumerId) ? localConsumerId : consumerId; - } - - - readonly struct ReceiverKey : - IEquatable - { - public bool Equals(ReceiverKey other) - { - return _queueName == other._queueName && _consumerId == other._consumerId; - } - - public override bool Equals(object obj) - { - return obj is ReceiverKey other && Equals(other); - } - - public override int GetHashCode() - { - unchecked - { - return (_queueName.GetHashCode() * 397) ^ _consumerId.GetHashCode(); - } - } - - public static bool operator ==(ReceiverKey left, ReceiverKey right) - { - return left.Equals(right); - } - - public static bool operator !=(ReceiverKey left, ReceiverKey right) - { - return !left.Equals(right); - } - - readonly string _queueName; - readonly long _consumerId; - - public ReceiverKey(string queueName, long consumerId) - { - _queueName = queueName; - _consumerId = consumerId; - } - } - - - class TopologyEntry - { - public TopologyEntry(Contracts.Topology topology) - { - Topology = topology; - } - - public TopologyHandle Handle { get; set; } - - public Contracts.Topology Topology { get; } - - public void Disconnect() - { - Handle?.Disconnect(); - - Topology.Valid = false; - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/ServerNodeContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/ServerNodeContext.cs deleted file mode 100644 index b1399ca64cd..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/ServerNodeContext.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using System.Collections.Generic; - using Grpc.Core; - - - public class ServerNodeContext : - NodeContext - { - readonly ServerCallContext _context; - - public ServerNodeContext(ServerCallContext context, Uri nodeAddress, Guid sessionId, IReadOnlyDictionary host) - { - _context = context; - NodeAddress = nodeAddress; - SessionId = sessionId; - - Host = new DictionaryHostInfo(host); - } - - public NodeType NodeType => NodeType.Server; - public Uri NodeAddress { get; } - public Guid SessionId { get; } - public HostInfo Host { get; set; } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcBusTopology.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcBusTopology.cs deleted file mode 100644 index 96f3b96e882..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcBusTopology.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MassTransit.GrpcTransport.Topology -{ - using Configuration; - using Transports; - - - public class GrpcBusTopology : - BusTopology, - IGrpcBusTopology - { - readonly IGrpcTopologyConfiguration _configuration; - - public GrpcBusTopology(IGrpcHostConfiguration hostConfiguration, IGrpcTopologyConfiguration configuration) - : base(hostConfiguration, configuration) - { - _configuration = configuration; - } - - public new IGrpcMessagePublishTopology Publish() - where T : class - { - return _configuration.Publish.GetMessageTopology(); - } - } -} \ No newline at end of file diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcConsumeTopology.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcConsumeTopology.cs deleted file mode 100644 index 59694cbca7e..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcConsumeTopology.cs +++ /dev/null @@ -1,75 +0,0 @@ -namespace MassTransit.GrpcTransport.Topology -{ - using System; - using System.Collections.Generic; - using System.Linq; - using Configuration; - using MassTransit.Configuration; - using Transports.Fabric; - - - public class GrpcConsumeTopology : - ConsumeTopology, - IGrpcConsumeTopologyConfigurator - { - readonly IMessageTopology _messageTopology; - readonly IGrpcPublishTopologyConfigurator _publishTopology; - readonly IList _specifications; - - public GrpcConsumeTopology(IMessageTopology messageTopology, IGrpcPublishTopologyConfigurator publishTopology) - { - _messageTopology = messageTopology; - _publishTopology = publishTopology; - _specifications = new List(); - } - - IGrpcMessageConsumeTopology IGrpcConsumeTopology.GetMessageTopology() - { - IMessageConsumeTopologyConfigurator configurator = base.GetMessageTopology(); - - return configurator as IGrpcMessageConsumeTopology; - } - - public void AddSpecification(IGrpcConsumeTopologySpecification specification) - { - if (specification == null) - throw new ArgumentNullException(nameof(specification)); - - _specifications.Add(specification); - } - - IGrpcMessageConsumeTopologyConfigurator IGrpcConsumeTopologyConfigurator.GetMessageTopology() - { - return GetMessageTopology() as IGrpcMessageConsumeTopologyConfigurator; - } - - public void Apply(IMessageFabricConsumeTopologyBuilder builder) - { - foreach (var specification in _specifications) - specification.Apply(builder); - - ForEach(x => x.Apply(builder)); - } - - public override IEnumerable Validate() - { - return base.Validate().Concat(_specifications.SelectMany(x => x.Validate())); - } - - public void Bind(string exchangeName, ExchangeType exchangeType = ExchangeType.FanOut, string routingKey = default) - { - var specification = new ExchangeBindingConsumeTopologySpecification(exchangeName, exchangeType, routingKey); - - _specifications.Add(specification); - } - - protected override IMessageConsumeTopologyConfigurator CreateMessageTopology(Type type) - { - var topology = new GrpcMessageConsumeTopology(_messageTopology.GetMessageTopology(), _publishTopology.GetMessageTopology()); - - OnMessageTopologyCreated(topology); - - return topology; - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcMessageConsumeTopology.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcMessageConsumeTopology.cs deleted file mode 100644 index ad2c81a6064..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcMessageConsumeTopology.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace MassTransit.GrpcTransport.Topology -{ - using System.Collections.Generic; - using System.Linq; - using Configuration; - using MassTransit.Configuration; - using Transports.Fabric; - - - public class GrpcMessageConsumeTopology : - MessageConsumeTopology, - IGrpcMessageConsumeTopologyConfigurator, - IGrpcMessageConsumeTopologyConfigurator - where TMessage : class - { - readonly IMessageTopology _messageTopology; - readonly IGrpcMessagePublishTopology _publishTopology; - readonly IList _specifications; - - public GrpcMessageConsumeTopology(IMessageTopology messageTopology, IGrpcMessagePublishTopology publishTopology) - { - _messageTopology = messageTopology; - _publishTopology = publishTopology; - _specifications = new List(); - } - - public void Apply(IMessageFabricConsumeTopologyBuilder builder) - { - foreach (var specification in _specifications) - specification.Apply(builder); - } - - public void Bind(ExchangeType? exchangeType = default, string routingKey = default) - { - if (!IsBindableMessageType) - { - _specifications.Add(new InvalidGrpcConsumeTopologySpecification(TypeCache.ShortName, "Is not a bindable message type")); - return; - } - - var specification = new ExchangeBindingConsumeTopologySpecification(_messageTopology.EntityName, exchangeType ?? _publishTopology.ExchangeType, - routingKey); - - _specifications.Add(specification); - } - - public override IEnumerable Validate() - { - return base.Validate().Concat(_specifications.SelectMany(x => x.Validate())); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcMessagePublishTopology.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcMessagePublishTopology.cs deleted file mode 100644 index 296ce16253f..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcMessagePublishTopology.cs +++ /dev/null @@ -1,88 +0,0 @@ -#nullable enable -namespace MassTransit.GrpcTransport.Topology -{ - using System; - using System.Collections.Generic; - using MassTransit.Configuration; - using MassTransit.Topology; - using Transports.Fabric; - - - public class GrpcMessagePublishTopology : - MessagePublishTopology, - IGrpcMessagePublishTopologyConfigurator - where TMessage : class - { - readonly IList _implementedMessageTypes; - readonly IMessageTopology _messageTopology; - - public GrpcMessagePublishTopology(IPublishTopology publishTopology, IMessageTopology messageTopology) - : base(publishTopology) - { - _messageTopology = messageTopology; - _implementedMessageTypes = new List(); - } - - public ExchangeType ExchangeType { get; set; } - - public void Apply(IMessageFabricPublishTopologyBuilder builder) - { - if (Exclude) - return; - - var exchangeName = _messageTopology.EntityName; - - builder.ExchangeDeclare(exchangeName, ExchangeType); - - if (builder.ExchangeName != null) - builder.ExchangeBind(builder.ExchangeName, exchangeName, builder.ExchangeType == ExchangeType.Topic ? "#" : default); - else - { - builder.ExchangeName = exchangeName; - builder.ExchangeType = ExchangeType; - } - - foreach (var configurator in _implementedMessageTypes) - configurator.Apply(builder); - } - - public override bool TryGetPublishAddress(Uri baseAddress, out Uri? publishAddress) - { - publishAddress = new GrpcEndpointAddress(new GrpcHostAddress(baseAddress), _messageTopology.EntityName, exchangeType: ExchangeType); - return true; - } - - public void AddImplementedMessageConfigurator(IGrpcMessagePublishTopologyConfigurator configurator, bool direct) - where T : class - { - var adapter = new TypeAdapter(configurator, direct); - - _implementedMessageTypes.Add(adapter); - } - - - class TypeAdapter : - IGrpcMessagePublishTopology - where T : class - { - readonly IGrpcMessagePublishTopologyConfigurator _configurator; - readonly bool _direct; - - public TypeAdapter(IGrpcMessagePublishTopologyConfigurator configurator, bool direct) - { - _configurator = configurator; - _direct = direct; - } - - public void Apply(IMessageFabricPublishTopologyBuilder builder) - { - if (_direct) - { - var implementedBuilder = builder.CreateImplementedBuilder(); - - _configurator.Apply(implementedBuilder); - } - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcPublishTopology.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcPublishTopology.cs deleted file mode 100644 index 71a123368de..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/Topology/GrpcPublishTopology.cs +++ /dev/null @@ -1,71 +0,0 @@ -namespace MassTransit.GrpcTransport.Topology -{ - using System; - using MassTransit.Topology; - using Metadata; - - - public class GrpcPublishTopology : - PublishTopology, - IGrpcPublishTopologyConfigurator - { - readonly IMessageTopology _messageTopology; - - public GrpcPublishTopology(IMessageTopology messageTopology) - { - _messageTopology = messageTopology; - } - - IGrpcMessagePublishTopology IGrpcPublishTopology.GetMessageTopology() - { - return GetMessageTopology() as IGrpcMessagePublishTopology; - } - - IGrpcMessagePublishTopologyConfigurator IGrpcPublishTopologyConfigurator.GetMessageTopology() - { - return GetMessageTopology() as IGrpcMessagePublishTopologyConfigurator; - } - - IGrpcMessagePublishTopologyConfigurator IGrpcPublishTopologyConfigurator.GetMessageTopology(Type messageType) - { - return GetMessageTopology(messageType) as IGrpcMessagePublishTopologyConfigurator; - } - - protected override IMessagePublishTopologyConfigurator CreateMessageTopology(Type type) - { - var topology = new GrpcMessagePublishTopology(this, _messageTopology.GetMessageTopology()); - - var connector = new ImplementedMessageTypeConnector(this, topology); - - ImplementedMessageTypeCache.EnumerateImplementedTypes(connector); - - OnMessageTopologyCreated(topology); - - return topology; - } - - - class ImplementedMessageTypeConnector : - IImplementedMessageType - where TMessage : class - { - readonly GrpcMessagePublishTopology _messagePublishTopologyConfigurator; - readonly IGrpcPublishTopologyConfigurator _publishTopology; - - public ImplementedMessageTypeConnector(IGrpcPublishTopologyConfigurator publishTopology, - GrpcMessagePublishTopology messagePublishTopologyConfigurator) - { - _publishTopology = publishTopology; - _messagePublishTopologyConfigurator = messagePublishTopologyConfigurator; - } - - public void ImplementsMessageType(bool direct) - where T : class - { - IGrpcMessagePublishTopologyConfigurator messageTopology = _publishTopology.GetMessageTopology(); - - _messagePublishTopologyConfigurator.AddImplementedMessageConfigurator(messageTopology, direct); - } - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/TransportGrpcReceiveEndpointContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/TransportGrpcReceiveEndpointContext.cs deleted file mode 100644 index da1e496c943..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/TransportGrpcReceiveEndpointContext.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using Configuration; - using Fabric; - using Transports; - using Transports.Fabric; - - - public class TransportGrpcReceiveEndpointContext : - BaseReceiveEndpointContext, - GrpcReceiveEndpointContext - { - readonly IGrpcReceiveEndpointConfiguration _configuration; - readonly IGrpcHostConfiguration _hostConfiguration; - - public TransportGrpcReceiveEndpointContext(IGrpcHostConfiguration hostConfiguration, IGrpcReceiveEndpointConfiguration configuration) - : base(hostConfiguration, configuration) - { - _hostConfiguration = hostConfiguration; - _configuration = configuration; - } - - public IMessageFabric MessageFabric => _hostConfiguration.TransportProvider.MessageFabric; - - public override void AddSendAgent(IAgent agent) - { - throw new NotSupportedException(); - } - - public override void AddConsumeAgent(IAgent agent) - { - throw new NotSupportedException(); - } - - public override Exception ConvertException(Exception exception, string message) - { - return exception; - } - - public IGrpcTransportProvider TransportProvider => _hostConfiguration.TransportProvider; - - public void ConfigureTopology(NodeContext nodeContext) - { - var builder = new MessageFabricConsumeTopologyBuilder(nodeContext, MessageFabric); - - var name = _configuration.InputAddress.GetEndpointName(); - - builder.Exchange = name; - builder.ExchangeDeclare(name, ExchangeType.FanOut); - - builder.Queue = name; - builder.QueueDeclare(name); - - builder.QueueBind(builder.Exchange, builder.Queue); - - _configuration.Topology.Consume.Apply(builder); - } - - protected override ISendTransportProvider CreateSendTransportProvider() - { - return new GrpcSendTransportProvider(_hostConfiguration.TransportProvider, this); - } - - protected override IPublishTransportProvider CreatePublishTransportProvider() - { - return new GrpcPublishTransportProvider(_hostConfiguration.TransportProvider, this); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/TransportGrpcSendContext.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/TransportGrpcSendContext.cs deleted file mode 100644 index e85ff47c8b0..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/TransportGrpcSendContext.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System.Threading; - using Context; - - - public class TransportGrpcSendContext : - MessageSendContext, - GrpcSendContext - where T : class - { - public TransportGrpcSendContext(string exchange, T message, CancellationToken cancellationToken) - : base(message, cancellationToken) - { - Exchange = exchange; - RoutingKey = default; - } - - public string Exchange { get; } - public string RoutingKey { get; set; } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/TransportMessageExtensions.cs b/src/Transports/MassTransit.GrpcTransport/GrpcTransport/TransportMessageExtensions.cs deleted file mode 100644 index 4664d57d53c..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/GrpcTransport/TransportMessageExtensions.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace MassTransit.GrpcTransport -{ - using System; - using Contracts; - using Google.Protobuf.WellKnownTypes; - using Metadata; - - - public static class TransportMessageExtensions - { - public static Node Initialize(this Node node, IGrpcHostNode hostNode) - { - node.Address = hostNode.NodeAddress.ToString(); - node.Version = "1.0-alpha"; - node.SessionId = hostNode.SessionId.ToString("D"); - - node.Host.Add(nameof(HostMetadataCache.Host.Assembly), HostMetadataCache.Host.Assembly); - node.Host.Add(nameof(HostMetadataCache.Host.AssemblyVersion), HostMetadataCache.Host.AssemblyVersion); - node.Host.Add(nameof(HostMetadataCache.Host.FrameworkVersion), HostMetadataCache.Host.FrameworkVersion); - node.Host.Add(nameof(HostMetadataCache.Host.MachineName), HostMetadataCache.Host.MachineName); - node.Host.Add(nameof(HostMetadataCache.Host.ProcessId), HostMetadataCache.Host.ProcessId.ToString()); - node.Host.Add(nameof(HostMetadataCache.Host.ProcessName), HostMetadataCache.Host.ProcessName); - node.Host.Add(nameof(HostMetadataCache.Host.MassTransitVersion), HostMetadataCache.Host.MassTransitVersion); - node.Host.Add(nameof(HostMetadataCache.Host.OperatingSystemVersion), HostMetadataCache.Host.OperatingSystemVersion); - - node.Topology.AddRange(hostNode.GetTopology()); - - return node; - } - - public static DateTime? ToDateTime(this NullableTimestamp value) - { - return value is {TimestampCase: NullableTimestamp.TimestampOneofCase.Value} - ? value.Value.ToDateTime() - : default; - } - - public static NullableTimestamp ToFutureDateTime(this TimeSpan? value) - { - return value.HasValue - ? new NullableTimestamp {Value = Timestamp.FromDateTime(DateTime.UtcNow + value.Value)} - : default; - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/MassTransit.GrpcTransport.csproj b/src/Transports/MassTransit.GrpcTransport/MassTransit.GrpcTransport.csproj deleted file mode 100644 index 6d3cd626d9f..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/MassTransit.GrpcTransport.csproj +++ /dev/null @@ -1,44 +0,0 @@ - - - - - netstandard2.0;netstandard2.1;net6.0 - - - - $(TargetFrameworks);net462 - - - - MassTransit - - - - MassTransit.Grpc - MassTransit.Grpc - MassTransit;grpc - MassTransit gRPC transport support; $(Description) - - - - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - - - - - - - - - - diff --git a/src/Transports/MassTransit.GrpcTransport/MassTransit.GrpcTransport.csproj.DotSettings b/src/Transports/MassTransit.GrpcTransport/MassTransit.GrpcTransport.csproj.DotSettings deleted file mode 100644 index 75571538601..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/MassTransit.GrpcTransport.csproj.DotSettings +++ /dev/null @@ -1,4 +0,0 @@ - - True - True - True \ No newline at end of file diff --git a/src/Transports/MassTransit.GrpcTransport/Protos/transport.proto b/src/Transports/MassTransit.GrpcTransport/Protos/transport.proto deleted file mode 100644 index 857bc8daf20..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Protos/transport.proto +++ /dev/null @@ -1,158 +0,0 @@ -syntax = "proto3"; - -option csharp_namespace = "MassTransit.GrpcTransport.Contracts"; - -package MassTransit.GrpcTransport.Contracts; - -import "google/protobuf/timestamp.proto"; - -service TransportService { - rpc EventStream (stream TransportMessage) returns (stream TransportMessage) {} -} - -message TransportMessage { - string message_id = 1; - - oneof content { - Join join = 2; - Welcome welcome = 3; - Deliver deliver = 4; - Topology topology = 5; - } -} - -message Node { - string address = 1; - string version = 2; - string session_id = 3; - map host = 4; - repeated Topology topology = 5; -} - -message Join { - Node node = 1; -} - -message Part { - Node node = 1; -} - -message Welcome { - Node node = 1; - map peers = 2; -} - -message Envelope { - string message_id = 1; - - string request_id = 2; - string conversation_id = 3; - string correlation_id = 4; - string initiator_id = 5; - - string source_address = 6; - string destination_address = 7; - string response_address = 8; - string fault_address = 9; - - repeated string message_type = 10; - - string content_type = 11; - bytes body = 12; - - NullableTimestamp enqueue_time = 13; - NullableTimestamp expiration_time = 14; - google.protobuf.Timestamp sent_time = 15; - - map headers = 16; -} - -message Deliver { - int64 sequence_number = 1; - Envelope envelope = 2; - - oneof destination { - ExchangeDestination exchange = 3; - QueueDestination queue = 4; - ReceiverDestination receiver = 5; - } -} - -message ExchangeDestination { - string name = 1; - string routing_key = 2; -} - -message QueueDestination { - string name = 1; -} - -message ReceiverDestination { - string queue_name = 1; - int64 receiver_id = 2; -} - -message ConfirmDelivery { - repeated int64 sequence_numbers = 1; -} - -message Topology { - int64 sequence_number = 1; - bool valid = 2; - - oneof change { - Exchange exchange = 3; - ExchangeBind exchangeBind = 4; - Queue queue = 5; - QueueBind queueBind = 6; - Receiver receiver = 7; - } -} - -enum ExchangeType { - FanOut = 0; - Direct = 1; - Topic = 2; -} - -message Exchange { - string name = 1; - ExchangeType type = 2; -} - -message ExchangeBind { - string source = 1; - string destination = 2; - string routing_key = 3; -} - -message Queue { - string name = 1; -} - -message QueueBind { - string source = 1; - string destination = 2; -} - -message Receiver { - string queue_name = 1; - int64 receiver_id = 2; -} - -message Gossip { - repeated Peer peers = 1; - - message Peer { - string address = 1; - int32 heartbeat = 2; - repeated int32 suspect = 3; - } -} - -message NullableTimestamp { - oneof timestamp { - google.protobuf.Timestamp value = 1; - } -} - diff --git a/src/Transports/MassTransit.GrpcTransport/Serialization/GrpcMessageDeserializer.cs b/src/Transports/MassTransit.GrpcTransport/Serialization/GrpcMessageDeserializer.cs deleted file mode 100644 index 2ce18d482a9..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Serialization/GrpcMessageDeserializer.cs +++ /dev/null @@ -1,101 +0,0 @@ -#nullable enable -namespace MassTransit.Serialization -{ - using System; - using System.Net.Mime; - using System.Runtime.Serialization; - using GrpcTransport; - using Newtonsoft.Json; - using Newtonsoft.Json.Bson; - using Newtonsoft.Json.Linq; - - - public class GrpcMessageDeserializer : - IMessageDeserializer - { - readonly JsonSerializer _deserializer; - readonly IObjectDeserializer _objectDeserializer; - - public GrpcMessageDeserializer(JsonSerializer deserializer) - { - _deserializer = deserializer; - _objectDeserializer = new NewtonsoftObjectDeserializer(deserializer); - } - - public void Probe(ProbeContext context) - { - var scope = context.CreateScope("json"); - scope.Add("contentType", GrpcMessageSerializer.GrpcContentType.MediaType); - } - - public ContentType ContentType => GrpcMessageSerializer.GrpcContentType; - - public ConsumeContext Deserialize(ReceiveContext receiveContext) - { - var context = receiveContext as GrpcReceiveContext ?? throw new ArgumentException("Must be GrpcReceiveContext", nameof(receiveContext)); - - try - { - using var stream = receiveContext.Body.GetStream(); - using var jsonReader = new BsonDataReader(stream); - - var messageToken = _deserializer.Deserialize(jsonReader); - - var serializerContext = new NewtonsoftRawBsonSerializerContext(_deserializer, _objectDeserializer, context.Message, messageToken, - context.Message.MessageType, ContentType); - - var consumeContext = new BodyConsumeContext(receiveContext, serializerContext); - - consumeContext.AddOrUpdatePayload(() => context.Message, _ => context.Message); - - return consumeContext; - } - catch (JsonSerializationException ex) - { - throw new SerializationException("A JSON serialization exception occurred while deserializing the message", ex); - } - catch (SerializationException) - { - throw; - } - catch (Exception ex) - { - throw new SerializationException("An exception occurred while deserializing the message", ex); - } - } - - public SerializerContext Deserialize(MessageBody body, Headers headers, Uri? destinationAddress = null) - { - try - { - using var stream = body.GetStream(); - using var jsonReader = new BsonDataReader(stream); - - var messageToken = _deserializer.Deserialize(jsonReader); - - var messageContext = new RawMessageContext(headers, destinationAddress, RawSerializerOptions.Default); - - // TODO this is likely broken but fix it properly vs the original hack - return new NewtonsoftRawBsonSerializerContext(_deserializer, _objectDeserializer, messageContext, messageToken, headers.GetMessageTypes(), - ContentType); - } - catch (JsonSerializationException ex) - { - throw new SerializationException("A JSON serialization exception occurred while deserializing the message", ex); - } - catch (SerializationException) - { - throw; - } - catch (Exception ex) - { - throw new SerializationException("An exception occurred while deserializing the message", ex); - } - } - - public MessageBody GetMessageBody(string text) - { - return new Base64MessageBody(text); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Serialization/GrpcMessageSerializer.cs b/src/Transports/MassTransit.GrpcTransport/Serialization/GrpcMessageSerializer.cs deleted file mode 100644 index 3c0abb0f4ec..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Serialization/GrpcMessageSerializer.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace MassTransit.Serialization -{ - using System; - using System.Net.Mime; - using Newtonsoft.Json; - - - public class GrpcMessageSerializer : - IMessageSerializer - { - public const string ContentTypeHeaderValue = "application/bson"; - public static readonly ContentType GrpcContentType = new ContentType(ContentTypeHeaderValue); - - static readonly Lazy _deserializer; - static readonly Lazy _serializer; - - static GrpcMessageSerializer() - { - _deserializer = new Lazy(() => JsonSerializer.Create(BsonMessageSerializer.DeserializerSettings)); - _serializer = new Lazy(() => JsonSerializer.Create(BsonMessageSerializer.SerializerSettings)); - } - - public static JsonSerializer Deserializer => _deserializer.Value; - public static JsonSerializer Serializer => _serializer.Value; - - public MessageBody GetMessageBody(SendContext context) - where T : class - { - return new NewtonsoftRawBsonMessageBody(context); - } - - public ContentType ContentType => GrpcContentType; - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Serialization/GrpcSerializerFactory.cs b/src/Transports/MassTransit.GrpcTransport/Serialization/GrpcSerializerFactory.cs deleted file mode 100644 index fcaf807f387..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Serialization/GrpcSerializerFactory.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace MassTransit.Serialization -{ - using System.Net.Mime; - - - public class GrpcSerializerFactory : - ISerializerFactory - { - public ContentType ContentType => GrpcMessageSerializer.GrpcContentType; - - public IMessageSerializer CreateSerializer() - { - return new GrpcMessageSerializer(); - } - - public IMessageDeserializer CreateDeserializer() - { - return new GrpcMessageDeserializer(GrpcMessageSerializer.Deserializer); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Testing/GrpcTestHarness.cs b/src/Transports/MassTransit.GrpcTransport/Testing/GrpcTestHarness.cs deleted file mode 100644 index 06db1764eb0..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Testing/GrpcTestHarness.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace MassTransit.Testing -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Configuration; - using GrpcTransport.Configuration; - - - public class GrpcTestHarness : - BusTestHarness - { - readonly GrpcBusConfiguration _busConfiguration; - readonly string _inputQueueName; - readonly IEnumerable _specifications; - - public GrpcTestHarness(Uri baseAddress = default, string inputQueueName = default) - : this(Enumerable.Empty(), baseAddress, inputQueueName) - { - } - - public GrpcTestHarness(IEnumerable specifications, Uri baseAddress = default, string inputQueueName = default) - { - BaseAddress = baseAddress ?? new Uri("http://127.0.0.1:19796/"); - _inputQueueName = inputQueueName ?? "input-queue"; - - _busConfiguration = new GrpcBusConfiguration(new GrpcTopologyConfiguration(GrpcBus.MessageTopology), BaseAddress); - _specifications = specifications; - - InputQueueAddress = new Uri(BaseAddress, _inputQueueName); - } - - public Uri BaseAddress { get; } - - public override Uri InputQueueAddress { get; } - public override string InputQueueName => _inputQueueName; - - internal IGrpcHostConfiguration HostConfiguration => _busConfiguration?.HostConfiguration; - - public event Action OnConfigureGrpcBus; - public event Action OnConfigureGrpcHost; - public event Action OnConfigureGrpcReceiveEndpoint; - - protected virtual void ConfigureGrpcBus(IGrpcBusFactoryConfigurator configurator) - { - OnConfigureGrpcBus?.Invoke(configurator); - } - - protected virtual void ConfigureGrpcHost(IGrpcHostConfigurator configurator) - { - OnConfigureGrpcHost?.Invoke(configurator); - } - - protected virtual void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - OnConfigureGrpcReceiveEndpoint?.Invoke(configurator); - } - - public virtual Task> ConnectRequestClient() - where TRequest : class - { - return ConnectRequestClient(InputQueueAddress); - } - - public virtual Task> ConnectRequestClient(Uri destinationAddress) - where TRequest : class - { - return Task.FromResult(Bus.CreateRequestClient(destinationAddress, TestTimeout)); - } - - protected override IBusControl CreateBus() - { - var configurator = new GrpcBusFactoryConfigurator(_busConfiguration); - - configurator.Host(BaseAddress, x => - { - ConfigureGrpcHost(x); - }); - - ConfigureBus(configurator); - - ConfigureGrpcBus(configurator); - - configurator.ReceiveEndpoint(InputQueueName, e => - { - ConfigureReceiveEndpoint(e); - - ConfigureGrpcReceiveEndpoint(e); - }); - return configurator.Build(_busConfiguration, _specifications ?? Enumerable.Empty()); - } - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcBusTopology.cs b/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcBusTopology.cs deleted file mode 100644 index 399c44c6502..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcBusTopology.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MassTransit -{ - public interface IGrpcBusTopology : - IBusTopology - { - new IGrpcMessagePublishTopology Publish() - where T : class; - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcConsumeTopology.cs b/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcConsumeTopology.cs deleted file mode 100644 index 4ee1c1e384e..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcConsumeTopology.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace MassTransit -{ - using Configuration; - - - public interface IGrpcConsumeTopology : - IConsumeTopology - { - new IGrpcMessageConsumeTopology GetMessageTopology() - where T : class; - - /// - /// Apply the entire topology to the builder - /// - /// - void Apply(IMessageFabricConsumeTopologyBuilder builder); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcMessageConsumeTopology.cs b/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcMessageConsumeTopology.cs deleted file mode 100644 index 57d9ed04744..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcMessageConsumeTopology.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MassTransit -{ - public interface IGrpcMessageConsumeTopology : - IMessageConsumeTopology - where TMessage : class - { - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcMessagePublishTopology.cs b/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcMessagePublishTopology.cs deleted file mode 100644 index 6b5fa2b90b6..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcMessagePublishTopology.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace MassTransit -{ - using Configuration; - using Transports.Fabric; - - - public interface IGrpcMessagePublishTopology : - IMessagePublishTopology, - IGrpcMessagePublishTopology - where TMessage : class - { - ExchangeType ExchangeType { get; } - } - - - public interface IGrpcMessagePublishTopology - { - /// - /// Apply the message topology to the builder, including any implemented types - /// - /// The topology builder - void Apply(IMessageFabricPublishTopologyBuilder builder); - } -} diff --git a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcPublishTopology.cs b/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcPublishTopology.cs deleted file mode 100644 index 165fca7c3b6..00000000000 --- a/src/Transports/MassTransit.GrpcTransport/Topology/IGrpcPublishTopology.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace MassTransit -{ - public interface IGrpcPublishTopology : - IPublishTopology - { - new IGrpcMessagePublishTopology GetMessageTopology() - where T : class; - } -} diff --git a/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaFactoryConfigurator.cs b/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaFactoryConfigurator.cs index 53275195d69..f23e9c3f682 100644 --- a/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaFactoryConfigurator.cs +++ b/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaFactoryConfigurator.cs @@ -292,5 +292,11 @@ internal void TopicProducer(string topicName, ProducerConfig produ /// /// void SetHeadersSerializer(IHeadersSerializer serializer); + + /// + /// Set default serialization factory, this will be used for all producers and consumers + /// + /// + void SetSerializationFactory(IKafkaSerializerFactory factory); } } diff --git a/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaHostConfiguration.cs b/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaHostConfiguration.cs index 5365a2299cc..ee638b505d5 100644 --- a/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaHostConfiguration.cs +++ b/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaHostConfiguration.cs @@ -18,6 +18,13 @@ public interface IKafkaHostConfiguration : KafkaSendTransportContext CreateSendTransportContext(IBusInstance busInstance, string topic) where TValue : class; + IKafkaProducerSpecification CreateSpecification(string topicName, Action> configure) + where TValue : class; + + IKafkaProducerSpecification CreateSpecification(string topicName, ProducerConfig producerConfig, + Action> configure) + where TValue : class; + IKafkaConsumerSpecification CreateSpecification(string topicName, string groupId, Action> configure) where TValue : class; diff --git a/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaTopicReceiveEndpointConfigurator.cs b/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaTopicReceiveEndpointConfigurator.cs index 4af99558dbf..3c0b38f2b52 100644 --- a/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaTopicReceiveEndpointConfigurator.cs +++ b/src/Transports/MassTransit.KafkaIntegration/Configuration/IKafkaTopicReceiveEndpointConfigurator.cs @@ -27,6 +27,12 @@ public interface IKafkaTopicReceiveEndpointConfigurator : /// string GroupInstanceId { set; } + /// + /// Specifies starting consume offset for the topic. . This is can be useful when stream needs to be rewound + /// default: () + /// + long Offset { set; } + /// /// Sets interval before checkpoint, low interval will decrease throughput (default: 1min) /// diff --git a/src/Transports/MassTransit.KafkaIntegration/Configuration/KafkaIntegrationExtensions.cs b/src/Transports/MassTransit.KafkaIntegration/Configuration/KafkaIntegrationExtensions.cs index e175eb1df2e..e7abac7b127 100644 --- a/src/Transports/MassTransit.KafkaIntegration/Configuration/KafkaIntegrationExtensions.cs +++ b/src/Transports/MassTransit.KafkaIntegration/Configuration/KafkaIntegrationExtensions.cs @@ -2,6 +2,8 @@ namespace MassTransit { using System; using Confluent.Kafka; + using DependencyInjection; + using KafkaIntegration; using KafkaIntegration.Configuration; @@ -13,7 +15,9 @@ public static void UsingKafka(this IRiderRegistrationConfigurator configurator, throw new ArgumentNullException(nameof(configurator)); var factory = new KafkaRegistrationRiderFactory(configure); + configurator.SetRiderFactory(factory); + configurator.TryAddScoped((rider, provider) => rider.GetScopedTopicProducerProvider(provider)); } public static void UsingKafka(this IRiderRegistrationConfigurator configurator, ClientConfig clientConfig, @@ -26,6 +30,37 @@ public static void UsingKafka(this IRiderRegistrationConfigurator configurator, var factory = new KafkaRegistrationRiderFactory(clientConfig, configure); configurator.SetRiderFactory(factory); + configurator.TryAddScoped((rider, provider) => rider.GetScopedTopicProducerProvider(provider)); + } + + public static void UsingKafka(this IRiderRegistrationConfigurator configurator, + Action configure) + where TBus : class, IBus + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + + var factory = new KafkaRegistrationRiderFactory(configure); + + configurator.SetRiderFactory(factory); + configurator.TryAddScoped>((rider, provider) => + Bind.Create(rider.GetScopedTopicProducerProvider(provider))); + } + + public static void UsingKafka(this IRiderRegistrationConfigurator configurator, ClientConfig clientConfig, + Action configure) + where TBus : class, IBus + { + if (configurator == null) + throw new ArgumentNullException(nameof(configurator)); + if (clientConfig == null) + throw new ArgumentNullException(nameof(clientConfig)); + + var factory = new KafkaRegistrationRiderFactory(clientConfig, configure); + + configurator.SetRiderFactory(factory); + configurator.TryAddScoped>((rider, provider) => + Bind.Create(rider.GetScopedTopicProducerProvider(provider))); } } } diff --git a/src/Transports/MassTransit.KafkaIntegration/Configuration/KafkaProducerRegistrationExtensions.cs b/src/Transports/MassTransit.KafkaIntegration/Configuration/KafkaProducerRegistrationExtensions.cs index a4efc77b459..ef6827295f8 100644 --- a/src/Transports/MassTransit.KafkaIntegration/Configuration/KafkaProducerRegistrationExtensions.cs +++ b/src/Transports/MassTransit.KafkaIntegration/Configuration/KafkaProducerRegistrationExtensions.cs @@ -2,7 +2,6 @@ namespace MassTransit { using System; using Confluent.Kafka; - using DependencyInjection; using KafkaIntegration; using KafkaIntegration.Configuration; using Microsoft.Extensions.DependencyInjection; @@ -130,7 +129,7 @@ public static void AddProducer(this IRiderRegistrationConfigurator conf new KeyedTopicProducer(provider.GetRequiredService>(), keyResolver)); } - static ITopicProducer GetProducer(string topicName, IKafkaRider rider, IServiceProvider provider) + static ITopicProducer GetProducer(string topicName, ITopicProducerProvider rider, IServiceProvider provider) where T : class { var address = new Uri($"topic:{topicName}"); @@ -138,25 +137,13 @@ static ITopicProducer GetProducer(string topicName, IKafkaRide return GetProducer(address, rider, provider); } - static ITopicProducer GetProducer(Uri address, IKafkaRider rider, IServiceProvider provider) + static ITopicProducer GetProducer(Uri address, ITopicProducerProvider rider, IServiceProvider provider) where T : class { - ITopicProducer GetProducerFromRider() - { - var contextProvider = provider.GetService(); - if (contextProvider != null) - { - return contextProvider.HasContext - ? rider.GetProducer(address, contextProvider.GetContext()) - : rider.GetProducer(address); - } + var producerProvider = rider.GetScopedTopicProducerProvider(provider); + ITopicProducer producer = producerProvider.GetProducer(address); - return rider.GetProducer(address, provider.GetService()); - } - - ITopicProducer result = GetProducerFromRider(); - - return new ScopedTopicProducer(result, provider); + return new ScopedTopicProducer(producer, provider); } } } diff --git a/src/Transports/MassTransit.KafkaIntegration/Exceptions/KafkaConnectionException.cs b/src/Transports/MassTransit.KafkaIntegration/Exceptions/KafkaConnectionException.cs index ac728378f2a..19694a47a20 100644 --- a/src/Transports/MassTransit.KafkaIntegration/Exceptions/KafkaConnectionException.cs +++ b/src/Transports/MassTransit.KafkaIntegration/Exceptions/KafkaConnectionException.cs @@ -23,6 +23,9 @@ public KafkaConnectionException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected KafkaConnectionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.KafkaIntegration/IKafkaRider.cs b/src/Transports/MassTransit.KafkaIntegration/IKafkaRider.cs index b1c2ce81ab8..1aa64a0c13f 100644 --- a/src/Transports/MassTransit.KafkaIntegration/IKafkaRider.cs +++ b/src/Transports/MassTransit.KafkaIntegration/IKafkaRider.cs @@ -1,14 +1,13 @@ namespace MassTransit { - using System; + using KafkaIntegration; using Transports; public interface IKafkaRider : IRiderControl, + ITopicProducerProvider, IKafkaTopicEndpointConnector { - ITopicProducer GetProducer(Uri address, ConsumeContext consumeContext = default) - where TValue : class; } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ITopicProducerProvider.cs b/src/Transports/MassTransit.KafkaIntegration/ITopicProducerProvider.cs similarity index 85% rename from src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ITopicProducerProvider.cs rename to src/Transports/MassTransit.KafkaIntegration/ITopicProducerProvider.cs index 6b41a015cb6..fc7db5cdb22 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ITopicProducerProvider.cs +++ b/src/Transports/MassTransit.KafkaIntegration/ITopicProducerProvider.cs @@ -1,4 +1,4 @@ -namespace MassTransit.KafkaIntegration +namespace MassTransit { using System; diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaConsumeContext.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaConsumeContext.cs index 441ddaf0b82..221a03a1ecc 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaConsumeContext.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaConsumeContext.cs @@ -9,6 +9,7 @@ public interface KafkaConsumeContext string Topic { get; } int Partition { get; } long Offset { get; } + bool IsPartitionEof { get; } DateTime CheckpointUtcDateTime { get; } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaConsumeContextExtensions.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaConsumeContextExtensions.cs index 691201ff000..d4527d6157e 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaConsumeContextExtensions.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaConsumeContextExtensions.cs @@ -16,5 +16,10 @@ public static TKey GetKey(this ConsumeContext context) { return context.TryGetPayload(out KafkaConsumeContext consumeContext) ? consumeContext.Key : default; } + + public static bool IsPartitionEof(this ConsumeContext context) + { + return context.TryGetPayload(out KafkaConsumeContext consumeContext) ? consumeContext.IsPartitionEof : default; + } } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Checkpoints/BatchCheckpointer.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Checkpoints/BatchCheckpointer.cs index cd225a2816c..b771e9b07d7 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Checkpoints/BatchCheckpointer.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Checkpoints/BatchCheckpointer.cs @@ -42,7 +42,7 @@ public async Task Pending(IPendingConfirmation confirmation) public async ValueTask DisposeAsync() { - _channel.Writer.Complete(); + _channel.Writer.TryComplete(); await _checkpointTask.ConfigureAwait(false); } @@ -76,16 +76,17 @@ async Task ReadBatch() { try { - for (var i = 0; i < _settings.CheckpointMessageCount; i++) + while (batch.Count < _settings.CheckpointMessageCount) { - var confirmation = await _channel.Reader.ReadAsync(batchToken.Token).ConfigureAwait(false); - - await confirmation.Confirmed.OrCanceled(_cancellationToken).ConfigureAwait(false); - - batch.Add(confirmation); - - if (await _channel.Reader.WaitToReadAsync(batchToken.Token).ConfigureAwait(false) == false) + if (_channel.Reader.TryRead(out var confirmation)) + { + await confirmation.Confirmed.OrCanceled(_cancellationToken).ConfigureAwait(false); + batch.Add(confirmation); + } + else if (await _channel.Reader.WaitToReadAsync(batchToken.Token).ConfigureAwait(false) == false) + { break; + } } } catch (Exception) when (batch.Count > 0) diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Checkpoints/PendingConfirmationCollection.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Checkpoints/PendingConfirmationCollection.cs index 7e9b730e28d..9c1f9c260e9 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Checkpoints/PendingConfirmationCollection.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Checkpoints/PendingConfirmationCollection.cs @@ -9,17 +9,17 @@ namespace MassTransit.KafkaIntegration.Checkpoints public class PendingConfirmationCollection : IDisposable { - readonly ConcurrentDictionary _confirmations; - readonly TopicPartition _partition; + readonly CancellationToken _cancellationToken; + readonly ConcurrentDictionary _confirmations; readonly CancellationTokenRegistration? _registration; - public PendingConfirmationCollection(TopicPartition partition, CancellationToken cancellationToken) + public PendingConfirmationCollection(CancellationToken cancellationToken) { - _partition = partition; - _confirmations = new ConcurrentDictionary(); + _cancellationToken = cancellationToken; + _confirmations = new ConcurrentDictionary(); if (cancellationToken.CanBeCanceled) - _registration = cancellationToken.Register(() => Cancel(cancellationToken)); + _registration = cancellationToken.Register(() => Cancel()); } public void Dispose() @@ -27,36 +27,41 @@ public void Dispose() _registration?.Dispose(); } - public IPendingConfirmation Add(Offset offset) + public IPendingConfirmation Add(TopicPartitionOffset offset) { - var pendingConfirmation = new PendingConfirmation(_partition, offset); + _cancellationToken.ThrowIfCancellationRequested(); + + var pendingConfirmation = new PendingConfirmation(offset.TopicPartition, offset.Offset); return _confirmations.AddOrUpdate(offset, key => pendingConfirmation, (key, existing) => { - existing.Faulted($"Duplicate key: {key}, partition: {_partition}"); + existing.Faulted($"Duplicate key: {key}"); return pendingConfirmation; }); } - public void Complete(Offset offset) + public void Complete(TopicPartitionOffset offset) { if (_confirmations.TryRemove(offset, out var confirmation)) confirmation.Complete(); } - public void Faulted(Offset offset, Exception exception) + public void Canceled(TopicPartitionOffset offset, CancellationToken cancellationToken) + { + if (_confirmations.TryRemove(offset, out var confirmation)) + confirmation.Canceled(cancellationToken); + } + + public void Faulted(TopicPartitionOffset offset, Exception exception) { if (_confirmations.TryRemove(offset, out var confirmation)) confirmation.Faulted(exception); } - void Cancel(CancellationToken cancellationToken) + void Cancel() { foreach (var offset in _confirmations.Keys) - { - if (_confirmations.TryRemove(offset, out var confirmation)) - confirmation.Canceled(cancellationToken); - } + Canceled(offset, _cancellationToken); } } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaConsumerSpecification.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaConsumerSpecification.cs index 6fd51fb132a..b70a205b9e3 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaConsumerSpecification.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaConsumerSpecification.cs @@ -18,11 +18,13 @@ public class KafkaConsumerSpecification : readonly ReceiveEndpointObservable _endpointObservers; readonly IHeadersDeserializer _headersDeserializer; readonly IKafkaHostConfiguration _hostConfiguration; + readonly IKafkaSerializerFactory _kafkaSerializerFactory; readonly Action _oAuthBearerTokenRefreshHandler; readonly string _topicName; public KafkaConsumerSpecification(IKafkaHostConfiguration hostConfiguration, ConsumerConfig consumerConfig, string topicName, - IHeadersDeserializer headersDeserializer, Action> configure, + IHeadersDeserializer headersDeserializer, IKafkaSerializerFactory kafkaSerializerFactory, + Action> configure, Action oAuthBearerTokenRefreshHandler) { _hostConfiguration = hostConfiguration; @@ -30,6 +32,7 @@ public KafkaConsumerSpecification(IKafkaHostConfiguration hostConfiguration, Con _topicName = topicName; _endpointObservers = new ReceiveEndpointObservable(); _headersDeserializer = headersDeserializer; + _kafkaSerializerFactory = kafkaSerializerFactory; _configure = configure; _oAuthBearerTokenRefreshHandler = oAuthBearerTokenRefreshHandler; EndpointName = $"{KafkaTopicAddress.PathPrefix}/{_topicName}"; @@ -47,6 +50,8 @@ public ReceiveEndpoint CreateReceiveEndpoint(IBusInstance busInstance) endpointConfiguration, _oAuthBearerTokenRefreshHandler); configurator.ConnectReceiveEndpointObserver(_endpointObservers); configurator.SetHeadersDeserializer(_headersDeserializer); + configurator.SetKeyDeserializer(_kafkaSerializerFactory.GetDeserializer()); + configurator.SetValueDeserializer(_kafkaSerializerFactory.GetDeserializer()); _configure?.Invoke(configurator); IReadOnlyList result = Validate().Concat(configurator.Validate()) diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaFactoryConfigurator.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaFactoryConfigurator.cs index 9d093577254..24654fd11ae 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaFactoryConfigurator.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaFactoryConfigurator.cs @@ -17,15 +17,16 @@ public class KafkaFactoryConfigurator : { readonly ClientConfig _clientConfig; readonly Recycle _clientSupervisor; + readonly List> _configureSend; readonly ReceiveEndpointObservable _endpointObservers; readonly SingleThreadedDictionary _producers; readonly SendObservable _sendObservers; readonly SingleThreadedDictionary _topics; - Action _configureSend; IHeadersDeserializer _headersDeserializer; IHeadersSerializer _headersSerializer; bool _isHostConfigured; Action _oAuthBearerTokenRefreshHandler; + IKafkaSerializerFactory _serializerFactory; public KafkaFactoryConfigurator(ClientConfig clientConfig) { @@ -34,6 +35,8 @@ public KafkaFactoryConfigurator(ClientConfig clientConfig) _producers = new SingleThreadedDictionary(); _endpointObservers = new ReceiveEndpointObservable(); _sendObservers = new SendObservable(); + _serializerFactory = new DefaultKafkaSerializerFactory(); + _configureSend = new List>(); SetHeadersDeserializer(DictionaryHeadersSerialize.Deserializer); SetHeadersSerializer(DictionaryHeadersSerialize.Serializer); @@ -106,17 +109,7 @@ void IKafkaFactoryConfigurator.TopicProducer(string topicName, Pro if (producerConfig == null) throw new ArgumentNullException(nameof(producerConfig)); - var added = _producers.TryAdd(topicName, topic => - { - var configurator = new KafkaProducerSpecification(this, producerConfig, topicName, _oAuthBearerTokenRefreshHandler); - configurator.SetHeadersSerializer(_headersSerializer); - configure?.Invoke(configurator); - - configurator.ConnectSendObserver(_sendObservers); - if (_configureSend != null) - configurator.ConfigureSend(_configureSend); - return configurator; - }); + var added = _producers.TryAdd(topicName, topic => CreateSpecification(topic, producerConfig, configure)); if (!added) throw new ConfigurationException($"A topic producer with the same key was already added: {topicName}"); @@ -132,6 +125,11 @@ public void SetHeadersSerializer(IHeadersSerializer serializer) _headersSerializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); } + public void SetSerializationFactory(IKafkaSerializerFactory factory) + { + _serializerFactory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + public Acks? Acks { set => _clientConfig.Acks = value; @@ -264,14 +262,14 @@ public ConnectHandle ConnectSendObserver(ISendObserver observer) public void ConfigureSend(Action callback) { - _configureSend = callback ?? throw new ArgumentNullException(nameof(callback)); + _configureSend.Add(callback ?? throw new ArgumentNullException(nameof(callback))); } public KafkaSendTransportContext CreateSendTransportContext(IBusInstance busInstance, string topic) where TValue : class { if (!_producers.TryGetValue(topic, out var spec)) - throw new ConfigurationException($"Producer for topic: {topic} is not configured."); + spec = CreateSpecification(topic); if (spec is IKafkaProducerSpecification specification) return specification.CreateSendTransportContext(busInstance); @@ -279,6 +277,27 @@ public KafkaSendTransportContext CreateSendTransportContext).Name} message"); } + public IKafkaProducerSpecification CreateSpecification(string topicName, + Action> configure = null) + where TValue : class + { + return CreateSpecification(topicName, new ProducerConfig(), configure); + } + + public IKafkaProducerSpecification CreateSpecification(string topicName, ProducerConfig producerConfig, + Action> configure = null) + where TValue : class + { + var configurator = new KafkaProducerSpecification(this, producerConfig, topicName, _oAuthBearerTokenRefreshHandler, _configureSend); + configurator.SetHeadersSerializer(_headersSerializer); + configurator.SetKeySerializer(_serializerFactory.GetSerializer()); + configurator.SetValueSerializer(_serializerFactory.GetSerializer()); + configure?.Invoke(configurator); + + configurator.ConnectSendObserver(_sendObservers); + return configurator; + } + public IKafkaConsumerSpecification CreateSpecification(string topicName, string groupId, Action> configure) where TValue : class @@ -300,7 +319,8 @@ public IKafkaConsumerSpecification CreateSpecification(string topi consumerConfig.EnableAutoCommit = false; var specification = - new KafkaConsumerSpecification(this, consumerConfig, topicName, _headersDeserializer, configure, _oAuthBearerTokenRefreshHandler); + new KafkaConsumerSpecification(this, consumerConfig, topicName, _headersDeserializer, _serializerFactory, configure, + _oAuthBearerTokenRefreshHandler); specification.ConnectReceiveEndpointObserver(_endpointObservers); return specification; } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaProducerSpecification.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaProducerSpecification.cs index 39d3572151b..7b15a8553d2 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaProducerSpecification.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaProducerSpecification.cs @@ -14,29 +14,26 @@ public class KafkaProducerSpecification : IKafkaProducerConfigurator where TValue : class { + readonly List> _configureSend; readonly IKafkaHostConfiguration _hostConfiguration; readonly Action _oAuthBearerTokenRefreshHandler; readonly ProducerConfig _producerConfig; readonly SendObservable _sendObservers; readonly SerializationConfiguration _serialization; - Action _configureSend; IHeadersSerializer _headersSerializer; IAsyncSerializer _keySerializer; Action _statisticsHandler; IAsyncSerializer _valueSerializer; public KafkaProducerSpecification(IKafkaHostConfiguration hostConfiguration, ProducerConfig producerConfig, string topicName, - Action oAuthBearerTokenRefreshHandler) + Action oAuthBearerTokenRefreshHandler, List> configureSend) { _hostConfiguration = hostConfiguration; _producerConfig = producerConfig; TopicName = topicName; _oAuthBearerTokenRefreshHandler = oAuthBearerTokenRefreshHandler; + _configureSend = configureSend; _sendObservers = new SendObservable(); - - SetKeySerializer(SerializerTypes.TryGet() ?? new MassTransitAsyncJsonSerializer()); - SetValueSerializer(new MassTransitAsyncJsonSerializer()); - _serialization = new SerializationConfiguration(); } @@ -62,7 +59,7 @@ public int? QueueBufferingBackpressureThreshold public TimeSpan? RetryBackoff { - set => _producerConfig.RetryBackoffMs = value == null ? null : (int?)value.Value.TotalMilliseconds; + set => _producerConfig.RetryBackoffMs = (int?)value?.TotalMilliseconds; } public int? MessageSendMaxRetries @@ -72,7 +69,7 @@ public int? MessageSendMaxRetries public TimeSpan? Linger { - set => _producerConfig.LingerMs = value == null ? null : (int?)value.Value.TotalMilliseconds; + set => _producerConfig.LingerMs = (int?)value?.TotalMilliseconds; } public int? QueueBufferingMaxKbytes @@ -97,7 +94,7 @@ public bool? EnableIdempotence public TimeSpan? TransactionTimeout { - set => _producerConfig.TransactionTimeoutMs = value == null ? null : (int?)value.Value.TotalMilliseconds; + set => _producerConfig.TransactionTimeoutMs = (int?)value?.TotalMilliseconds; } public string TransactionalId @@ -112,12 +109,12 @@ public Partitioner? Partitioner public TimeSpan? MessageTimeout { - set => _producerConfig.MessageTimeoutMs = value == null ? null : (int?)value.Value.TotalMilliseconds; + set => _producerConfig.MessageTimeoutMs = (int?)value?.TotalMilliseconds; } public TimeSpan? RequestTimeout { - set => _producerConfig.RequestTimeoutMs = value == null ? null : (int?)value.Value.TotalMilliseconds; + set => _producerConfig.RequestTimeoutMs = (int?)value?.TotalMilliseconds; } public string DeliveryReportFields @@ -159,10 +156,10 @@ public void SetHeadersSerializer(IHeadersSerializer serializer) public KafkaSendTransportContext CreateSendTransportContext(IBusInstance busInstance, Action onStop = null) { - var producerConfig = _hostConfiguration.GetProducerConfig(_producerConfig); - ProducerBuilder CreateProducerBuilder() { + var producerConfig = _hostConfiguration.GetProducerConfig(_producerConfig); + ProducerBuilder producerBuilder = new ProducerBuilder(producerConfig) .SetKeySerializer(Serializers.ByteArray) .SetValueSerializer(Serializers.ByteArray); @@ -189,16 +186,20 @@ public IEnumerable Validate() { if (string.IsNullOrEmpty(TopicName)) yield return this.Failure("Topic", "should not be empty"); + + if (_headersSerializer == null) + yield return this.Failure("HeadersSerializer", "should not be null"); + + if (_keySerializer == null) + yield return this.Failure("KeySerializer", "should not be null"); + + if (_valueSerializer == null) + yield return this.Failure("ValueSerializer", "should not be null"); } public ConnectHandle ConnectSendObserver(ISendObserver observer) { return _sendObservers.Connect(observer); } - - public void ConfigureSend(Action callback) - { - _configureSend = callback ?? throw new ArgumentNullException(nameof(callback)); - } } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaTopicReceiveEndpointConfiguration.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaTopicReceiveEndpointConfiguration.cs index 56e39516334..e43b00b77bb 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaTopicReceiveEndpointConfiguration.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Configuration/KafkaTopicReceiveEndpointConfiguration.cs @@ -4,6 +4,7 @@ namespace MassTransit.KafkaIntegration.Configuration using System.Collections.Generic; using Confluent.Kafka; using MassTransit.Configuration; + using MassTransit.Middleware; using Middleware; using Serializers; using Transports; @@ -40,9 +41,6 @@ public KafkaTopicReceiveEndpointConfiguration(IKafkaHostConfiguration hostConfig _options = new OptionsSet(); Topic = topic; - SetKeyDeserializer(DeserializerTypes.TryGet() ?? new MassTransitJsonDeserializer()); - SetValueDeserializer(new MassTransitJsonDeserializer()); - CheckpointInterval = TimeSpan.FromMinutes(1); CheckpointMessageCount = 5000; MessageLimit = 10000; @@ -53,7 +51,13 @@ public KafkaTopicReceiveEndpointConfiguration(IKafkaHostConfiguration hostConfig _consumerConfigurator = new PipeConfigurator(); + // https://github.com/confluentinc/confluent-kafka-dotnet/blob/0e6bc8be05988f0cafacfe2b71aa8950aabe8cb5/src/Confluent.Kafka/ConsumerBuilder.cs#L387 + Offset = Confluent.Kafka.Offset.Unset; + PublishFaults = false; + + this.DiscardFaultedMessages(); + this.DiscardSkippedMessages(); } public override Uri HostAddress => _endpointConfiguration.HostAddress; @@ -75,12 +79,12 @@ public PartitionAssignmentStrategy? PartitionAssignmentStrategy public TimeSpan? SessionTimeout { - set => _consumerConfig.SessionTimeoutMs = value == null ? (int?)null : Convert.ToInt32(value.Value.TotalMilliseconds); + set => _consumerConfig.SessionTimeoutMs = (int?)value?.TotalMilliseconds; } public TimeSpan? HeartbeatInterval { - set => _consumerConfig.HeartbeatIntervalMs = value == null ? (int?)null : Convert.ToInt32(value.Value.TotalMilliseconds); + set => _consumerConfig.HeartbeatIntervalMs = (int?)value?.TotalMilliseconds; } public string GroupProtocolType @@ -90,12 +94,12 @@ public string GroupProtocolType public TimeSpan? CoordinatorQueryInterval { - set => _consumerConfig.CoordinatorQueryIntervalMs = value == null ? (int?)null : Convert.ToInt32(value.Value.TotalMilliseconds); + set => _consumerConfig.CoordinatorQueryIntervalMs = (int?)value?.TotalMilliseconds; } public TimeSpan? MaxPollInterval { - set => _consumerConfig.MaxPollIntervalMs = value == null ? (int?)null : Convert.ToInt32(value.Value.TotalMilliseconds); + set => _consumerConfig.MaxPollIntervalMs = (int?)value?.TotalMilliseconds; } public bool? EnableAutoOffsetStore @@ -151,7 +155,7 @@ public void SetHeadersDeserializer(IHeadersDeserializer deserializer) public void SetOffsetsCommittedHandler(Action offsetsCommittedHandler) { - _offsetsCommittedHandler = _offsetsCommittedHandler ?? throw new ArgumentNullException(nameof(offsetsCommittedHandler)); + _offsetsCommittedHandler = offsetsCommittedHandler ?? throw new ArgumentNullException(nameof(offsetsCommittedHandler)); } public void SetStatisticsHandler(Action statisticsHandler) @@ -177,6 +181,7 @@ public void CreateIfMissing(Action configure) int ReceiveSettings.ConcurrentMessageLimit => Transport.GetConcurrentMessageLimit(); + public long Offset { get; set; } public string Topic { get; } public ushort MessageLimit { get; set; } @@ -187,6 +192,12 @@ public override IEnumerable Validate() if (_headersDeserializer == null) yield return this.Failure("HeadersDeserializer", "should not be null"); + if (_keyDeserializer == null) + yield return this.Failure("KeyDeserializer", "should not be null"); + + if (_valueDeserializer == null) + yield return this.Failure("ValueDeserializer", "should not be null"); + if (_options.TryGetOptions(out KafkaTopicOptions options)) { foreach (var result in options.Validate()) @@ -204,10 +215,10 @@ public override ReceiveEndpointContext CreateReceiveEndpointContext() KafkaReceiveEndpointContext CreateReceiveKafkaEndpointContext() { - var consumerConfig = _hostConfiguration.GetConsumerConfig(_consumerConfig); - ConsumerBuilder CreateConsumerBuilder() { + var consumerConfig = _hostConfiguration.GetConsumerConfig(_consumerConfig); + ConsumerBuilder consumerBuilder = new ConsumerBuilder(consumerConfig) .SetLogHandler((c, message) => _busInstance.HostConfiguration.ReceiveLogContext?.Debug?.Log(message.Message)); @@ -221,7 +232,7 @@ ConsumerBuilder CreateConsumerBuilder() return consumerBuilder; } - var builder = new KafkaReceiveEndpointBuilder(_busInstance, _hostConfiguration, consumerConfig.GroupId, this, + var builder = new KafkaReceiveEndpointBuilder(_busInstance, _hostConfiguration, _consumerConfig.GroupId, this, this, _headersDeserializer, _keyDeserializer, _valueDeserializer, CreateConsumerBuilder); ApplySpecifications(builder); @@ -235,6 +246,7 @@ public ReceiveEndpoint Build() if (_options.TryGetOptions(out KafkaTopicOptions options)) _consumerConfigurator.UseFilter(new ConfigureKafkaTopologyFilter(_hostConfiguration.Configuration, options)); + _consumerConfigurator.UseFilter(new ReceiveEndpointDependencyFilter(context)); _consumerConfigurator.UseFilter(new KafkaConsumerFilter(context)); IPipe consumerPipe = _consumerConfigurator.Build(); diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ConsumerLockContext.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ConsumerLockContext.cs index db1f05dcf4f..95ed1830019 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ConsumerLockContext.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ConsumerLockContext.cs @@ -14,20 +14,23 @@ public class ConsumerLockContext : IConsumerLockContext, KafkaConsumerBuilderContext { - readonly CancellationToken _cancellationToken; - readonly ConsumerContext _context; - - readonly SingleThreadedDictionary _data = - new SingleThreadedDictionary(); - + readonly SingleThreadedDictionary _data; + readonly PendingConfirmationCollection _pending; readonly ReceiveSettings _receiveSettings; public ConsumerLockContext(ConsumerContext context, ReceiveSettings receiveSettings, CancellationToken cancellationToken) { _context = context; _receiveSettings = receiveSettings; - _cancellationToken = cancellationToken; + _pending = new PendingConfirmationCollection(cancellationToken); + _data = new SingleThreadedDictionary(); + } + + public ValueTask DisposeAsync() + { + _pending.Dispose(); + return default; } public Task Pending(ConsumeResult result) @@ -41,147 +44,75 @@ public Task Complete(ConsumeResult result) { LogContext.SetCurrentIfNull(_context.LogContext); - if (_data.TryGetValue(result.TopicPartition, out var data)) - data.Complete(result); + _pending.Complete(result.TopicPartitionOffset); return Task.CompletedTask; } - public Task Faulted(ConsumeResult result, Exception exception) + public void Canceled(ConsumeResult result, CancellationToken cancellationToken) { LogContext.SetCurrentIfNull(_context.LogContext); - if (_data.TryGetValue(result.TopicPartition, out var data)) - data.Faulted(result, exception); - - return Task.CompletedTask; + _pending.Canceled(result.TopicPartitionOffset, cancellationToken); } - public ValueTask DisposeAsync() + public Task Faulted(ConsumeResult result, Exception exception) { - return default; - } + LogContext.SetCurrentIfNull(_context.LogContext); - public Task Push(ConsumeResult partition, Func method, CancellationToken cancellationToken = default) - { - return _data[partition.TopicPartition].Push(method); - } + _pending.Faulted(result.TopicPartitionOffset, exception); - public Task Run(ConsumeResult partition, Func method, CancellationToken cancellationToken = default) - { - return _data[partition.TopicPartition].Run(method); + return Task.CompletedTask; } - public void OnAssigned(IConsumer consumer, IEnumerable partitions) + public IEnumerable OnAssigned(IConsumer consumer, IEnumerable partitions) { LogContext.SetCurrentIfNull(_context.LogContext); foreach (var partition in partitions) { - if (!_data.TryAdd(partition, p => new PartitionCheckpointData(partition, consumer, _receiveSettings, _cancellationToken))) + if (!_data.TryAdd(partition, p => new PartitionCheckpointData(consumer, _receiveSettings, _pending))) continue; - LogContext.Debug?.Log("Partition: {PartitionId} was assigned to: {MemberId}", partition, consumer.MemberId); + LogContext.Debug?.Log("Partition: {PartitionId} with {Offset} was assigned to: {MemberId}", partition, _receiveSettings.Offset, + consumer.MemberId); + + yield return new TopicPartitionOffset(partition, _receiveSettings.Offset); } } - public void OnPartitionLost(IConsumer consumer, IEnumerable partitions) + public IEnumerable OnPartitionLost(IConsumer consumer, IEnumerable partitions) { LogContext.SetCurrentIfNull(_context.LogContext); - async Task LostAndDelete(TopicPartitionOffset topicPartition) + Task LostAndDelete(TopicPartitionOffset topicPartition) { - if (!_data.TryGetValue(topicPartition.TopicPartition, out var data)) - return false; + if (!_data.TryRemove(topicPartition.TopicPartition, out var data)) + return Task.CompletedTask; - await data.Lost().ConfigureAwait(false); LogContext.Debug?.Log("Partition: {PartitionId} was lost on {MemberId}", topicPartition.TopicPartition, consumer.MemberId); - return _data.TryRemove(topicPartition.TopicPartition, out _); + return data.Lost(); } TaskUtil.Await(Task.WhenAll(partitions.Select(partition => LostAndDelete(partition)))); + return Array.Empty(); } - public void OnUnAssigned(IConsumer consumer, IEnumerable partitions) + public IEnumerable OnUnAssigned(IConsumer consumer, IEnumerable partitions) { LogContext.SetCurrentIfNull(_context.LogContext); - async Task CloseAndDelete(TopicPartitionOffset topicPartition) + Task CloseAndDelete(TopicPartitionOffset topicPartition) { - if (!_data.TryGetValue(topicPartition.TopicPartition, out var data)) - return false; + if (!_data.TryRemove(topicPartition.TopicPartition, out var data)) + return Task.CompletedTask; - await data.Close().ConfigureAwait(false); LogContext.Debug?.Log("Partition: {PartitionId} was closed on {MemberId}", topicPartition.TopicPartition, consumer.MemberId); - return _data.TryRemove(topicPartition.TopicPartition, out _); + return data.Close(); } TaskUtil.Await(Task.WhenAll(partitions.Select(partition => CloseAndDelete(partition)))); - } - - - sealed class PartitionCheckpointData - { - readonly CancellationToken _cancellationToken; - readonly CancellationTokenSource _cancellationTokenSource; - readonly ICheckpointer _checkpointer; - readonly ChannelExecutor _executor; - readonly PendingConfirmationCollection _pending; - - public PartitionCheckpointData(TopicPartition partition, IConsumer consumer, ReceiveSettings settings, - CancellationToken cancellationToken) - { - _cancellationToken = cancellationToken; - _cancellationTokenSource = new CancellationTokenSource(); - _pending = new PendingConfirmationCollection(partition, _cancellationToken); - _executor = new ChannelExecutor(settings.PrefetchCount, settings.ConcurrentMessageLimit); - _checkpointer = new BatchCheckpointer(consumer, settings, _cancellationTokenSource.Token); - } - - public Task Pending(ConsumeResult result) - { - if (_cancellationToken.IsCancellationRequested) - return Task.CompletedTask; - - var pendingConfirmation = _pending.Add(result.Offset); - return _checkpointer.Pending(pendingConfirmation); - } - - public void Complete(ConsumeResult result) - { - _pending.Complete(result.Offset); - } - - public void Faulted(ConsumeResult result, Exception exception) - { - _pending.Faulted(result.Offset, exception); - } - - public Task Lost() - { - _cancellationTokenSource.Cancel(); - return Close(); - } - - public async Task Close() - { - await _executor.DisposeAsync().ConfigureAwait(false); - await _checkpointer.DisposeAsync().ConfigureAwait(false); - - _pending.Dispose(); - _cancellationTokenSource.Cancel(); - _cancellationTokenSource.Dispose(); - } - - public Task Push(Func method) - { - return _executor.Push(method, _cancellationTokenSource.Token); - } - - public Task Run(Func method) - { - return _executor.Run(method, _cancellationTokenSource.Token); - } + return Array.Empty(); } } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/IConsumerLockContext.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/IConsumerLockContext.cs index a5180b6819b..c923267e9d5 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/IConsumerLockContext.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/IConsumerLockContext.cs @@ -1,16 +1,17 @@ namespace MassTransit.KafkaIntegration { using System; + using System.Threading; using System.Threading.Tasks; using Confluent.Kafka; - using Util; public interface IConsumerLockContext : - IChannelExecutorPool> + IAsyncDisposable { Task Pending(ConsumeResult result); Task Complete(ConsumeResult result); Task Faulted(ConsumeResult result, Exception exception); + void Canceled(ConsumeResult result, CancellationToken cancellationToken); } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaBaseReceiveContext.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaBaseReceiveContext.cs index 1a4626d8da0..f0e5acb9ac7 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaBaseReceiveContext.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaBaseReceiveContext.cs @@ -1,35 +1,32 @@ namespace MassTransit.KafkaIntegration { using System; - using System.Threading.Tasks; using Confluent.Kafka; - using Serialization; using Transports; public sealed class KafkaReceiveContext : BaseReceiveContext, - KafkaConsumeContext, - ReceiveLockContext + KafkaConsumeContext where TValue : class { readonly KafkaReceiveEndpointContext _context; + readonly Lazy _headerProvider; readonly Lazy _key; - readonly IConsumerLockContext _lockContext; readonly ConsumeResult _result; - readonly Lazy _headerProvider; - public KafkaReceiveContext(ConsumeResult result, KafkaReceiveEndpointContext context, IConsumerLockContext lockContext) + public KafkaReceiveContext(ConsumeResult result, KafkaReceiveEndpointContext context) : base(false, context) { _result = result; _context = context; - _lockContext = lockContext; + Body = new BytesMessageBody(_result.Message.Value); InputAddress = context.GetInputAddress(_result.Topic); _key = new Lazy(() => _context.KeyDeserializer.DeserializeKey(result)); - _headerProvider = new Lazy(() => _context.HeadersDeserializer.Deserialize(_result.Message.Headers)); + _headerProvider = new Lazy(() => + new KafkaHeaderProvider(_result.Message, _context.HeadersDeserializer.Deserialize(_result.Message.Headers))); var messageContext = new KafkaMessageContext(_result, this); @@ -42,7 +39,7 @@ public KafkaReceiveContext(ConsumeResult result, KafkaReceiveEnd protected override IHeaderProvider HeaderProvider => _headerProvider.Value; - public override MessageBody Body => new NotSupportedMessageBody(); + public override MessageBody Body { get; } public string GroupId => _context.GroupId; @@ -53,22 +50,8 @@ public KafkaReceiveContext(ConsumeResult result, KafkaReceiveEnd public int Partition => _result.Partition; public long Offset => _result.Offset; + public bool IsPartitionEof => _result.IsPartitionEOF; public DateTime CheckpointUtcDateTime => _result.Message.Timestamp.UtcDateTime; - - public Task Complete() - { - return _lockContext.Complete(_result); - } - - public Task Faulted(Exception exception) - { - return _lockContext.Faulted(_result, exception); - } - - public Task ValidateLockStatus() - { - return Task.CompletedTask; - } } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaConsumerBuilderContext.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaConsumerBuilderContext.cs index e5ca595d15f..53884635894 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaConsumerBuilderContext.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaConsumerBuilderContext.cs @@ -6,8 +6,8 @@ namespace MassTransit.KafkaIntegration public interface KafkaConsumerBuilderContext { - void OnAssigned(IConsumer consumer, IEnumerable partitions); - void OnUnAssigned(IConsumer consumer, IEnumerable partitions); - void OnPartitionLost(IConsumer consumer, IEnumerable partitions); + IEnumerable OnAssigned(IConsumer consumer, IEnumerable partitions); + IEnumerable OnUnAssigned(IConsumer consumer, IEnumerable partitions); + IEnumerable OnPartitionLost(IConsumer consumer, IEnumerable partitions); } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaHeaderProvider.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaHeaderProvider.cs new file mode 100644 index 00000000000..a4ad0bbf40c --- /dev/null +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaHeaderProvider.cs @@ -0,0 +1,37 @@ +namespace MassTransit.KafkaIntegration +{ + using System; + using System.Collections.Generic; + using Confluent.Kafka; + using Transports; + + + public class KafkaHeaderProvider : + IHeaderProvider + { + readonly Message _message; + readonly IHeaderProvider _provider; + + public KafkaHeaderProvider(Message message, IHeaderProvider provider) + { + _message = message; + _provider = provider; + } + + public IEnumerable> GetAll() + { + return _provider.GetAll(); + } + + public bool TryGetHeader(string key, out object value) + { + if (MessageHeaders.TransportSentTime.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + value = _message.Timestamp.UtcDateTime; + return true; + } + + return _provider.TryGetHeader(key, out value); + } + } +} diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaMessageConsumer.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaMessageConsumer.cs index 622b8d8235f..d15d8d3799c 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaMessageConsumer.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaMessageConsumer.cs @@ -1,10 +1,10 @@ namespace MassTransit.KafkaIntegration { using System; + using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Confluent.Kafka; - using Internals; using Logging; using MassTransit.Middleware; using Transports; @@ -12,68 +12,67 @@ public class KafkaMessageConsumer : - Agent, + ConsumerAgent, + KafkaConsumerBuilderContext, IKafkaMessageConsumer where TValue : class { readonly CancellationTokenSource _cancellationTokenSource; readonly CancellationTokenSource _checkpointTokenSource; readonly IConsumer _consumer; - readonly Task _consumeTask; readonly KafkaReceiveEndpointContext _context; - readonly TaskCompletionSource _deliveryComplete; - readonly IReceivePipeDispatcher _dispatcher; readonly IChannelExecutorPool> _executorPool; - readonly IConsumerLockContext _lockContext; + readonly SemaphoreSlim _limit; + readonly ConsumerLockContext _lockContext; readonly ReceiveSettings _receiveSettings; public KafkaMessageConsumer(ReceiveSettings receiveSettings, KafkaReceiveEndpointContext context, ConsumerContext consumerContext) + : base(context) { _receiveSettings = receiveSettings; _context = context; + _limit = new SemaphoreSlim(receiveSettings.PrefetchCount); - _deliveryComplete = TaskUtil.GetTask(); _cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(Stopping); _checkpointTokenSource = CancellationTokenSource.CreateLinkedTokenSource(Stopped); - var lockContext = new ConsumerLockContext(consumerContext, receiveSettings, _checkpointTokenSource.Token); + _lockContext = new ConsumerLockContext(consumerContext, receiveSettings, _checkpointTokenSource.Token); - _dispatcher = context.CreateReceivePipeDispatcher(); - _consumer = consumerContext.CreateConsumer(lockContext, HandleKafkaError); - _dispatcher.ZeroActivity += HandleDeliveryComplete; + _consumer = consumerContext.CreateConsumer(this, HandleKafkaError); - _executorPool = new CombinedChannelExecutorPool(lockContext, receiveSettings); - _lockContext = lockContext; + IHashGenerator hashGenerator = new Murmur3UnsafeHashGenerator(); + _executorPool = new PartitionChannelExecutorPool>(x => x.Message.Key, hashGenerator, + receiveSettings.ConcurrentMessageLimit, + receiveSettings.ConcurrentDeliveryLimit); - _consumeTask = Task.Run(() => Consume()); - _consumeTask.ContinueWith(async _ => - { - try - { - if (!IsStopping) - await this.Stop("Consume Loop Exited").ConfigureAwait(false); - } - catch (Exception exception) - { - LogContext.Warning?.Log(exception, "Stop Faulted"); - } - }); + TrySetConsumeTask(Task.Run(() => Consume())); } - public long DeliveryCount => _dispatcher.DispatchCount; - - public int ConcurrentDeliveryCount => _dispatcher.MaxConcurrentDispatchCount; + public IEnumerable OnAssigned(IConsumer consumer, IEnumerable partitions) + { + SetReady(); + return _lockContext.OnAssigned(consumer, partitions); + } - async Task Consume() + public IEnumerable OnUnAssigned(IConsumer consumer, IEnumerable partitions) { - _consumer.Subscribe(_receiveSettings.Topic); + return _lockContext.OnUnAssigned(consumer, partitions); + } - SetReady(); + public IEnumerable OnPartitionLost(IConsumer consumer, IEnumerable partitions) + { + return _lockContext.OnPartitionLost(consumer, partitions); + } + async Task Consume() + { try { + _consumer.Subscribe(_receiveSettings.Topic); + while (!_cancellationTokenSource.IsCancellationRequested) { ConsumeResult consumeResult = _consumer.Consume(_cancellationTokenSource.Token); + await _limit.WaitAsync(_cancellationTokenSource.Token).ConfigureAwait(false); await _lockContext.Pending(consumeResult).ConfigureAwait(false); await _executorPool.Push(consumeResult, () => Handle(consumeResult), Stopping).ConfigureAwait(false); } @@ -81,10 +80,12 @@ async Task Consume() catch (OperationCanceledException exception) when (exception.CancellationToken == Stopping || exception.CancellationToken == _cancellationTokenSource.Token) { + SetNotReady(exception); } catch (Exception exception) { LogContext.Warning?.Log(exception, "Consume Loop faulted"); + SetNotReady(exception); } } @@ -93,11 +94,16 @@ async Task Handle(ConsumeResult result) if (IsStopping) return; - var context = new KafkaReceiveContext(result, _context, _lockContext); + var context = new KafkaReceiveContext(result, _context); + var cancellationToken = context.CancellationToken; + + CancellationTokenRegistration? registration = null; + if (cancellationToken.CanBeCanceled) + registration = cancellationToken.Register(() => _lockContext.Canceled(result, cancellationToken)); try { - await _dispatcher.Dispatch(context, context).ConfigureAwait(false); + await Dispatch(result.TopicPartitionOffset, context, new KafkaReceiveLockContext(result, _lockContext)).ConfigureAwait(false); } catch (Exception exception) { @@ -105,105 +111,60 @@ async Task Handle(ConsumeResult result) } finally { + registration?.Dispose(); context.Dispose(); + _limit.Release(); } } void HandleKafkaError(IConsumer consumer, Error error) { - EnabledLogger? logger = error.IsFatal ? LogContext.Error : LogContext.Warning; - logger?.Log("Consumer [{MemberId}] error ({Code}): {Reason} on {Topic}", consumer.MemberId, error.Code, error.Reason, _receiveSettings.Topic); - if (_cancellationTokenSource.IsCancellationRequested) return; - var activeDispatchCount = _dispatcher.ActiveDispatchCount; - if (activeDispatchCount == 0 || error.IsLocalError) + EnabledLogger? logger = error.IsFatal ? LogContext.Error : LogContext.Warning; + logger?.Log("Consumer [{MemberId}] error ({Code}): {Reason} on {Topic}. IsFatal({IsFatal})", consumer.MemberId, error.Code, error.Reason, + _receiveSettings.Topic, error.IsFatal); + + if (error.IsLocalError || error.IsFatal) { _cancellationTokenSource.Cancel(); - _deliveryComplete.TrySetResult(true); + SetNotReady(new KafkaException(error)); } } - Task HandleDeliveryComplete() + protected override async Task ActiveAndActualAgentsCompleted(StopContext context) { - if (IsStopping) - { - LogContext.Debug?.Log("Consumer shutdown completed: {InputAddress}", _context.InputAddress); - - _deliveryComplete.TrySetResult(true); - } + await base.ActiveAndActualAgentsCompleted(context).ConfigureAwait(false); - return Task.CompletedTask; - } - - protected override Task StopAgent(StopContext context) - { - LogContext.Debug?.Log("Stopping consumer: {InputAddress}", _context.InputAddress); - - SetCompleted(ActiveAndActualAgentsCompleted(context)); - - return Completed; - } - - async Task ActiveAndActualAgentsCompleted(StopContext context) - { - if (_dispatcher.ActiveDispatchCount > 0) - { - try - { - await _deliveryComplete.Task.OrCanceled(context.CancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - LogContext.Warning?.Log("Stop canceled waiting for message consumers to complete: {InputAddress}", _context.InputAddress); - } - } - - await _consumeTask.ConfigureAwait(false); await _executorPool.DisposeAsync().ConfigureAwait(false); // It is time to cancel pending tasks as we already drained current pool _checkpointTokenSource.Cancel(); - _consumer.Close(); - - _consumer.Dispose(); - _cancellationTokenSource.Dispose(); - _checkpointTokenSource.Dispose(); - } - - - class CombinedChannelExecutorPool : - IChannelExecutorPool> - { - readonly IChannelExecutorPool> _keyExecutorPool; - readonly IChannelExecutorPool> _partitionExecutorPool; - - public CombinedChannelExecutorPool(IChannelExecutorPool> partitionExecutorPool, ReceiveSettings receiveSettings) + try { - _partitionExecutorPool = partitionExecutorPool; - IHashGenerator hashGenerator = new Murmur3UnsafeHashGenerator(); - _keyExecutorPool = new PartitionChannelExecutorPool>(x => x.Message.Key, hashGenerator, - receiveSettings.ConcurrentMessageLimit, - receiveSettings.ConcurrentDeliveryLimit); + _consumer.Close(); } - - public Task Push(ConsumeResult result, Func handle, CancellationToken cancellationToken) + catch (Exception e) { - return _partitionExecutorPool.Push(result, () => _keyExecutorPool.Run(result, handle, cancellationToken), cancellationToken); + LogContext.Error?.Log(e, "Consumer [{MemberId}] close faulted on {Topic}", _consumer.MemberId, _receiveSettings.Topic); } - public Task Run(ConsumeResult result, Func method, CancellationToken cancellationToken = default) + try { - return _partitionExecutorPool.Run(result, () => _keyExecutorPool.Run(result, method, cancellationToken), cancellationToken); + _consumer.Dispose(); } - - public async ValueTask DisposeAsync() + catch (Exception) { - await _partitionExecutorPool.DisposeAsync().ConfigureAwait(false); - await _keyExecutorPool.DisposeAsync().ConfigureAwait(false); + //ignored } + + _cancellationTokenSource.Dispose(); + + await _lockContext.DisposeAsync().ConfigureAwait(false); + _checkpointTokenSource.Dispose(); + _limit.Dispose(); } } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaReceiveLockContext.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaReceiveLockContext.cs new file mode 100644 index 00000000000..300503c554d --- /dev/null +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaReceiveLockContext.cs @@ -0,0 +1,36 @@ +namespace MassTransit.KafkaIntegration +{ + using System; + using System.Threading.Tasks; + using Confluent.Kafka; + using Transports; + + + public class KafkaReceiveLockContext : + ReceiveLockContext + { + readonly IConsumerLockContext _lockContext; + readonly ConsumeResult _result; + + public KafkaReceiveLockContext(ConsumeResult result, IConsumerLockContext lockContext) + { + _result = result; + _lockContext = lockContext; + } + + public Task Complete() + { + return _lockContext.Complete(_result); + } + + public Task Faulted(Exception exception) + { + return _lockContext.Faulted(_result, exception); + } + + public Task ValidateLockStatus() + { + return Task.CompletedTask; + } + } +} diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaRider.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaRider.cs index 5b0e1ff0cf6..af7dcfd82f7 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaRider.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaRider.cs @@ -14,42 +14,29 @@ public class KafkaRider : IKafkaRider { readonly IBusInstance _busInstance; + readonly IRiderRegistrationContext _context; readonly IReceiveEndpointCollection _endpoints; readonly IKafkaHostConfiguration _hostConfiguration; - readonly IRiderRegistrationContext _registrationContext; Lazy _producerProvider; public KafkaRider(IKafkaHostConfiguration hostConfiguration, IBusInstance busInstance, IReceiveEndpointCollection endpoints, - IRiderRegistrationContext registrationContext) + IRiderRegistrationContext context) { _hostConfiguration = hostConfiguration; _busInstance = busInstance; _endpoints = endpoints; - _registrationContext = registrationContext; + _context = context; Reset(); } - public ITopicProducer GetProducer(Uri address, ConsumeContext consumeContext) - where TValue : class - { - if (address == null) - throw new ArgumentNullException(nameof(address)); - - var provider = consumeContext == null - ? _producerProvider.Value - : new ConsumeContextTopicProducerProvider(_producerProvider.Value, consumeContext); - - return provider.GetProducer(address); - } - public HostReceiveEndpointHandle ConnectTopicEndpoint(string topicName, string groupId, Action> configure) where TValue : class { var specification = _hostConfiguration.CreateSpecification(topicName, groupId, configurator => { - configure?.Invoke(_registrationContext, configurator); + configure?.Invoke(_context, configurator); }); _endpoints.Add(specification.EndpointName, specification.CreateReceiveEndpoint(_busInstance)); @@ -63,7 +50,7 @@ public HostReceiveEndpointHandle ConnectTopicEndpoint(string topic { var specification = _hostConfiguration.CreateSpecification(topicName, consumerConfig, configurator => { - configure?.Invoke(_registrationContext, configurator); + configure?.Invoke(_context, configurator); }); _endpoints.Add(specification.EndpointName, specification.CreateReceiveEndpoint(_busInstance)); @@ -87,6 +74,17 @@ public IEnumerable CheckEndpointHealth() return _endpoints.CheckEndpointHealth(); } + public ConnectHandle ConnectSendObserver(ISendObserver observer) + { + return _producerProvider.Value.ConnectSendObserver(observer); + } + + public ITopicProducer GetProducer(Uri address) + where TValue : class + { + return _producerProvider.Value.GetProducer(address); + } + void Reset() { _producerProvider = new Lazy(() => new TopicProducerProvider(_busInstance, _hostConfiguration)); @@ -112,7 +110,7 @@ public RiderAgent(IClientContextSupervisor supervisor, IReceiveEndpointCollectio protected override async Task StopAgent(StopContext context) { - await _endpoints.Stop(context.CancellationToken).ConfigureAwait(false); + await _endpoints.StopEndpoints(context.CancellationToken).ConfigureAwait(false); await _supervisor.Stop(context).ConfigureAwait(false); await base.StopAgent(context).ConfigureAwait(false); _onStop(); diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaTopicSendTransportContext.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaTopicSendTransportContext.cs index e9180b3935c..f504d63733f 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaTopicSendTransportContext.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/KafkaTopicSendTransportContext.cs @@ -26,11 +26,12 @@ public class KafkaTopicSendTransportContext : public KafkaTopicSendTransportContext(IHostConfiguration hostConfiguration, string topicName, IProducerContextSupervisor supervisor, IHeadersSerializer headersSerializer, IAsyncSerializer keySerializer, - IAsyncSerializer valueSerializer, Action configureSend, ISerialization serialization) + IAsyncSerializer valueSerializer, IReadOnlyList> configureSend, ISerialization serialization) : base(hostConfiguration, serialization) { var sendConfiguration = new SendPipeConfiguration(hostConfiguration.Topology.SendTopology); - configureSend?.Invoke(sendConfiguration.Configurator); + for (var i = 0; i < configureSend.Count; i++) + configureSend[i](sendConfiguration.Configurator); _hostConfiguration = hostConfiguration; _supervisor = supervisor; diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Middleware/ConfigureKafkaTopologyFilter.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Middleware/ConfigureKafkaTopologyFilter.cs index 7ae836e3a32..7032357f5a7 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Middleware/ConfigureKafkaTopologyFilter.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Middleware/ConfigureKafkaTopologyFilter.cs @@ -1,69 +1,72 @@ -namespace MassTransit.KafkaIntegration.Middleware +namespace MassTransit.KafkaIntegration.Middleware; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Confluent.Kafka; +using Confluent.Kafka.Admin; +using Logging; + + +public class ConfigureKafkaTopologyFilter : + IFilter + where TValue : class { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Confluent.Kafka; - using Confluent.Kafka.Admin; - using Logging; + readonly AdminClientConfig _config; + readonly KafkaTopicOptions _options; + readonly TopicSpecification _specification; + public ConfigureKafkaTopologyFilter(IReadOnlyDictionary clientConfig, KafkaTopicOptions options) + { + _options = options; + _specification = _options.ToSpecification(); + _config = new AdminClientConfig(clientConfig.ToDictionary(x => x.Key, x => x.Value)); + } - public class ConfigureKafkaTopologyFilter : - IFilter - where TValue : class + public async Task Send(ConsumerContext context, IPipe next) { - readonly AdminClientConfig _config; - readonly KafkaTopicOptions _options; - readonly TopicSpecification _specification; + OneTimeContext> oneTimeContext = + await context.OneTimeSetup>(() => CreateTopic()).ConfigureAwait(false); - public ConfigureKafkaTopologyFilter(IReadOnlyDictionary clientConfig, KafkaTopicOptions options) + try { - _options = options; - _specification = _options.ToSpecification(); - _config = new AdminClientConfig(clientConfig.ToDictionary(x => x.Key, x => x.Value)); + await next.Send(context).ConfigureAwait(false); } - - public async Task Send(ConsumerContext context, IPipe next) + catch (Exception) { - await context.OneTimeSetup>(_ => CreateTopic(), () => new Context()).ConfigureAwait(false); + oneTimeContext.Evict(); - await next.Send(context).ConfigureAwait(false); + throw; } + } + + public void Probe(ProbeContext context) + { + var scope = context.CreateFilterScope("configureTopology"); + scope.Add("specifications", _options); + } - public void Probe(ProbeContext context) + async Task CreateTopic() + { + var client = new AdminClientBuilder(_config).Build(); + try { - var scope = context.CreateFilterScope("configureTopology"); - scope.Add("specifications", _options); + var options = new CreateTopicsOptions { RequestTimeout = _options.RequestTimeout }; + LogContext.Debug?.Log("Creating topic: {Topic}", _specification.Name); + await client.CreateTopicsAsync(new[] { _specification }, options).ConfigureAwait(false); } - - async Task CreateTopic() + catch (CreateTopicsException e) { - var client = new AdminClientBuilder(_config).Build(); - try - { - var options = new CreateTopicsOptions { RequestTimeout = _options.RequestTimeout }; - LogContext.Debug?.Log("Creating topic: {Topic}", _specification.Name); - await client.CreateTopicsAsync(new[] { _specification }, options).ConfigureAwait(false); - } - catch (CreateTopicsException e) - { - if (!e.Results.All(x => x.Error.Reason.EndsWith("already exists.", StringComparison.OrdinalIgnoreCase))) - { - EnabledLogger? logger = e.Error.IsFatal ? LogContext.Error : LogContext.Debug; - logger?.Log("An error occured creating topics. {Errors}", string.Join(", ", e.Results.Select(x => $"{x.Topic}:{x.Error.Reason}"))); - } - } - finally + if (!e.Results.All(x => x.Error.Reason.EndsWith("already exists.", StringComparison.OrdinalIgnoreCase))) { - client.Dispose(); + EnabledLogger? logger = e.Error.IsFatal ? LogContext.Error : LogContext.Debug; + logger?.Log("An error occured creating topics. {Errors}", string.Join(", ", e.Results.Select(x => $"{x.Topic}:{x.Error.Reason}"))); } } - - - class Context : - ConfigureTopologyContext + finally { + client.Dispose(); } } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Middleware/KafkaConsumerFilter.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Middleware/KafkaConsumerFilter.cs index 85720dd5cb9..a80699bcb03 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Middleware/KafkaConsumerFilter.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Middleware/KafkaConsumerFilter.cs @@ -22,11 +22,16 @@ public async Task Send(ConsumerContext context, IPipe next) { var receiveSettings = _context.GetPayload(); var consumers = new IKafkaMessageConsumer[receiveSettings.ConcurrentConsumerLimit]; + for (var i = 0; i < consumers.Length; i++) consumers[i] = new KafkaMessageConsumer(receiveSettings, _context, context); var supervisor = CreateConsumerSupervisor(consumers); + await supervisor.Ready.ConfigureAwait(false); + + _context.AddConsumeAgent(supervisor); + await _context.TransportObservers.NotifyReady(_context.InputAddress).ConfigureAwait(false); try @@ -53,8 +58,6 @@ Supervisor CreateConsumerSupervisor(IKafkaMessageConsumer[] actual { var supervisor = new ConsumerSupervisor(actualConsumers); - _context.AddConsumeAgent(supervisor); - supervisor.SetReady(); return supervisor; @@ -68,6 +71,9 @@ public ConsumerSupervisor(IKafkaMessageConsumer[] consumers) { foreach (IKafkaMessageConsumer consumer in consumers) { + if (IsStopping) + return; + consumer.Completed.ContinueWith(async _ => { try diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/PartitionCheckpointData.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/PartitionCheckpointData.cs new file mode 100644 index 00000000000..441f634331b --- /dev/null +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/PartitionCheckpointData.cs @@ -0,0 +1,43 @@ +namespace MassTransit.KafkaIntegration +{ + using System.Threading; + using System.Threading.Tasks; + using Checkpoints; + using Confluent.Kafka; + + + public class PartitionCheckpointData + { + readonly CancellationTokenSource _cancellationTokenSource; + readonly ICheckpointer _checkpointer; + readonly PendingConfirmationCollection _pending; + + public PartitionCheckpointData(IConsumer consumer, ReceiveSettings settings, PendingConfirmationCollection pending) + { + _pending = pending; + _cancellationTokenSource = new CancellationTokenSource(); + + _checkpointer = new BatchCheckpointer(consumer, settings, _cancellationTokenSource.Token); + } + + public Task Pending(ConsumeResult result) + { + var pendingConfirmation = _pending.Add(result.TopicPartitionOffset); + return _checkpointer.Pending(pendingConfirmation); + } + + public Task Lost() + { + _cancellationTokenSource.Cancel(); + return Close(); + } + + public async Task Close() + { + await _checkpointer.DisposeAsync().ConfigureAwait(false); + + _cancellationTokenSource.Cancel(); + _cancellationTokenSource.Dispose(); + } + } +} diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ReceiveSettings.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ReceiveSettings.cs index f77df06c577..b9a94e65522 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ReceiveSettings.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/ReceiveSettings.cs @@ -5,6 +5,7 @@ namespace MassTransit.KafkaIntegration public interface ReceiveSettings { + long Offset { get; } string Topic { get; } ushort MessageLimit { get; } ushort CheckpointMessageCount { get; } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/DefaultSerializationFactory.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/DefaultSerializationFactory.cs new file mode 100644 index 00000000000..0433b86d85f --- /dev/null +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/DefaultSerializationFactory.cs @@ -0,0 +1,23 @@ +namespace MassTransit.KafkaIntegration.Serializers +{ + using System.Net.Mime; + using Confluent.Kafka; + using Serialization; + + + public class DefaultKafkaSerializerFactory : + IKafkaSerializerFactory + { + public ContentType ContentType => SystemTextJsonMessageSerializer.JsonContentType; + + public IDeserializer GetDeserializer() + { + return DeserializerTypes.TryGet() ?? new MassTransitJsonDeserializer(); + } + + public IAsyncSerializer GetSerializer() + { + return SerializerTypes.TryGet() ?? new MassTransitAsyncJsonSerializer(); + } + } +} diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/DictionaryHeadersSerialize.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/DictionaryHeadersSerialize.cs index 87fe92d54ec..e9ae3b35c63 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/DictionaryHeadersSerialize.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/DictionaryHeadersSerialize.cs @@ -3,6 +3,7 @@ namespace MassTransit.KafkaIntegration.Serializers using System; using System.Collections.Generic; using System.Linq; + using System.Text; using Confluent.Kafka; using Transports; @@ -28,11 +29,14 @@ class DictionaryHeadersDeserializer : { public IHeaderProvider Deserialize(Headers headers) { - return new DictionaryHeaderProvider(headers.ToDictionary(x => x.Key, x => - { - var valueBytes = x.GetValueBytes(); - return valueBytes != null ? (object)MessageDefaults.Encoding.GetString(valueBytes) : null; - })); + var dictionary = headers.GroupBy(header => header.Key) + .ToDictionary(group => group.Key, group => + { + var valueBytes = group.First().GetValueBytes(); + return valueBytes != null ? (object)Encoding.UTF8.GetString(valueBytes) : null; + }); + + return new DictionaryHeaderProvider(dictionary); } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/KafkaSerializerFactory.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/KafkaSerializerFactory.cs new file mode 100644 index 00000000000..81fff98dd46 --- /dev/null +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/Serializers/KafkaSerializerFactory.cs @@ -0,0 +1,13 @@ +namespace MassTransit.KafkaIntegration.Serializers +{ + using System.Net.Mime; + using Confluent.Kafka; + + + public interface IKafkaSerializerFactory + { + ContentType ContentType { get; } + IDeserializer GetDeserializer(); + IAsyncSerializer GetSerializer(); + } +} diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/TopicProducer.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/TopicProducer.cs index 5a50b05e3d4..ce3c9dbbf57 100644 --- a/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/TopicProducer.cs +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaIntegration/TopicProducer.cs @@ -105,6 +105,7 @@ public async Task Send(ProducerContext context) sendContext.CancellationToken.ThrowIfCancellationRequested(); StartedActivity? activity = LogContext.Current?.StartSendActivity(_context, sendContext); + StartedInstrument? instrument = LogContext.Current?.StartSendInstrument(_context, sendContext); try { if (_context.SendObservers.Count > 0) @@ -126,12 +127,14 @@ public async Task Send(ProducerContext context) await _context.SendObservers.SendFault(sendContext, exception).ConfigureAwait(false); activity?.AddExceptionEvent(exception); + instrument?.AddException(exception); throw; } finally { activity?.Stop(); + instrument?.Stop(); } } diff --git a/src/Transports/MassTransit.KafkaIntegration/KafkaTopicProducerProviderExtensions.cs b/src/Transports/MassTransit.KafkaIntegration/KafkaTopicProducerProviderExtensions.cs new file mode 100644 index 00000000000..63349f066d1 --- /dev/null +++ b/src/Transports/MassTransit.KafkaIntegration/KafkaTopicProducerProviderExtensions.cs @@ -0,0 +1,34 @@ +namespace MassTransit +{ + using System; + using Confluent.Kafka; + using DependencyInjection; + using KafkaIntegration; + using Microsoft.Extensions.DependencyInjection; + + + public static class KafkaTopicProducerProviderExtensions + { + public static ITopicProducer GetProducer(this ITopicProducerProvider provider, Uri address) + where TValue : class + { + return GetProducer(provider, address, context => default); + } + + public static ITopicProducer GetProducer(this ITopicProducerProvider provider, Uri address, + KafkaKeyResolver keyResolver) + where TValue : class + { + ITopicProducer producer = provider.GetProducer(address); + return new KeyedTopicProducer(producer, keyResolver); + } + + internal static ITopicProducerProvider GetScopedTopicProducerProvider(this ITopicProducerProvider producerProvider, IServiceProvider provider) + { + var contextProvider = provider.GetService(); + return contextProvider is { HasContext: true } + ? new ConsumeContextTopicProducerProvider(producerProvider, contextProvider.GetContext()) + : producerProvider; + } + } +} diff --git a/src/Transports/MassTransit.KafkaIntegration/MassTransit.KafkaIntegration.csproj b/src/Transports/MassTransit.KafkaIntegration/MassTransit.KafkaIntegration.csproj index 698efe9ec4e..c3cf29f5f9b 100644 --- a/src/Transports/MassTransit.KafkaIntegration/MassTransit.KafkaIntegration.csproj +++ b/src/Transports/MassTransit.KafkaIntegration/MassTransit.KafkaIntegration.csproj @@ -1,12 +1,13 @@ - + - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 + Denys Kozhevnikov, $(Authors) - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -25,8 +26,7 @@ - - + diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqHostConfigurator.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqHostConfigurator.cs index 0fa69438dc7..0f5367ece55 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqHostConfigurator.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqHostConfigurator.cs @@ -1,13 +1,10 @@ -namespace MassTransit +#nullable enable +namespace MassTransit { using System; - using System.Threading.Tasks; using RabbitMQ.Client; - public delegate Task RefreshConnectionFactoryCallback(ConnectionFactory connectionFactory); - - public interface IRabbitMqHostConfigurator { /// @@ -18,11 +15,21 @@ public interface IRabbitMqHostConfigurator RefreshConnectionFactoryCallback OnRefreshConnectionFactory { set; } + /// + /// Sets the credential provider, overriding the default username/password credentials + /// + ICredentialsProvider CredentialsProvider { set; } + + /// + /// Sets the credentials refresher, allowing access token based credentials to be refreshed + /// + ICredentialsRefresher CredentialsRefresher { set; } + /// /// Configure the use of SSL to connection to RabbitMQ /// - /// - void UseSsl(Action configureSsl); + /// + void UseSsl(Action? configure = null); /// /// Specifies the heartbeat interval, in seconds, used to maintain the connection to RabbitMQ. @@ -90,5 +97,10 @@ public interface IRabbitMqHostConfigurator /// Configure the Max message size for RabbitMQ.Client /// void MaxMessageSize(uint maxMessageSize); + + /// + /// Sets the connection name for the connection to RabbitMQ + /// + void ConnectionName(string? connectionName); } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqReceiveEndpointConfigurator.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqReceiveEndpointConfigurator.cs index 61c1b5ff7e9..a590a824cde 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqReceiveEndpointConfigurator.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqReceiveEndpointConfigurator.cs @@ -61,5 +61,18 @@ void Bind(Action callback = null) /// /// The consumer tag to use for this receive endpoint. void OverrideConsumerTag(string consumerTag); + + /// + /// Configure receive endpoint to use a stream + /// + /// + void Stream(Action callback = null); + + /// + /// Configure receive endpoint to use a stream + /// + /// Overrides the default consumer tag with the specified name + /// + void Stream(string consumerTag, Action callback = null); } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqStreamConfigurator.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqStreamConfigurator.cs new file mode 100644 index 00000000000..e455d9a8ea7 --- /dev/null +++ b/src/Transports/MassTransit.RabbitMqTransport/Configuration/IRabbitMqStreamConfigurator.cs @@ -0,0 +1,50 @@ +#nullable enable +namespace MassTransit; + +using System; + + +public interface IRabbitMqStreamConfigurator +{ + /// + /// Set the maximum length of the stream, in bytes + /// + long MaxLength { set; } + + /// + /// Set the maximum age of messages in the stream + /// + TimeSpan MaxAge { set; } + + /// + /// Set the maximum segment size for the stream + /// + long MaxSegmentSize { set; } + + /// + /// Set the stream filter value for the consumer + /// + string Filter { set; } + + /// + /// Begin consuming messages from the specified offset + /// + /// + void FromOffset(long offset); + + /// + /// Begin consuming messages from the specified timestamp + /// + /// + void FromTimestamp(DateTime timestamp); + + /// + /// Begin consuming messages from the first message in the stream + /// + void FromFirst(); + + /// + /// Begin consuming messages from the last message in the stream + /// + void FromLast(); +} diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqHostConfigurationExtensions.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqHostConfigurationExtensions.cs index 6a2fd030715..7eec54d6f97 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqHostConfigurationExtensions.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqHostConfigurationExtensions.cs @@ -137,9 +137,7 @@ public static void ReceiveEndpoint(this IRabbitMqBusFactoryConfigurator configur } /// - /// Declare a ReceiveEndpoint using a unique generated queue name. This queue defaults to auto-delete - /// and non-durable. By default all services bus instances include a default receiveEndpoint that is - /// of this type (created automatically upon the first receiver binding). + /// Declare a receive endpoint using the endpoint . /// /// /// diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqHostSettings.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqHostSettings.cs index 15823b5e33c..f215eae2a5e 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqHostSettings.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqHostSettings.cs @@ -148,6 +148,16 @@ public interface RabbitMqHostSettings /// uint? MaxMessageSize { get; } + /// + /// The credential provider, overriding the default username/password credentials + /// + ICredentialsProvider CredentialsProvider { get; } + + /// + /// The credentials refresher, allowing access token based credentials to be refreshed + /// + ICredentialsRefresher CredentialsRefresher { get; } + /// /// Called prior to the connection factory being used to connect, so that any settings can be updated. /// Typically this would be the username/password in response to an expired token, etc. diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqSslOptions.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqSslOptions.cs index 4510d55db3d..be6d669b123 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqSslOptions.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqSslOptions.cs @@ -1,5 +1,9 @@ namespace MassTransit { + using System.Security.Authentication; + using RabbitMqTransport.Configuration; + + public class RabbitMqSslOptions { public string ServerName { get; set; } @@ -7,5 +11,6 @@ public class RabbitMqSslOptions public string CertPath { get; set; } public string CertPassphrase { get; set; } public bool CertIdentity { get; set; } + public SslProtocols Protocol { get; set; } = ConfigurationHostSettings.DefaultSslProtocols; } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqTestHarnessOptions.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqTestHarnessOptions.cs index a10a26583b7..ca9c349ebfd 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqTestHarnessOptions.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqTestHarnessOptions.cs @@ -1,6 +1,10 @@ #nullable enable namespace MassTransit { + using System; + using RabbitMQ.Client; + + public class RabbitMqTestHarnessOptions { /// @@ -18,5 +22,11 @@ public class RabbitMqTestHarnessOptions /// If the root virtual host is being used, ensure that the virtual host can be cleaned to avoid accidental destruction /// public bool ForceCleanRootVirtualHost { get; set; } + + /// + /// If specified, and create virtual host if not exists is specified, will call this method to apply additional configuration + /// to the virtual host after it has been created. + /// + public Action? ConfigureVirtualHostCallback { get; set; } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqTransportOptions.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqTransportOptions.cs index cb708dfe477..4830b62cdc0 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqTransportOptions.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RabbitMqTransportOptions.cs @@ -28,6 +28,7 @@ public RabbitMqTransportOptions() public string VHost { get; set; } public string User { get; set; } public string Pass { get; set; } + public string ConnectionName { get; set; } public bool UseSsl { diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/RefreshConnectionFactoryCallback.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RefreshConnectionFactoryCallback.cs new file mode 100644 index 00000000000..ee212957f87 --- /dev/null +++ b/src/Transports/MassTransit.RabbitMqTransport/Configuration/RefreshConnectionFactoryCallback.cs @@ -0,0 +1,7 @@ +namespace MassTransit; + +using System.Threading.Tasks; +using RabbitMQ.Client; + + +public delegate Task RefreshConnectionFactoryCallback(ConnectionFactory connectionFactory); diff --git a/src/Transports/MassTransit.RabbitMqTransport/Configuration/Topology/RabbitMqRoutingKeyConventionExtensions.cs b/src/Transports/MassTransit.RabbitMqTransport/Configuration/Topology/RabbitMqRoutingKeyConventionExtensions.cs deleted file mode 100644 index 63bbf329334..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/Configuration/Topology/RabbitMqRoutingKeyConventionExtensions.cs +++ /dev/null @@ -1,58 +0,0 @@ -namespace MassTransit -{ - using System; - using RabbitMqTransport; - using RabbitMqTransport.Configuration; - - - public static class RabbitMqRoutingKeyConventionExtensions - { - public static void UseRoutingKeyFormatter(this IMessageSendTopologyConfigurator configurator, IMessageRoutingKeyFormatter formatter) - where T : class - { - configurator.UpdateConvention>( - update => - { - update.SetFormatter(formatter); - - return update; - }); - } - - /// - /// Use the routing key formatter for the specified message type - /// - /// - /// - /// - public static void UseRoutingKeyFormatter(this ISendTopologyConfigurator configurator, IMessageRoutingKeyFormatter formatter) - where T : class - { - configurator.GetMessageTopology().UseRoutingKeyFormatter(formatter); - } - - /// - /// Use the delegate to format the routing key, using Empty if the string is null upon return - /// - /// - /// - /// - public static void UseRoutingKeyFormatter(this ISendTopologyConfigurator configurator, Func, string> formatter) - where T : class - { - configurator.GetMessageTopology().UseRoutingKeyFormatter(new DelegateRoutingKeyFormatter(formatter)); - } - - /// - /// Use the delegate to format the routing key, using Empty if the string is null upon return - /// - /// - /// - /// - public static void UseRoutingKeyFormatter(this IMessageSendTopologyConfigurator configurator, Func, string> formatter) - where T : class - { - configurator.UseRoutingKeyFormatter(new DelegateRoutingKeyFormatter(formatter)); - } - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageNotAcknowledgedException.cs b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageNotAcknowledgedException.cs index 9d4c2ec5652..cee103fbbf3 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageNotAcknowledgedException.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageNotAcknowledgedException.cs @@ -20,6 +20,9 @@ public MessageNotAcknowledgedException(Uri uri, string message) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MessageNotAcknowledgedException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageNotConfirmedException.cs b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageNotConfirmedException.cs index 010cb768d5c..b4625a12d1c 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageNotConfirmedException.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageNotConfirmedException.cs @@ -30,6 +30,9 @@ public MessageNotConfirmedException(Uri uri, string message, Exception innerExce { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MessageNotConfirmedException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageReturnedException.cs b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageReturnedException.cs index 3a1d232389d..c54c7dd1287 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageReturnedException.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/MessageReturnedException.cs @@ -25,6 +25,9 @@ public MessageReturnedException(Uri uri, string message, Exception innerExceptio { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected MessageReturnedException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/RabbitMqAddressException.cs b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/RabbitMqAddressException.cs index ee8c5b56958..0229c8fbe26 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/RabbitMqAddressException.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/RabbitMqAddressException.cs @@ -27,6 +27,9 @@ public RabbitMqAddressException(string message, Exception innerException) HelpLink = DefaultHelpLink; } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif public RabbitMqAddressException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/RabbitMqConnectionException.cs b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/RabbitMqConnectionException.cs index 9e2f5c31c05..f15db1b4ca8 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Exceptions/RabbitMqConnectionException.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Exceptions/RabbitMqConnectionException.cs @@ -23,6 +23,9 @@ public RabbitMqConnectionException(string message, Exception innerException) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected RabbitMqConnectionException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/src/Transports/MassTransit.RabbitMqTransport/MassTransit.RabbitMqTransport.csproj b/src/Transports/MassTransit.RabbitMqTransport/MassTransit.RabbitMqTransport.csproj index 98baa49156b..09baa6170d3 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/MassTransit.RabbitMqTransport.csproj +++ b/src/Transports/MassTransit.RabbitMqTransport/MassTransit.RabbitMqTransport.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -21,19 +21,14 @@ - - + - - + + - - - - - + diff --git a/src/Transports/MassTransit.RabbitMqTransport/NullableAttributes.cs b/src/Transports/MassTransit.RabbitMqTransport/NullableAttributes.cs new file mode 100644 index 00000000000..3f38561b675 --- /dev/null +++ b/src/Transports/MassTransit.RabbitMqTransport/NullableAttributes.cs @@ -0,0 +1,24 @@ +#if !NET6_0_OR_GREATER && !NETSTANDARD2_1 +namespace System.Diagnostics.CodeAnalysis +{ + using System; + + + [AttributeUsage(AttributeTargets.Parameter)] + sealed class NotNullWhenAttribute : + Attribute + { + /// Initializes the attribute with the specified return value condition. + /// + /// The return value condition. If the method returns this value, the associated parameter will not be null. + /// + public NotNullWhenAttribute(bool returnValue) + { + ReturnValue = returnValue; + } + + /// Gets the return value condition. + public bool ReturnValue { get; } + } +} +#endif diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqBasicConsumeContext.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqBasicConsumeContext.cs index 083ad08c984..78bf4ec991f 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqBasicConsumeContext.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqBasicConsumeContext.cs @@ -1,6 +1,5 @@ namespace MassTransit { - using System; using RabbitMQ.Client; @@ -30,11 +29,5 @@ public interface RabbitMqBasicConsumeContext : /// The basic properties of the message /// IBasicProperties Properties { get; } - - /// - /// The message body, since it's a byte array on RabbitMQ - /// - [Obsolete("This is a fail, we need to use the Body of receive context")] - byte[] Body { get; } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqBusFactory.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqBusFactory.cs index 00298672852..696f11c82f4 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqBusFactory.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqBusFactory.cs @@ -1,7 +1,6 @@ namespace MassTransit { using System; - using System.Threading; using Configuration; using RabbitMqTransport; using RabbitMqTransport.Configuration; @@ -10,8 +9,6 @@ public static class RabbitMqBusFactory { - public static IMessageTopologyConfigurator MessageTopology => Cached.MessageTopologyValue.Value; - /// /// Configure and create a bus for RabbitMQ /// @@ -19,7 +16,7 @@ public static class RabbitMqBusFactory /// public static IBusControl Create(Action configure = null) { - var topologyConfiguration = new RabbitMqTopologyConfiguration(MessageTopology); + var topologyConfiguration = new RabbitMqTopologyConfiguration(CreateMessageTopology()); var busConfiguration = new RabbitMqBusConfiguration(topologyConfiguration); var configurator = new RabbitMqBusFactoryConfigurator(busConfiguration); @@ -29,18 +26,19 @@ public static IBusControl Create(Action configu return configurator.Build(busConfiguration); } + public static IMessageTopologyConfigurator CreateMessageTopology() + { + return new MessageTopology(Cached.EntityNameFormatter); + } + static class Cached { - internal static readonly Lazy MessageTopologyValue = - new Lazy(() => new MessageTopology(_entityNameFormatter), - LazyThreadSafetyMode.PublicationOnly); - - static readonly IEntityNameFormatter _entityNameFormatter; + internal static readonly IEntityNameFormatter EntityNameFormatter; static Cached() { - _entityNameFormatter = new MessageNameFormatterEntityNameFormatter(new RabbitMqMessageNameFormatter()); + EntityNameFormatter = new MessageNameFormatterEntityNameFormatter(new RabbitMqMessageNameFormatter()); } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqEndpointAddress.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqEndpointAddress.cs index 6c335cf7621..b0bcdbd672f 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqEndpointAddress.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqEndpointAddress.cs @@ -23,7 +23,7 @@ public readonly struct RabbitMqEndpointAddress const string DelayedTypeKey = "delayedtype"; const string SingleActiveConsumerKey = "singleactiveconsumer"; - const string DelayedMessageExchangeType = "x-delayed-message"; + public const string DelayedMessageExchangeType = "x-delayed-message"; public readonly string Scheme; public readonly string Host; @@ -186,12 +186,18 @@ public RabbitMqEndpointAddress(Uri hostAddress, string exchangeName, string exch AlternateExchange = alternateExchange; } - public RabbitMqEndpointAddress GetDelayAddress() + public RabbitMqDelaySettings GetDelaySettings() { - var name = $"{Name}_delay"; + var delayExchangeName = $"{Name}_delay"; - return new RabbitMqEndpointAddress(Scheme, Host, Port, VirtualHost, name, DelayedMessageExchangeType, Durable, AutoDelete, false, - default, ExchangeType, BindExchanges, AlternateExchange, SingleActiveConsumer); + var delayExchangeAddress = new RabbitMqEndpointAddress(Scheme, Host, Port, VirtualHost, delayExchangeName, DelayedMessageExchangeType, Durable, + AutoDelete, false, default, RabbitMQ.Client.ExchangeType.Fanout, null, null, false); + + var delaySettings = new RabbitMqDelaySettings(delayExchangeAddress); + + delaySettings.BindToExchange(this); + + return delaySettings; } public Uri ToShortAddress() diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqRequestClientExtensions.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqRequestClientExtensions.cs index 3ba8dae116b..fc86d73bf86 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqRequestClientExtensions.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqRequestClientExtensions.cs @@ -1,50 +1,58 @@ -namespace MassTransit -{ - using System.Threading.Tasks; - using RabbitMqTransport; - - - public static class RabbitMqRequestClientExtensions - { - /// - /// Creates a new RPC client factory on RabbitMQ using the direct reply-to feature - /// - /// The connector, typically the bus instance - /// The default request timeout - /// - public static Task CreateReplyToClientFactory(this IReceiveConnector connector, RequestTimeout timeout = default) - { - var endpointDefinition = new ReplyToEndpointDefinition(default, 1000); - - var receiveEndpointHandle = connector.ConnectReceiveEndpoint(endpointDefinition, KebabCaseEndpointNameFormatter.Instance); - - return receiveEndpointHandle.CreateClientFactory(timeout); - } - - - class ReplyToEndpointDefinition : - IEndpointDefinition - { - public ReplyToEndpointDefinition(int? concurrentMessageLimit = default, int? prefetchCount = default) - { - ConcurrentMessageLimit = concurrentMessageLimit; - PrefetchCount = prefetchCount; - } - - public string GetEndpointName(IEndpointNameFormatter formatter) - { - return RabbitMqExchangeNames.ReplyTo; - } - - public bool IsTemporary => false; - public int? PrefetchCount { get; } - public int? ConcurrentMessageLimit { get; } - public bool ConfigureConsumeTopology => false; - - public void Configure(T configurator) - where T : IReceiveEndpointConfigurator - { - } - } - } -} +namespace MassTransit +{ + using RabbitMqTransport; + + + public static class RabbitMqRequestClientExtensions + { + /// + /// Creates a new RPC client factory on RabbitMQ using the direct reply-to feature + /// + /// The connector, typically the bus instance + /// The default request timeout + /// + public static IClientFactory CreateReplyToClientFactory(this IReceiveConnector connector, RequestTimeout timeout = default) + { + var endpointDefinition = new ReplyToEndpointDefinition(default, 1000); + + var receiveEndpointHandle = connector.ConnectReceiveEndpoint(endpointDefinition, KebabCaseEndpointNameFormatter.Instance); + + return receiveEndpointHandle.CreateClientFactory(timeout); + } + + /// + /// Enables the RabbitMQ Reply-To response endpoint, which uses the AMQP reply-to header for replies without using the bus endpoint + /// + /// + public static void SetRabbitMqReplyToRequestClientFactory(this IBusRegistrationConfigurator configurator) + { + configurator.SetRequestClientFactory((bus, timeout) => CreateReplyToClientFactory(bus, timeout)); + } + + + class ReplyToEndpointDefinition : + IEndpointDefinition + { + public ReplyToEndpointDefinition(int? concurrentMessageLimit = default, int? prefetchCount = default) + { + ConcurrentMessageLimit = concurrentMessageLimit; + PrefetchCount = prefetchCount; + } + + public string GetEndpointName(IEndpointNameFormatter formatter) + { + return RabbitMqExchangeNames.ReplyTo; + } + + public bool IsTemporary => false; + public int? PrefetchCount { get; } + public int? ConcurrentMessageLimit { get; } + public bool ConfigureConsumeTopology => false; + + public void Configure(T configurator) + where T : IReceiveEndpointConfigurator + { + } + } + } +} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqSendContextExtensions.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqSendContextExtensions.cs index a8a0d26e794..ba464d2eaec 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqSendContextExtensions.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqSendContextExtensions.cs @@ -7,6 +7,8 @@ public static class RabbitMqSendContextExtensions { + const string StreamFilterValueHeaderName = "x-stream-filter-value"; + public static void SetTransportHeader(this RabbitMqSendContext context, string key, object value) { SetHeader(context.BasicProperties, key, value); @@ -82,5 +84,32 @@ public static bool TrySetAwaitAck(this SendContext context, bool awaitAck) sendContext.AwaitAck = awaitAck; return true; } + + /// + /// Sets the filter value used for server-side streams filtering. + /// + /// + /// + public static void SetStreamFilterValue(this SendContext context, string value) + { + if (!context.TryGetPayload(out RabbitMqSendContext sendContext)) + throw new ArgumentException("The RabbitMqSendContext was not available"); + + sendContext.Headers.Set(StreamFilterValueHeaderName, value); + } + + /// + /// Sets the filter value used for server-side streams filtering. + /// + /// + /// + public static bool TrySetStreamFilterValue(this SendContext context, string value) + { + if (!context.TryGetPayload(out RabbitMqSendContext sendContext)) + return false; + + sendContext.Headers.Set(StreamFilterValueHeaderName, value); + return true; + } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/AmqpTimestampExtensions.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/AmqpTimestampExtensions.cs new file mode 100644 index 00000000000..e5baf16c677 --- /dev/null +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/AmqpTimestampExtensions.cs @@ -0,0 +1,40 @@ +namespace MassTransit.RabbitMqTransport; + +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using Internals; +using RabbitMQ.Client; + + +public static class AmqpTimestampExtensions +{ + /// + /// Assign a dictionary key to the value of the as an . + /// + /// The dictionary + /// The dictionary key + /// The timestamp + public static void SetAmqpTimestamp(this IDictionary dictionary, string key, DateTime timestamp) + { + dictionary[key] = TryConvert(timestamp, out AmqpTimestamp? result) + ? result + : timestamp.ToString("O"); + } + + static bool TryConvert(DateTime input, [NotNullWhen(true)] out AmqpTimestamp? result) + { + if (input >= DateTimeConstants.Epoch) + { + var timeSpan = input - DateTimeConstants.Epoch; + if (timeSpan.TotalSeconds <= long.MaxValue) + { + result = new AmqpTimestamp((long)timeSpan.TotalSeconds); + return true; + } + } + + result = default; + return false; + } +} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/BatchPublisher.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/BatchPublisher.cs index 15b969c091b..8f1b04a982a 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/BatchPublisher.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/BatchPublisher.cs @@ -14,14 +14,14 @@ public class BatchPublisher : IPublisher { readonly PendingConfirmationCollection _confirmations; - readonly ChannelExecutor _executor; + readonly TaskExecutor _executor; readonly IPublisher _immediatePublisher; readonly IModel _model; readonly Channel _publishChannel; readonly Task _publishTask; readonly BatchSettings _settings; - public BatchPublisher(ChannelExecutor executor, IModel model, BatchSettings settings, PendingConfirmationCollection confirmations) + public BatchPublisher(TaskExecutor executor, IModel model, BatchSettings settings, PendingConfirmationCollection confirmations) { _executor = executor; _model = model; @@ -61,7 +61,7 @@ async Task PublishAsync() public async ValueTask DisposeAsync() { - _publishChannel.Writer.Complete(); + _publishChannel.Writer.TryComplete(); await _publishTask.ConfigureAwait(false); } @@ -88,23 +88,26 @@ async Task WaitForBatch() async Task ReadBatch() { var batchToken = new CancellationTokenSource(_settings.Timeout); - var batch = new List(); + var batch = new List(_settings.MessageLimit); try { try { - for (int i = 0, - batchLength = 0; - i < _settings.MessageLimit && batchLength < _settings.SizeLimit; - i++) - { - var publish = await _publishChannel.Reader.ReadAsync(batchToken.Token).ConfigureAwait(false); - - batch.Add(publish); - batchLength += publish.Length; + var messageCount = 0; + var batchLength = 0; - if (await _publishChannel.Reader.WaitToReadAsync(batchToken.Token).ConfigureAwait(false) == false) + while (messageCount < _settings.MessageLimit && batchLength < _settings.SizeLimit) + { + if (_publishChannel.Reader.TryRead(out var publish)) + { + batch.Add(publish); + batchLength += publish.Length; + messageCount++; + } + else if (await _publishChannel.Reader.WaitToReadAsync(batchToken.Token).ConfigureAwait(false) == false) + { break; + } } } catch (OperationCanceledException exception) when (exception.CancellationToken == batchToken.Token && batch.Count > 0) diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/ConfigurationHostSettings.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/ConfigurationHostSettings.cs index 583242f9209..661834d138f 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/ConfigurationHostSettings.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/ConfigurationHostSettings.cs @@ -1,3 +1,4 @@ +#nullable enable namespace MassTransit.RabbitMqTransport.Configuration { using System; @@ -12,13 +13,15 @@ namespace MassTransit.RabbitMqTransport.Configuration class ConfigurationHostSettings : RabbitMqHostSettings { + internal const SslProtocols DefaultSslProtocols = SslProtocols.Tls12; + readonly ConfigurationBatchSettings _batchSettings; readonly Lazy _hostAddress; public ConfigurationHostSettings() { var defaultOptions = new SslOption(); - SslProtocol = SslProtocols.Tls; + SslProtocol = DefaultSslProtocols; AcceptablePolicyErrors = defaultOptions.AcceptablePolicyErrors | SslPolicyErrors.RemoteCertificateChainErrors; @@ -34,26 +37,26 @@ public ConfigurationHostSettings() _hostAddress = new Lazy(FormatHostAddress); } - public RefreshConnectionFactoryCallback OnRefreshConnectionFactory { get; set; } + public RefreshConnectionFactoryCallback? OnRefreshConnectionFactory { get; set; } - public string Host { get; set; } + public string? Host { get; set; } public int Port { get; set; } - public string VirtualHost { get; set; } - public string Username { get; set; } - public string Password { get; set; } + public string? VirtualHost { get; set; } + public string? Username { get; set; } + public string? Password { get; set; } public TimeSpan Heartbeat { get; set; } public bool Ssl { get; set; } public SslProtocols SslProtocol { get; set; } - public string SslServerName { get; set; } + public string? SslServerName { get; set; } public SslPolicyErrors AcceptablePolicyErrors { get; set; } - public string ClientCertificatePath { get; set; } - public string ClientCertificatePassphrase { get; set; } - public X509Certificate ClientCertificate { get; set; } + public string? ClientCertificatePath { get; set; } + public string? ClientCertificatePassphrase { get; set; } + public X509Certificate? ClientCertificate { get; set; } public bool UseClientCertificateAsAuthenticationIdentity { get; set; } - public LocalCertificateSelectionCallback CertificateSelectionCallback { get; set; } - public RemoteCertificateValidationCallback CertificateValidationCallback { get; set; } - public IRabbitMqEndpointResolver EndpointResolver { get; set; } - public string ClientProvidedName { get; set; } + public LocalCertificateSelectionCallback? CertificateSelectionCallback { get; set; } + public RemoteCertificateValidationCallback? CertificateValidationCallback { get; set; } + public IRabbitMqEndpointResolver? EndpointResolver { get; set; } + public string? ClientProvidedName { get; set; } public bool PublisherConfirmation { get; set; } public Uri HostAddress => _hostAddress.Value; public ushort RequestedChannelMax { get; set; } @@ -63,6 +66,9 @@ public ConfigurationHostSettings() public TimeSpan ContinuationTimeout { get; set; } public uint? MaxMessageSize { get; set; } + public ICredentialsProvider? CredentialsProvider { get; set; } + public ICredentialsRefresher? CredentialsRefresher { get; set; } + public Task Refresh(ConnectionFactory connectionFactory) { return OnRefreshConnectionFactory?.Invoke(connectionFactory) ?? Task.CompletedTask; diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqExchangeConfigurator.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqExchangeConfigurator.cs index 3126d6849e1..16efdafab64 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqExchangeConfigurator.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqExchangeConfigurator.cs @@ -53,7 +53,9 @@ public void SetExchangeArgument(string key, TimeSpan value) public virtual RabbitMqEndpointAddress GetEndpointAddress(Uri hostAddress) { - return new RabbitMqEndpointAddress(hostAddress, ExchangeName, ExchangeType, Durable, AutoDelete); + return new RabbitMqEndpointAddress(hostAddress, ExchangeName, ExchangeType, Durable, AutoDelete, + delayedType: ExchangeArguments.TryGetValue("x-delayed-type", out var argument) ? (string)argument : default, + alternateExchange: ExchangeArguments.TryGetValue(RabbitMQ.Client.Headers.AlternateExchange, out argument) ? (string)argument : default); } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqHostConfiguration.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqHostConfiguration.cs index 1ea834efbc9..3e32a7ca209 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqHostConfiguration.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqHostConfiguration.cs @@ -38,9 +38,11 @@ public RabbitMqHostConfiguration(IRabbitMqBusConfiguration busConfiguration, IRa ReceiveTransportRetryPolicy = Retry.CreatePolicy(x => { x.Handle(); + x.Handle(); x.Handle(exception => exception.Message.Contains("CONNECTION_FORCED") || exception.Message.Contains("End of stream") + || exception.Message.Contains("Bad frame") || exception.Message.Contains("Unexpected Exception")); x.Handle(exception => exception.ChannelShouldBeClosed()); x.Handle(exception => exception.Message.Contains("Pipelining of requests forbidden")); diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqHostConfigurator.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqHostConfigurator.cs index fc23bbe42f2..c0549c40f6b 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqHostConfigurator.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqHostConfigurator.cs @@ -1,26 +1,22 @@ +#nullable enable namespace MassTransit.RabbitMqTransport.Configuration { using System; - using System.Security.Authentication; + using RabbitMQ.Client; public class RabbitMqHostConfigurator : IRabbitMqHostConfigurator { - static readonly char[] _pathSeparator = { '/' }; + static readonly char[] _pathSeparator = ['/']; readonly ConfigurationHostSettings _settings; - public RabbitMqHostConfigurator(Uri hostAddress, string connectionName = null) + public RabbitMqHostConfigurator(Uri hostAddress, string? connectionName = null) { _settings = hostAddress.GetConfigurationHostSettings(); if (_settings.Port == 5671) - { - UseSsl(s => - { - s.Protocol = SslProtocols.Tls12; - }); - } + UseSsl(); _settings.VirtualHost = Uri.UnescapeDataString(GetVirtualHost(hostAddress)); @@ -28,7 +24,7 @@ public RabbitMqHostConfigurator(Uri hostAddress, string connectionName = null) _settings.ClientProvidedName = connectionName; } - public RabbitMqHostConfigurator(string host, string virtualHost, ushort port = 5672, string connectionName = null) + public RabbitMqHostConfigurator(string host, string virtualHost, ushort port = 5672, string? connectionName = null) { _settings = new ConfigurationHostSettings { @@ -37,6 +33,13 @@ public RabbitMqHostConfigurator(string host, string virtualHost, ushort port = 5 VirtualHost = virtualHost }; + if (_settings.Port == 5671) + { + UseSsl(s => + { + }); + } + if (!string.IsNullOrEmpty(connectionName)) _settings.ClientProvidedName = connectionName; } @@ -48,11 +51,11 @@ public bool PublisherConfirmation set => _settings.PublisherConfirmation = value; } - public void UseSsl(Action configureSsl) + public void UseSsl(Action? configure = null) { var configurator = new RabbitMqSslConfigurator(_settings); - configureSsl(configurator); + configure?.Invoke(configurator); _settings.Ssl = true; _settings.ClientCertificatePassphrase = configurator.CertificatePassphrase; @@ -111,6 +114,16 @@ public void Password(string password) _settings.Password = password; } + public ICredentialsProvider CredentialsProvider + { + set => _settings.CredentialsProvider = value; + } + + public ICredentialsRefresher CredentialsRefresher + { + set => _settings.CredentialsRefresher = value; + } + public void UseCluster(Action configureCluster) { var configurator = new RabbitMqClusterConfigurator(_settings); @@ -134,6 +147,11 @@ public void RequestedConnectionTimeout(TimeSpan timeSpan) _settings.RequestedConnectionTimeout = timeSpan; } + public void ConnectionName(string? connectionName) + { + _settings.ClientProvidedName = connectionName; + } + string GetVirtualHost(Uri address) { var segments = address.AbsolutePath.Split(_pathSeparator, StringSplitOptions.RemoveEmptyEntries); diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqReceiveEndpointConfiguration.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqReceiveEndpointConfiguration.cs index 837775331c1..c0e7ed98e4d 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqReceiveEndpointConfiguration.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqReceiveEndpointConfiguration.cs @@ -71,6 +71,7 @@ public void Build(IHost host) _modelConfigurator.UseFilter(new PurgeOnStartupFilter(_settings.QueueName)); _modelConfigurator.UseFilter(new PrefetchCountFilter(_settings.PrefetchCount)); + _modelConfigurator.UseFilter(new ReceiveEndpointDependencyFilter(context)); _modelConfigurator.UseFilter(new RabbitMqConsumerFilter(context)); } @@ -160,6 +161,25 @@ public bool ExclusiveConsumer set => _settings.ExclusiveConsumer = value; } + public void Stream(Action callback = null) + { + _settings.QueueArguments[Headers.XQueueType] = "stream"; + + var configurator = new RabbitMqStreamConfigurator(_settings); + + callback?.Invoke(configurator); + } + + public void Stream(string consumerTag, Action callback = null) + { + if (string.IsNullOrWhiteSpace(consumerTag)) + throw new ArgumentNullException(nameof(consumerTag)); + + _settings.ConsumerTag = consumerTag; + + Stream(callback); + } + public bool Lazy { set => _settings.Lazy = value; diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqRegistrationBusFactory.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqRegistrationBusFactory.cs index 998f7bb39f8..17c5130c4bb 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqRegistrationBusFactory.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqRegistrationBusFactory.cs @@ -16,7 +16,7 @@ public class RabbitMqRegistrationBusFactory : readonly Action _configure; public RabbitMqRegistrationBusFactory(Action configure) - : this(new RabbitMqBusConfiguration(new RabbitMqTopologyConfiguration(RabbitMqBusFactory.MessageTopology)), configure) + : this(new RabbitMqBusConfiguration(new RabbitMqTopologyConfiguration(RabbitMqBusFactory.CreateMessageTopology())), configure) { } @@ -35,7 +35,7 @@ public override IBusInstance CreateBus(IBusRegistrationContext context, IEnumera var options = context.GetRequiredService>().Get(busName); - configurator.Host(options.Host, options.Port, options.VHost, h => + configurator.Host(options.Host, options.Port, options.VHost, options.ConnectionName, h => { if (!string.IsNullOrWhiteSpace(options.User)) h.Username(options.User); @@ -47,7 +47,7 @@ public override IBusInstance CreateBus(IBusRegistrationContext context, IEnumera { h.UseSsl(s => { - var sslOptions = context.GetRequiredService>().Value; + var sslOptions = context.GetRequiredService>().Get(busName); if (!string.IsNullOrWhiteSpace(sslOptions.ServerName)) s.ServerName = sslOptions.ServerName; @@ -57,6 +57,8 @@ public override IBusInstance CreateBus(IBusRegistrationContext context, IEnumera s.CertificatePassphrase = sslOptions.CertPassphrase; s.UseCertificateAsAuthenticationIdentity = sslOptions.CertIdentity; + s.Protocol = sslOptions.Protocol; + if (sslOptions.Trust) { s.AllowPolicyErrors(SslPolicyErrors.RemoteCertificateNameMismatch | SslPolicyErrors.RemoteCertificateChainErrors diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqSslConfigurator.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqSslConfigurator.cs index 69fe9793937..55b6a7634d6 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqSslConfigurator.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqSslConfigurator.cs @@ -48,22 +48,5 @@ public void EnforcePolicyErrors(SslPolicyErrors policyErrors) public LocalCertificateSelectionCallback CertificateSelectionCallback { get; set; } public RemoteCertificateValidationCallback CertificateValidationCallback { get; set; } - - /// - /// Configures the rabbit mq client connection for Sll properties. - /// - /// Builder with appropriate properties set. - /// A connection factory builder - /// - /// SSL configuration in Rabbit MQ is a complex topic. In order to ensure that rabbit can work without client presenting a client certificate - /// and working just like an SSL enabled web-site which does not require certificate you need to have the following settings in your rabbitmq.config - /// file. - /// {ssl_options, [{cacertfile,"/path_to/cacert.pem"}, - /// {certfile,"/path_to/server/cert.pem"}, - /// {keyfile,"/path_to/server/key.pem"}, - /// {verify,verify_none}, - /// {fail_if_no_peer_cert,false}]} - /// The last 2 lines are the important ones. - /// } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqStreamConfigurator.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqStreamConfigurator.cs new file mode 100644 index 00000000000..ef73b282267 --- /dev/null +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/RabbitMqStreamConfigurator.cs @@ -0,0 +1,72 @@ +#nullable enable +namespace MassTransit.RabbitMqTransport.Configuration; + +using System; + + +public class RabbitMqStreamConfigurator : + IRabbitMqStreamConfigurator +{ + readonly RabbitMqReceiveSettings _settings; + + public RabbitMqStreamConfigurator(RabbitMqReceiveSettings settings) + { + _settings = settings; + } + + public long MaxLength + { + set => _settings.QueueArguments["x-max-length-bytes"] = value; + } + + public TimeSpan MaxAge + { + set + { + string? text = null; + if (value.TotalDays >= 1) + text = $"{value.TotalDays:F0}D"; + else if (value.TotalHours >= 1) + text = $"{value.TotalHours:F0}h"; + else if (value.TotalMinutes >= 1) + text = $"{value.TotalMinutes:F0}m"; + else if (value.TotalSeconds >= 1) + text = $"{value.TotalSeconds:F0}s"; + + _settings.QueueArguments["x-max-age"] = text; + } + } + + public long MaxSegmentSize + { + set => _settings.QueueArguments["x-stream-max-segment-size-bytes"] = value; + } + + public string Filter + { + set => _settings.ConsumeArguments["x-stream-filter"] = value; + } + + public void FromOffset(long offset) + { + _settings.ConsumeArguments["x-stream-offset"] = offset; + } + + public void FromTimestamp(DateTime timestamp) + { + if (timestamp.Kind == DateTimeKind.Local) + timestamp = timestamp.ToUniversalTime(); + + _settings.ConsumeArguments.SetAmqpTimestamp("x-stream-offset", timestamp); + } + + public void FromFirst() + { + _settings.ConsumeArguments["x-stream-offset"] = "first"; + } + + public void FromLast() + { + _settings.ConsumeArguments["x-stream-offset"] = "last"; + } +} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/ExchangeBindingConsumeTopologySpecification.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/ExchangeBindingConsumeTopologySpecification.cs index 69b9c63a2e8..b14fbb0b605 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/ExchangeBindingConsumeTopologySpecification.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/ExchangeBindingConsumeTopologySpecification.cs @@ -13,7 +13,7 @@ public class ExchangeBindingConsumeTopologySpecification : IRabbitMqExchangeToExchangeBindingConfigurator, IRabbitMqConsumeTopologySpecification { - readonly IList _specifications; + readonly List _specifications; public ExchangeBindingConsumeTopologySpecification(string exchangeName, string exchangeType, bool durable = true, bool autoDelete = false) : base(exchangeName, exchangeType, durable, autoDelete) diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/ExchangeToExchangeBindingConsumeTopologySpecification.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/ExchangeToExchangeBindingConsumeTopologySpecification.cs index fe2339a2860..80917dc122a 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/ExchangeToExchangeBindingConsumeTopologySpecification.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/ExchangeToExchangeBindingConsumeTopologySpecification.cs @@ -13,7 +13,7 @@ public class ExchangeToExchangeBindingConsumeTopologySpecification : IRabbitMqExchangeToExchangeBindingConfigurator, IRabbitMqConsumeTopologySpecification { - readonly IList _specifications; + readonly List _specifications; public ExchangeToExchangeBindingConsumeTopologySpecification(string exchangeName, string exchangeType, bool durable = true, bool autoDelete = false) : base(exchangeName, exchangeType, durable, autoDelete) diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/IRoutingKeyMessageSendTopologyConvention.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/IRoutingKeyMessageSendTopologyConvention.cs deleted file mode 100644 index c062d989111..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/IRoutingKeyMessageSendTopologyConvention.cs +++ /dev/null @@ -1,13 +0,0 @@ -namespace MassTransit.RabbitMqTransport.Configuration -{ - using MassTransit.Configuration; - - - public interface IRoutingKeyMessageSendTopologyConvention : - IMessageSendTopologyConvention - where TMessage : class - { - void SetFormatter(IRoutingKeyFormatter formatter); - void SetFormatter(IMessageRoutingKeyFormatter formatter); - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/IRoutingKeySendTopologyConvention.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/IRoutingKeySendTopologyConvention.cs deleted file mode 100644 index e87ab4b2d11..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/IRoutingKeySendTopologyConvention.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit.RabbitMqTransport.Configuration -{ - using MassTransit.Configuration; - - - public interface IRoutingKeySendTopologyConvention : - ISendTopologyConvention - { - /// - /// The default, non-message specific routing key formatter used by messages - /// when no specific convention has been specified. - /// - IRoutingKeyFormatter DefaultFormatter { get; set; } - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/RoutingKeyMessageSendTopologyConvention.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/RoutingKeyMessageSendTopologyConvention.cs deleted file mode 100644 index 002483ecc07..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/RoutingKeyMessageSendTopologyConvention.cs +++ /dev/null @@ -1,47 +0,0 @@ -namespace MassTransit.RabbitMqTransport.Configuration -{ - using MassTransit.Configuration; - - - public class RoutingKeyMessageSendTopologyConvention : - IRoutingKeyMessageSendTopologyConvention - where TMessage : class - { - IMessageRoutingKeyFormatter _formatter; - - public RoutingKeyMessageSendTopologyConvention(IRoutingKeyFormatter formatter) - { - if (formatter != null) - SetFormatter(formatter); - } - - bool IMessageSendTopologyConvention.TryGetMessageSendTopology(out IMessageSendTopology messageSendTopology) - { - if (_formatter != null) - { - messageSendTopology = new SetRoutingKeyMessageSendTopology(_formatter); - return true; - } - - messageSendTopology = null; - return false; - } - - bool IMessageSendTopologyConvention.TryGetMessageSendTopologyConvention(out IMessageSendTopologyConvention convention) - { - convention = this as IMessageSendTopologyConvention; - - return convention != null; - } - - public void SetFormatter(IRoutingKeyFormatter formatter) - { - _formatter = new MessageRoutingKeyFormatter(formatter); - } - - public void SetFormatter(IMessageRoutingKeyFormatter formatter) - { - _formatter = formatter; - } - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/RoutingKeySendTopologyConvention.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/RoutingKeySendTopologyConvention.cs deleted file mode 100644 index f3c06c43e9f..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/RoutingKeySendTopologyConvention.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace MassTransit.RabbitMqTransport.Configuration -{ - using MassTransit.Configuration; - - - public class RoutingKeySendTopologyConvention : - IRoutingKeySendTopologyConvention - { - readonly ITopologyConventionCache _cache; - - public RoutingKeySendTopologyConvention() - { - DefaultFormatter = new EmptyRoutingKeyFormatter(); - - _cache = new TopologyConventionCache(typeof(IRoutingKeyMessageSendTopologyConvention<>), new Factory()); - } - - bool IMessageSendTopologyConvention.TryGetMessageSendTopologyConvention(out IMessageSendTopologyConvention convention) - { - return _cache.GetOrAdd>().TryGetMessageSendTopologyConvention(out convention); - } - - public IRoutingKeyFormatter DefaultFormatter { get; set; } - - - class Factory : - IConventionTypeFactory - { - IMessageSendTopologyConvention IConventionTypeFactory.Create() - { - return new RoutingKeyMessageSendTopologyConvention(null); - } - } - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/SetRoutingKeyMessageSendTopology.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/SetRoutingKeyMessageSendTopology.cs deleted file mode 100644 index 7bd4e3aeb18..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Configuration/Topology/SetRoutingKeyMessageSendTopology.cs +++ /dev/null @@ -1,51 +0,0 @@ -namespace MassTransit.RabbitMqTransport.Configuration -{ - using System; - using System.Threading.Tasks; - using MassTransit.Configuration; - using Middleware; - - - public class SetRoutingKeyMessageSendTopology : - IMessageSendTopology - where T : class - { - readonly IFilter> _filter; - - public SetRoutingKeyMessageSendTopology(IMessageRoutingKeyFormatter routingKeyFormatter) - { - if (routingKeyFormatter == null) - throw new ArgumentNullException(nameof(routingKeyFormatter)); - - _filter = new Proxy(new SetRoutingKeyFilter(routingKeyFormatter)); - } - - public void Apply(ITopologyPipeBuilder> builder) - { - builder.AddFilter(_filter); - } - - - class Proxy : - IFilter> - { - readonly IFilter> _filter; - - public Proxy(IFilter> filter) - { - _filter = filter; - } - - public Task Send(SendContext context, IPipe> next) - { - var rabbitMqSendContext = context.GetPayload>(); - - return _filter.Send(rabbitMqSendContext, next); - } - - public void Probe(ProbeContext context) - { - } - } - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ConnectionContextFactory.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ConnectionContextFactory.cs index 3123b7141f1..0c100c51bad 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ConnectionContextFactory.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ConnectionContextFactory.cs @@ -33,8 +33,7 @@ IPipeContextAgent IPipeContextFactory.Crea void HandleShutdown(object sender, ShutdownEventArgs args) { - if (args.Initiator != ShutdownInitiator.Application) - contextHandle.Stop(args.ReplyText); + contextHandle.Stop(args.ReplyText); } context.ContinueWith(task => diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ConnectionContextSupervisor.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ConnectionContextSupervisor.cs index bcd76f24891..b55430006e3 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ConnectionContextSupervisor.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ConnectionContextSupervisor.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using Configuration; using Middleware; - using Topology; using Transports; @@ -40,7 +39,7 @@ public Task CreateSendTransport(RabbitMqReceiveEndpointContext r var brokerTopology = settings.GetBrokerTopology(); - IPipe configureTopology = new ConfigureRabbitMqTopologyFilter(settings, brokerTopology).ToPipe(); + var configureTopology = new ConfigureRabbitMqTopologyFilter(settings, brokerTopology); return CreateSendTransport(receiveEndpointContext, modelContextSupervisor, configureTopology, settings.ExchangeName, endpointAddress); } @@ -57,7 +56,7 @@ public Task CreatePublishTransport(RabbitMqReceiveEndpointCon var brokerTopology = publishTopology.GetBrokerTopology(); - IPipe configureTopology = new ConfigureRabbitMqTopologyFilter(settings, brokerTopology).ToPipe(); + var configureTopology = new ConfigureRabbitMqTopologyFilter(settings, brokerTopology); var endpointAddress = settings.GetSendAddress(_hostConfiguration.HostAddress); @@ -66,19 +65,15 @@ public Task CreatePublishTransport(RabbitMqReceiveEndpointCon } Task CreateSendTransport(ReceiveEndpointContext receiveEndpointContext, IModelContextSupervisor modelContextSupervisor, - IPipe pipe, string exchangeName, RabbitMqEndpointAddress endpointAddress) + ConfigureRabbitMqTopologyFilter filter, string exchangeName, RabbitMqEndpointAddress endpointAddress) { var supervisor = new ModelContextSupervisor(modelContextSupervisor); - var delayedExchangeAddress = endpointAddress.GetDelayAddress(); - - var delaySettings = new RabbitMqDelaySettings(delayedExchangeAddress); - - delaySettings.BindToExchange(exchangeName); + var delaySettings = endpointAddress.GetDelaySettings(); IPipe delayPipe = new ConfigureRabbitMqTopologyFilter(delaySettings, delaySettings.GetBrokerTopology()).ToPipe(); - var sendTransportContext = new RabbitMqSendTransportContext(_hostConfiguration, receiveEndpointContext, supervisor, pipe, exchangeName, + var sendTransportContext = new RabbitMqSendTransportContext(_hostConfiguration, receiveEndpointContext, supervisor, filter, exchangeName, delayPipe, delaySettings.ExchangeName); var transport = new SendTransport(sendTransportContext); diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/DelegateRoutingKeyFormatter.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/DelegateRoutingKeyFormatter.cs deleted file mode 100644 index a09e686ff83..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/DelegateRoutingKeyFormatter.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace MassTransit.RabbitMqTransport -{ - using System; - - - public class DelegateRoutingKeyFormatter : - IMessageRoutingKeyFormatter - where TMessage : class - { - readonly Func, string> _formatter; - - public DelegateRoutingKeyFormatter(Func, string> formatter) - { - _formatter = formatter; - } - - public string FormatRoutingKey(RabbitMqSendContext context) - { - return _formatter(context) ?? ""; - } - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/EmptyRoutingKeyFormatter.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/EmptyRoutingKeyFormatter.cs deleted file mode 100644 index fc2757929aa..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/EmptyRoutingKeyFormatter.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MassTransit.RabbitMqTransport -{ - public class EmptyRoutingKeyFormatter : - IRoutingKeyFormatter - { - public string FormatRoutingKey(RabbitMqSendContext context) - where T : class - { - return context.RoutingKey; - } - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/IMessageRoutingKeyFormatter.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/IMessageRoutingKeyFormatter.cs deleted file mode 100644 index bcceccfabed..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/IMessageRoutingKeyFormatter.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MassTransit.RabbitMqTransport -{ - public interface IMessageRoutingKeyFormatter - where TMessage : class - { - string FormatRoutingKey(RabbitMqSendContext context); - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/IRoutingKeyFormatter.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/IRoutingKeyFormatter.cs deleted file mode 100644 index b1ec1abceea..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/IRoutingKeyFormatter.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace MassTransit.RabbitMqTransport -{ - public interface IRoutingKeyFormatter - { - /// - /// Format the routing key for the send context, so that it can be passed to RabbitMQ - /// - /// The message type - /// The message send context - /// The routing key to specify in the transport - string FormatRoutingKey(RabbitMqSendContext context) - where T : class; - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ImmediatePublisher.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ImmediatePublisher.cs index 53626ff8f00..ac6eb4704c2 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ImmediatePublisher.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ImmediatePublisher.cs @@ -10,10 +10,10 @@ public class ImmediatePublisher : IPublisher { readonly PendingConfirmationCollection _confirmations; - readonly ChannelExecutor _executor; + readonly TaskExecutor _executor; readonly IModel _model; - public ImmediatePublisher(ChannelExecutor executor, IModel model, PendingConfirmationCollection confirmations) + public ImmediatePublisher(TaskExecutor executor, IModel model, PendingConfirmationCollection confirmations) { _executor = executor; _model = model; diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/MessageRoutingKeyFormatter.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/MessageRoutingKeyFormatter.cs deleted file mode 100644 index b0261985758..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/MessageRoutingKeyFormatter.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace MassTransit.RabbitMqTransport -{ - public class MessageRoutingKeyFormatter : - IMessageRoutingKeyFormatter - where TMessage : class - { - readonly IRoutingKeyFormatter _formatter; - - public MessageRoutingKeyFormatter(IRoutingKeyFormatter formatter) - { - _formatter = formatter; - } - - public string FormatRoutingKey(RabbitMqSendContext context) - { - return _formatter.FormatRoutingKey(context); - } - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Middleware/ConfigureRabbitMqTopologyFilter.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Middleware/ConfigureRabbitMqTopologyFilter.cs index a7afb82b865..ea12df2c3e6 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Middleware/ConfigureRabbitMqTopologyFilter.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Middleware/ConfigureRabbitMqTopologyFilter.cs @@ -1,107 +1,113 @@ -namespace MassTransit.RabbitMqTransport.Middleware +namespace MassTransit.RabbitMqTransport.Middleware; + +using System; +using System.Linq; +using System.Threading.Tasks; +using Topology; + + +/// +/// Configures the broker with the supplied topology once the model is created, to ensure +/// that the exchanges, queues, and bindings for the model are properly configured in RabbitMQ. +/// +public class ConfigureRabbitMqTopologyFilter : + IFilter + where TSettings : class { - using System; - using System.Linq; - using System.Threading.Tasks; - using Topology; - - - /// - /// Configures the broker with the supplied topology once the model is created, to ensure - /// that the exchanges, queues, and bindings for the model are properly configured in RabbitMQ. - /// - public class ConfigureRabbitMqTopologyFilter : - IFilter - where TSettings : class + readonly BrokerTopology _brokerTopology; + readonly TSettings _settings; + + public ConfigureRabbitMqTopologyFilter(TSettings settings, BrokerTopology brokerTopology) { - readonly BrokerTopology _brokerTopology; - readonly TSettings _settings; + _settings = settings; + _brokerTopology = brokerTopology; + } - public ConfigureRabbitMqTopologyFilter(TSettings settings, BrokerTopology brokerTopology) - { - _settings = settings; - _brokerTopology = brokerTopology; - } + public async Task Send(ModelContext context, IPipe next) + { + OneTimeContext> oneTimeContext = await Configure(context); - async Task IFilter.Send(ModelContext context, IPipe next) + try { - await context.OneTimeSetup>(async payload => - { - await ConfigureTopology(context).ConfigureAwait(false); - - context.GetOrAddPayload(() => _settings); - }, () => new Context()).ConfigureAwait(false); - await next.Send(context).ConfigureAwait(false); } - - void IProbeSite.Probe(ProbeContext context) + catch (Exception) { - var scope = context.CreateFilterScope("configureTopology"); + oneTimeContext.Evict(); - _brokerTopology.Probe(scope); + throw; } + } - async Task ConfigureTopology(ModelContext context) + public async Task>> Configure(ModelContext context) + { + return await context.OneTimeSetup>(() => { - await Task.WhenAll(_brokerTopology.Queues.Select(queue => Declare(context, (Queue)queue))).ConfigureAwait(false); - - await Task.WhenAll(_brokerTopology.Exchanges.Select(exchange => Declare(context, exchange))).ConfigureAwait(false); + context.GetOrAddPayload(() => _settings); + return ConfigureTopology(context); + }).ConfigureAwait(false); + } - await Task.WhenAll(_brokerTopology.QueueBindings.Select(binding => Bind(context, binding))).ConfigureAwait(false); + public void Probe(ProbeContext context) + { + var scope = context.CreateFilterScope("configureTopology"); - await Task.WhenAll(_brokerTopology.ExchangeBindings.Select(binding => Bind(context, binding))).ConfigureAwait(false); - } + _brokerTopology.Probe(scope); + } - static Task Declare(ModelContext context, Exchange exchange) - { - RabbitMqLogMessages.DeclareExchange(exchange); + async Task ConfigureTopology(ModelContext context) + { + await Task.WhenAll(_brokerTopology.Queues.Select(queue => Declare(context, queue))).ConfigureAwait(false); - return context.ExchangeDeclare(exchange.ExchangeName, exchange.ExchangeType, exchange.Durable, exchange.AutoDelete, exchange.ExchangeArguments); - } + await Task.WhenAll(_brokerTopology.Exchanges.Select(exchange => Declare(context, exchange))).ConfigureAwait(false); - static async Task Declare(ModelContext context, Queue queue) - { - try - { - var ok = await context.QueueDeclare(queue.QueueName, queue.Durable, queue.Exclusive, queue.AutoDelete, queue.QueueArguments) - .ConfigureAwait(false); + await Task.WhenAll(_brokerTopology.QueueBindings.Select(binding => Bind(context, binding))).ConfigureAwait(false); - RabbitMqLogMessages.DeclareQueue(queue, ok.ConsumerCount, ok.MessageCount); + await Task.WhenAll(_brokerTopology.ExchangeBindings.Select(binding => Bind(context, binding))).ConfigureAwait(false); + } - await Task.Delay(10).ConfigureAwait(false); - } - catch (Exception exception) - { - LogContext.Error?.Log(exception, "Declare queue faulted: {Queue}", queue); + static Task Declare(ModelContext context, Exchange exchange) + { + RabbitMqLogMessages.DeclareExchange(exchange); - throw; - } - } + return context.ExchangeDeclare(exchange.ExchangeName, exchange.ExchangeType, exchange.Durable, exchange.AutoDelete, exchange.ExchangeArguments); + } - static async Task Bind(ModelContext context, ExchangeToExchangeBinding binding) + static async Task Declare(ModelContext context, Queue queue) + { + try { - RabbitMqLogMessages.BindToExchange(binding); - - await context.ExchangeBind(binding.Destination.ExchangeName, binding.Source.ExchangeName, binding.RoutingKey, binding.Arguments) + var ok = await context.QueueDeclare(queue.QueueName, queue.Durable, queue.Exclusive, queue.AutoDelete, queue.QueueArguments) .ConfigureAwait(false); + RabbitMqLogMessages.DeclareQueue(queue, ok.ConsumerCount, ok.MessageCount); + await Task.Delay(10).ConfigureAwait(false); } - - static async Task Bind(ModelContext context, ExchangeToQueueBinding binding) + catch (Exception exception) { - RabbitMqLogMessages.BindToQueue(binding); + LogContext.Error?.Log(exception, "Declare queue faulted: {Queue}", queue); - await context.QueueBind(binding.Destination.QueueName, binding.Source.ExchangeName, binding.RoutingKey, binding.Arguments).ConfigureAwait(false); - - await Task.Delay(10).ConfigureAwait(false); + throw; } + } + static async Task Bind(ModelContext context, ExchangeToExchangeBinding binding) + { + RabbitMqLogMessages.BindToExchange(binding); - class Context : - ConfigureTopologyContext - { - } + await context.ExchangeBind(binding.Destination.ExchangeName, binding.Source.ExchangeName, binding.RoutingKey, binding.Arguments) + .ConfigureAwait(false); + + await Task.Delay(10).ConfigureAwait(false); + } + + static async Task Bind(ModelContext context, ExchangeToQueueBinding binding) + { + RabbitMqLogMessages.BindToQueue(binding); + + await context.QueueBind(binding.Destination.QueueName, binding.Source.ExchangeName, binding.RoutingKey, binding.Arguments).ConfigureAwait(false); + + await Task.Delay(10).ConfigureAwait(false); } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Middleware/SetRoutingKeyFilter.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Middleware/SetRoutingKeyFilter.cs deleted file mode 100644 index 141fddece90..00000000000 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Middleware/SetRoutingKeyFilter.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace MassTransit.RabbitMqTransport.Middleware -{ - using System.Threading.Tasks; - - - public class SetRoutingKeyFilter : - IFilter> - where T : class - { - readonly IMessageRoutingKeyFormatter _routingKeyFormatter; - - public SetRoutingKeyFilter(IMessageRoutingKeyFormatter routingKeyFormatter) - { - _routingKeyFormatter = routingKeyFormatter; - } - - public Task Send(RabbitMqSendContext context, IPipe> next) - { - var routingKey = _routingKeyFormatter.FormatRoutingKey(context); - - context.RoutingKey = routingKey; - - return next.Send(context); - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("SetCorrelationId"); - } - } -} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ModelContext.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ModelContext.cs index 1df4754efd9..193d3319301 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ModelContext.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ModelContext.cs @@ -52,7 +52,8 @@ public interface ModelContext : Task BasicNack(ulong deliveryTag, bool multiple, bool requeue); - Task BasicConsume(string queue, bool noAck, bool exclusive, IDictionary arguments, IBasicConsumer consumer, string consumerTag); + Task BasicConsume(string queue, bool noAck, bool exclusive, IDictionary arguments, IBasicConsumer consumer, + string consumerTag); Task BasicCancel(string consumerTag); } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ModelContextFactory.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ModelContextFactory.cs index f933272b5fc..4e721a66187 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ModelContextFactory.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/ModelContextFactory.cs @@ -25,8 +25,7 @@ IPipeContextAgent IPipeContextFactory.CreateContext( void HandleShutdown(object sender, ShutdownEventArgs args) { - if (args.Initiator != ShutdownInitiator.Application) - asyncContext.Stop(args.ReplyText); + asyncContext.Stop(args.ReplyText); } context.ContinueWith(task => diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/OperationInterruptedExceptionExtensions.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/OperationInterruptedExceptionExtensions.cs index bd5d252cd80..ab400682abf 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/OperationInterruptedExceptionExtensions.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/OperationInterruptedExceptionExtensions.cs @@ -13,6 +13,7 @@ public static bool ChannelShouldBeClosed(this OperationInterruptedException ex) 404 => true, // not found 405 => true, // locked 406 => true, // precondition failed + 491 => true, // the channel was already closed (MT-internal) _ => false }; } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqAddressExtensions.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqAddressExtensions.cs index 24b3c0e53ee..3f8dd02e3f4 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqAddressExtensions.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqAddressExtensions.cs @@ -17,7 +17,7 @@ public static ReceiveSettings GetReceiveSettings(this Uri address) var hostAddress = new RabbitMqHostAddress(address); var endpointAddress = new RabbitMqEndpointAddress(hostAddress, address); - var topologyConfiguration = new RabbitMqTopologyConfiguration(RabbitMqBusFactory.MessageTopology); + var topologyConfiguration = new RabbitMqTopologyConfiguration(RabbitMqBusFactory.CreateMessageTopology()); var endpointConfiguration = new RabbitMqEndpointConfiguration(topologyConfiguration); var settings = new RabbitMqReceiveSettings(endpointConfiguration, endpointAddress.Name, endpointAddress.ExchangeType, endpointAddress.Durable, endpointAddress.AutoDelete) @@ -61,11 +61,12 @@ public static ConnectionFactory GetConnectionFactory(this RabbitMqHostSettings s if (settings.UseClientCertificateAsAuthenticationIdentity) { - factory.AuthMechanisms.Clear(); - factory.AuthMechanisms.Add(new ExternalMechanismFactory()); + factory.AuthMechanisms = new List { new ExternalMechanismFactory() }; factory.UserName = ""; factory.Password = ""; } + else if (settings.CredentialsProvider != null) + factory.CredentialsProvider = settings.CredentialsProvider; else { if (!string.IsNullOrWhiteSpace(settings.Username)) @@ -75,6 +76,8 @@ public static ConnectionFactory GetConnectionFactory(this RabbitMqHostSettings s factory.Password = settings.Password; } + factory.CredentialsRefresher = settings.CredentialsRefresher; + ApplySslOptions(settings, factory.Ssl); factory.ClientProperties ??= new Dictionary(); @@ -168,5 +171,10 @@ static string UriDecode(string uri) { return Uri.UnescapeDataString(uri.Replace("+", "%2B")); } + + public static bool IsReplyToAddress(this Uri address) + { + return address?.AbsolutePath?.EndsWith(RabbitMqExchangeNames.ReplyTo) ?? false; + } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqBasicConsumer.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqBasicConsumer.cs index 89d2440d95e..0b3d737453a 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqBasicConsumer.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqBasicConsumer.cs @@ -1,32 +1,27 @@ namespace MassTransit.RabbitMqTransport { using System; - using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; - using Internals; - using MassTransit.Middleware; + using Context; using RabbitMQ.Client; using RabbitMQ.Client.Events; using Transports; - using Util; /// /// Receives messages from RabbitMQ, pushing them to the InboundPipe of the service endpoint. /// public class RabbitMqBasicConsumer : - Agent, + ConsumerAgent, IAsyncBasicConsumer, IBasicConsumer, RabbitMqDeliveryMetrics { readonly RabbitMqReceiveEndpointContext _context; - readonly TaskCompletionSource _deliveryComplete; - readonly IReceivePipeDispatcher _dispatcher; readonly SemaphoreSlim _limit; + readonly ModelContext _model; - readonly ConcurrentDictionary _pending; readonly ReceiveSettings _receiveSettings; string _consumerTag; @@ -39,23 +34,19 @@ public class RabbitMqBasicConsumer : /// The model context for the consumer /// The topology public RabbitMqBasicConsumer(ModelContext model, RabbitMqReceiveEndpointContext context) + : base(context) { _model = model; _context = context; _receiveSettings = model.GetPayload(); - _pending = new ConcurrentDictionary(); - - _dispatcher = context.CreateReceivePipeDispatcher(); - _dispatcher.ZeroActivity += HandleDeliveryComplete; - - _deliveryComplete = TaskUtil.GetTask(); - if (context.ConcurrentMessageLimit.HasValue) _limit = new SemaphoreSlim(context.ConcurrentMessageLimit.Value); ConsumerCancelled += OnConsumerCancelled; + + TrySetManualConsumeTask(); } /// @@ -128,13 +119,7 @@ public void HandleBasicCancelOk(string consumerTag) LogContext.Debug?.Log("Consumer Cancel Ok: {InputAddress} - {ConsumerTag}", _context.InputAddress, consumerTag); - if (_dispatcher.ActiveDispatchCount == 0) - { - _deliveryComplete.TrySetResult(true); - SetCompleted(Task.CompletedTask); - } - else - SetCompleted(_deliveryComplete.Task); + TrySetConsumeCompleted(); } /// @@ -148,17 +133,9 @@ public void HandleBasicCancel(string consumerTag) LogContext.Debug?.Log("Consumer Canceled: {InputAddress} - {ConsumerTag}", _context.InputAddress, consumerTag); - CancelPendingConsumers(); - - ConsumerCancelled?.Invoke(this, new ConsumerEventArgs(new[] {consumerTag})); + ConsumerCancelled?.Invoke(this, new ConsumerEventArgs(new[] { consumerTag })); - if (_dispatcher.ActiveDispatchCount == 0) - { - _deliveryComplete.TrySetResult(true); - SetCompleted(Task.CompletedTask); - } - else - SetCompleted(_deliveryComplete.Task); + TrySetConsumeCanceled(); } public void HandleModelShutdown(object model, ShutdownEventArgs reason) @@ -167,17 +144,23 @@ public void HandleModelShutdown(object model, ShutdownEventArgs reason) LogContext.Debug?.Log( "Consumer Model Shutdown: {InputAddress} - {ConsumerTag}, Concurrent Peak: {MaxConcurrentDeliveryCount}, {ReplyCode}-{ReplyText}", - _context.InputAddress, _consumerTag, _dispatcher.MaxConcurrentDispatchCount, reason.ReplyCode, reason.ReplyText); - - CancelPendingConsumers(); + _context.InputAddress, _consumerTag, ConcurrentDeliveryCount, reason.ReplyCode, reason.ReplyText); - _deliveryComplete.TrySetResult(false); - SetCompleted(Task.CompletedTask); + TrySetConsumeCanceled(); } public void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redelivered, string exchange, string routingKey, IBasicProperties properties, ReadOnlyMemory body) { + try + { + _limit?.Wait(Stopping); + } + catch (OperationCanceledException) + { + return; + } + var bodyBytes = body.ToArray(); Task.Run(async () => @@ -187,18 +170,14 @@ public void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redel var context = new RabbitMqReceiveContext(exchange, routingKey, _consumerTag, deliveryTag, bodyBytes, redelivered, properties, _context, _receiveSettings, _model, _model.ConnectionContext); - var added = _pending.TryAdd(deliveryTag, context); - if (!added && deliveryTag != 1) // DIRECT REPLY-TO fixed value - LogContext.Warning?.Log("Duplicate BasicDeliver: {DeliveryTag}", deliveryTag); - - var receiveLock = _receiveSettings.NoAck ? default : new RabbitMqReceiveLockContext(_model, deliveryTag); - - if (_limit != null) - await _limit.WaitAsync(context.CancellationToken).ConfigureAwait(false); - try { - await _dispatcher.Dispatch(context, receiveLock).ConfigureAwait(false); + if (IsStopping) + return; + + await Dispatch(deliveryTag, context, + _receiveSettings.NoAck ? NoLockReceiveContext.Instance : new RabbitMqReceiveLockContext(_model, deliveryTag)) + .ConfigureAwait(false); } catch (Exception exception) { @@ -208,9 +187,6 @@ public void HandleBasicDeliver(string consumerTag, ulong deliveryTag, bool redel { _limit?.Release(); - if (added) - _pending.TryRemove(deliveryTag, out _); - context.Dispose(); } }); @@ -224,14 +200,9 @@ event EventHandler IBasicConsumer.ConsumerCancelled string RabbitMqDeliveryMetrics.ConsumerTag => _consumerTag; - long DeliveryMetrics.DeliveryCount => _dispatcher.DispatchCount; - - int DeliveryMetrics.ConcurrentDeliveryCount => _dispatcher.MaxConcurrentDispatchCount; - - void CancelPendingConsumers() + protected override bool IsTrackable(ulong deliveryTag) { - foreach (var context in _pending.Values) - context.Cancel(); + return deliveryTag != 1 || _context.IsNotReplyTo; } Task OnConsumerCancelled(object obj, ConsumerEventArgs args) @@ -241,58 +212,31 @@ Task OnConsumerCancelled(object obj, ConsumerEventArgs args) return Task.CompletedTask; } - Task HandleDeliveryComplete() - { - if (IsStopping) - _deliveryComplete.TrySetResult(true); - - return Task.CompletedTask; - } - protected override async Task StopAgent(StopContext context) { - LogContext.Debug?.Log("Stopping Consumer: {InputAddress} - {ConsumerTag}", _context.InputAddress, _consumerTag); - - await CancelAndWaitForDeliveryComplete(context).ConfigureAwait(false); - try { - await Completed.ConfigureAwait(false); - - _limit?.Dispose(); + await base.StopAgent(context).ConfigureAwait(false); } - catch (OperationCanceledException) + finally { - foreach (var pendingContext in _pending.Values) - pendingContext.Cancel(); - - throw; + _limit?.Dispose(); } } - async Task CancelAndWaitForDeliveryComplete(StopContext context) + protected override async Task ActiveAndActualAgentsCompleted(StopContext context) { try { - await _model.BasicCancel(_consumerTag).ConfigureAwait(false); + if (IsGracefulShutdown) + await _model.BasicCancel(_consumerTag).ConfigureAwait(false); } catch (Exception exception) { LogContext.Warning?.Log(exception, "BasicCancel faulted: {InputAddress} - {ConsumerTag}", _context.InputAddress, _consumerTag); } - if (_dispatcher.ActiveDispatchCount > 0) - { - try - { - await _deliveryComplete.Task.OrCanceled(context.CancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - LogContext.Warning?.Log("Stop canceled waiting for message consumers to complete: {InputAddress} - {ConsumerTag}", - _context.InputAddress, _consumerTag); - } - } + await base.ActiveAndActualAgentsCompleted(context).ConfigureAwait(false); } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqConnectionContext.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqConnectionContext.cs index 2a53c6a6e92..6b79531e21a 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqConnectionContext.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqConnectionContext.cs @@ -15,7 +15,7 @@ public class RabbitMqConnectionContext : ConnectionContext, IAsyncDisposable { - readonly ChannelExecutor _executor; + readonly TaskExecutor _executor; public RabbitMqConnectionContext(IConnection connection, IRabbitMqHostConfiguration hostConfiguration, string description, CancellationToken cancellationToken) @@ -34,7 +34,7 @@ public RabbitMqConnectionContext(IConnection connection, IRabbitMqHostConfigurat StopTimeout = TimeSpan.FromSeconds(30); - _executor = new ChannelExecutor(1); + _executor = new TaskExecutor(); connection.ConnectionShutdown += OnConnectionShutdown; } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqDeadLetterTransport.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqDeadLetterTransport.cs index 76e2ec16fb1..a7c1c160397 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqDeadLetterTransport.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqDeadLetterTransport.cs @@ -1,15 +1,16 @@ namespace MassTransit.RabbitMqTransport { using System.Threading.Tasks; + using Middleware; using RabbitMQ.Client; using Transports; public class RabbitMqDeadLetterTransport : - RabbitMqMoveTransport, + RabbitMqMoveTransport, IDeadLetterTransport { - public RabbitMqDeadLetterTransport(string exchange, IFilter topologyFilter) + public RabbitMqDeadLetterTransport(string exchange, ConfigureRabbitMqTopologyFilter topologyFilter) : base(exchange, topologyFilter) { } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqErrorTransport.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqErrorTransport.cs index 3ff2237b20c..5bd7905e25d 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqErrorTransport.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqErrorTransport.cs @@ -1,15 +1,16 @@ namespace MassTransit.RabbitMqTransport { using System.Threading.Tasks; + using Middleware; using RabbitMQ.Client; using Transports; public class RabbitMqErrorTransport : - RabbitMqMoveTransport, + RabbitMqMoveTransport, IErrorTransport { - public RabbitMqErrorTransport(string exchange, IFilter topologyFilter) + public RabbitMqErrorTransport(string exchange, ConfigureRabbitMqTopologyFilter topologyFilter) : base(exchange, topologyFilter) { } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqHeaderProvider.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqHeaderProvider.cs index 80994ff0697..a6dae460cf2 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqHeaderProvider.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqHeaderProvider.cs @@ -3,12 +3,15 @@ namespace MassTransit.RabbitMqTransport using System; using System.Collections.Generic; using System.Text; + using Initializers.TypeConverters; using Transports; public class RabbitMqHeaderProvider : IHeaderProvider { + static readonly DateTimeTypeConverter _dateTimeConverter = new DateTimeTypeConverter(); + readonly RabbitMqBasicConsumeContext _context; public RabbitMqHeaderProvider(RabbitMqBasicConsumeContext context) @@ -69,6 +72,15 @@ public bool TryGetHeader(string key, out object value) return value != default; } + if (MessageHeaders.TransportSentTime.Equals(key, StringComparison.OrdinalIgnoreCase) && _context.Properties.IsTimestampPresent()) + { + if (_dateTimeConverter.TryConvert(_context.Properties.Timestamp.UnixTime, out var result)) + { + value = result; + return true; + } + } + if (RabbitMqHeaders.Exchange.Equals(key, StringComparison.OrdinalIgnoreCase)) { value = _context.Exchange; diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMessageNameFormatter.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMessageNameFormatter.cs index d4ccdf97cf3..315754449a4 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMessageNameFormatter.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMessageNameFormatter.cs @@ -14,7 +14,7 @@ public RabbitMqMessageNameFormatter() _formatter = new DefaultMessageNameFormatter("::", "--", ":", "-"); } - public MessageName GetMessageName(Type type) + public string GetMessageName(Type type) { return _formatter.GetMessageName(type); } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMessageSendContext.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMessageSendContext.cs index dc115addd46..b74ed87a006 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMessageSendContext.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMessageSendContext.cs @@ -32,48 +32,35 @@ public override void ReadPropertiesFrom(IReadOnlyDictionary prop { base.ReadPropertiesFrom(properties); - Exchange = ReadString(properties, PropertyNames.Exchange, Exchange); - RoutingKey = ReadString(properties, PropertyNames.RoutingKey, ""); - - BasicProperties.AppId = ReadString(properties, PropertyNames.AppId); - BasicProperties.Priority = ReadByte(properties, PropertyNames.Priority); - BasicProperties.ReplyTo = ReadString(properties, PropertyNames.ReplyTo); - BasicProperties.Type = ReadString(properties, PropertyNames.Type); - BasicProperties.UserId = ReadString(properties, PropertyNames.UserId); + Exchange = ReadString(properties, RabbitMqTransportPropertyNames.Exchange, Exchange); + RoutingKey = ReadString(properties, RabbitMqTransportPropertyNames.RoutingKey, ""); + + BasicProperties.AppId = ReadString(properties, RabbitMqTransportPropertyNames.AppId); + BasicProperties.Priority = ReadByte(properties, RabbitMqTransportPropertyNames.Priority); + BasicProperties.ReplyTo = ReadString(properties, RabbitMqTransportPropertyNames.ReplyTo); + BasicProperties.Type = ReadString(properties, RabbitMqTransportPropertyNames.Type); + BasicProperties.UserId = ReadString(properties, RabbitMqTransportPropertyNames.UserId); } public override void WritePropertiesTo(IDictionary properties) { base.WritePropertiesTo(properties); - properties[PropertyNames.Exchange] = Exchange; + properties[RabbitMqTransportPropertyNames.Exchange] = Exchange; if (!string.IsNullOrWhiteSpace(RoutingKey)) - properties[PropertyNames.RoutingKey] = RoutingKey; + properties[RabbitMqTransportPropertyNames.RoutingKey] = RoutingKey; if (BasicProperties.IsAppIdPresent()) - properties[PropertyNames.AppId] = BasicProperties.AppId; + properties[RabbitMqTransportPropertyNames.AppId] = BasicProperties.AppId; if (BasicProperties.IsPriorityPresent()) - properties[PropertyNames.Priority] = BasicProperties.Priority; + properties[RabbitMqTransportPropertyNames.Priority] = BasicProperties.Priority; if (BasicProperties.IsReplyToPresent()) - properties[PropertyNames.ReplyTo] = BasicProperties.ReplyTo; + properties[RabbitMqTransportPropertyNames.ReplyTo] = BasicProperties.ReplyTo; if (BasicProperties.IsTypePresent()) - properties[PropertyNames.Type] = BasicProperties.Type; + properties[RabbitMqTransportPropertyNames.Type] = BasicProperties.Type; if (BasicProperties.IsUserIdPresent()) - properties[PropertyNames.UserId] = BasicProperties.UserId; - } - - - static class PropertyNames - { - public const string Exchange = "RMQ-Exchange"; - public const string RoutingKey = "RMQ-RoutingKey"; - - public const string AppId = "RMQ-AppId"; - public const string Priority = "RMQ-Priority"; - public const string ReplyTo = "RMQ-ReplyTo"; - public const string Type = "RMQ-Type"; - public const string UserId = "RMQ-UserId"; + properties[RabbitMqTransportPropertyNames.UserId] = BasicProperties.UserId; } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqModelContext.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqModelContext.cs index b461d988fb0..d1521d21b3d 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqModelContext.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqModelContext.cs @@ -20,7 +20,7 @@ public class RabbitMqModelContext : { readonly PendingConfirmationCollection _confirmations; readonly ConnectionContext _connectionContext; - readonly ChannelExecutor _executor; + readonly TaskExecutor _executor; readonly IModel _model; readonly IPublisher _publisher; @@ -39,10 +39,10 @@ public RabbitMqModelContext(ConnectionContext connectionContext, IModel model, C _model.ContinuationTimeout = _connectionContext.ContinuationTimeout; - _executor = new ChannelExecutor(1); + _executor = new TaskExecutor(1); _publisher = connectionContext.BatchSettings.Enabled - ? (IPublisher)new BatchPublisher(_executor, model, connectionContext.BatchSettings, _confirmations) + ? new BatchPublisher(_executor, model, connectionContext.BatchSettings, _confirmations) : new ImmediatePublisher(_executor, model, _confirmations); _model.ModelShutdown += OnModelShutdown; @@ -132,9 +132,15 @@ public Task BasicQos(uint prefetchSize, ushort prefetchCount, bool global) public Task BasicAck(ulong deliveryTag, bool multiple) { - return _model.IsClosed - ? TaskUtil.Faulted(new InvalidOperationException($"The channel was closed: {_model.CloseReason} {_model.ChannelNumber}")) - : _executor.Run(() => _model.BasicAck(deliveryTag, multiple), CancellationToken); + if (_model.IsClosed) + { + return TaskUtil.Faulted(new OperationInterruptedException(new ShutdownEventArgs(ShutdownInitiator.Peer, 491, + $"Channel is already closed: {_model.CloseReason}"))); + } + + _model.BasicAck(deliveryTag, multiple); + + return Task.CompletedTask; } public async Task BasicNack(ulong deliveryTag, bool multiple, bool requeue) @@ -144,7 +150,7 @@ public async Task BasicNack(ulong deliveryTag, bool multiple, bool requeue) try { - await _executor.Run(() => _model.BasicNack(deliveryTag, multiple, requeue), CancellationToken).ConfigureAwait(false); + _model.BasicNack(deliveryTag, multiple, requeue); } catch (ChannelClosedException) // if we are shutting down, the broker would already nack prefetched messages anyway { @@ -157,9 +163,9 @@ public Task BasicConsume(string queue, bool noAck, bool exclusive, IDict return RunRpc(() => _model.BasicConsume(consumer, queue, noAck, consumerTag, false, exclusive, arguments), CancellationToken); } - public Task BasicCancel(string consumerTag) + public async Task BasicCancel(string consumerTag) { - return _executor.Run(() => _model.BasicCancel(consumerTag), CancellationToken); + await _executor.Run(() => _model.BasicCancel(consumerTag), CancellationToken); } void OnBasicReturn(object model, BasicReturnEventArgs args) @@ -206,7 +212,7 @@ void OnAcknowledged(object model, BasicAckEventArgs args) async Task RunRpc(Action callback, CancellationToken cancellationToken) { if (_model.IsClosed) - throw new InvalidOperationException($"The channel was closed: {_model.CloseReason} {_model.ChannelNumber}"); + throw new OperationInterruptedException(new ShutdownEventArgs(ShutdownInitiator.Peer, 491, $"Channel is already closed: {_model.CloseReason}")); try { @@ -227,7 +233,7 @@ async Task RunRpc(Action callback, CancellationToken cancellationToken) async Task RunRpc(Func callback, CancellationToken cancellationToken) { if (_model.IsClosed) - throw new InvalidOperationException($"The channel was closed: {_model.CloseReason} {_model.ChannelNumber}"); + throw new OperationInterruptedException(new ShutdownEventArgs(ShutdownInitiator.Peer, 491, $"Channel is already closed: {_model.CloseReason}")); try { diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMoveTransport.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMoveTransport.cs index 562425a5f34..3dffba808aa 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMoveTransport.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqMoveTransport.cs @@ -3,15 +3,17 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; + using Middleware; using RabbitMQ.Client; - public class RabbitMqMoveTransport + public class RabbitMqMoveTransport + where TSettings : class { readonly string _exchange; - readonly IFilter _topologyFilter; + readonly ConfigureRabbitMqTopologyFilter _topologyFilter; - protected RabbitMqMoveTransport(string exchange, IFilter topologyFilter) + protected RabbitMqMoveTransport(string exchange, ConfigureRabbitMqTopologyFilter topologyFilter) { _topologyFilter = topologyFilter; _exchange = exchange; @@ -22,7 +24,7 @@ protected async Task Move(ReceiveContext context, Action()).ConfigureAwait(false); + OneTimeContext> oneTimeContext = await _topologyFilter.Configure(modelContext).ConfigureAwait(false); IBasicProperties properties; var routingKey = ""; @@ -48,8 +50,15 @@ protected async Task Move(ReceiveContext context, Action(() => new ModelContextSupervisor(hostConfiguration.ConnectionContextSupervisor)); } public BrokerTopology BrokerTopology { get; } public bool ExclusiveConsumer { get; } + public bool IsNotReplyTo { get; } public IModelContextSupervisor ModelContextSupervisor => _modelContext.Supervisor; diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqReceiveContext.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqReceiveContext.cs index fe6f3dcf57b..63c4b7a930d 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqReceiveContext.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqReceiveContext.cs @@ -1,15 +1,19 @@ namespace MassTransit.RabbitMqTransport { using System; + using System.Collections.Generic; using System.Net.Mime; using System.Threading.Tasks; + using Context; using RabbitMQ.Client; using Transports; public sealed class RabbitMqReceiveContext : BaseReceiveContext, - RabbitMqBasicConsumeContext + RabbitMqBasicConsumeContext, + TransportReceiveContext, + ITransportSequenceNumber { public RabbitMqReceiveContext(string exchange, string routingKey, string consumerTag, ulong deliveryTag, byte[] body, bool redelivered, IBasicProperties properties, RabbitMqReceiveEndpointContext receiveEndpointContext, params object[] payloads) @@ -34,7 +38,28 @@ public RabbitMqReceiveContext(string exchange, string routingKey, string consume public string RoutingKey { get; } public IBasicProperties Properties { get; } - byte[] RabbitMqBasicConsumeContext.Body => Body.GetBytes(); + public ulong? SequenceNumber => DeliveryTag; + + public IDictionary GetTransportProperties() + { + var properties = new Lazy>(() => new Dictionary()); + + if (!string.IsNullOrWhiteSpace(RoutingKey)) + properties.Value[RabbitMqTransportPropertyNames.RoutingKey] = RoutingKey; + + if (Properties.IsAppIdPresent()) + properties.Value[RabbitMqTransportPropertyNames.AppId] = Properties.AppId; + if (Properties.IsPriorityPresent()) + properties.Value[RabbitMqTransportPropertyNames.Priority] = Properties.Priority; + if (Properties.IsReplyToPresent()) + properties.Value[RabbitMqTransportPropertyNames.ReplyTo] = Properties.ReplyTo; + if (Properties.IsTypePresent()) + properties.Value[RabbitMqTransportPropertyNames.Type] = Properties.Type; + if (Properties.IsUserIdPresent()) + properties.Value[RabbitMqTransportPropertyNames.UserId] = Properties.UserId; + + return properties.IsValueCreated ? properties.Value : null; + } protected override ContentType GetContentType() { @@ -77,7 +102,9 @@ public async Task GetSendEndpoint(Uri address) { var endpoint = await _sendEndpointProvider.GetSendEndpoint(address).ConfigureAwait(false); - return new ReplyToSendEndpoint(endpoint, _replyTo); + return address.IsReplyToAddress() + ? new ReplyToSendEndpoint(endpoint, _replyTo) + : endpoint; } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqReceiveEndpointContext.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqReceiveEndpointContext.cs index 8435bdf31cd..a8d5d42c176 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqReceiveEndpointContext.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqReceiveEndpointContext.cs @@ -11,6 +11,8 @@ public interface RabbitMqReceiveEndpointContext : bool ExclusiveConsumer { get; } + bool IsNotReplyTo { get; } + IModelContextSupervisor ModelContextSupervisor { get; } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqSendTransportContext.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqSendTransportContext.cs index d8977f8db4d..d000fa6c288 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqSendTransportContext.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqSendTransportContext.cs @@ -11,7 +11,7 @@ namespace MassTransit.RabbitMqTransport using Initializers.TypeConverters; using Internals; using Logging; - using RabbitMQ.Client; + using Middleware; using Transports; @@ -21,7 +21,7 @@ public class RabbitMqSendTransportContext : { static readonly DateTimeOffsetTypeConverter _dateTimeOffsetConverter = new DateTimeOffsetTypeConverter(); static readonly DateTimeTypeConverter _dateTimeConverter = new DateTimeTypeConverter(); - readonly IPipe _configureTopologyPipe; + readonly ConfigureRabbitMqTopologyFilter _configureTopologyFilter; readonly IPipe _delayConfigureTopologyPipe; readonly string _delayExchange; readonly string _exchange; @@ -31,14 +31,14 @@ public class RabbitMqSendTransportContext : public RabbitMqSendTransportContext(IRabbitMqHostConfiguration hostConfiguration, ReceiveEndpointContext receiveEndpointContext, IModelContextSupervisor supervisor, - IPipe configureTopologyPipe, string exchange, + ConfigureRabbitMqTopologyFilter configureTopologyFilter, string exchange, IPipe delayConfigureTopologyPipe, string delayExchange) : base(hostConfiguration, receiveEndpointContext.Serialization) { _hostConfiguration = hostConfiguration; _supervisor = supervisor; - _configureTopologyPipe = configureTopologyPipe; + _configureTopologyFilter = configureTopologyFilter; _exchange = exchange; _delayConfigureTopologyPipe = delayConfigureTopologyPipe; @@ -73,6 +73,8 @@ public async Task> CreateSendContext(ModelContext context, T m await pipe.Send(sendContext).ConfigureAwait(false); + CopyIncomingPropertiesIfPresent(sendContext); + if (sendContext.Exchange.Equals(RabbitMqExchangeNames.ReplyTo) && string.IsNullOrWhiteSpace(sendContext.RoutingKey)) throw new TransportException(sendContext.DestinationAddress, "RoutingKey must be specified when sending to reply-to address"); @@ -89,6 +91,8 @@ public override async Task> CreateSendContext(T message, IPipe await pipe.Send(sendContext).ConfigureAwait(false); + CopyIncomingPropertiesIfPresent(sendContext); + if (sendContext.Exchange.Equals(RabbitMqExchangeNames.ReplyTo) && string.IsNullOrWhiteSpace(sendContext.RoutingKey)) throw new TransportException(sendContext.DestinationAddress, "RoutingKey must be specified when sending to reply-to address"); @@ -103,7 +107,8 @@ public async Task Send(ModelContext transportContext, SendContext sendCont sendContext.CancellationToken.ThrowIfCancellationRequested(); - await _configureTopologyPipe.Send(transportContext).ConfigureAwait(false); + OneTimeContext> oneTimeContext = + await _configureTopologyFilter.Configure(transportContext).ConfigureAwait(false); sendContext.CancellationToken.ThrowIfCancellationRequested(); @@ -138,8 +143,8 @@ public async Task Send(ModelContext transportContext, SendContext sendCont .ToString("F0", CultureInfo.InvariantCulture); } - if (context.RequestId.HasValue && (context.ResponseAddress?.AbsolutePath?.EndsWith(RabbitMqExchangeNames.ReplyTo) ?? false)) - context.BasicProperties.ReplyTo = RabbitMqExchangeNames.ReplyTo; + if (context.RequestId.HasValue && context.ResponseAddress.IsReplyToAddress()) + context.BasicProperties.ReplyTo ??= RabbitMqExchangeNames.ReplyTo; var delay = context.Delay?.TotalMilliseconds; if (delay > 0 && exchange != "") @@ -161,7 +166,19 @@ public async Task Send(ModelContext transportContext, SendContext sendCont var publishTask = transportContext.BasicPublishAsync(exchange, routingKey, context.Mandatory, context.BasicProperties, body, context.AwaitAck); - await publishTask.OrCanceled(context.CancellationToken).ConfigureAwait(false); + try + { + await publishTask.OrCanceled(context.CancellationToken).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + throw; + } + catch (Exception) + { + oneTimeContext.Evict(); + throw; + } } static void SetHeaders(IDictionary dictionary, SendHeaders headers) @@ -188,19 +205,13 @@ static void SetHeaders(IDictionary dictionary, SendHeaders heade switch (header.Value) { case DateTimeOffset value: - if (_dateTimeOffsetConverter.TryConvert(value, out long result)) - dictionary[header.Key] = new AmqpTimestamp(result); - else if (_dateTimeOffsetConverter.TryConvert(value, out string text)) - dictionary[header.Key] = text; - + dictionary.SetAmqpTimestamp(header.Key, value.UtcDateTime); break; case DateTime value: - if (_dateTimeConverter.TryConvert(value, out result)) - dictionary[header.Key] = new AmqpTimestamp(result); - else if (_dateTimeConverter.TryConvert(value, out string text)) - dictionary[header.Key] = text; - + if (value.Kind == DateTimeKind.Local) + value = value.ToUniversalTime(); + dictionary.SetAmqpTimestamp(header.Key, value); break; case Guid value: @@ -236,5 +247,22 @@ static void SetHeaders(IDictionary dictionary, SendHeaders heade } } } + + static void CopyIncomingPropertiesIfPresent(RabbitMqSendContext context) + where T : class + { + if (context.TryGetPayload(out var consumeContext) + && consumeContext.TryGetPayload(out var basicConsumeContext)) + { + if (context.BasicProperties.IsPriorityPresent() == false) + { + if (basicConsumeContext.Properties.IsPriorityPresent()) + context.TrySetPriority(basicConsumeContext.Properties.Priority); + } + + if (!string.IsNullOrWhiteSpace(basicConsumeContext.Properties.ReplyTo) && context.ResponseAddress.IsReplyToAddress()) + context.BasicProperties.ReplyTo = basicConsumeContext.Properties.ReplyTo; + } + } } } diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqTransportPropertyNames.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqTransportPropertyNames.cs new file mode 100644 index 00000000000..3956c98fa74 --- /dev/null +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/RabbitMqTransportPropertyNames.cs @@ -0,0 +1,14 @@ +namespace MassTransit.RabbitMqTransport +{ + static class RabbitMqTransportPropertyNames + { + public const string Exchange = "RMQ-Exchange"; + public const string RoutingKey = "RMQ-RoutingKey"; + + public const string AppId = "RMQ-AppId"; + public const string Priority = "RMQ-Priority"; + public const string ReplyTo = "RMQ-ReplyTo"; + public const string Type = "RMQ-Type"; + public const string UserId = "RMQ-UserId"; + } +} diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqConsumeTopology.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqConsumeTopology.cs index 7251a583bce..d6dd40c5366 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqConsumeTopology.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqConsumeTopology.cs @@ -12,7 +12,7 @@ public class RabbitMqConsumeTopology : { readonly IMessageTopology _messageTopology; readonly IRabbitMqPublishTopology _publishTopology; - readonly IList _specifications; + readonly List _specifications; public RabbitMqConsumeTopology(IMessageTopology messageTopology, IRabbitMqPublishTopology publishTopology) : base(255) @@ -85,7 +85,7 @@ public override IEnumerable Validate() return base.Validate().Concat(_specifications.SelectMany(x => x.Validate())); } - protected override IMessageConsumeTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessageConsumeTopologyConfigurator CreateMessageTopology() { var exchangeTypeSelector = new MessageExchangeTypeSelector(ExchangeTypeSelector); diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqEntityNameValidator.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqEntityNameValidator.cs index 33f18916098..ad6b1a11391 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqEntityNameValidator.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqEntityNameValidator.cs @@ -6,7 +6,7 @@ namespace MassTransit.RabbitMqTransport.Topology public class RabbitMqEntityNameValidator : IEntityNameValidator { - static readonly Regex _regex = new Regex(@"^[A-Za-z0-9\-_\.:]+$", RegexOptions.Compiled); + static readonly Regex _regex = new Regex(@"^[\w\p{L}\-_\.:]+$", RegexOptions.Compiled); public static IEntityNameValidator Validator => Cached.EntityNameValidator; diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqMessageConsumeTopology.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqMessageConsumeTopology.cs index df5e8e58d6f..7391eb83618 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqMessageConsumeTopology.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqMessageConsumeTopology.cs @@ -14,7 +14,7 @@ public class RabbitMqMessageConsumeTopology : { readonly IMessageTopology _messageTopology; readonly IRabbitMqMessagePublishTopology _publishTopology; - readonly IList _specifications; + readonly List _specifications; public RabbitMqMessageConsumeTopology(IMessageTopology messageTopology, IMessageExchangeTypeSelector exchangeTypeSelector, IRabbitMqMessagePublishTopology publishTopology) diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqMessagePublishTopology.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqMessagePublishTopology.cs index 5e5e0edeb59..bf2798c5b02 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqMessagePublishTopology.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqMessagePublishTopology.cs @@ -3,6 +3,7 @@ namespace MassTransit.RabbitMqTransport.Topology { using System; using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; using Configuration; using MassTransit.Topology; using RabbitMQ.Client; @@ -14,9 +15,9 @@ public class RabbitMqMessagePublishTopology : where TMessage : class { readonly RabbitMqExchangeConfigurator _exchange; - readonly IList _implementedMessageTypes; + readonly List _implementedMessageTypes; readonly IRabbitMqPublishTopology _publishTopology; - readonly IList _specifications; + readonly List _specifications; public RabbitMqMessagePublishTopology(IRabbitMqPublishTopology publishTopology, IMessageTopology messageTopology, IMessageExchangeTypeSelector exchangeTypeSelector) @@ -67,7 +68,7 @@ public void Apply(IPublishEndpointBrokerTopologyBuilder builder) configurator.Apply(builder); } - public override bool TryGetPublishAddress(Uri baseAddress, out Uri? publishAddress) + public override bool TryGetPublishAddress(Uri baseAddress, [NotNullWhen(true)] out Uri? publishAddress) { publishAddress = _exchange.GetEndpointAddress(baseAddress); return true; diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqPublishTopology.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqPublishTopology.cs index 7d4cac6cbcd..8cdafd0f9f7 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqPublishTopology.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqPublishTopology.cs @@ -50,7 +50,7 @@ IRabbitMqMessagePublishTopologyConfigurator IRabbitMqPublishTopologyConfigura return GetMessageTopology() as IRabbitMqMessagePublishTopologyConfigurator; } - protected override IMessagePublishTopologyConfigurator CreateMessageTopology(Type type) + protected override IMessagePublishTopologyConfigurator CreateMessageTopology() { var exchangeTypeSelector = new MessageExchangeTypeSelector(ExchangeTypeSelector); diff --git a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqSendSettings.cs b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqSendSettings.cs index cf60b7da707..05109ec97db 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqSendSettings.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/RabbitMqTransport/Topology/RabbitMqSendSettings.cs @@ -11,7 +11,7 @@ public class RabbitMqSendSettings : RabbitMqExchangeConfigurator, SendSettings { - readonly IList _exchangeBindings; + readonly List _exchangeBindings; bool _bindToQueue; string _queueName; @@ -35,7 +35,7 @@ public RabbitMqSendSettings(RabbitMqEndpointAddress address) } if (!string.IsNullOrWhiteSpace(address.AlternateExchange)) - SetExchangeArgument("alternate-exchange", address.AlternateExchange); + SetExchangeArgument(Headers.AlternateExchange, address.AlternateExchange); if (address.SingleActiveConsumer) SetQueueArgument(Headers.XSingleActiveConsumer, true); @@ -46,8 +46,9 @@ public RabbitMqSendSettings(RabbitMqEndpointAddress address) public RabbitMqEndpointAddress GetSendAddress(Uri hostAddress) { return new RabbitMqEndpointAddress(hostAddress, ExchangeName, ExchangeType, Durable, AutoDelete, _bindToQueue, _queueName, - ExchangeArguments.ContainsKey("x-delayed-type") ? (string)ExchangeArguments["x-delayed-type"] : default, - _exchangeBindings.Count > 0 ? _exchangeBindings.Select(x => x.ExchangeName).ToArray() : default); + ExchangeArguments.TryGetValue("x-delayed-type", out var argument) ? (string)argument : default, + _exchangeBindings.Count > 0 ? _exchangeBindings.Select(x => x.ExchangeName).ToArray() : default, + alternateExchange: ExchangeArguments.TryGetValue(Headers.AlternateExchange, out argument) ? (string)argument : default); } public BrokerTopology GetBrokerTopology() @@ -80,13 +81,22 @@ public void BindToQueue(string queueName) public void BindToExchange(string exchangeName, Action configure = null) { - var specification = new ExchangeBindingPublishTopologySpecification(exchangeName, RabbitMQ.Client.ExchangeType.Fanout, Durable, AutoDelete); + string exchangeType = ExchangeArguments.TryGetValue("x-delayed-type", out var argument) ? (string)argument : RabbitMQ.Client.ExchangeType.Fanout; + var specification = new ExchangeBindingPublishTopologySpecification(exchangeName, exchangeType, Durable, AutoDelete); configure?.Invoke(specification); _exchangeBindings.Add(specification); } + public void BindToExchange(RabbitMqEndpointAddress address) + { + string exchangeType = ExchangeArguments.TryGetValue("x-delayed-type", out var argument) ? (string)argument : RabbitMQ.Client.ExchangeType.Fanout; + var specification = new ExchangeBindingPublishTopologySpecification(address.Name, address.ExchangeType, address.Durable, address.AutoDelete); + + _exchangeBindings.Add(specification); + } + public void SetQueueArgument(string key, object value) { if (key == null) diff --git a/src/Transports/MassTransit.RabbitMqTransport/Testing/RabbitMqTestHarnessHostedService.cs b/src/Transports/MassTransit.RabbitMqTransport/Testing/RabbitMqTestHarnessHostedService.cs index 8a0310f1718..575ae267184 100644 --- a/src/Transports/MassTransit.RabbitMqTransport/Testing/RabbitMqTestHarnessHostedService.cs +++ b/src/Transports/MassTransit.RabbitMqTransport/Testing/RabbitMqTestHarnessHostedService.cs @@ -39,6 +39,9 @@ public async Task StartAsync(CancellationToken cancellationToken) if (_testOptions.CleanVirtualHost) await CleanVirtualHost(); + + if (_testOptions.ConfigureVirtualHostCallback != null) + await ConfigureVirtualHost(); } public Task StopAsync(CancellationToken cancellationToken) @@ -125,6 +128,38 @@ async Task CleanVirtualHost() } } + async Task ConfigureVirtualHost() + { + var virtualHost = _transportOptions.VHost; + + var factory = new ConnectionFactory + { + HostName = _transportOptions.Host, + Port = _transportOptions.Port, + VirtualHost = virtualHost ?? "/", + UserName = _transportOptions.User, + Password = _transportOptions.Pass + }; + + var connection = factory.CreateConnection(); + try + { + using var model = connection.CreateModel(); + model.ConfirmSelect(); + + _testOptions.ConfigureVirtualHostCallback(model); + + model.Close(); + + connection.Close(200, "Completed (Ok)"); + } + catch (Exception ex) + { + if (connection.IsOpen) + connection.Close(500, $"Completed (not OK): {ex.Message}"); + } + } + async Task> GetVirtualHostEntities(string element) { using var client = GetHttpClient(); diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/IPostgresSqlHostConfigurator.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/IPostgresSqlHostConfigurator.cs new file mode 100644 index 00000000000..8338eff277a --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/IPostgresSqlHostConfigurator.cs @@ -0,0 +1,7 @@ +namespace MassTransit +{ + public interface IPostgresSqlHostConfigurator : + ISqlHostConfigurator + { + } +} \ No newline at end of file diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/PostgresBusFactoryConfiguratorExtensions.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/PostgresBusFactoryConfiguratorExtensions.cs new file mode 100644 index 00000000000..d27b4aaeb69 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/PostgresBusFactoryConfiguratorExtensions.cs @@ -0,0 +1,80 @@ +namespace MassTransit +{ + using System; + using Npgsql; + using SqlTransport.Configuration; + + + public static class PostgresBusFactoryConfiguratorExtensions + { + /// + /// Configure the bus to use the PostgreSQL database transport + /// + /// The registration configurator (configured via AddMassTransit) + /// The configuration callback for the bus factory + public static void UsingPostgres(this IBusRegistrationConfigurator configurator, + Action? configure = null) + { + configurator.SetBusFactory(new SqlRegistrationBusFactory((context, cfg) => + { + cfg.UsePostgres(context); + + configure?.Invoke(context, cfg); + })); + } + + /// + /// Configure the bus to use the PostgreSQL database transport + /// + /// The registration configurator (configured via AddMassTransit) + /// + /// Connection string to be used/parsed by the transport. are not + /// used with this overload + /// + /// The configuration callback for the bus factory + public static void UsingPostgres(this IBusRegistrationConfigurator configurator, string connectionString, + Action? configure = null) + { + configurator.SetBusFactory(new SqlRegistrationBusFactory((context, cfg) => + { + cfg.UsePostgres(connectionString); + + configure?.Invoke(context, cfg); + })); + } + + /// + /// Configure the bus to use the PostgreSQL database transport + /// + /// The registration configurator (configured via AddMassTransit) + /// A preconfigured data source used to create connections + /// The configuration callback for the bus factory + public static void UsingPostgres(this IBusRegistrationConfigurator configurator, NpgsqlDataSource dataSource, + Action? configure = null) + { + configurator.SetBusFactory(new SqlRegistrationBusFactory((context, cfg) => + { + cfg.UsePostgres(dataSource); + + configure?.Invoke(context, cfg); + })); + } + + /// + /// Configure the bus to use the PostgreSQL database transport + /// + /// The registration configurator (configured via AddMassTransit) + /// Resolve the data source from the container + /// The configuration callback for the bus factory + public static void UsingPostgres(this IBusRegistrationConfigurator configurator, Func dataSourceProvider, + Action? configure = null) + { + configurator.SetBusFactory(new SqlRegistrationBusFactory((context, cfg) => + { + cfg.UsePostgres(dataSourceProvider(context)); + + configure?.Invoke(context, cfg); + })); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/PostgresHostConfigurationExtensions.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/PostgresHostConfigurationExtensions.cs new file mode 100644 index 00000000000..c7afb59d090 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/PostgresHostConfigurationExtensions.cs @@ -0,0 +1,74 @@ +namespace MassTransit +{ + using System; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using Npgsql; + using SqlTransport.PostgreSql; + + + public static class PostgresHostConfigurationExtensions + { + /// + /// Configures the database transport to use PostgreSQL as the storage engine + /// + /// + /// The MassTransit host address of the database + /// + public static void UsePostgres(this ISqlBusFactoryConfigurator configurator, Uri hostAddress, Action? configure = null) + { + var hostConfigurator = new PostgresSqlHostConfigurator(hostAddress); + + configure?.Invoke(hostConfigurator); + + configurator.Host(hostConfigurator.Settings); + } + + /// + /// Configures the database transport to use PostgreSQL as the storage engine + /// + /// + /// A valid PostgreSQL connection string + /// + public static void UsePostgres(this ISqlBusFactoryConfigurator configurator, string connectionString, Action? configure = null) + { + var hostConfigurator = new PostgresSqlHostConfigurator(connectionString); + + configure?.Invoke(hostConfigurator); + + configurator.Host(hostConfigurator.Settings); + } + + /// + /// Configures the database transport to use PostgreSQL as the storage engine + /// + /// + /// A preconfigured data source used to create connections + /// + public static void UsePostgres(this ISqlBusFactoryConfigurator configurator, NpgsqlDataSource dataSource, + Action? configure = null) + { + var hostConfigurator = new PostgresSqlHostConfigurator(dataSource); + + configure?.Invoke(hostConfigurator); + + configurator.Host(hostConfigurator.Settings); + } + + /// + /// Configures the database transport to use PostgreSQL as the storage engine + /// + /// + /// The bus registration context, used to retrieve the DbTransportOptions + /// + public static void UsePostgres(this ISqlBusFactoryConfigurator configurator, IBusRegistrationContext context, + Action? configure = null) + { + var hostConfigurator = new PostgresSqlHostConfigurator(context.GetRequiredService>().Value); + + configure?.Invoke(hostConfigurator); + + configurator.Host(hostConfigurator.Settings); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/PostgresSqlTransportConfigurationExtensions.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/PostgresSqlTransportConfigurationExtensions.cs new file mode 100644 index 00000000000..e0256fa031e --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/Configuration/PostgresSqlTransportConfigurationExtensions.cs @@ -0,0 +1,42 @@ +namespace MassTransit +{ + using System; + using Microsoft.Extensions.DependencyInjection; + using SqlTransport; + using SqlTransport.PostgreSql; + + + public static class PostgresSqlTransportConfigurationExtensions + { + public static IServiceCollection AddPostgresMigrationHostedService(this IServiceCollection services, bool create = true, bool delete = false) + { + services.AddPostgresMigrationHostedService(options => + { + options.CreateDatabase = create; + options.CreateInfrastructure = create; + options.DeleteDatabase = delete; + }); + + return services; + } + + public static IServiceCollection AddPostgresMigrationHostedService(this IServiceCollection services, Action? configure) + { + services.AddTransient(); + + services.AddOptions(); + services.AddOptions() + .Configure(options => + { + options.CreateDatabase = true; + options.CreateInfrastructure = true; + options.DeleteDatabase = false; + + configure?.Invoke(options); + }); + services.AddHostedService(); + + return services; + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/MassTransit.SqlTransport.PostgreSql.csproj b/src/Transports/MassTransit.SqlTransport.PostgreSql/MassTransit.SqlTransport.PostgreSql.csproj new file mode 100644 index 00000000000..9517029c344 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/MassTransit.SqlTransport.PostgreSql.csproj @@ -0,0 +1,29 @@ + + + + netstandard2.0;net6.0;net8.0 + MassTransit + enable + + + + $(TargetFrameworks);net472 + + + + MassTransit.SqlTransport.PostgreSQL + MassTransit.SqlTransport.PostgreSQL + MassTransit;Database;Transport;PostgreSQL;npgsql + MassTransit PostgreSQL Database Transport; $(Description) + + + + + + + + + + + + diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/MassTransit.SqlTransport.PostgreSql.csproj.DotSettings b/src/Transports/MassTransit.SqlTransport.PostgreSql/MassTransit.SqlTransport.PostgreSql.csproj.DotSettings new file mode 100644 index 00000000000..6840e166805 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/MassTransit.SqlTransport.PostgreSql.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/EnumParameter.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/EnumParameter.cs new file mode 100644 index 00000000000..a2da9171471 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/EnumParameter.cs @@ -0,0 +1,33 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + using System; + using System.Data; + using Dapper; + using Npgsql; + + + public class EnumParameter : + SqlMapper.ICustomQueryParameter + { + readonly string _dataTypeName; + readonly string? _value; + + public EnumParameter(string? value, string dataTypeName) + { + _value = value; + _dataTypeName = dataTypeName; + } + + public void AddParameter(IDbCommand command, string name) + { + var parameter = new NpgsqlParameter + { + ParameterName = name, + Value = _value != null ? _value : DBNull.Value, + DataTypeName = _dataTypeName + }; + + command.Parameters.Add(parameter); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/Helpers/NotifyChannel.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/Helpers/NotifyChannel.cs new file mode 100644 index 00000000000..f5b7f37cc22 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/Helpers/NotifyChannel.cs @@ -0,0 +1,23 @@ +namespace MassTransit.SqlTransport.PostgreSql.Helpers; + +public static class NotifyChannel +{ + // Postgres channel name is an identifier and can contain maximum 63 characters. + // We need to reserve 19 characters for queueId being a long and + // 5 characters for the _msg_ separator. Schema can be at most 39 characters. + // https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS + const int MaxSchemaNameLength = 39; + const string DefaultSchemaName = "transport"; + + public static string SanitizeSchemaName(string? schemaName) + { + if (string.IsNullOrWhiteSpace(schemaName)) + { + return DefaultSchemaName; + } + + return schemaName!.Length > MaxSchemaNameLength + ? schemaName.Substring(0, MaxSchemaNameLength) + : schemaName; + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/IPostgresSqlTransportConnection.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/IPostgresSqlTransportConnection.cs new file mode 100644 index 00000000000..934cf952ecd --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/IPostgresSqlTransportConnection.cs @@ -0,0 +1,12 @@ +namespace MassTransit.SqlTransport.PostgreSql; + +using Npgsql; + + +public interface IPostgresSqlTransportConnection : + ISqlTransportConnection +{ + NpgsqlConnection Connection { get; } + + NpgsqlCommand CreateCommand(string commandText); +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/JsonParameter.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/JsonParameter.cs new file mode 100644 index 00000000000..0f5c16777c4 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/JsonParameter.cs @@ -0,0 +1,27 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + using System; + using System.Data; + using Dapper; + using Npgsql; + using NpgsqlTypes; + + + public class JsonParameter : + SqlMapper.ICustomQueryParameter + { + readonly string? _value; + + public JsonParameter(string? value) + { + _value = value; + } + + public void AddParameter(IDbCommand command, string name) + { + var parameter = new NpgsqlParameter(name, NpgsqlDbType.Jsonb) { Value = _value != null ? _value : DBNull.Value }; + + command.Parameters.Add(parameter); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresClientContext.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresClientContext.cs new file mode 100644 index 00000000000..e215b81e76d --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresClientContext.cs @@ -0,0 +1,286 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using Dapper; + using Npgsql; + using Serialization; + using Topology; + + + public class PostgresClientContext : + SqlClientContext + { + readonly Guid _consumerId; + readonly PostgresDbConnectionContext _context; + readonly string _createQueueSql; + readonly string _createQueueSubscriptionSql; + readonly string _createTopicSql; + readonly string _createTopicSubscriptionSql; + readonly string _deleteMessageSql; + readonly string _deleteScheduledMessageSql; + readonly string _moveMessageTypeSql; + readonly string _processMetricsSql; + readonly string _publishSql; + readonly string _purgeQueueSql; + readonly string _receivePartitionedSql; + readonly string _receiveSql; + readonly string _renewLockSql; + readonly string _sendSql; + readonly string _touchQueueSql; + readonly string _unlockSql; + + public PostgresClientContext(PostgresDbConnectionContext context, CancellationToken cancellationToken) + : base(context, cancellationToken) + { + _context = context; + _consumerId = NewId.NextGuid(); + + _createQueueSubscriptionSql = string.Format(SqlStatements.DbCreateQueueSubscriptionSql, _context.Schema); + _receiveSql = string.Format(SqlStatements.DbReceiveSql, _context.Schema); + _receivePartitionedSql = string.Format(SqlStatements.DbReceivePartitionedSql, _context.Schema); + _sendSql = string.Format(SqlStatements.DbEnqueueSql, _context.Schema); + _createTopicSubscriptionSql = string.Format(SqlStatements.DbCreateTopicSubscriptionSql, _context.Schema); + _processMetricsSql = string.Format(SqlStatements.DbProcessMetricsSql, _context.Schema); + _publishSql = string.Format(SqlStatements.DbPublishSql, _context.Schema); + _purgeQueueSql = string.Format(SqlStatements.DbPurgeQueueSql, _context.Schema); + _createTopicSql = string.Format(SqlStatements.DbCreateTopicSql, _context.Schema); + _createQueueSql = string.Format(SqlStatements.DbCreateQueueSql, _context.Schema); + _deleteMessageSql = string.Format(SqlStatements.DbDeleteMessageSql, _context.Schema); + _deleteScheduledMessageSql = string.Format(SqlStatements.DbDeleteScheduledMessageSql, _context.Schema); + _moveMessageTypeSql = string.Format(SqlStatements.DbMoveMessageSql, _context.Schema); + _renewLockSql = string.Format(SqlStatements.DbRenewLockSql, _context.Schema); + _touchQueueSql = string.Format(SqlStatements.DbTouchQueueSql, _context.Schema); + _unlockSql = string.Format(SqlStatements.DbUnlockSql, _context.Schema); + } + + public override Task CreateQueue(Queue queue) + { + return _context.Query((x, t) => x.ExecuteScalarAsync(_createQueueSql, new + { + queue_name = queue.QueueName, + auto_delete = (int?)queue.AutoDeleteOnIdle?.TotalSeconds + }, t), CancellationToken); + } + + public override Task CreateTopic(Topic topic) + { + return _context.Query((x, t) => x.ExecuteScalarAsync(_createTopicSql, new { topic_name = topic.TopicName }), CancellationToken); + } + + public override Task CreateTopicSubscription(TopicToTopicSubscription subscription) + { + return _context.Query((x, t) => x.ExecuteScalarAsync(_createTopicSubscriptionSql, new + { + source_topic_name = subscription.Source.TopicName, + destination_topic_name = subscription.Destination.TopicName, + type = (int)subscription.SubscriptionType, + routing_key = subscription.RoutingKey, + filter = new JsonParameter(null) + }), CancellationToken); + } + + public override Task CreateQueueSubscription(TopicToQueueSubscription subscription) + { + return _context.Query((x, t) => x.ExecuteScalarAsync(_createQueueSubscriptionSql, new + { + source_topic_name = subscription.Source.TopicName, + destination_queue_name = subscription.Destination.QueueName, + type = (int)subscription.SubscriptionType, + routing_key = subscription.RoutingKey, + filter = new JsonParameter(null) + }), CancellationToken); + } + + public override Task PurgeQueue(string queueName, CancellationToken cancellationToken) + { + return _context.Query((x, t) => x.ExecuteScalarAsync(_purgeQueueSql, new { queue_name = queueName }), CancellationToken); + } + + public override async Task> ReceiveMessages(string queueName, SqlReceiveMode mode, int messageLimit, + int concurrentLimit, TimeSpan lockDuration) + { + try + { + if (mode == SqlReceiveMode.Normal) + { + return await _context.Query((x, t) => x.QueryAsync(_receiveSql, new + { + queue_name = queueName, + fetch_consumer_id = _consumerId, + fetch_lock_id = NewId.NextGuid(), + lock_duration = lockDuration, + fetch_count = messageLimit + }), CancellationToken).ConfigureAwait(false); + } + + var ordered = mode switch + { + SqlReceiveMode.PartitionedOrdered => 1, + SqlReceiveMode.PartitionedOrderedConcurrent => 1, + _ => 0 + }; + + return await _context.Query((x, t) => x.QueryAsync(_receivePartitionedSql, new + { + queue_name = queueName, + fetch_consumer_id = _consumerId, + fetch_lock_id = NewId.NextGuid(), + lock_duration = lockDuration, + fetch_count = messageLimit, + concurrent_count = concurrentLimit, + ordered + }), CancellationToken).ConfigureAwait(false); + } + catch (PostgresException exception) when (exception.ErrorCode == 40001) + { + return Array.Empty(); + } + } + + public override Task TouchQueue(string queueName) + { + return _context.Query((x, t) => x.QueryAsync(_touchQueueSql, new { queue_name = queueName }), CancellationToken); + } + + public override Task Send(string queueName, SqlMessageSendContext context) + { + IEnumerable> headers = context.Headers.GetAll().ToList(); + var headersAsJson = headers.Any() ? JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options) : null; + + Guid? schedulingTokenId = context.Headers.Get(MessageHeaders.SchedulingTokenId); + + return _context.Query((x, t) => x.ExecuteScalarAsync(_sendSql, new + { + entity_name = queueName, + priority = (int)(context.Priority ?? 100), + transport_message_id = context.TransportMessageId, + body = new JsonParameter(context.Body.GetString()), + binary_body = default(byte[]?), + content_type = context.ContentType?.MediaType, + message_type = string.Join(";", context.SupportedMessageTypes), + message_id = context.MessageId, + correlation_id = context.CorrelationId, + conversation_id = context.ConversationId, + request_id = context.RequestId, + initiator_id = context.InitiatorId, + source_address = context.SourceAddress, + destination_address = context.DestinationAddress, + response_address = context.ResponseAddress, + fault_address = context.FaultAddress, + sent_time = context.SentTime, + headers = new JsonParameter(headersAsJson), + host = new JsonParameter(HostInfoCache.HostInfoJson), + partition_key = context.PartitionKey, + routing_key = context.RoutingKey, + delay = context.Delay, + scheduling_token_id = schedulingTokenId + }), CancellationToken); + } + + public override Task Publish(string topicName, SqlMessageSendContext context) + { + IEnumerable> headers = context.Headers.GetAll().ToList(); + var headersAsJson = headers.Any() ? JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options) : null; + + Guid? schedulingTokenId = context.Headers.Get(MessageHeaders.SchedulingTokenId); + + return _context.Query((x, t) => x.ExecuteScalarAsync(_publishSql, new + { + entity_name = topicName, + priority = (int)(context.Priority ?? 100), + transport_message_id = context.TransportMessageId, + body = new JsonParameter(context.Body.GetString()), + binary_body = default(byte[]?), + content_type = context.ContentType?.MediaType, + message_type = string.Join(";", context.SupportedMessageTypes), + message_id = context.MessageId, + correlation_id = context.CorrelationId, + conversation_id = context.ConversationId, + request_id = context.RequestId, + initiator_id = context.InitiatorId, + source_address = context.SourceAddress, + destination_address = context.DestinationAddress, + response_address = context.ResponseAddress, + fault_address = context.FaultAddress, + sent_time = context.SentTime, + headers = new JsonParameter(headersAsJson), + host = new JsonParameter(HostInfoCache.HostInfoJson), + partition_key = context.PartitionKey, + routing_key = context.RoutingKey, + delay = context.Delay, + scheduling_token_id = schedulingTokenId + }), CancellationToken); + } + + public override async Task DeleteMessage(Guid lockId, long messageDeliveryId) + { + var result = await _context.Query((x, t) => x.ExecuteScalarAsync(_deleteMessageSql, new + { + message_delivery_id = messageDeliveryId, + lock_id = lockId + }), CancellationToken); + + return result == messageDeliveryId; + } + + public override async Task DeleteScheduledMessage(Guid tokenId, CancellationToken cancellationToken) + { + IEnumerable? result = await _context.Query((x, t) => x.QueryAsync(_deleteScheduledMessageSql, new + { + token_id = tokenId, + }), cancellationToken); + + return result.Any(); + } + + public override async Task MoveMessage(Guid lockId, long messageDeliveryId, string queueName, SqlQueueType queueType, SendHeaders sendHeaders) + { + IEnumerable> headers = sendHeaders.GetAll().ToList(); + var headersAsJson = headers.Any() ? JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options) : null; + + var result = await _context.Query((x, t) => x.ExecuteScalarAsync(_moveMessageTypeSql, new + { + message_delivery_id = messageDeliveryId, + lock_id = lockId, + queue_name = queueName, + queue_type = (int)queueType, + headers = new JsonParameter(headersAsJson), + }), CancellationToken); + + return result == messageDeliveryId; + } + + public override async Task RenewLock(Guid lockId, long messageDeliveryId, TimeSpan duration) + { + var result = await _context.Query((x, t) => x.ExecuteScalarAsync(_renewLockSql, new + { + message_delivery_id = messageDeliveryId, + lock_id = lockId, + duration + }), CancellationToken); + + return result == messageDeliveryId; + } + + public override async Task Unlock(Guid lockId, long messageDeliveryId, TimeSpan delay, SendHeaders sendHeaders) + { + IEnumerable> headers = sendHeaders.GetAll().ToList(); + var headersAsJson = headers.Any() ? JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options) : null; + + var result = await _context.Query((x, t) => x.ExecuteScalarAsync(_unlockSql, new + { + message_delivery_id = messageDeliveryId, + lock_id = lockId, + delay, + headers = new JsonParameter(headersAsJson), + }), CancellationToken); + + return result == messageDeliveryId; + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresConnectionContextFactory.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresConnectionContextFactory.cs new file mode 100644 index 00000000000..63cf225a270 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresConnectionContextFactory.cs @@ -0,0 +1,25 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + using Configuration; + using Transports; + + + public class PostgresConnectionContextFactory : + ConnectionContextFactory + { + readonly ISqlHostConfiguration _hostConfiguration; + readonly PostgresSqlHostSettings _hostSettings; + + public PostgresConnectionContextFactory(ISqlHostConfiguration hostConfiguration) + { + _hostConfiguration = hostConfiguration; + _hostSettings = hostConfiguration.Settings as PostgresSqlHostSettings + ?? throw new ConfigurationException("The host settings were not of the expected type"); + } + + protected override ConnectionContext CreateConnection(ITransportSupervisor supervisor) + { + return new PostgresDbConnectionContext(_hostConfiguration, supervisor); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresDatabaseMigrator.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresDatabaseMigrator.cs new file mode 100644 index 00000000000..bd83496254d --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresDatabaseMigrator.cs @@ -0,0 +1,1329 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Dapper; + using Helpers; + using Microsoft.Extensions.Logging; + + + public class PostgresDatabaseMigrator : + ISqlTransportDatabaseMigrator + { + const string DbExistsSql = "SELECT COUNT(*) FROM pg_database WHERE datname = '{0}'"; + const string DbCreateSql = """CREATE DATABASE "{0}" """; + const string SchemaCreateSql = """CREATE SCHEMA IF NOT EXISTS "{0}" """; + const string GrantConnectSql = """GRANT CONNECT ON DATABASE "{0}" to "{1}";"""; + const string DropSql = """DROP DATABASE "{0}" WITH (force)"""; + const string RoleExistsSql = "SELECT COUNT(*) FROM pg_catalog.pg_roles WHERE rolname = '{0}'"; + const string CreateRoleSql = """CREATE ROLE "{0}" """; + const string GrantRoleToPrincipalSql = """GRANT "{0}" TO "{1}";"""; + + const string GrantRoleSql = """ + GRANT USAGE ON SCHEMA "{1}" TO "{0}"; + ALTER SCHEMA "{1}" OWNER TO "{0}"; + GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA "{1}" TO "{0}"; + GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA "{1}" TO "{0}"; + ALTER DEFAULT PRIVILEGES IN SCHEMA "{1}" GRANT ALL PRIVILEGES ON TABLES TO "{0}"; + ALTER DEFAULT PRIVILEGES IN SCHEMA "{1}" GRANT ALL PRIVILEGES ON SEQUENCES TO "{0}"; + """; + + const string CreateUserSql = """ + CREATE USER "{1}" WITH PASSWORD '{2}'; + GRANT "{0}" TO "{1}"; + """; + + const string CreateInfrastructureSql = """ + SET ROLE "{1}"; + + CREATE OR REPLACE FUNCTION "{0}".create_constraint_if_not_exists (t_name text, c_name text, constraint_sql text) + RETURNS void AS + $$ + BEGIN + IF NOT EXISTS (SELECT constraint_name + FROM information_schema.constraint_column_usage + WHERE table_name = t_name AND table_schema = '{0}' + AND constraint_name = c_name AND constraint_schema = '{0}') THEN + EXECUTE constraint_sql; + END IF; + END; + $$ LANGUAGE plpgsql; + + CREATE SEQUENCE IF NOT EXISTS "{0}".topology_seq AS bigint; + + CREATE TABLE IF NOT EXISTS "{0}".queue + ( + id bigint not null primary key default nextval('"{0}".topology_seq'), + updated timestamptz not null default (now() at time zone 'utc'), + + name text not null, + type integer not null, + auto_delete integer + ); + + SELECT "{0}".create_constraint_if_not_exists('queue', 'unique_queue', + 'CREATE UNIQUE INDEX IF NOT EXISTS queue_uqx ON "{0}".queue (type, name) INCLUDE (id);ALTER TABLE "{0}".queue ADD CONSTRAINT unique_queue UNIQUE USING INDEX queue_uqx;'); + + CREATE INDEX IF NOT EXISTS queue_auto_delete_ndx ON "{0}".queue (auto_delete) INCLUDE (id); + + CREATE TABLE IF NOT EXISTS "{0}".topic + ( + id bigint not null primary key default nextval('"{0}".topology_seq'), + updated timestamptz not null default (now() at time zone 'utc'), + + name text not null + ); + + SELECT "{0}".create_constraint_if_not_exists('topic', 'unique_topic', + 'CREATE UNIQUE INDEX IF NOT EXISTS topic_uqx ON "{0}".topic (name) INCLUDE (id);ALTER TABLE "{0}".topic ADD CONSTRAINT unique_topic UNIQUE USING INDEX topic_uqx;'); + + CREATE TABLE IF NOT EXISTS "{0}".topic_subscription + ( + id bigint not null primary key default nextval('"{0}".topology_seq'), + updated timestamptz not null default (now() at time zone 'utc'), + + source_id bigint not null references "{0}".topic (id) ON DELETE CASCADE, + destination_id bigint not null references "{0}".topic (id) ON DELETE CASCADE, + + sub_type integer not null, + routing_key text not null, + filter jsonb not null + ); + + SELECT "{0}".create_constraint_if_not_exists('topic_subscription', 'unique_topic_subscription', + 'CREATE UNIQUE INDEX IF NOT EXISTS topic_subscription_uqx ON "{0}".topic_subscription (source_id, destination_id, sub_type, routing_key, filter) INCLUDE (id);ALTER TABLE "{0}".topic_subscription ADD CONSTRAINT unique_topic_subscription UNIQUE USING INDEX topic_subscription_uqx;'); + + CREATE INDEX IF NOT EXISTS topic_subscription_source_id_ndx ON "{0}".topic_subscription (source_id) INCLUDE (id, destination_id); + + CREATE INDEX IF NOT EXISTS topic_subscription_destination_id_ndx ON "{0}".topic_subscription (destination_id) INCLUDE (id, source_id); + + CREATE TABLE IF NOT EXISTS "{0}".queue_subscription + ( + id bigint not null primary key default nextval('"{0}".topology_seq'), + updated timestamptz not null default (now() at time zone 'utc'), + + source_id bigint not null references "{0}".topic (id) ON DELETE CASCADE, + destination_id bigint not null references "{0}".queue (id) ON DELETE CASCADE, + + sub_type integer not null, + routing_key text not null, + filter jsonb not null + ); + + SELECT "{0}".create_constraint_if_not_exists('queue_subscription', 'unique_queue_subscription', + 'CREATE UNIQUE INDEX IF NOT EXISTS queue_subscription_uqx ON "{0}".queue_subscription (source_id, destination_id, sub_type, routing_key, filter);ALTER TABLE "{0}".queue_subscription ADD CONSTRAINT unique_queue_subscription UNIQUE USING INDEX queue_subscription_uqx;'); + + CREATE INDEX IF NOT EXISTS queue_subscription_source_id_ndx ON "{0}".queue_subscription (source_id) INCLUDE (id, destination_id); + + CREATE INDEX IF NOT EXISTS queue_subscription_destination_id_ndx ON "{0}".queue_subscription (destination_id) INCLUDE (id, source_id); + + CREATE TABLE IF NOT EXISTS "{0}".message + ( + transport_message_id uuid not null primary key, + + content_type text, + message_type text, + body jsonb, + binary_body bytea, + + message_id uuid, + correlation_id uuid, + conversation_id uuid, + request_id uuid, + initiator_id uuid, + scheduling_token_id uuid, + + source_address text, + destination_address text, + response_address text, + fault_address text, + + sent_time timestamptz NOT NULL DEFAULT (now() at time zone 'utc'), + + headers jsonb, + host jsonb + ); + + CREATE INDEX IF NOT EXISTS message_scheduling_token_id_ndx ON "{0}".message (scheduling_token_id) where message.scheduling_token_id IS NOT NULL; + + CREATE TABLE IF NOT EXISTS "{0}".message_delivery + ( + message_delivery_id bigserial not null primary key, + transport_message_id uuid not null REFERENCES "{0}".message ON DELETE CASCADE, + + queue_id bigint not null, + priority smallint not null, + enqueue_time timestamptz not null, + expiration_time timestamptz, + + partition_key text, + routing_key text, + + consumer_id uuid, + lock_id uuid, + + delivery_count int not null, + max_delivery_count int not null, + last_delivered timestamptz, + transport_headers jsonb + ); + + CREATE INDEX IF NOT EXISTS message_delivery_fetch_ndx on "{0}".message_delivery (queue_id, priority, enqueue_time, message_delivery_id); + + CREATE INDEX IF NOT EXISTS message_delivery_fetch_part_ndx on "{0}".message_delivery (queue_id, partition_key, priority, enqueue_time, message_delivery_id); + + CREATE INDEX IF NOT EXISTS message_delivery_transport_message_id_ndx ON "{0}".message_delivery (transport_message_id); + + CREATE OR REPLACE FUNCTION "{0}".create_queue(queue_name text, auto_delete integer DEFAULT NULL) + RETURNS integer + AS + $$ + DECLARE + v_queue_id bigint; + BEGIN + IF queue_name IS NULL OR LENGTH(queue_name) < 1 THEN + RAISE EXCEPTION 'Queue names must not be null or empty'; + END IF; + + INSERT INTO "{0}".queue (name, type, auto_delete) VALUES (queue_name, 1, auto_delete) + ON CONFLICT ON CONSTRAINT unique_queue DO + UPDATE SET updated = (now() at time zone 'utc'), auto_delete = COALESCE(create_queue.auto_delete, excluded.auto_delete) + RETURNING queue.id INTO v_queue_id; + + INSERT INTO "{0}".queue (name, type, auto_delete) VALUES (queue_name, 2, auto_delete) + ON CONFLICT ON CONSTRAINT unique_queue DO + UPDATE SET updated = (now() at time zone 'utc'), auto_delete = COALESCE(create_queue.auto_delete, excluded.auto_delete); + + INSERT INTO "{0}".queue (name, type, auto_delete) VALUES (queue_name, 3, auto_delete) + ON CONFLICT ON CONSTRAINT unique_queue DO + UPDATE SET updated = (now() at time zone 'utc'), auto_delete = COALESCE(create_queue.auto_delete, excluded.auto_delete); + + RETURN v_queue_id; + + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION "{0}".create_topic(topic_name text) + RETURNS integer + AS + $$ + DECLARE + v_topic_id bigint; + BEGIN + IF topic_name IS NULL OR LENGTH(topic_name) < 1 THEN + RAISE EXCEPTION 'Topic names must not be null or empty'; + END IF; + + INSERT INTO "{0}".topic (name) VALUES (topic_name) + ON CONFLICT ON CONSTRAINT unique_topic DO + UPDATE SET updated = (now() at time zone 'utc') + RETURNING topic.id INTO v_topic_id; + + RETURN v_topic_id; + + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION "{0}".create_topic_subscription(source_topic_name text, destination_topic_name text, type integer, + routing_key text DEFAULT '', filter jsonb DEFAULT '{{}}') + RETURNS integer + AS + $$ + DECLARE + v_topic_subscription_id bigint; + v_source_id bigint; + v_destination_id bigint; + BEGIN + IF source_topic_name IS NULL OR LENGTH(source_topic_name) < 1 THEN + RAISE EXCEPTION 'Topic names must not be null or empty'; + END IF; + IF destination_topic_name IS NULL OR LENGTH(destination_topic_name) < 1 THEN + RAISE EXCEPTION 'Topic names must not be null or empty'; + END IF; + + SELECT INTO v_source_id t.Id FROM "{0}".topic t WHERE t.name = source_topic_name; + IF v_source_id IS NULL THEN + RAISE EXCEPTION 'Source topic not found: %', source_topic_name; + END IF; + SELECT INTO v_destination_id t.Id FROM "{0}".topic t WHERE t.name = destination_topic_name; + IF v_destination_id IS NULL THEN + RAISE EXCEPTION 'Destination topic not found: %', destination_topic_name; + END IF; + + INSERT INTO "{0}".topic_subscription (source_id, destination_id, sub_type, routing_key, filter) + VALUES (v_source_id, v_destination_id, type, COALESCE(create_topic_subscription.routing_key, ''), COALESCE(create_topic_subscription.filter, '{{}}'::jsonb)) + ON CONFLICT ON CONSTRAINT unique_topic_subscription DO + UPDATE SET updated = (now() at time zone 'utc') + RETURNING topic_subscription.id INTO v_topic_subscription_id; + + RETURN v_topic_subscription_id; + + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION "{0}".create_queue_subscription(source_topic_name text, destination_queue_name text, type integer, + routing_key text DEFAULT '', filter jsonb DEFAULT '{{}}') + RETURNS integer + AS + $$ + DECLARE + v_queue_subscription_id bigint; + v_source_id bigint; + v_destination_id bigint; + BEGIN + IF source_topic_name IS NULL OR LENGTH(source_topic_name) < 1 THEN + RAISE EXCEPTION 'Topic names must not be null or empty'; + END IF; + IF destination_queue_name IS NULL OR LENGTH(destination_queue_name) < 1 THEN + RAISE EXCEPTION 'Queue names must not be null or empty'; + END IF; + + SELECT INTO v_source_id t.Id FROM "{0}".topic t WHERE t.name = source_topic_name; + IF v_source_id IS NULL THEN + RAISE EXCEPTION 'Source topic not found: %', source_topic_name; + END IF; + SELECT INTO v_destination_id q.Id FROM "{0}".queue q WHERE q.name = destination_queue_name AND q.type = 1; + IF v_destination_id IS NULL THEN + RAISE EXCEPTION 'Destination queue not found: %', destination_queue_name; + END IF; + + INSERT INTO "{0}".queue_subscription (source_id, destination_id, sub_type, routing_key, filter) + VALUES (v_source_id, v_destination_id, type, COALESCE(create_queue_subscription.routing_key, ''), COALESCE(create_queue_subscription.filter, '{{}}'::jsonb)) + ON CONFLICT ON CONSTRAINT unique_queue_subscription DO + UPDATE SET updated = (now() at time zone 'utc') + RETURNING queue_subscription.id INTO v_queue_subscription_id; + + RETURN v_queue_subscription_id; + + END; + $$ LANGUAGE plpgsql; + + + CREATE OR REPLACE FUNCTION "{0}".purge_queue(queue_name text) + RETURNS bigint + AS + $$ + BEGIN + IF queue_name IS NULL OR LENGTH(queue_name) < 1 THEN + RAISE EXCEPTION 'Queue name must not be null'; + END IF; + + WITH msgs AS ( + DELETE FROM "{0}".message_delivery md + USING (SELECT mdx.message_delivery_id + FROM "{0}".message_delivery mdx + INNER JOIN "{0}".queue q on mdx.queue_id = q.Id + WHERE q.name = queue_name) mds + WHERE md.message_delivery_id = mds.message_delivery_id + RETURNING md.transport_message_id) + DELETE FROM "{0}".message m + USING msgs + WHERE m.transport_message_id = msgs.transport_message_id + AND NOT EXISTS(SELECT FROM "{0}".message_delivery md WHERE md.transport_message_id = m.transport_message_id); + + RETURN 0; + END; + $$ LANGUAGE plpgsql; + + + CREATE OR REPLACE FUNCTION "{0}".fetch_messages( + queue_name text + , fetch_consumer_id uuid + , fetch_lock_id uuid + , lock_duration interval + , fetch_count integer DEFAULT 1) + RETURNS TABLE( + transport_message_id uuid + , queue_id bigint + , priority smallint + , message_delivery_id bigint + , consumer_id uuid + , lock_id uuid + , enqueue_time timestamp with time zone + , expiration_time timestamp with time zone + , delivery_count integer + , partition_key text + , routing_key text + , transport_headers jsonb + , content_type text + , message_type text + , body jsonb + , binary_body bytea + , message_id uuid + , correlation_id uuid + , conversation_id uuid + , request_id uuid + , initiator_id uuid + , source_address text + , destination_address text + , response_address text + , fault_address text + , sent_time timestamp with time zone + , headers jsonb + , host jsonb) + AS + $$ + DECLARE + v_queue_id bigint; + v_enqueue_time timestamptz; + v_now timestamptz; + BEGIN + SELECT INTO v_queue_id q.Id FROM "{0}".queue q WHERE q.name = queue_name AND q.type = 1; + IF v_queue_id IS NULL THEN + RAISE EXCEPTION 'Queue not found: %', queue_name; + END IF; + + v_now := (now() at time zone 'utc'); + v_enqueue_time := v_now + lock_duration; + + RETURN QUERY WITH msgs AS ( + SELECT md.* + FROM "{0}".message_delivery md + WHERE md.queue_id = v_queue_id + AND md.enqueue_time <= v_now + AND md.delivery_count < md.max_delivery_count + ORDER BY md.priority, md.enqueue_time, md.message_delivery_id + LIMIT fetch_count FOR UPDATE OF md SKIP LOCKED) + UPDATE "{0}".message_delivery dm + SET delivery_count = dm.delivery_count + 1, + last_delivered = v_now, + consumer_id = fetch_consumer_id, + lock_id = fetch_lock_id, + enqueue_time = v_enqueue_time + FROM msgs + INNER JOIN "{0}".message m on msgs.transport_message_id = m.transport_message_id + WHERE dm.message_delivery_id = msgs.message_delivery_id + RETURNING + dm.transport_message_id, + dm.queue_id, + dm.priority, + dm.message_delivery_id, + dm.consumer_id, + dm.lock_id, + dm.enqueue_time, + dm.expiration_time, + dm.delivery_count, + dm.partition_key, + dm.routing_key, + dm.transport_headers, + m.content_type, + m.message_type, + m.body, + m.binary_body, + m.message_id, + m.correlation_id, + m.conversation_id, + m.request_id, + m.initiator_id, + m.source_address, + m.destination_address, + m.response_address, + m.fault_address, + m.sent_time, + m.headers, + m.host; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION "{0}".fetch_messages_partitioned( + queue_name text + , fetch_consumer_id uuid + , fetch_lock_id uuid + , lock_duration interval + , fetch_count integer DEFAULT 1 + , concurrent_count integer DEFAULT 1 + , ordered integer DEFAULT 0) + RETURNS TABLE( + transport_message_id uuid + , queue_id bigint + , priority smallint + , message_delivery_id bigint + , consumer_id uuid + , lock_id uuid + , enqueue_time timestamp with time zone + , expiration_time timestamp with time zone + , delivery_count integer + , partition_key text + , routing_key text + , transport_headers jsonb + , content_type text + , message_type text + , body jsonb + , binary_body bytea + , message_id uuid + , correlation_id uuid + , conversation_id uuid + , request_id uuid + , initiator_id uuid + , source_address text + , destination_address text + , response_address text + , fault_address text + , sent_time timestamp with time zone + , headers jsonb + , host jsonb) + AS + $$ + DECLARE + v_queue_id bigint; + v_enqueue_time timestamptz; + v_now timestamptz; + BEGIN + SELECT INTO v_queue_id q.Id FROM "{0}".queue q WHERE q.name = queue_name AND q.type = 1; + IF v_queue_id IS NULL THEN + RAISE EXCEPTION 'Queue not found: %', queue_name; + END IF; + + v_now := (now() at time zone 'utc'); + v_enqueue_time := v_now + lock_duration; + + RETURN QUERY WITH msgs AS ( + SELECT md.* + FROM "{0}".message_delivery md + WHERE md.message_delivery_id IN ( + WITH ready AS ( + SELECT mdx.message_delivery_id, mdx.enqueue_time, mdx.lock_id, mdx.priority, + row_number() over ( partition by mdx.partition_key + order by mdx.priority, mdx.enqueue_time, mdx.message_delivery_id ) as row_normal, + row_number() over ( partition by mdx.partition_key + order by mdx.priority, mdx.message_delivery_id,mdx.enqueue_time ) as row_ordered, + first_value(CASE WHEN mdx.enqueue_time > v_now THEN mdx.consumer_id END) over (partition by mdx.partition_key + order by mdx.enqueue_time DESC, mdx.message_delivery_id DESC) as consumer_id, + sum(CASE WHEN mdx.enqueue_time > v_now AND mdx.consumer_id = fetch_consumer_id AND mdx.lock_id IS NOT NULL THEN 1 END) + over (partition by mdx.partition_key + order by mdx.enqueue_time DESC, mdx.message_delivery_id DESC) as active_count + FROM "{0}".message_delivery mdx + WHERE mdx.queue_id = v_queue_id + AND mdx.delivery_count < mdx.max_delivery_count + ) + SELECT ready.message_delivery_id + FROM ready + WHERE ( ( ordered = 0 AND ready.row_normal <= concurrent_count) OR ( ordered = 1 AND ready.row_ordered <= concurrent_count ) ) + AND ready.enqueue_time <= v_now + AND (ready.consumer_id IS NULL OR ready.consumer_id = fetch_consumer_id) + AND (active_count < concurrent_count OR active_count IS NULL) + ORDER BY ready.priority, ready.enqueue_time, ready.message_delivery_id + LIMIT fetch_count FOR UPDATE SKIP LOCKED) + FOR UPDATE OF md SKIP LOCKED) + UPDATE "{0}".message_delivery dm + SET delivery_count = dm.delivery_count + 1, + last_delivered = v_now, + consumer_id = fetch_consumer_id, + lock_id = fetch_lock_id, + enqueue_time = v_enqueue_time + FROM msgs + INNER JOIN "{0}".message m on msgs.transport_message_id = m.transport_message_id + WHERE dm.message_delivery_id = msgs.message_delivery_id + RETURNING + dm.transport_message_id, + dm.queue_id, + dm.priority, + dm.message_delivery_id, + dm.consumer_id, + dm.lock_id, + dm.enqueue_time, + dm.expiration_time, + dm.delivery_count, + dm.partition_key, + dm.routing_key, + dm.transport_headers, + m.content_type, + m.message_type, + m.body, + m.binary_body, + m.message_id, + m.correlation_id, + m.conversation_id, + m.request_id, + m.initiator_id, + m.source_address, + m.destination_address, + m.response_address, + m.fault_address, + m.sent_time, + m.headers, + m.host; + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION "{0}".delete_message(message_delivery_id bigint, lock_id uuid) + RETURNS bigint + LANGUAGE PLPGSQL + AS + $$ + DECLARE + v_message_delivery_id bigint; + v_queue_id bigint; + v_transport_message_id uuid; + BEGIN + DELETE FROM "{0}".message_delivery md + WHERE md.message_delivery_id = delete_message.message_delivery_id + AND md.lock_id = delete_message.lock_id + RETURNING md.message_delivery_id, md.transport_message_id, md.queue_id INTO v_message_delivery_id, v_transport_message_id, v_queue_id; + + IF v_transport_message_id IS NOT NULL THEN + DELETE FROM "{0}".message m + WHERE m.transport_message_id = v_transport_message_id + AND NOT EXISTS(SELECT FROM "{0}".message_delivery md WHERE md.transport_message_id = v_transport_message_id); + + INSERT INTO "{0}".queue_metric_capture (captured, queue_id, consume_count, error_count, dead_letter_count) + VALUES (now() at time zone 'utc', v_queue_id, 1, 0, 0); + + END IF; + + RETURN v_message_delivery_id; + END; + $$; + + CREATE OR REPLACE FUNCTION "{0}".touch_queue(queue_name text) + RETURNS bigint + LANGUAGE PLPGSQL + AS + $$ + DECLARE + v_queue_id bigint; + BEGIN + IF queue_name IS NULL OR LENGTH(queue_name) < 1 THEN + RAISE EXCEPTION 'Queue name must not be null'; + END IF; + + SELECT INTO v_queue_id q.Id FROM "{0}".queue q WHERE q.name = queue_name AND q.type = 1; + IF v_queue_id IS NULL THEN + RAISE EXCEPTION 'Queue not found: %', queue_name; + END IF; + + INSERT INTO "{0}".queue_metric_capture (captured, queue_id, consume_count, error_count, dead_letter_count) + VALUES (now() at time zone 'utc', v_queue_id, 0, 0, 0); + + RETURN v_queue_id; + END; + $$; + + CREATE OR REPLACE FUNCTION "{0}".delete_scheduled_message(token_id uuid) + RETURNS TABLE (transport_message_id uuid) + LANGUAGE PLPGSQL + AS + $$ + BEGIN + RETURN QUERY DELETE FROM "{0}".message tm + USING "{0}".message as m + LEFT JOIN "{0}".message_delivery md ON md.transport_message_id = m.transport_message_id + WHERE tm.transport_message_id = m.transport_message_id + AND m.scheduling_token_id = token_id + AND md.delivery_count = 0 + AND md.lock_id IS NULL + RETURNING tm.transport_message_id; + END; + $$; + + CREATE OR REPLACE FUNCTION "{0}".renew_message_lock(message_delivery_id bigint, lock_id uuid, duration interval) + RETURNS bigint + LANGUAGE PLPGSQL + AS + $$ + DECLARE + v_message_delivery_id bigint; + v_queue_id bigint; + BEGIN + IF duration < INTERVAL '1 seconds' THEN + RAISE EXCEPTION 'Invalid lock duration'; + END IF; + + UPDATE "{0}".message_delivery md + SET enqueue_time = (now() at time zone 'utc') + duration + WHERE md.message_delivery_id = renew_message_lock.message_delivery_id AND md.lock_id = renew_message_lock.lock_id + RETURNING md.message_delivery_id, md.queue_id INTO v_message_delivery_id, v_queue_id; + + IF v_queue_id IS NOT NULL THEN + INSERT INTO "{0}".queue_metric_capture (captured, queue_id, consume_count, error_count, dead_letter_count) + VALUES (now() at time zone 'utc', v_queue_id, 0, 0, 0); + END IF; + + RETURN v_message_delivery_id; + END; + $$; + + CREATE OR REPLACE FUNCTION "{0}".move_message(message_delivery_id bigint, lock_id uuid, queue_name text, queue_type integer, headers jsonb) + RETURNS bigint + LANGUAGE PLPGSQL + AS + $$ + DECLARE + v_message_delivery_id bigint; + v_queue_id bigint; + v_source_queue_id bigint; + v_enqueue_time timestamptz; + BEGIN + SELECT INTO v_queue_id q.Id FROM "{0}".queue q WHERE q.name = queue_name AND q.type = queue_type; + IF v_queue_id IS NULL THEN + RAISE EXCEPTION 'Queue not found: %', queue_name; + END IF; + + v_enqueue_time := (now() at time zone 'utc'); + + UPDATE "{0}".message_delivery md + SET enqueue_time = v_enqueue_time, queue_id = v_queue_id, lock_id = NULL, consumer_id = NULL, transport_headers = headers + FROM (SELECT mdx.message_delivery_id, queue_id, consumer_id FROM "{0}".message_delivery mdx + WHERE mdx.message_delivery_id = move_message.message_delivery_id AND mdx.lock_id = move_message.lock_id FOR UPDATE) mdy + WHERE mdy.message_delivery_id = md.message_delivery_id + RETURNING md.message_delivery_id, mdy.queue_id INTO v_message_delivery_id, v_source_queue_id; + + IF v_source_queue_id IS NOT NULL THEN + INSERT INTO "{0}".queue_metric_capture (captured, queue_id, consume_count, error_count, dead_letter_count) + VALUES (now() at time zone 'utc', v_source_queue_id, 0, + CASE WHEN queue_type = 2 THEN 1 ELSE 0 END, CASE WHEN queue_type = 3 THEN 1 ELSE 0 END); + END IF; + + RETURN v_message_delivery_id; + END; + $$; + + CREATE OR REPLACE FUNCTION "{0}".unlock_message(message_delivery_id bigint, lock_id uuid, delay interval, headers jsonb) + RETURNS bigint + LANGUAGE PLPGSQL + AS + $$ + DECLARE + v_message_delivery_id bigint; + v_enqueue_time timestamptz; + v_queue_id bigint; + BEGIN + v_enqueue_time := (now() at time zone 'utc'); + IF delay > INTERVAL '0 seconds' THEN + v_enqueue_time = v_enqueue_time + delay; + END IF; + + UPDATE "{0}".message_delivery md + SET enqueue_time = v_enqueue_time, consumer_id = NULL, transport_headers = headers + WHERE md.message_delivery_id = unlock_message.message_delivery_id AND md.lock_id = unlock_message.lock_id + RETURNING md.message_delivery_id, md.queue_id INTO v_message_delivery_id, v_queue_id; + + IF v_queue_id IS NOT NULL THEN + INSERT INTO "{0}".queue_metric_capture (captured, queue_id, consume_count, error_count, dead_letter_count) + VALUES (now() at time zone 'utc', v_queue_id, 0, 0, 0); + END IF; + + RETURN v_message_delivery_id; + END; + $$; + + CREATE OR REPLACE FUNCTION "{0}".send_message( + entity_name text + , priority integer DEFAULT NULL + , transport_message_id uuid DEFAULT gen_random_uuid() + , body jsonb DEFAULT NULL + , binary_body bytea DEFAULT NULL + , content_type text DEFAULT NULL + , message_type text DEFAULT NULL + , message_id uuid DEFAULT NULL + , correlation_id uuid DEFAULT NULL + , conversation_id uuid DEFAULT NULL + , request_id uuid DEFAULT NULL + , initiator_id uuid DEFAULT NULL + , source_address text DEFAULT NULL + , destination_address text DEFAULT NULL + , response_address text DEFAULT NULL + , fault_address text DEFAULT NULL + , sent_time timestamptz DEFAULT NULL + , headers jsonb DEFAULT NULL + , host jsonb DEFAULT NULL + , partition_key text DEFAULT NULL + , routing_key text DEFAULT NULL + , delay interval DEFAULT INTERVAL '0 seconds' + , scheduling_token_id uuid DEFAULT NULL + , max_delivery_count int DEFAULT 10 + ) + RETURNS bigint AS + $$ + DECLARE + v_queue_id bigint; + v_enqueue_time timestamptz; + BEGIN + if entity_name is null or length(entity_name) < 1 then + raise exception 'Queue names must not be null or empty'; + end if; + + SELECT INTO v_queue_id q.Id FROM "{0}".queue q WHERE q.name = entity_name AND q.type = 1; + if v_queue_id IS NULL THEN + raise exception 'Queue not found'; + end if; + + v_enqueue_time := (now() at time zone 'utc'); + IF delay > INTERVAL '0 seconds' THEN + v_enqueue_time = v_enqueue_time + delay; + END IF; + + INSERT INTO "{0}".message (transport_message_id, body, binary_body, content_type, message_type, message_id, correlation_id, conversation_id, request_id, initiator_id, + source_address, destination_address, response_address, fault_address, sent_time, headers, host, scheduling_token_id) + VALUES (transport_message_id, body, binary_body, content_type, message_type, message_id, correlation_id, conversation_id, request_id, initiator_id, + source_address, destination_address, response_address, fault_address, sent_time, headers, host, scheduling_token_id); + INSERT INTO "{0}".message_delivery (queue_id, transport_message_id, priority, enqueue_time, delivery_count, max_delivery_count, partition_key, routing_key) + VALUES (v_queue_id, send_message.transport_message_id, send_message.priority, v_enqueue_time, 0, send_message.max_delivery_count, send_message.partition_key, send_message.routing_key); + + RETURN 1; + + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION "{0}".publish_message( + entity_name text + , priority integer DEFAULT NULL + , transport_message_id uuid DEFAULT gen_random_uuid() + , body jsonb DEFAULT NULL + , binary_body bytea DEFAULT NULL + , content_type text DEFAULT NULL + , message_type text DEFAULT NULL + , message_id uuid DEFAULT NULL + , correlation_id uuid DEFAULT NULL + , conversation_id uuid DEFAULT NULL + , request_id uuid DEFAULT NULL + , initiator_id uuid DEFAULT NULL + , source_address text DEFAULT NULL + , destination_address text DEFAULT NULL + , response_address text DEFAULT NULL + , fault_address text DEFAULT NULL + , sent_time timestamptz DEFAULT NULL + , headers jsonb DEFAULT NULL + , host jsonb DEFAULT NULL + , partition_key text DEFAULT NULL + , routing_key text DEFAULT NULL + , delay interval DEFAULT INTERVAL '0 seconds' + , scheduling_token_id uuid DEFAULT NULL + , max_delivery_count int DEFAULT 10 + ) + RETURNS bigint AS + $$ + DECLARE + v_topic_id bigint; + v_enqueue_time timestamptz; + v_publish_count bigint; + BEGIN + IF entity_name IS NULL OR LENGTH(entity_name) < 1 THEN + RAISE EXCEPTION 'Topic names must not be null or empty'; + END IF; + + SELECT INTO v_topic_id t.Id FROM "{0}".topic t WHERE t.name = entity_name; + if v_topic_id IS NULL THEN + RAISE EXCEPTION 'Topic not found'; + END IF; + + v_enqueue_time := (now() at time zone 'utc'); + IF delay > INTERVAL '0 seconds' THEN + v_enqueue_time = v_enqueue_time + delay; + END IF; + + INSERT INTO "{0}".message (transport_message_id, body, binary_body, content_type, message_type, message_id, correlation_id, conversation_id, request_id, initiator_id, + source_address, destination_address, response_address, fault_address, sent_time, headers, host, scheduling_token_id) + VALUES (transport_message_id, body, binary_body, content_type, message_type, message_id, correlation_id, conversation_id, request_id, initiator_id, + source_address, destination_address, response_address, fault_address, sent_time, headers, host, scheduling_token_id); + + WITH delivered AS ( + INSERT INTO "{0}".message_delivery (queue_id, transport_message_id, priority, enqueue_time, delivery_count, max_delivery_count, partition_key, routing_key) + WITH RECURSIVE fabric AS ( + SELECT source_id, destination_id + FROM "{0}".topic t + LEFT JOIN "{0}".topic_subscription ts ON t.id = ts.source_id + AND CASE + WHEN ts.sub_type = 1 THEN true + WHEN ts.sub_type = 2 THEN publish_message.routing_key = ts.routing_key + WHEN ts.sub_type = 3 THEN publish_message.routing_key ~ ts.routing_key + ELSE false END + WHERE t.id = v_topic_id + + UNION ALL + + SELECT ts.source_id, ts.destination_id + FROM "{0}".topic_subscription ts, fabric + WHERE ts.source_id = fabric.destination_id + AND CASE + WHEN ts.sub_type = 1 THEN true + WHEN ts.sub_type = 2 THEN publish_message.routing_key = ts.routing_key + WHEN ts.sub_type = 3 THEN publish_message.routing_key ~ ts.routing_key + ELSE false END + ) + SELECT DISTINCT qs.destination_id, publish_message.transport_message_id, publish_message.priority, v_enqueue_time, 0, publish_message.max_delivery_count, publish_message.partition_key, publish_message.routing_key + FROM "{0}".queue_subscription qs, fabric + WHERE CASE + WHEN qs.sub_type = 1 THEN true + WHEN qs.sub_type = 2 THEN publish_message.routing_key = qs.routing_key + WHEN qs.sub_type = 3 THEN publish_message.routing_key ~ qs.routing_key + ELSE false END + AND (qs.source_id = fabric.destination_id OR qs.source_id = v_topic_id) + RETURNING message_delivery_id) + SELECT COUNT(d.message_delivery_id) FROM delivered d INTO v_publish_count; + + IF v_publish_count = 0 THEN + DELETE FROM "{0}".message WHERE message.transport_message_id = publish_message.transport_message_id; + END IF; + + RETURN v_publish_count; + + END; + $$ LANGUAGE plpgsql; + + CREATE OR REPLACE FUNCTION "{0}".notify_msg() + RETURNS trigger AS + $$ + DECLARE + v_payload json; + BEGIN + IF NEW.enqueue_time <= (now() at time zone 'utc') THEN + v_payload = json_build_object( + 'message_delivery_id', NEW.message_delivery_id, + 'enqueue_time', to_char(NEW.enqueue_time, 'YYYY-MM-DD"T"HH24:MI:SS.MS"Z"') + ); + + PERFORM pg_notify('{2}_msg_' || NEW.queue_id, v_payload::text); + END IF; + + RETURN NEW; + END; + $$ LANGUAGE plpgsql VOLATILE; + + DO $$ + BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM pg_trigger t + JOIN pg_class c ON c.oid = t.tgrelid + WHERE c.relname = 'message_delivery' + AND t.tgname = 'message_delivery_notify_trigger' + ) THEN + CREATE TRIGGER message_delivery_notify_trigger AFTER INSERT OR UPDATE ON "{0}".message_delivery + FOR EACH ROW EXECUTE PROCEDURE "{0}".notify_msg(); + END IF; + END $$; + + CREATE UNLOGGED TABLE IF NOT EXISTS "{0}".queue_metric_capture ( + queue_metric_id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + captured timestamptz NOT NULL, + queue_id bigint NOT NULL, + consume_count int NOT NULL, + error_count int NOT NULL, + dead_letter_count int NOT NULL + ); + + CREATE TABLE IF NOT EXISTS "{0}".queue_metric ( + queue_metric_id bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY, + start_time timestamptz NOT NULL, + duration interval NOT NULL, + queue_id bigint NOT NULL, + consume_count bigint NOT NULL, + error_count bigint NOT NULL, + dead_letter_count bigint NOT NULL + ); + + SELECT "{0}".create_constraint_if_not_exists('queue_metric', 'unique_queue_metric', + 'CREATE UNIQUE INDEX IF NOT EXISTS queue_metric_ndx ON "{0}".queue_metric (start_time, duration, queue_id);ALTER TABLE "{0}".queue_metric ADD CONSTRAINT unique_queue_metric UNIQUE USING INDEX queue_metric_ndx;'); + + CREATE INDEX IF NOT EXISTS queue_metric_queue_id ON "{0}".queue_metric (queue_id, start_time) INCLUDE (duration); + + CREATE OR REPLACE FUNCTION "{0}".process_metrics(row_limit int DEFAULT 10000) + RETURNS int + LANGUAGE PLPGSQL + AS + $$ + BEGIN + WITH metrics AS ( + DELETE FROM "{0}".queue_metric_capture + WHERE queue_metric_id < COALESCE((SELECT MIN(queue_metric_id) FROM "{0}".queue_metric_capture), 0) + row_limit + RETURNING * + ) + INSERT INTO "{0}".queue_metric (start_time, duration, queue_id, consume_count, error_count, dead_letter_count) + SELECT date_trunc('minute', m.captured), + interval '1 minute', + m.queue_id, + sum(m.consume_count), + sum(m.error_count), + sum(m.dead_letter_count) + FROM metrics m + GROUP BY date_trunc('minute', m.captured), m.queue_id + ON CONFLICT ON CONSTRAINT unique_queue_metric DO + UPDATE SET consume_count = queue_metric.consume_count + excluded.consume_count, + error_count = queue_metric.error_count + excluded.error_count, + dead_letter_count = queue_metric.dead_letter_count + excluded.dead_letter_count; + + WITH metrics AS ( + DELETE FROM "{0}".queue_metric + WHERE duration = interval '1 minute' AND start_time < (now() at time zone 'utc') - interval '8 hours' + RETURNING * + ) + INSERT INTO "{0}".queue_metric (start_time, duration, queue_id, consume_count, error_count, dead_letter_count) + SELECT date_trunc('hour', m.start_time), interval '1 hour', m.queue_id, + sum(m.consume_count), + sum(m.error_count), + sum(m.dead_letter_count) + FROM metrics m + GROUP BY date_trunc('hour', m.start_time), m.queue_id + ON CONFLICT ON CONSTRAINT unique_queue_metric DO + UPDATE SET consume_count = queue_metric.consume_count + excluded.consume_count, + error_count = queue_metric.error_count + excluded.error_count, + dead_letter_count = queue_metric.dead_letter_count + excluded.dead_letter_count; + + WITH metrics AS ( + DELETE FROM "{0}".queue_metric + WHERE duration = interval '1 hour' AND start_time < (now() at time zone 'utc') - interval '48 hours' + RETURNING * + ) + INSERT INTO "{0}".queue_metric (start_time, duration, queue_id, consume_count, error_count, dead_letter_count) + SELECT date_trunc('day', m.start_time), interval '1 day', m.queue_id, + sum(m.consume_count), + sum(m.error_count), + sum(m.dead_letter_count) + FROM metrics m + GROUP BY date_trunc('day', m.start_time), m.queue_id + ON CONFLICT ON CONSTRAINT unique_queue_metric DO + UPDATE SET consume_count = queue_metric.consume_count + excluded.consume_count, + error_count = queue_metric.error_count + excluded.error_count, + dead_letter_count = queue_metric.dead_letter_count + excluded.dead_letter_count; + + DELETE FROM "{0}".queue_metric + WHERE start_time < (now() at time zone 'utc') - interval '90 days'; + + RETURN 0; + END; + $$; + + CREATE OR REPLACE FUNCTION "{0}".purge_topology() + RETURNS int + LANGUAGE PLPGSQL + AS + $$ + BEGIN + WITH expired AS (SELECT q.id, q.name, (now() at time zone 'utc') - make_interval(secs => q.auto_delete) as expires_at + FROM "{0}".queue q + WHERE q.type = 1 AND q.auto_delete IS NOT NULL AND (now() at time zone 'utc') - make_interval(secs => q.auto_delete) > updated), + metrics AS (SELECT qm.queue_id, MAX(start_time) as start_time + FROM "{0}".queue_metric qm + INNER JOIN expired q2 on q2.id = qm.queue_id + WHERE start_time + duration > q2.expires_at + GROUP BY qm.queue_id) + DELETE FROM "{0}".queue qd + USING (SELECT qdx.name FROM expired qdx WHERE qdx.id NOT IN (SELECT queue_id FROM metrics)) exp + WHERE qd.name = exp.name; + + RETURN 0; + END; + $$; + + CREATE OR REPLACE FUNCTION "{0}".requeue_messages(queue_name text, source_queue_type int, target_queue_type int, message_count int, + delay interval DEFAULT INTERVAL '0 seconds', redelivery_count int DEFAULT 10) + RETURNS int + LANGUAGE PLPGSQL + AS + $$ + DECLARE + v_source_queue_id bigint; + v_target_queue_id bigint; + v_requeue_count int; + v_enqueue_time timestamptz; + BEGIN + IF NOT source_queue_type BETWEEN 1 AND 3 THEN + RAISE EXCEPTION 'Invalid source queue type: %', source_queue_type; + END IF; + IF NOT target_queue_type BETWEEN 1 AND 3 THEN + RAISE EXCEPTION 'Invalid target queue type: %', target_queue_type; + END IF; + IF source_queue_type = target_queue_type THEN + RAISE EXCEPTION 'Source and target queue type must not be the same'; + END IF; + + SELECT INTO v_source_queue_id q.Id FROM "{0}".queue q WHERE q.name = queue_name AND q.type = source_queue_type; + IF v_source_queue_id IS NULL THEN + RAISE EXCEPTION 'Queue not found: %', queue_name; + END IF; + + SELECT INTO v_target_queue_id q.Id FROM "{0}".queue q WHERE q.name = queue_name AND q.type = target_queue_type; + IF v_target_queue_id IS NULL THEN + RAISE EXCEPTION 'Queue not found: %', queue_name; + END IF; + + v_enqueue_time := (now() at time zone 'utc') + delay; + + UPDATE "{0}".message_delivery md + SET enqueue_time = v_enqueue_time, + queue_id = v_target_queue_id, + max_delivery_count = md.delivery_count + redelivery_count + FROM (SELECT mdx.message_delivery_id, queue_id + FROM "{0}".message_delivery mdx + WHERE mdx.queue_id = v_source_queue_id + AND mdx.lock_id IS NULL + AND mdx.consumer_id IS NULL + AND (mdx.expiration_time IS NULL OR mdx.expiration_time > v_enqueue_time) + ORDER BY mdx.message_delivery_id FOR UPDATE LIMIT message_count) mdy + WHERE mdy.message_delivery_id = md.message_delivery_id; + GET DIAGNOSTICS v_requeue_count = ROW_COUNT; + + RETURN v_requeue_count; + END; + $$; + + CREATE OR REPLACE FUNCTION "{0}".requeue_message( + message_delivery_id bigint, + target_queue_type int, + delay interval DEFAULT INTERVAL '0 seconds', + redelivery_count int DEFAULT 10) + RETURNS int + LANGUAGE PLPGSQL + AS + $$ + DECLARE + v_source_queue_id bigint; + v_source_queue_name text; + v_source_queue_type int; + v_target_queue_id bigint; + v_requeue_count int; + v_enqueue_time timestamptz; + BEGIN + IF NOT target_queue_type BETWEEN 1 AND 3 THEN + RAISE EXCEPTION 'Invalid target queue type: %', target_queue_type; + END IF; + + SELECT INTO v_source_queue_id md.queue_id + FROM "{0}".message_delivery md + WHERE md.message_delivery_id = requeue_message.message_delivery_id; + IF v_source_queue_id IS NULL THEN + RAISE EXCEPTION 'Message delivery not found: %', requeue_message.message_delivery_id; + END IF; + + SELECT INTO v_source_queue_name, v_source_queue_type q.name, q.type + FROM "{0}".queue q + WHERE q.id = v_source_queue_id; + IF v_source_queue_name IS NULL THEN + RAISE EXCEPTION 'Queue not found: %', v_source_queue_id; + END IF; + + SELECT INTO v_target_queue_id q.Id + FROM "{0}".queue q + WHERE q.name = v_source_queue_name + AND q.type = target_queue_type + AND q.type != v_source_queue_type; + IF v_target_queue_id IS NULL THEN + RAISE EXCEPTION 'Queue type not found: %', target_queue_type; + END IF; + + v_enqueue_time := (now() at time zone 'utc') + delay; + + UPDATE "{0}".message_delivery md + SET enqueue_time = v_enqueue_time, + queue_id = v_target_queue_id, + max_delivery_count = md.delivery_count + redelivery_count + FROM (SELECT mdx.message_delivery_id, queue_id + FROM "{0}".message_delivery mdx + WHERE mdx.queue_id = v_source_queue_id + AND mdx.lock_id IS NULL + AND mdx.consumer_id IS NULL + AND (mdx.expiration_time IS NULL OR mdx.expiration_time > v_enqueue_time) + AND mdx.message_delivery_id = requeue_message.message_delivery_id FOR UPDATE) mdy + WHERE mdy.message_delivery_id = md.message_delivery_id; + GET DIAGNOSTICS v_requeue_count = ROW_COUNT; + + RETURN v_requeue_count; + END; + $$; + + CREATE OR REPLACE VIEW "{0}".queues + AS + SELECT x.queue_name, + MAX(x.queue_auto_delete) as queue_auto_delete, + SUM(x.message_ready) as ready, + SUM(x.message_scheduled) as scheduled, + SUM(x.message_error) as errored, + SUM(x.message_dead_letter) as dead_lettered, + SUM(x.message_locked) as locked, + COALESCE(SUM(x.consume_count), 0)::bigint as consume_count, + COALESCE(SUM(x.error_count), 0)::bigint as error_count, + COALESCE(SUM(x.dead_letter_count), 0)::bigint as dead_letter_count, + COALESCE(MAX(x.duration), 0)::int as count_duration + + FROM (SELECT q.name as queue_name, + q.auto_delete as queue_auto_delete, + qm.consume_count, + qm.error_count, + qm.dead_letter_count, + qm.duration, + + CASE + WHEN q.type = 1 + AND md.message_delivery_id IS NOT NULL + AND md.enqueue_time <= (now() at time zone 'utc') THEN 1 + ELSE 0 END as message_ready, + CASE + WHEN q.type = 1 + AND md.message_delivery_id IS NOT NULL + AND md.lock_id IS NULL + AND md.enqueue_time > (now() at time zone 'utc') THEN 1 + ELSE 0 END as message_scheduled, + CASE + WHEN q.type = 1 + AND md.message_delivery_id IS NOT NULL + AND md.lock_id IS NOT NULL + AND md.delivery_count >= 1 + AND md.enqueue_time > (now() at time zone 'utc') THEN 1 + ELSE 0 END as message_locked, + CASE + WHEN q.type = 2 + AND md.message_delivery_id IS NOT NULL THEN 1 + ELSE 0 END as message_error, + CASE + WHEN q.type = 3 + AND md.message_delivery_id IS NOT NULL THEN 1 + ELSE 0 END as message_dead_letter + FROM "{0}".queue q + LEFT JOIN "{0}".message_delivery md ON q.id = md.queue_id + LEFT JOIN (SELECT DISTINCT ON (qm.queue_id) qm.queue_id, + q2.name as queue_name, + qm.consume_count as consume_count, + qm.error_count as error_count, + qm.dead_letter_count as dead_letter_count, + EXTRACT(EPOCH FROM qm.duration) as duration + FROM "{0}".queue_metric qm + INNER JOIN "{0}".queue q2 on qm.queue_id = q2.id + WHERE q2.type = 1 + AND qm.start_time >= (now() at time zone 'utc') - interval '1 minutes' + ORDER BY qm.queue_id, qm.start_time DESC) qm ON qm.queue_id = q.id) x + GROUP BY x.queue_name; + + CREATE OR REPLACE VIEW "{0}".subscriptions + AS + SELECT t.name as topic_name, 'topic' as destination_type, t2.name as destination_name, ts.sub_type as subscription_type, ts.routing_key + FROM "{0}".topic t + JOIN "{0}".topic_subscription ts ON t.id = ts.source_id + JOIN "{0}".topic t2 on t2.id = ts.destination_id + UNION + SELECT t.name as topic_name, 'queue' as destination_type, q.name as destination_name, qs.sub_type as subscription_type, qs.routing_key + FROM "{0}".queue_subscription qs + LEFT JOIN "{0}".queue q on qs.destination_id = q.id + LEFT JOIN "{0}".topic t on qs.source_id = t.id; + + SET ROLE none; + """; + + readonly ILogger _logger; + + public PostgresDatabaseMigrator(ILogger logger) + { + _logger = logger; + } + + public async Task CreateDatabase(SqlTransportOptions options, CancellationToken cancellationToken = default) + { + await CreateDatabaseIfNotExist(options, cancellationToken).ConfigureAwait(false); + } + + public async Task DeleteDatabase(SqlTransportOptions options, CancellationToken cancellationToken = default) + { + await using var connection = PostgresSqlTransportConnection.GetSystemDatabaseConnection(options); + await connection.Open(cancellationToken).ConfigureAwait(false); + + var result = await connection.Connection.ExecuteScalarAsync(string.Format(DbExistsSql, options.Database)).ConfigureAwait(false); + if (result == 1) + { + await connection.Connection.ExecuteScalarAsync(string.Format(DropSql, options.Database)).ConfigureAwait(false); + + _logger.LogInformation("Database {Database} deleted", options.Database); + } + } + + public async Task CreateInfrastructure(SqlTransportOptions options, CancellationToken cancellationToken) + { + await CreateSchemaIfNotExist(options, cancellationToken).ConfigureAwait(false); + + await using var connection = PostgresSqlTransportConnection.GetDatabaseConnection(options); + await connection.Open(cancellationToken).ConfigureAwait(false); + + try + { + var sanitizedSchemaName = NotifyChannel.SanitizeSchemaName(options.Schema); + + await connection.Connection.ExecuteScalarAsync(string.Format(CreateInfrastructureSql, options.Schema, options.Role, sanitizedSchemaName)) + .ConfigureAwait(false); + + _logger.LogDebug("Transport infrastructure in schema {Schema} created (or updated)", options.Schema); + } + finally + { + await connection.Close().ConfigureAwait(false); + } + } + + async Task CreateDatabaseIfNotExist(SqlTransportOptions options, CancellationToken cancellationToken) + { + await using var connection = PostgresSqlTransportConnection.GetSystemDatabaseConnection(options); + await connection.Open(cancellationToken).ConfigureAwait(false); + + try + { + var result = await connection.Connection.ExecuteScalarAsync(string.Format(DbExistsSql, options.Database)).ConfigureAwait(false); + if (result == 1) + _logger.LogDebug("Database {Database} already exists", options.Database); + else + { + await connection.Connection.ExecuteScalarAsync(string.Format(DbCreateSql, options.Database)).ConfigureAwait(false); + + _logger.LogInformation("Database {Database} created", options.Database); + } + } + finally + { + await connection.Close().ConfigureAwait(false); + } + } + + async Task CreateSchemaIfNotExist(SqlTransportOptions options, CancellationToken cancellationToken) + { + await using var connection = PostgresSqlTransportConnection.GetDatabaseAdminConnection(options); + await connection.Open(cancellationToken).ConfigureAwait(false); + + try + { + await connection.Connection.ExecuteScalarAsync(string.Format(SchemaCreateSql, options.Schema)).ConfigureAwait(false); + + _logger.LogDebug("Schema {Schema} created", options.Schema); + + await GrantAccess(connection, options); + } + finally + { + await connection.Close().ConfigureAwait(false); + } + } + + async Task GrantAccess(IPostgresSqlTransportConnection connection, SqlTransportOptions options) + { + var result = await connection.Connection.ExecuteScalarAsync(string.Format(RoleExistsSql, options.Role)).ConfigureAwait(false); + if (result != 1) + { + await connection.Connection.ExecuteScalarAsync(string.Format(CreateRoleSql, options.Role)).ConfigureAwait(false); + + _logger.LogDebug("Role {Role} created", options.Role); + } + + var principal = PostgresSqlTransportConnection.GetAdminMigrationPrincipal(options); + if (!string.Equals(options.Role, principal, StringComparison.Ordinal)) + { + await connection.Connection.ExecuteScalarAsync(string.Format(GrantRoleToPrincipalSql, options.Role, principal)) + .ConfigureAwait(false); + } + + await connection.Connection.ExecuteScalarAsync(string.Format(GrantRoleSql, options.Role, options.Schema, principal)) + .ConfigureAwait(false); + + _logger.LogDebug("Role {Role} granted access to schema {Schema}", options.Role, options.Schema); + + await connection.Connection.ExecuteScalarAsync(string.Format(GrantConnectSql, options.Database, options.Role)).ConfigureAwait(false); + + _logger.LogDebug("Role {Role} granted connect to database {Database}", options.Role, options.Database); + + result = await connection.Connection.ExecuteScalarAsync(string.Format(RoleExistsSql, options.Username)).ConfigureAwait(false); + if (result != 1) + { + await connection.Connection.ExecuteScalarAsync(string.Format(CreateUserSql, options.Role, options.Username, options.Password)) + .ConfigureAwait(false); + + _logger.LogDebug("User role {Username} created", options.Username); + } + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresDbConnectionContext.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresDbConnectionContext.cs new file mode 100644 index 00000000000..8d01fdc16a3 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresDbConnectionContext.cs @@ -0,0 +1,368 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Data; + using System.Threading; + using System.Threading.Tasks; + using Configuration; + using Dapper; + using Helpers; + using Logging; + using MassTransit.Middleware; + using Npgsql; + using RetryPolicies; + using Transports; + using Util; + + + public class PostgresDbConnectionContext : + BasePipeContext, + ConnectionContext, + IAsyncDisposable + { + readonly NotificationAgent _agent; + readonly NpgsqlDataSource _dataSource; + readonly TaskExecutor _executor; + readonly ISqlHostConfiguration _hostConfiguration; + readonly PostgresSqlHostSettings _hostSettings; + readonly IRetryPolicy _retryPolicy; + + static PostgresDbConnectionContext() + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + SqlMapper.AddTypeHandler(new UriTypeHandler()); + } + + public PostgresDbConnectionContext(ISqlHostConfiguration hostConfiguration, ITransportSupervisor supervisor) + : base(supervisor.Stopped) + { + _hostConfiguration = hostConfiguration; + + _hostSettings = hostConfiguration.Settings as PostgresSqlHostSettings + ?? throw new ConfigurationException("The host settings were not of the expected type"); + + _dataSource = _hostSettings.GetDataSource(); + + _retryPolicy = Retry.CreatePolicy(x => x.Immediate(10).Handle(ex => ex.IsTransient)); + + Topology = hostConfiguration.Topology; + + _agent = new NotificationAgent(this, hostConfiguration); + supervisor.AddConsumeAgent(_agent); + + supervisor.AddConsumeAgent(new MaintenanceAgent(this, hostConfiguration)); + + _executor = new TaskExecutor(hostConfiguration.Settings.ConnectionLimit); + } + + public ISqlBusTopology Topology { get; } + + public IsolationLevel IsolationLevel => _hostSettings.IsolationLevel; + + public Uri HostAddress => _hostConfiguration.HostAddress; + + public string? Schema => _hostSettings.Schema; + + public ClientContext CreateClientContext(CancellationToken cancellationToken) + { + return new PostgresClientContext(this, cancellationToken); + } + + async Task ConnectionContext.CreateConnection(CancellationToken cancellationToken) + { + return await CreateConnection(cancellationToken).ConfigureAwait(false); + } + + public Task Query(Func> callback, CancellationToken cancellationToken) + { + return _executor.Run(() => + { + return _retryPolicy.Retry(async () => + { + await using var connection = await CreateConnection(cancellationToken).ConfigureAwait(false); + + #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + await using var transaction = await connection.Connection.BeginTransactionAsync(_hostSettings.IsolationLevel, cancellationToken) + .ConfigureAwait(false); + #else + await using var transaction = connection.Connection.BeginTransaction(_hostSettings.IsolationLevel); + #endif + + var result = await callback(connection.Connection, transaction).ConfigureAwait(false); + + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + + return result; + }, false, cancellationToken); + }, cancellationToken); + } + + public Task DelayUntilMessageReady(long queueId, TimeSpan timeout, CancellationToken cancellationToken) + { + var queueToken = _agent.GetCancellationTokenForQueue(queueId); + + async Task WaitAsync() + { + using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, queueToken); + var delayTask = Task.Delay(timeout, cts.Token); + await Task.WhenAny(delayTask).ConfigureAwait(false); + + cts.Cancel(); + + if (queueToken.IsCancellationRequested) + _agent.RemoveTokenForQueue(queueId); + } + + return WaitAsync(); + } + + public async ValueTask DisposeAsync() + { + if (_hostSettings.IsProvidedDataSource == false) + await _dataSource.DisposeAsync().ConfigureAwait(false); + + TransportLogMessages.DisconnectedHost(_hostConfiguration.HostAddress.ToString()); + } + + async Task CreateConnection(CancellationToken cancellationToken) + { + var connection = new PostgresSqlTransportConnection(_dataSource.CreateConnection()); + + await connection.Open(cancellationToken).ConfigureAwait(false); + + return connection; + } + + + class NotificationAgent : + Agent + { + readonly PostgresDbConnectionContext _context; + readonly ISqlHostConfiguration _hostConfiguration; + readonly ILogContext? _logContext; + readonly ConcurrentDictionary _notificationTokens; + CancellationTokenSource _listenTokenSource; + + public NotificationAgent(PostgresDbConnectionContext context, ISqlHostConfiguration hostConfiguration) + { + _context = context; + _hostConfiguration = hostConfiguration; + _logContext = hostConfiguration.LogContext; + + _notificationTokens = new ConcurrentDictionary(); + _listenTokenSource = new CancellationTokenSource(); + + var runTask = Task.Run(() => ListenForNotifications(), Stopping); + + SetReady(runTask); + + SetCompleted(runTask); + } + + public CancellationToken GetCancellationTokenForQueue(long queueId) + { + var added = false; + var notifyTokenSource = _notificationTokens.GetOrAdd(queueId, _ => + { + added = true; + return new CancellationTokenSource(); + }); + + if (added) + _listenTokenSource.Cancel(); + + if (notifyTokenSource.IsCancellationRequested) + _notificationTokens.TryRemove(queueId, out _); + + return notifyTokenSource.Token; + } + + public void RemoveTokenForQueue(long queueId) + { + if (_notificationTokens.TryGetValue(queueId, out var existing)) + { + var newValue = new CancellationTokenSource(); + if (!_notificationTokens.TryUpdate(queueId, newValue, existing)) + newValue.Dispose(); + } + } + + async Task ListenForNotifications() + { + LogContext.SetCurrentIfNull(_logContext); + + while (!Stopping.IsCancellationRequested) + { + try + { + await _hostConfiguration.Retry(async () => + { + await using var connection = await _context.CreateConnection(Stopping); + + var queueIds = new HashSet(_notificationTokens.Keys); + var sanitizedSchemaName = NotifyChannel.SanitizeSchemaName(_context.Schema); + + connection.Connection.Notification += OnConnectionOnNotification; + + foreach (var queueId in queueIds) + { + await connection.Connection.ExecuteScalarAsync($"LISTEN \"{sanitizedSchemaName}_msg_{queueId}\"", Stopping) + .ConfigureAwait(false); + + // LogContext.Debug?.Log("LISTEN \"{sanitizedSchemaName}_msg_{queueId}\"", queueId); + } + + while (!Stopping.IsCancellationRequested) + { + try + { + using var linkedTokenSource = CancellationTokenSource.CreateLinkedTokenSource(_listenTokenSource.Token, Stopping); + + await connection.Connection.WaitAsync(linkedTokenSource.Token).ConfigureAwait(false); + } + catch (OperationCanceledException) + { + } + + if (_listenTokenSource.IsCancellationRequested) + _listenTokenSource = new CancellationTokenSource(); + + foreach (var queueId in _notificationTokens.Keys) + { + if (queueIds.Contains(queueId)) + continue; + + await connection.Connection.ExecuteScalarAsync($"LISTEN \"{sanitizedSchemaName}_msg_{queueId}\"", Stopping) + .ConfigureAwait(false); + + // LogContext.Debug?.Log("LISTEN \"{sanitizedSchemaName}_msg_{queueId}\"", queueId); + + queueIds.Add(queueId); + } + } + }, Stopping, Stopping); + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + LogContext.Debug?.Log(exception, "PgSql notification faulted"); + } + } + } + + void OnConnectionOnNotification(object sender, NpgsqlNotificationEventArgs args) + { + LogContext.SetCurrentIfNull(_logContext); + + var index = args.Channel.LastIndexOf('_'); + if (index > 0 && long.TryParse(args.Channel.Substring(index + 1), out var queueId) && _notificationTokens.TryGetValue(queueId, out var source)) + { + // LogContext.Debug?.Log("NOTIFY {Channel}", args.Channel); + source.Cancel(); + } + } + } + + + class MaintenanceAgent : + Agent + { + readonly PostgresDbConnectionContext _context; + readonly ISqlHostConfiguration _hostConfiguration; + readonly ILogContext? _logContext; + + public MaintenanceAgent(PostgresDbConnectionContext context, ISqlHostConfiguration hostConfiguration) + { + _context = context; + _hostConfiguration = hostConfiguration; + _logContext = hostConfiguration.LogContext; + + var runTask = Task.Run(() => PerformMaintenance(), Stopping); + + SetReady(runTask); + + SetCompleted(runTask); + } + + async Task PerformMaintenance() + { + LogContext.SetCurrentIfNull(_logContext); + + var processMetricsSql = string.Format(SqlStatements.DbProcessMetricsSql, _context.Schema); + var purgeTopologySql = string.Format(SqlStatements.DbPurgeTopologySql, _context.Schema); + + var random = new Random(); + + var cleanupInterval = _hostConfiguration.Settings.QueueCleanupInterval + + TimeSpan.FromSeconds(random.Next(0, (int)(_hostConfiguration.Settings.QueueCleanupInterval.TotalSeconds / 10))); + + while (!Stopping.IsCancellationRequested) + { + DateTime? lastCleanup = null; + + try + { + var maintenanceInterval = _hostConfiguration.Settings.MaintenanceInterval + + TimeSpan.FromSeconds(random.Next(0, (int)(_hostConfiguration.Settings.MaintenanceInterval.TotalSeconds / 10))); + + try + { + await Task.Delay(maintenanceInterval, Stopping); + } + catch (OperationCanceledException) + { + using var timeoutToken = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + + try + { + await _context.Query( + (x, t) => x.ExecuteScalarAsync(processMetricsSql, + new { row_limit = _hostConfiguration.Settings.MaintenanceBatchSize }, t), timeoutToken.Token); + + if (lastCleanup == null) + await _context.Query((x, t) => x.ExecuteScalarAsync(purgeTopologySql, t), timeoutToken.Token); + } + catch (ObjectDisposedException) + { + } + catch (OperationCanceledException) + { + } + catch (TimeoutException) + { + } + } + + await _hostConfiguration.Retry(async () => + { + await _context.Query((x, t) => x.ExecuteScalarAsync(processMetricsSql, new + { + row_limit = _hostConfiguration.Settings.MaintenanceBatchSize, + }, t), Stopping); + + if (lastCleanup == null || lastCleanup < DateTime.UtcNow - cleanupInterval) + { + await _context.Query((x, t) => x.ExecuteScalarAsync(purgeTopologySql, t), Stopping); + + lastCleanup = DateTime.UtcNow; + cleanupInterval = _hostConfiguration.Settings.QueueCleanupInterval + + TimeSpan.FromSeconds(random.Next(0, (int)(_hostConfiguration.Settings.QueueCleanupInterval.TotalSeconds / 10))); + } + }, Stopping, Stopping); + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + LogContext.Debug?.Log(exception, "PostgreSQL Maintenance Faulted"); + } + } + } + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresSqlHostConfigurator.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresSqlHostConfigurator.cs new file mode 100644 index 00000000000..0084f5332b7 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresSqlHostConfigurator.cs @@ -0,0 +1,47 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + using System; + using Configuration; + using Npgsql; + + + public class PostgresSqlHostConfigurator : + SqlHostConfigurator, + IPostgresSqlHostConfigurator + { + readonly PostgresSqlHostSettings _settings; + + public PostgresSqlHostConfigurator(PostgresSqlHostSettings settings) + : base(settings) + { + _settings = settings; + } + + public PostgresSqlHostConfigurator(Uri hostAddress) + : this(new PostgresSqlHostSettings(hostAddress)) + { + } + + public PostgresSqlHostConfigurator(SqlTransportOptions options) + : this(new PostgresSqlHostSettings(options)) + { + } + + public PostgresSqlHostConfigurator(string connectionString) + : this(new PostgresSqlHostSettings(connectionString)) + { + } + + public PostgresSqlHostConfigurator(NpgsqlDataSource dataSource) + : this(new PostgresSqlHostSettings(dataSource)) + { + } + + public SqlHostSettings Settings => _settings; + + public override string? ConnectionString + { + set => _settings.ConnectionString = value; + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresSqlHostSettings.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresSqlHostSettings.cs new file mode 100644 index 00000000000..5922c7b4398 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresSqlHostSettings.cs @@ -0,0 +1,132 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + using System; + using System.Linq; + using Configuration; + using Npgsql; + + + public class PostgresSqlHostSettings : + ConfigurationSqlHostSettings + { + readonly NpgsqlDataSource? _dataSource; + NpgsqlConnectionStringBuilder? _builder; + + public PostgresSqlHostSettings(Uri hostAddress) + : base(hostAddress) + { + } + + public PostgresSqlHostSettings(string connectionString) + { + ConnectionString = connectionString; + } + + public PostgresSqlHostSettings(NpgsqlDataSource dataSource) + { + if (dataSource == null) + throw new ArgumentNullException(nameof(dataSource)); + + _dataSource = dataSource; + + IsProvidedDataSource = true; + + ConnectionString = dataSource.ConnectionString; + } + + public PostgresSqlHostSettings(SqlTransportOptions options) + { + var builder = PostgresSqlTransportConnection.CreateBuilder(options); + + ParseHost(builder.Host); + if (builder.Port > 0 && builder.Port != NpgsqlConnection.DefaultPort) + Port = options.Port; + + Database = builder.Database; + Schema = options.Schema; + + Username = builder.Username; + Password = builder.Password; + + _builder = builder; + + if (options.ConnectionLimit.HasValue) + ConnectionLimit = options.ConnectionLimit.Value; + } + + public string? MultipleHosts { get; set; } + + /// + /// If true, the data source was provided by the developer and should not be disposed + /// + public bool IsProvidedDataSource { get; private set; } + + public string? ConnectionString + { + set + { + var builder = new NpgsqlConnectionStringBuilder(value); + + ParseHost(builder.Host); + if (builder.Port > 0 && builder.Port != NpgsqlConnection.DefaultPort) + Port = builder.Port; + + Database = builder.Database; + + Username = builder.Username; + Password = builder.Password; + + Schema = builder.SearchPath ?? "transport"; + + _builder = builder; + } + } + + public NpgsqlDataSource GetDataSource() + { + if (_dataSource != null) + return _dataSource; + + var builder = _builder ??= new NpgsqlConnectionStringBuilder + { + Host = MultipleHosts ?? Host, + Username = Username, + Password = Password, + Database = Database + }; + + if (Port.HasValue && Port.Value != NpgsqlConnection.DefaultPort) + builder.Port = Port.Value; + + return NpgsqlDataSource.Create(builder); + } + + public override ConnectionContextFactory CreateConnectionContextFactory(ISqlHostConfiguration hostConfiguration) + { + return new PostgresConnectionContextFactory(hostConfiguration); + } + + void ParseHost(string? host) + { + var hostSegments = host?.Split(','); + if (hostSegments?.Length > 1) + { + Host = hostSegments[0].Split(':').First().Trim(); + MultipleHosts = host!.Trim(); + } + else + { + var segments = host?.Split(':'); + if (segments?.Length == 1) + Host = segments[0].Trim(); + else if (segments?.Length == 2) + { + Host = segments[0].Trim(); + + if (int.TryParse(segments[1], out var port) && port != 0 && port != NpgsqlConnection.DefaultPort) + Port = port; + } + } + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresSqlTransportConnection.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresSqlTransportConnection.cs new file mode 100644 index 00000000000..c54b77c2170 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/PostgresSqlTransportConnection.cs @@ -0,0 +1,119 @@ +namespace MassTransit.SqlTransport.PostgreSql; + +using System; +using System.Threading; +using System.Threading.Tasks; +using Npgsql; + + +public class PostgresSqlTransportConnection : + IPostgresSqlTransportConnection +{ + public PostgresSqlTransportConnection(NpgsqlConnection connection) + { + Connection = connection; + } + + public ValueTask DisposeAsync() + { + return Connection.DisposeAsync(); + } + + public NpgsqlConnection Connection { get; } + + public NpgsqlCommand CreateCommand(string commandText) + { + var command = new NpgsqlCommand(commandText); + command.Connection = Connection; + + return command; + } + + public Task Open(CancellationToken cancellationToken = default) + { + return Connection.OpenAsync(cancellationToken); + } + + public Task Close() + { + return Connection.CloseAsync(); + } + + public static PostgresSqlTransportConnection GetSystemDatabaseConnection(SqlTransportOptions options) + { + var builder = CreateBuilder(options); + + builder.Database = "postgres"; + + if (!string.IsNullOrWhiteSpace(options.AdminUsername)) + builder.Username = options.AdminUsername; + if (!string.IsNullOrWhiteSpace(options.AdminPassword)) + builder.Password = options.AdminPassword; + + return new PostgresSqlTransportConnection(new NpgsqlConnection(builder.ToString())); + } + + public static PostgresSqlTransportConnection GetDatabaseAdminConnection(SqlTransportOptions options) + { + var builder = CreateBuilder(options); + + if (!string.IsNullOrWhiteSpace(options.AdminUsername)) + builder.Username = options.AdminUsername; + if (!string.IsNullOrWhiteSpace(options.AdminPassword)) + builder.Password = options.AdminPassword; + + return new PostgresSqlTransportConnection(new NpgsqlConnection(builder.ToString())); + } + + public static PostgresSqlTransportConnection GetDatabaseConnection(SqlTransportOptions options) + { + return new PostgresSqlTransportConnection(new NpgsqlConnection(CreateBuilder(options).ToString())); + } + + public static NpgsqlConnectionStringBuilder CreateBuilder(SqlTransportOptions options) + { + var builder = new NpgsqlConnectionStringBuilder(options.ConnectionString); + + if (!string.IsNullOrWhiteSpace(options.Host)) + builder.Host = options.Host; + else if (!string.IsNullOrWhiteSpace(builder.Host)) + options.Host = builder.Host; + + if (!string.IsNullOrWhiteSpace(options.Database)) + builder.Database = options.Database; + else if (!string.IsNullOrWhiteSpace(builder.Database)) + options.Database = builder.Database; + + if (!string.IsNullOrWhiteSpace(options.Username)) + builder.Username = options.Username; + else if (!string.IsNullOrWhiteSpace(builder.Username)) + options.Username = builder.Username; + + if (!string.IsNullOrWhiteSpace(options.Password)) + builder.Password = options.Password; + else if (!string.IsNullOrWhiteSpace(builder.Password)) + options.Password = builder.Password; + + if (options.Port.HasValue) + builder.Port = options.Port.Value; + else if (builder.Port != NpgsqlConnection.DefaultPort) + options.Port = builder.Port; + + if (string.IsNullOrWhiteSpace(options.Schema)) + options.Schema = "transport"; + + if (string.IsNullOrWhiteSpace(options.Role)) + options.Role = "transport"; + + return builder; + } + + public static string? GetAdminMigrationPrincipal(SqlTransportOptions options) + { + var principal = options.AdminUsername ?? options.Username ?? "postgres"; + + return principal.Contains("@") + ? principal.Substring(0, principal.IndexOf("@", StringComparison.Ordinal)) + : principal; + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/SqlStatements.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/SqlStatements.cs new file mode 100644 index 00000000000..daad26e3ee4 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/SqlStatements.cs @@ -0,0 +1,42 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + static class SqlStatements + { + public const string DbCreateQueueSql = """SELECT * FROM "{0}".create_queue(@queue_name,@auto_delete)"""; + public const string DbCreateTopicSql = """SELECT * FROM "{0}".create_topic(@topic_name)"""; + + public const string DbCreateTopicSubscriptionSql = + """SELECT * FROM "{0}".create_topic_subscription(@source_topic_name,@destination_topic_name,@type,@routing_key,@filter)"""; + + public const string DbCreateQueueSubscriptionSql = + """SELECT * FROM "{0}".create_queue_subscription(@source_topic_name,@destination_queue_name,@type,@routing_key,@filter)"""; + + public const string DbPurgeQueueSql = """SELECT * FROM "{0}".purge_queue(@queue_name)"""; + + public const string DbEnqueueSql = """ + SELECT * FROM "{0}".send_message(@entity_name,@priority,@transport_message_id,@body,@binary_body,@content_type, + @message_type,@message_id,@correlation_id,@conversation_id,@request_id,@initiator_id,@source_address,@destination_address,@response_address,@fault_address, + @sent_time,@headers,@host,@partition_key,@routing_key,@delay,@scheduling_token_id) + """; + + public const string DbPublishSql = """ + SELECT * FROM "{0}".publish_message(@entity_name,@priority,@transport_message_id,@body,@binary_body,@content_type, + @message_type,@message_id,@correlation_id,@conversation_id,@request_id,@initiator_id,@source_address,@destination_address,@response_address,@fault_address, + @sent_time,@headers,@host,@partition_key,@routing_key,@delay,@scheduling_token_id) + """; + + public const string DbProcessMetricsSql = """SELECT * FROM "{0}".process_metrics(@row_limit)"""; + public const string DbPurgeTopologySql = """SELECT * FROM "{0}".purge_topology()"""; + public const string DbReceiveSql = """SELECT * FROM "{0}".fetch_messages(@queue_name,@fetch_consumer_id,@fetch_lock_id,@lock_duration,@fetch_count)"""; + + public const string DbReceivePartitionedSql = + """SELECT * FROM "{0}".fetch_messages_partitioned(@queue_name,@fetch_consumer_id,@fetch_lock_id,@lock_duration,@fetch_count,@concurrent_count,@ordered)"""; + + public const string DbMoveMessageSql = """SELECT * FROM "{0}".move_message(@message_delivery_id,@lock_id,@queue_name,@queue_type,@headers)"""; + public const string DbDeleteMessageSql = """SELECT * FROM "{0}".delete_message(@message_delivery_id,@lock_id)"""; + public const string DbDeleteScheduledMessageSql = """SELECT * FROM "{0}".delete_scheduled_message(@token_id)"""; + public const string DbRenewLockSql = """SELECT * FROM "{0}".renew_message_lock(@message_delivery_id,@lock_id,@duration)"""; + public const string DbTouchQueueSql = """SELECT * FROM "{0}".touch_queue(@queue_name)"""; + public const string DbUnlockSql = """SELECT * FROM "{0}".unlock_message(@message_delivery_id,@lock_id,@delay,@headers)"""; + } +} diff --git a/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/UriTypeHandler.cs b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/UriTypeHandler.cs new file mode 100644 index 00000000000..3fdcb96e409 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.PostgreSql/SqlTransport/PostgreSql/UriTypeHandler.cs @@ -0,0 +1,24 @@ +namespace MassTransit.SqlTransport.PostgreSql +{ + using System; + using System.Data; + using Dapper; + + + public class UriTypeHandler : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, Uri? value) + { + parameter.DbType = DbType.String; + parameter.Value = value != null ? value.ToString() : DBNull.Value; + } + + public override Uri Parse(object value) + { + if (value is string text && !string.IsNullOrWhiteSpace(text)) + return new Uri(text); + + return null!; + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/ISqlServerSqlHostConfigurator.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/ISqlServerSqlHostConfigurator.cs new file mode 100644 index 00000000000..bd5bb9a2456 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/ISqlServerSqlHostConfigurator.cs @@ -0,0 +1,7 @@ +namespace MassTransit +{ + public interface ISqlServerSqlHostConfigurator : + ISqlHostConfigurator + { + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/SqlServerBusFactoryConfiguratorExtensions.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/SqlServerBusFactoryConfiguratorExtensions.cs new file mode 100644 index 00000000000..cd4f8a778df --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/SqlServerBusFactoryConfiguratorExtensions.cs @@ -0,0 +1,45 @@ +namespace MassTransit +{ + using System; + using SqlTransport.Configuration; + + + public static class SqlServerBusFactoryConfiguratorExtensions + { + /// + /// Configure the bus to use the SQL Server transport + /// + /// The registration configurator (configured via AddMassTransit) + /// The configuration callback for the bus factory + public static void UsingSqlServer(this IBusRegistrationConfigurator configurator, + Action? configure = null) + { + configurator.SetBusFactory(new SqlRegistrationBusFactory((context, cfg) => + { + cfg.UseSqlServer(context); + + configure?.Invoke(context, cfg); + })); + } + + /// + /// Configure the bus to use the PostgreSQL database transport + /// + /// The registration configurator (configured via AddMassTransit) + /// + /// Connection string to be used/parsed by the transport. are not + /// used with this overload + /// + /// The configuration callback for the bus factory + public static void UsingSqlServer(this IBusRegistrationConfigurator configurator, string connectionString, + Action? configure = null) + { + configurator.SetBusFactory(new SqlRegistrationBusFactory((context, cfg) => + { + cfg.UseSqlServer(connectionString); + + configure?.Invoke(context, cfg); + })); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/SqlServerDbTransportConfigurationExtensions.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/SqlServerDbTransportConfigurationExtensions.cs new file mode 100644 index 00000000000..7c0c3318a96 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/SqlServerDbTransportConfigurationExtensions.cs @@ -0,0 +1,42 @@ +namespace MassTransit +{ + using System; + using Microsoft.Extensions.DependencyInjection; + using SqlTransport; + using SqlTransport.SqlServer; + + + public static class SqlServerDbTransportConfigurationExtensions + { + public static IServiceCollection AddSqlServerMigrationHostedService(this IServiceCollection services, bool create = true, bool delete = false) + { + services.AddSqlServerMigrationHostedService(options => + { + options.CreateDatabase = create; + options.CreateInfrastructure = create; + options.DeleteDatabase = delete; + }); + + return services; + } + + public static IServiceCollection AddSqlServerMigrationHostedService(this IServiceCollection services, Action? configure) + { + services.AddTransient(); + + services.AddOptions(); + services.AddOptions() + .Configure(options => + { + options.CreateDatabase = true; + options.CreateInfrastructure = true; + options.DeleteDatabase = false; + + configure?.Invoke(options); + }); + services.AddHostedService(); + + return services; + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/SqlServerHostConfigurationExtensions.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/SqlServerHostConfigurationExtensions.cs new file mode 100644 index 00000000000..d42925baf7b --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/Configuration/SqlServerHostConfigurationExtensions.cs @@ -0,0 +1,57 @@ +namespace MassTransit +{ + using System; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using SqlTransport.SqlServer; + + + public static class SqlServerHostConfigurationExtensions + { + /// + /// Configures the database transport to use SQL Server as the storage engine + /// + /// + /// The MassTransit host address of the database + /// + public static void UseSqlServer(this ISqlBusFactoryConfigurator configurator, Uri hostAddress, Action? configure = null) + { + var hostConfigurator = new SqlServerSqlHostConfigurator(hostAddress); + + configure?.Invoke(hostConfigurator); + + configurator.Host(hostConfigurator.Settings); + } + + /// + /// Configures the database transport to use SQL Server as the storage engine + /// + /// + /// The bus registration context, used to retrieve the DbTransportOptions + /// + public static void UseSqlServer(this ISqlBusFactoryConfigurator configurator, IBusRegistrationContext context, + Action? configure = null) + { + var hostConfigurator = new SqlServerSqlHostConfigurator(context.GetRequiredService>().Value); + + configure?.Invoke(hostConfigurator); + + configurator.Host(hostConfigurator.Settings); + } + + /// + /// Configures the database transport to use SQL Server as the storage engine + /// + /// + /// A valid SQL Server connection string + /// + public static void UseSqlServer(this ISqlBusFactoryConfigurator configurator, string connectionString, Action? configure = null) + { + var hostConfigurator = new SqlServerSqlHostConfigurator(connectionString); + + configure?.Invoke(hostConfigurator); + + configurator.Host(hostConfigurator.Settings); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/MassTransit.SqlTransport.SqlServer.csproj b/src/Transports/MassTransit.SqlTransport.SqlServer/MassTransit.SqlTransport.SqlServer.csproj new file mode 100644 index 00000000000..00afc06427b --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/MassTransit.SqlTransport.SqlServer.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.0;net6.0;net8.0 + MassTransit + enable + + + + $(TargetFrameworks);net472 + + + + MassTransit.SqlTransport.SqlServer + MassTransit.SqlTransport.SQL Server + MassTransit;Database;Transport;SQL Server;Microsoft;Azure;SQL + MassTransit SQL Server Transport; $(Description) + + + + + + + + + + + diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/MassTransit.SqlTransport.SqlServer.csproj.DotSettings b/src/Transports/MassTransit.SqlTransport.SqlServer/MassTransit.SqlTransport.SqlServer.csproj.DotSettings new file mode 100644 index 00000000000..6840e166805 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/MassTransit.SqlTransport.SqlServer.csproj.DotSettings @@ -0,0 +1,2 @@ + + True \ No newline at end of file diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/ISqlServerSqlTransportConnection.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/ISqlServerSqlTransportConnection.cs new file mode 100644 index 00000000000..30e6966dd20 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/ISqlServerSqlTransportConnection.cs @@ -0,0 +1,10 @@ +namespace MassTransit.SqlTransport.SqlServer; + +using Microsoft.Data.SqlClient; + + +public interface ISqlServerSqlTransportConnection : + ISqlTransportConnection +{ + SqlConnection Connection { get; } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerClientContext.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerClientContext.cs new file mode 100644 index 00000000000..e272526b534 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerClientContext.cs @@ -0,0 +1,323 @@ +namespace MassTransit.SqlTransport.SqlServer +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Linq; + using System.Text.Json; + using System.Threading; + using System.Threading.Tasks; + using Dapper; + using Microsoft.Data.SqlClient; + using Serialization; + using Topology; + + + public class SqlServerClientContext : + SqlClientContext + { + readonly Guid _consumerId; + readonly SqlServerDbConnectionContext _context; + readonly string _createQueueSql; + readonly string _createQueueSubscriptionSql; + readonly string _createTopicSql; + readonly string _createTopicSubscriptionSql; + readonly string _deleteMessageSql; + readonly string _deleteScheduledMessageSql; + readonly string _moveMessageTypeSql; + readonly string _publishSql; + readonly string _purgeQueueSql; + readonly string _receivePartitionedSql; + readonly string _receiveSql; + readonly string _renewMessageLockSql; + readonly string _sendSql; + readonly string _touchQueueSql; + readonly string _unlockSql; + + public SqlServerClientContext(SqlServerDbConnectionContext context, CancellationToken cancellationToken) + : base(context, cancellationToken) + { + _context = context; + _consumerId = NewId.NextGuid(); + + _createQueueSql = $"{_context.Schema}.CreateQueue"; + _createTopicSql = $"{_context.Schema}.CreateTopic"; + _createTopicSubscriptionSql = $"{_context.Schema}.CreateTopicSubscription"; + _createQueueSubscriptionSql = $"{_context.Schema}.CreateQueueSubscription"; + _sendSql = $"{_context.Schema}.SendMessage"; + _publishSql = $"{_context.Schema}.PublishMessage"; + _purgeQueueSql = $"{_context.Schema}.PurgeQueue"; + _receiveSql = $"{_context.Schema}.FetchMessages"; + _receivePartitionedSql = $"{_context.Schema}.FetchMessagesPartitioned"; + _deleteMessageSql = $"{_context.Schema}.DeleteMessage"; + _renewMessageLockSql = $"{_context.Schema}.RenewMessageLock"; + _touchQueueSql = $"{_context.Schema}.TouchQueue"; + _unlockSql = $"{_context.Schema}.UnlockMessage"; + _moveMessageTypeSql = $"{_context.Schema}.MoveMessage"; + _deleteScheduledMessageSql = $"{_context.Schema}.DeleteScheduledMessage"; + } + + public override async Task CreateQueue(Queue queue) + { + var result = await Execute(_createQueueSql, new + { + queueName = queue.QueueName, + autoDelete = (int?)queue.AutoDeleteOnIdle?.TotalSeconds + }); + + return result ?? throw new SqlTopologyException("Create queue failed"); + } + + public override async Task CreateTopic(Topic topic) + { + var result = await Execute(_createTopicSql, new { topicName = topic.TopicName }); + + return result ?? throw new SqlTopologyException("Create topic failed"); + } + + public override async Task CreateTopicSubscription(TopicToTopicSubscription subscription) + { + var result = await Execute(_createTopicSubscriptionSql, new + { + SourceTopicName = subscription.Source.TopicName, + DestinationTopicName = subscription.Destination.TopicName, + SubscriptionType = (int)subscription.SubscriptionType, + RoutingKey = subscription.RoutingKey ?? "", + Filter = "{}" + }); + + return result ?? throw new SqlTopologyException("Create topic subscription failed"); + } + + public override async Task CreateQueueSubscription(TopicToQueueSubscription subscription) + { + var result = await Execute(_createQueueSubscriptionSql, new + { + SourceTopicName = subscription.Source.TopicName, + DestinationQueueName = subscription.Destination.QueueName, + SubscriptionType = (int)subscription.SubscriptionType, + RoutingKey = subscription.RoutingKey ?? "", + Filter = "{}" + }); + + return result ?? throw new SqlTopologyException("Create queue subscription failed"); + } + + public override async Task PurgeQueue(string queueName, CancellationToken cancellationToken) + { + var result = await Execute(_purgeQueueSql, new { QueueName = queueName }); + + return result ?? throw new SqlTopologyException("Purge queue failed"); + } + + public override async Task> ReceiveMessages(string queueName, SqlReceiveMode mode, int messageLimit, + int concurrentLimit, TimeSpan lockDuration) + { + try + { + if (mode == SqlReceiveMode.Normal) + { + return await Query(_receiveSql, new + { + queueName, + consumerId = _consumerId, + lockId = NewId.NextGuid(), + lockDuration = (int)lockDuration.TotalSeconds, + fetchCount = messageLimit + }).ConfigureAwait(false); + } + + var ordered = mode switch + { + SqlReceiveMode.PartitionedOrdered => 1, + SqlReceiveMode.PartitionedOrderedConcurrent => 1, + _ => 0 + }; + + return await Query(_receivePartitionedSql, new + { + queueName, + consumerId = _consumerId, + lockId = NewId.NextGuid(), + lockDuration = (int)lockDuration.TotalSeconds, + fetchCount = messageLimit, + concurrentCount = concurrentLimit, + ordered + }).ConfigureAwait(false); + } + catch (SqlException exception) when (exception.Number == 1205) + { + return Array.Empty(); + } + } + + public override Task TouchQueue(string queueName) + { + return Query(_touchQueueSql, new { queueName }); + } + + public override Task Send(string queueName, SqlMessageSendContext context) + { + IEnumerable> headers = context.Headers.GetAll().ToList(); + var headersAsJson = headers.Any() ? JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options) : null; + + Guid? schedulingTokenId = context.Headers.Get(MessageHeaders.SchedulingTokenId); + + return Execute(_sendSql, new + { + entityName = queueName, + priority = (int)(context.Priority ?? 100), + transportMessageId = context.TransportMessageId, + body = context.Body.GetString(), + binaryBody = default(byte[]?), + contentType = context.ContentType?.MediaType, + messageType = string.Join(";", context.SupportedMessageTypes), + messageId = context.MessageId, + correlationId = context.CorrelationId, + conversationId = context.ConversationId, + requestId = context.RequestId, + initiatorId = context.InitiatorId, + sourceAddress = context.SourceAddress, + destinationAddress = context.DestinationAddress, + responseAddress = context.ResponseAddress, + faultAddress = context.FaultAddress, + sentTime = context.SentTime, + headers = headersAsJson, + host = HostInfoCache.HostInfoJson, + partitionKey = context.PartitionKey, + routingKey = context.RoutingKey, + delay = (int?)context.Delay?.TotalSeconds, + schedulingTokenId + }); + } + + public override Task Publish(string topicName, SqlMessageSendContext context) + { + IEnumerable> headers = context.Headers.GetAll().ToList(); + var headersAsJson = headers.Any() ? JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options) : null; + + Guid? schedulingTokenId = context.Headers.Get(MessageHeaders.SchedulingTokenId); + + return Execute(_publishSql, new + { + entityName = topicName, + priority = (int)(context.Priority ?? 100), + transportMessageId = context.TransportMessageId, + body = context.Body.GetString(), + binaryBody = default(byte[]?), + contentType = context.ContentType?.MediaType, + messageType = string.Join(";", context.SupportedMessageTypes), + messageId = context.MessageId, + correlationId = context.CorrelationId, + conversationId = context.ConversationId, + requestId = context.RequestId, + initiatorId = context.InitiatorId, + sourceAddress = context.SourceAddress, + destinationAddress = context.DestinationAddress, + responseAddress = context.ResponseAddress, + faultAddress = context.FaultAddress, + sentTime = context.SentTime, + headers = headersAsJson, + host = HostInfoCache.HostInfoJson, + partitionKey = context.PartitionKey, + routingKey = context.RoutingKey, + delay = (int?)context.Delay?.TotalSeconds, + schedulingTokenId + }); + } + + public override async Task DeleteMessage(Guid lockId, long messageDeliveryId) + { + var result = await Execute(_deleteMessageSql, new + { + messageDeliveryId, + lockId, + }).ConfigureAwait(false); + + return result == messageDeliveryId; + } + + public override async Task DeleteScheduledMessage(Guid tokenId, CancellationToken cancellationToken) + { + IEnumerable result = await Query(_deleteScheduledMessageSql, new + { + tokenId, + }, cancellationToken).ConfigureAwait(false); + + return result.Any(); + } + + public override async Task MoveMessage(Guid lockId, long messageDeliveryId, string queueName, SqlQueueType queueType, SendHeaders sendHeaders) + { + IEnumerable> headers = sendHeaders.GetAll().ToList(); + var headersAsJson = headers.Any() ? JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options) : null; + + var result = await Execute(_moveMessageTypeSql, new + { + messageDeliveryId, + lockId, + queueName, + queueType, + headers = headersAsJson + }).ConfigureAwait(false); + + return result == messageDeliveryId; + } + + public override async Task RenewLock(Guid lockId, long messageDeliveryId, TimeSpan duration) + { + var result = await Execute(_renewMessageLockSql, new + { + messageDeliveryId, + lockId, + duration = (int)duration.TotalSeconds + }).ConfigureAwait(false); + + return result == messageDeliveryId; + } + + public override async Task Unlock(Guid lockId, long messageDeliveryId, TimeSpan delay, SendHeaders sendHeaders) + { + IEnumerable> headers = sendHeaders.GetAll().ToList(); + var headersAsJson = headers.Any() ? JsonSerializer.Serialize(headers, SystemTextJsonMessageSerializer.Options) : null; + + var result = await Execute(_unlockSql, new + { + messageDeliveryId, + lockId, + delay = delay > TimeSpan.Zero ? Math.Max((int)delay.TotalSeconds, 1) : 0, + headers = headersAsJson + }).ConfigureAwait(false); + + return result == messageDeliveryId; + } + + Task Execute(string functionName, object values) + where T : struct + { + return _context.Query((connection, transaction) => connection + .ExecuteScalarAsync(functionName, values, transaction, commandType: CommandType.StoredProcedure), CancellationToken); + } + + Task QuerySingle(string functionName, object values) + where T : class + { + return _context.Query((connection, transaction) => connection + .QuerySingleAsync(functionName, values, transaction, commandType: CommandType.StoredProcedure), CancellationToken); + } + + Task> Query(string functionName, object values) + where T : class + { + return _context.Query((connection, transaction) => connection + .QueryAsync(functionName, values, transaction, commandType: CommandType.StoredProcedure), CancellationToken); + } + + Task> Query(string functionName, object values, CancellationToken cancellationToken) + where T : class + { + return _context.Query((connection, transaction) => connection + .QueryAsync(functionName, values, transaction, commandType: CommandType.StoredProcedure), cancellationToken); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerConnectionContextFactory.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerConnectionContextFactory.cs new file mode 100644 index 00000000000..9835e1538ee --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerConnectionContextFactory.cs @@ -0,0 +1,25 @@ +namespace MassTransit.SqlTransport.SqlServer +{ + using Configuration; + using Transports; + + + public class SqlServerConnectionContextFactory : + ConnectionContextFactory + { + readonly ISqlHostConfiguration _hostConfiguration; + readonly SqlServerSqlHostSettings _hostSettings; + + public SqlServerConnectionContextFactory(ISqlHostConfiguration hostConfiguration) + { + _hostConfiguration = hostConfiguration; + _hostSettings = hostConfiguration.Settings as SqlServerSqlHostSettings + ?? throw new ConfigurationException("The host settings were not of the expected type"); + } + + protected override ConnectionContext CreateConnection(ITransportSupervisor supervisor) + { + return new SqlServerDbConnectionContext(_hostConfiguration, supervisor); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerDatabaseMigrator.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerDatabaseMigrator.cs new file mode 100644 index 00000000000..78aff552a1b --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerDatabaseMigrator.cs @@ -0,0 +1,1758 @@ +namespace MassTransit.SqlTransport.SqlServer +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using Dapper; + using Microsoft.Extensions.Logging; + + + public class SqlServerDatabaseMigrator : + ISqlTransportDatabaseMigrator + { + const string DbExistsSql = @"SELECT [database_id] from [sys].[databases] WHERE name = '{0}'"; + const string DbCreateSql = @"CREATE DATABASE [{0}]"; + + const string SchemaCreateSql = @"USE [{0}]; +IF (SCHEMA_ID('{1}') IS NULL) +BEGIN + EXEC('CREATE SCHEMA [{1}] AUTHORIZATION [dbo]') +END"; + + const string DropSql = @"USE master; +ALTER DATABASE [{0}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; +DROP DATABASE [{0}];"; + + const string RoleExistsSql = @"SELECT DATABASE_PRINCIPAL_ID('{0}')"; + const string CreateRoleSql = @"CREATE ROLE {0} AUTHORIZATION [dbo]"; + + const string GrantRoleSql = @"ALTER AUTHORIZATION ON SCHEMA::{1} TO [{0}]; +GRANT CREATE TABLE to {0}; +GRANT CREATE PROCEDURE to {0}; +GRANT CREATE VIEW to {0}; +GRANT REFERENCES to {0}; +"; + + const string LoginExistsSql = @"SELECT 1 FROM sys.sql_logins WHERE [name] = '{0}'"; + const string CreateLoginSql = @"CREATE LOGIN {0} WITH PASSWORD = '{1}';"; + + const string CreateUserSql = @" +IF ORIGINAL_LOGIN() != '{1}' OR CURRENT_USER = '{1}' +BEGIN + CREATE USER [{1}] FOR LOGIN [{1}] WITH DEFAULT_SCHEMA = [{0}] +END +"; + + const string IsRoleMemberSql = @" +IF ORIGINAL_LOGIN() = '{1}' AND CURRENT_USER = 'dbo' +BEGIN + SELECT 1 +END +ELSE +BEGIN + SELECT IS_ROLEMEMBER('{0}', '{1}') +END +"; + + const string AddRoleMemberSql = @"USE [{0}]; +IF ORIGINAL_LOGIN() = '{1}' AND CURRENT_USER = 'dbo' +BEGIN + EXEC sp_addrolemember '{2}', 'dbo'; +END +ELSE +BEGIN + EXEC sp_addrolemember '{2}', '{1}'; +END +"; + + const string CreateInfrastructureSql = @" +IF OBJECT_ID('{0}.TopologySequence', 'SO') IS NULL +BEGIN + CREATE SEQUENCE [{0}].[TopologySequence] AS BIGINT START WITH 1 INCREMENT BY 1 +END; + +IF OBJECT_ID('{0}.Queue', 'U') IS NULL +BEGIN + CREATE TABLE {0}.Queue + ( + Id bigint not null primary key default next value for [{0}].[TopologySequence], + Updated datetime2 not null default GETUTCDATE(), + + Name nvarchar(256) not null, + Type tinyint not null, + AutoDelete integer + ) +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_Queue_Name_Type' AND objects.name = 'Queue') +BEGIN + CREATE INDEX IX_Queue_Name_Type ON {0}.Queue (Name, Type) INCLUDE (Id); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_Queue_AutoDelete' AND objects.name = 'Queue') +BEGIN + CREATE INDEX IX_Queue_AutoDelete ON {0}.Queue (AutoDelete) INCLUDE (Id); +END; + +IF OBJECT_ID('{0}.Topic', 'U') IS NULL +BEGIN + CREATE TABLE {0}.Topic + ( + Id bigint not null primary key default next value for [{0}].[TopologySequence], + Updated datetime2 not null default GETUTCDATE(), + + Name nvarchar(256) not null + ) +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_Topic_Name' AND objects.name = 'Topic') +BEGIN + CREATE INDEX IX_Topic_Name ON {0}.Topic (Name) INCLUDE (Id); +END; + +IF OBJECT_ID('{0}.TopicSubscription', 'U') IS NULL +BEGIN + CREATE TABLE {0}.TopicSubscription + ( + Id bigint not null primary key default next value for [{0}].[TopologySequence], + Updated datetime2 not null default GETUTCDATE(), + + SourceId bigint not null references {0}.Topic (Id), + DestinationId bigint not null references {0}.Topic (Id), + + SubType tinyint not null, + RoutingKey nvarchar(256) not null, + Filter nvarchar(1024) not null + ); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_TopicSubscription_Unique' AND objects.name = 'TopicSubscription') +BEGIN + CREATE UNIQUE INDEX IX_TopicSubscription_Unique ON {0}.TopicSubscription (SourceId, DestinationId, SubType, RoutingKey, Filter); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_TopicSubscription_Source' AND objects.name = 'TopicSubscription') +BEGIN + CREATE INDEX IX_TopicSubscription_Source ON {0}.TopicSubscription (SourceId) INCLUDE (Id, DestinationId, SubType, RoutingKey, Filter); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_TopicSubscription_Destination' AND objects.name = 'TopicSubscription') +BEGIN + CREATE INDEX IX_TopicSubscription_Destination ON {0}.TopicSubscription (DestinationId) INCLUDE (Id, SourceId, SubType, RoutingKey, Filter); +END; + +IF OBJECT_ID('{0}.DELETE_Topic', 'TR') IS NULL +BEGIN + EXEC(' + CREATE TRIGGER [{0}].[DELETE_Topic] ON {0}.Topic INSTEAD OF DELETE + AS + BEGIN + SET NOCOUNT ON; + DELETE FROM [{0}].[TopicSubscription] WHERE SourceId IN (SELECT Id FROM DELETED); + DELETE FROM [{0}].[TopicSubscription] WHERE DestinationId IN (SELECT Id FROM DELETED); + DELETE FROM [{0}].[Topic] WHERE Id IN (SELECT Id FROM DELETED); + END; + '); +END; + +IF OBJECT_ID('{0}.QueueSubscription', 'U') IS NULL +BEGIN + CREATE TABLE {0}.QueueSubscription + ( + Id bigint not null primary key default next value for [{0}].[TopologySequence], + Updated datetime2 not null default GETUTCDATE(), + + SourceId bigint not null references {0}.Topic (Id) ON DELETE CASCADE, + DestinationId bigint not null references {0}.Queue (Id) ON DELETE CASCADE, + + SubType tinyint not null, + RoutingKey nvarchar(256) not null, + Filter nvarchar(1024) not null + ); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_QueueSubscription_Unique' AND objects.name = 'QueueSubscription') +BEGIN + CREATE UNIQUE INDEX IX_QueueSubscription_Unique ON {0}.QueueSubscription (SourceId, DestinationId, SubType, RoutingKey, Filter); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_QueueSubscription_Source' AND objects.name = 'QueueSubscription') +BEGIN + CREATE INDEX IX_QueueSubscription_Source ON {0}.QueueSubscription (SourceId) INCLUDE (Id, DestinationId, SubType, RoutingKey, Filter); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_QueueSubscription_Destination' AND objects.name = 'QueueSubscription') +BEGIN + CREATE INDEX IX_QueueSubscription_Destination ON {0}.QueueSubscription (DestinationId) INCLUDE (Id, SourceId, SubType, RoutingKey, Filter); +END; + +IF OBJECT_ID('{0}.Message', 'U') IS NULL +BEGIN + CREATE TABLE {0}.Message + ( + TransportMessageId uniqueidentifier not null primary key, + + ContentType nvarchar(max), + MessageType nvarchar(max), + Body nvarchar(max), + BinaryBody varbinary(max), + + MessageId uniqueidentifier, + CorrelationId uniqueidentifier, + ConversationId uniqueidentifier, + RequestId uniqueidentifier, + InitiatorId uniqueidentifier, + SourceAddress nvarchar(max), + DestinationAddress nvarchar(max), + ResponseAddress nvarchar(max), + FaultAddress nvarchar(max), + + SentTime datetime2 NOT NULL DEFAULT GETUTCDATE(), + + Headers nvarchar(max), + Host nvarchar(max), + + SchedulingTokenId uniqueidentifier + ); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_Message_SchedulingTokenId' AND objects.name = 'Message') +BEGIN + CREATE INDEX IX_Message_SchedulingTokenId ON {0}.Message (SchedulingTokenId) where Message.SchedulingTokenId IS NOT NULL; +END; + +IF OBJECT_ID('{0}.DeliverySequence', 'SO') IS NULL +BEGIN + CREATE SEQUENCE [{0}].[DeliverySequence] AS BIGINT START WITH 1 INCREMENT BY 1 +END; + +IF OBJECT_ID('{0}.MessageDelivery', 'U') IS NULL +BEGIN + CREATE TABLE {0}.MessageDelivery + ( + MessageDeliveryId bigint not null primary key default next value for [{0}].[DeliverySequence], + + TransportMessageId uniqueidentifier not null REFERENCES {0}.Message ON DELETE CASCADE, + QueueId bigint not null, + + Priority smallint not null, + EnqueueTime datetime2 not null, + ExpirationTime datetime2, + + PartitionKey nvarchar(128), + RoutingKey nvarchar(256), + + ConsumerId uniqueidentifier, + LockId uniqueidentifier, + + DeliveryCount int not null, + MaxDeliveryCount int not null, + LastDelivered datetime2, + TransportHeaders nvarchar(max) + ); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_MessageDelivery_Fetch' AND objects.name = 'MessageDelivery') +BEGIN + CREATE INDEX IX_MessageDelivery_Fetch ON {0}.MessageDelivery (QueueId, Priority, EnqueueTime, MessageDeliveryId); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_MessageDelivery_FetchPart' AND objects.name = 'MessageDelivery') +BEGIN + CREATE INDEX IX_MessageDelivery_FetchPart ON {0}.MessageDelivery (QueueId, PartitionKey, Priority, EnqueueTime, MessageDeliveryId); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_MessageDelivery_TransportMessageId' AND objects.name = 'MessageDelivery') +BEGIN + CREATE INDEX IX_MessageDelivery_TransportMessageId ON {0}.MessageDelivery (TransportMessageId); +END; + +IF OBJECT_ID('{0}.QueueMetricCapture', 'U') IS NULL +BEGIN + CREATE TABLE {0}.QueueMetricCapture + ( + Id bigint not null identity(1,1), + + Captured datetime2 not null, + QueueId bigint not null, + ConsumeCount bigint not null, + ErrorCount bigint not null, + DeadLetterCount bigint not null, + + CONSTRAINT [PK_QueueMetricCapture] PRIMARY KEY CLUSTERED + ( + [Id] ASC + ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) + ); +END; + +IF OBJECT_ID('{0}.QueueMetric', 'U') IS NULL +BEGIN + CREATE TABLE {0}.QueueMetric + ( + Id bigint not null identity(1,1), + + StartTime datetime2 not null, + Duration int not null, + QueueId bigint not null, + ConsumeCount bigint not null, + ErrorCount bigint not null, + DeadLetterCount bigint not null, + + CONSTRAINT [PK_QueueMetric] PRIMARY KEY CLUSTERED + ( + [Id] ASC + ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) + ); +END; + +IF NOT EXISTS(SELECT TOP 1 1 FROM sys.indexes indexes + INNER JOIN sys.objects objects ON indexes.object_id = objects.object_id + WHERE indexes.name ='IX_QueueMetric_Unique' AND objects.name = 'QueueMetric') +BEGIN + CREATE UNIQUE INDEX IX_QueueMetric_Unique ON {0}.QueueMetric (StartTime, Duration, QueueId); +END; +"; + + const string SqlFnCreateQueue = @" +CREATE OR ALTER PROCEDURE {0}.CreateQueue + @QueueName nvarchar(256), + @AutoDelete integer = NULL +AS +BEGIN + SET NOCOUNT ON; + + IF @QueueName IS NULL OR LEN(@QueueName) < 1 + BEGIN + THROW 50000, 'Queue name was null or empty', 1; + END + + DECLARE @QueueTable table (Id BIGINT, Type tinyint) + MERGE INTO {0}.Queue WITH (ROWLOCK) AS target + USING (VALUES + (@QueueName, 1, @AutoDelete), + (@QueueName, 2, @AutoDelete), + (@QueueName, 3, @AutoDelete) + ) AS source (Name, Type, AutoDelete) + ON (target.Name = source.Name AND target.Type = source.Type) + WHEN MATCHED THEN UPDATE SET Updated = GETUTCDATE(), AutoDelete = COALESCE(source.AutoDelete, target.AutoDelete) + WHEN NOT MATCHED THEN INSERT (Name, Type, AutoDelete) + VALUES (source.Name, source.Type, source.AutoDelete) + OUTPUT inserted.Id, inserted.Type INTO @QueueTable; + + SET NOCOUNT OFF + SELECT TOP 1 Id FROM @QueueTable WHERE Type = 1; +END"; + + const string SqlFnCreateTopic = @" +CREATE OR ALTER PROCEDURE {0}.CreateTopic + @TopicName nvarchar(256) +AS +BEGIN + SET NOCOUNT ON; + + IF @TopicName IS NULL OR LEN(@TopicName) < 1 + BEGIN + THROW 50000, 'Topic name was null or empty', 1; + END + + DECLARE @TopicTable table (Id BIGINT) + MERGE INTO {0}.Topic WITH (ROWLOCK) AS target + USING (VALUES (@TopicName)) AS source (Name) + ON (target.Name = source.Name) + WHEN MATCHED THEN UPDATE SET Updated = GETUTCDATE() + WHEN NOT MATCHED THEN INSERT (Name) + VALUES (source.Name) + OUTPUT inserted.Id INTO @TopicTable; + + SET NOCOUNT OFF + SELECT TOP 1 Id FROM @TopicTable; +END; +"; + + const string SqlFnCreateTopicSubscription = @" +CREATE OR ALTER PROCEDURE {0}.CreateTopicSubscription + @SourceTopicName nvarchar(256), + @DestinationTopicName nvarchar(256), + @SubscriptionType tinyint = 1, + @RoutingKey varchar(256) = '', + @Filter varchar(1024) = '{{}}' +AS +BEGIN + SET NOCOUNT ON; + + IF @SourceTopicName IS NULL OR LEN(@SourceTopicName) < 1 + BEGIN + THROW 50000, 'Source topic name was null or empty', 1; + END + + IF @DestinationTopicName IS NULL OR LEN(@DestinationTopicName) < 1 + BEGIN + THROW 50000, 'Destination topic name was null or empty', 1; + END + + DECLARE @SourceTopicId BIGINT + SELECT @SourceTopicId = t.Id FROM {0}.Topic t WHERE t.Name = @SourceTopicName; + IF @SourceTopicId IS NULL + BEGIN + THROW 50000, 'Source topic not found', 1; + END + + DECLARE @DestinationTopicId BIGINT + SELECT @DestinationTopicId = t.Id FROM {0}.Topic t WHERE t.Name = @DestinationTopicName; + IF @DestinationTopicId IS NULL + BEGIN + THROW 50000, 'Destination topic not found', 1; + END + + DECLARE @ResultTable table (Id BIGINT) + MERGE INTO {0}.TopicSubscription WITH (ROWLOCK) AS target + USING (VALUES (@SourceTopicId, @DestinationTopicId, @SubscriptionType, COALESCE(@RoutingKey, ''), COALESCE(@Filter, '{{}}'))) + AS source (SourceId, DestinationId, SubType, RoutingKey, Filter) + ON (target.SourceId = source.SourceId AND target.DestinationId = source.DestinationId AND target.SubType = source.SubType + AND target.RoutingKey = source.RoutingKey AND target.Filter = source.Filter) + WHEN MATCHED THEN UPDATE SET Updated = GETUTCDATE() + WHEN NOT MATCHED THEN INSERT (SourceId, DestinationId, SubType, RoutingKey, Filter) + VALUES (source.SourceId, source.DestinationId, source.SubType, source.RoutingKey, source.Filter) + OUTPUT inserted.Id INTO @ResultTable; + + SET NOCOUNT OFF + SELECT TOP 1 Id FROM @ResultTable; +END; +"; + + const string SqlFnCreateQueueSubscription = @" +CREATE OR ALTER PROCEDURE {0}.CreateQueueSubscription + @SourceTopicName nvarchar(256), + @DestinationQueueName nvarchar(256), + @SubscriptionType tinyint = 1, + @RoutingKey varchar(256) = '', + @Filter varchar(1024) = '{{}}' +AS +BEGIN + SET NOCOUNT ON; + + IF @SourceTopicName IS NULL OR LEN(@SourceTopicName) < 1 + BEGIN + THROW 50000, 'Source topic name was null or empty', 1; + END + + IF @DestinationQueueName IS NULL OR LEN(@DestinationQueueName) < 1 + BEGIN + THROW 50000, 'Destination queue name was null or empty', 1; + END + + DECLARE @SourceTopicId BIGINT + SELECT @SourceTopicId = t.Id FROM {0}.Topic t WHERE t.Name = @SourceTopicName; + IF @SourceTopicId IS NULL + BEGIN + THROW 50000, 'Destination topic name was null or empty', 1; + END + + DECLARE @DestinationQueueId BIGINT + SELECT @DestinationQueueId = q.Id FROM {0}.Queue q WHERE q.Name = @DestinationQueueName AND q.Type = 1; + IF @DestinationQueueId IS NULL + BEGIN + THROW 50000, 'Destination queue not found', 1; + END + + DECLARE @ResultTable table (Id BIGINT) + MERGE INTO {0}.QueueSubscription WITH (ROWLOCK) AS target + USING (VALUES (@SourceTopicId, @DestinationQueueId, @SubscriptionType, COALESCE(@RoutingKey, ''), COALESCE(@Filter, '{{}}'))) + AS source (SourceId, DestinationId, SubType, RoutingKey, Filter) + ON (target.SourceId = source.SourceId AND target.DestinationId = source.DestinationId AND target.SubType = source.SubType + AND target.RoutingKey = source.RoutingKey AND target.Filter = source.Filter) + WHEN MATCHED THEN UPDATE SET Updated = GETUTCDATE() + WHEN NOT MATCHED THEN INSERT (SourceId, DestinationId, SubType, RoutingKey, Filter) + VALUES (source.SourceId, source.DestinationId, source.SubType, source.RoutingKey, source.Filter) + OUTPUT inserted.Id INTO @ResultTable; + + SET NOCOUNT OFF + SELECT TOP 1 Id FROM @ResultTable; +END; +"; + + const string SqlFnPublish = @" +CREATE OR ALTER PROCEDURE {0}.PublishMessage + @entityName varchar(256), + @priority int = 100, + @transportMessageId uniqueidentifier, + @body nvarchar(max) = NULL, + @binaryBody varbinary(max) = NULL, + @contentType varchar(max) = NULL, + @messageType varchar(max) = NULL, + @messageId uniqueidentifier = NULL, + @correlationId uniqueidentifier = NULL, + @conversationId uniqueidentifier = NULL, + @requestId uniqueidentifier = NULL, + @initiatorId uniqueidentifier = NULL, + @sourceAddress varchar(max) = NULL, + @destinationAddress varchar(max) = NULL, + @responseAddress varchar(max) = NULL, + @faultAddress varchar(max) = NULL, + @sentTime datetimeoffset = NULL, + @headers nvarchar(max) = NULL, + @host nvarchar(max) = NULL, + @partitionKey nvarchar(128) = NULL, + @routingKey nvarchar(256) = NULL, + @delay int = 0, + @schedulingTokenId uniqueidentifier = NULL, + @maxDeliveryCount int = 10 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @vTopicId bigint; + DECLARE @vRowCount bigint; + DECLARE @vEnqueueTime datetimeoffset; + DECLARE @vRow table ( + queueId bigint, + transportMessageId uniqueidentifier, + priority int, + enqueueTime datetimeoffset, + routingKey varchar(100) + ); + + IF @entityName IS NULL OR LEN(@entityName) < 1 + BEGIN + THROW 50000, 'Topic names must not be null or empty', 1; + END; + + SELECT @vTopicId = t.Id + FROM {0}.Topic t + WHERE t.Name = @entityName; + + IF @vTopicId IS NULL + BEGIN + THROW 50000, 'Topic not found', 1; + END; + + SET @vEnqueueTime = GETUTCDATE(); + + IF @delay > 0 + BEGIN + SET @vEnqueueTime = DATEADD(SECOND, @delay, @vEnqueueTime); + END; + + INSERT INTO {0}.Message ( + TransportMessageId, Body, BinaryBody, ContentType, MessageType, MessageId, + CorrelationId, ConversationId, RequestId, InitiatorId, + SourceAddress, DestinationAddress, ResponseAddress, FaultAddress, + SentTime, Headers, Host, SchedulingTokenId + ) + VALUES ( + @transportMessageId, @body, @binaryBody, @contentType, @messageType, @messageId, + @correlationId, @conversationId, @requestId, @initiatorId, + @sourceAddress, @destinationAddress, @responseAddress, @faultAddress, + @sentTime, @headers, @host, @schedulingTokenId + ); + + ;WITH Fabric AS ( + SELECT ts.SourceId, ts.DestinationId + FROM {0}.Topic t + LEFT JOIN {0}.TopicSubscription ts ON t.Id = ts.SourceId + AND ( + (ts.SubType = 1) + OR (ts.SubType = 2 AND @routingKey = ts.RoutingKey) + OR (ts.SubType = 3 AND @routingKey LIKE ts.RoutingKey) + ) + WHERE t.Id = @vTopicId + + UNION ALL + + SELECT ts.SourceId, ts.DestinationId + FROM {0}.TopicSubscription ts + JOIN Fabric ON ts.SourceId = fabric.DestinationId + WHERE + (ts.SubType = 1) + OR (ts.SubType = 2 AND @routingKey = ts.RoutingKey) + OR (ts.SubType = 3 AND @routingKey LIKE ts.RoutingKey) + ) + INSERT INTO {0}.MessageDelivery (QueueId, TransportMessageId, Priority, EnqueueTime, DeliveryCount, MaxDeliveryCount, PartitionKey, RoutingKey) + OUTPUT inserted.QueueId, inserted.TransportMessageId, inserted.Priority, inserted.EnqueueTime, inserted.RoutingKey INTO @vRow + SELECT DISTINCT qs.DestinationId, @transportMessageId, @priority, @vEnqueueTime, 0, @maxDeliveryCount, @partitionKey, @routingKey + FROM {0}.QueueSubscription qs + JOIN Fabric ON (qs.SourceId = fabric.DestinationId OR qs.SourceId = @vTopicId) + AND ( (qs.SubType = 1) + OR (qs.SubType = 2 AND @routingKey = qs.RoutingKey) + OR (qs.SubType = 3 AND @routingKey LIKE qs.RoutingKey)); + + SELECT @vRowCount = COUNT(*) FROM @vRow; + + IF @vRowCount = 0 + BEGIN + DELETE FROM {0}.Message WHERE TransportMessageId = @transportMessageId; + END; + + RETURN @vRowCount; +END; +"; + + const string SqlFnSend = @" +CREATE OR ALTER PROCEDURE {0}.SendMessage + @entityName varchar(256), + @priority int = 100, + @transportMessageId uniqueidentifier, + @body nvarchar(max) = NULL, + @binaryBody varbinary(max) = NULL, + @contentType varchar(max) = NULL, + @messageType varchar(max) = NULL, + @messageId uniqueidentifier = NULL, + @correlationId uniqueidentifier = NULL, + @conversationId uniqueidentifier = NULL, + @requestId uniqueidentifier = NULL, + @initiatorId uniqueidentifier = NULL, + @sourceAddress varchar(max) = NULL, + @destinationAddress varchar(max) = NULL, + @responseAddress varchar(max) = NULL, + @faultAddress varchar(max) = NULL, + @sentTime datetimeoffset = NULL, + @headers nvarchar(max) = NULL, + @host nvarchar(max) = NULL, + @partitionKey nvarchar(128) = NULL, + @routingKey nvarchar(256) = NULL, + @delay int = 0, + @schedulingTokenId uniqueidentifier = NULL, + @maxDeliveryCount int = 10 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @vQueueId int; + DECLARE @vEnqueueTime datetimeoffset; + + IF @entityName IS NULL OR LEN(@entityName) < 1 + BEGIN + THROW 50000, 'Queue names must not be null or empty', 1; + END; + + SELECT @vQueueId = q.Id FROM {0}.Queue q WHERE q.Name = @entityName AND q.type = 1; + IF @vQueueId IS NULL + BEGIN + THROW 50000, 'Queue not found', 1; + END; + + SET @vEnqueueTime = GETUTCDATE(); + + IF @delay > 0 + BEGIN + SET @vEnqueueTime = DATEADD(SECOND, @delay, @vEnqueueTime); + END; + + INSERT INTO {0}.Message ( + TransportMessageId, Body, BinaryBody, ContentType, MessageType, MessageId, + CorrelationId, ConversationId, RequestId, InitiatorId, + SourceAddress, DestinationAddress, ResponseAddress, FaultAddress, + SentTime, Headers, Host, SchedulingTokenId + ) + VALUES ( + @transportMessageId, @body, @binaryBody, @contentType, @messageType, @messageId, + @correlationId, @conversationId, @requestId, @initiatorId, + @sourceAddress, @destinationAddress, @responseAddress, @faultAddress, + @sentTime, @headers, @host, @schedulingTokenId + ); + + INSERT INTO {0}.MessageDelivery (QueueId, TransportMessageId, Priority, EnqueueTime, DeliveryCount, MaxDeliveryCount, PartitionKey, RoutingKey) + VALUES (@vQueueId, @transportMessageId, @priority, @vEnqueueTime, 0, @maxDeliveryCount, @partitionKey, @routingKey) + + RETURN 1; +END; +"; + + const string SqlFnFetchMessages = @" +CREATE OR ALTER PROCEDURE {0}.FetchMessages + @queueName varchar(256), + @consumerId uniqueidentifier, + @lockId uniqueidentifier, + @lockDuration int, + @fetchCount int = 1 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @queueId bigint; + DECLARE @enqueueTime datetime2; + DECLARE @now datetime2; + + SELECT @queueId = q.Id + FROM {0}.Queue q + WHERE q.Name = @queueName AND q.Type = 1; + + IF @queueId IS NULL + BEGIN + THROW 50000, 'Queue not found', 1; + END; + + IF @lockDuration <= 0 + BEGIN + THROW 50000, 'Invalid lock duration', 1; + END; + + SET @now = SYSUTCDATETIME(); + SET @enqueueTime = DATEADD(SECOND, @lockDuration, @now); + + DECLARE @ResultTable TABLE ( + TransportMessageId uniqueidentifier, + QueueId bigint, + Priority smallint, + MessageDeliveryId bigint, + ConsumerId uniqueidentifier, + LockId uniqueidentifier, + EnqueueTime datetime2, + ExpirationTime datetime2, + DeliveryCount int, + PartitionKey text, + RoutingKey text, + TransportHeaders nvarchar(max), + ContentType text, + MessageType text, + Body nvarchar(max), + BinaryBody varbinary(max), + MessageId uniqueidentifier, + CorrelationId uniqueidentifier, + ConversationId uniqueidentifier, + RequestId uniqueidentifier, + InitiatorId uniqueidentifier, + SourceAddress text, + DestinationAddress text, + ResponseAddress text, + FaultAddress text, + SentTime datetime2, + Headers nvarchar(max), + Host nvarchar(max) + ); + + WITH msgs AS ( + SELECT + md.* + FROM + {0}.MessageDelivery md WITH (ROWLOCK, READPAST, UPDLOCK) + WHERE + md.QueueId = @queueId + AND md.EnqueueTime <= @now + AND md.DeliveryCount < md.MaxDeliveryCount + ORDER BY + md.Priority ASC, + md.EnqueueTime ASC, + md.MessageDeliveryId ASC + OFFSET 0 ROWS + FETCH NEXT @fetchCount ROWS ONLY + ) + UPDATE dm + SET + DeliveryCount = dm.DeliveryCount + 1, + LastDelivered = @now, + ConsumerId = @consumerId, + LockId = @lockId, + EnqueueTime = @enqueueTime + OUTPUT + inserted.TransportMessageId, + inserted.QueueId, + inserted.Priority, + inserted.MessageDeliveryId, + inserted.ConsumerId, + inserted.LockId, + inserted.EnqueueTime, + inserted.ExpirationTime, + inserted.DeliveryCount, + inserted.PartitionKey, + inserted.RoutingKey, + inserted.TransportHeaders, + m.ContentType, + m.MessageType, + m.Body, + m.BinaryBody, + m.MessageId, + m.CorrelationId, + m.ConversationId, + m.RequestId, + m.InitiatorId, + m.SourceAddress, + m.DestinationAddress, + m.ResponseAddress, + m.FaultAddress, + m.SentTime, + m.Headers, + m.Host + INTO + @ResultTable ( + TransportMessageId , + QueueId , + Priority , + MessageDeliveryId , + ConsumerId , + LockId , + EnqueueTime , + ExpirationTime , + DeliveryCount , + PartitionKey , + RoutingKey , + TransportHeaders, + ContentType , + MessageType , + Body, + BinaryBody, + MessageId , + CorrelationId , + ConversationId , + RequestId , + InitiatorId , + SourceAddress , + DestinationAddress , + ResponseAddress , + FaultAddress , + SentTime , + Headers, + Host + ) + FROM + {0}.MessageDelivery dm + INNER JOIN msgs ON dm.MessageDeliveryId = msgs.MessageDeliveryId + INNER JOIN {0}.Message m ON msgs.TransportMessageId = m.TransportMessageId; + + SELECT * FROM @ResultTable; +END"; + + const string SqlFnFetchMessagesPartitioned = @" +CREATE OR ALTER PROCEDURE {0}.FetchMessagesPartitioned + @queueName varchar(256), + @consumerId uniqueidentifier, + @lockId uniqueidentifier, + @lockDuration int, + @fetchCount int = 1, + @concurrentCount int = 1, + @ordered int = 0 +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @queueId bigint; + DECLARE @enqueueTime datetime2; + DECLARE @now datetime2; + + SELECT @queueId = q.Id + FROM {0}.Queue q + WHERE q.Name = @queueName AND q.Type = 1; + + IF @queueId IS NULL + BEGIN + THROW 50000, 'Queue not found', 1; + END; + + IF @lockDuration <= 0 + BEGIN + THROW 50000, 'Invalid lock duration', 1; + END; + + SET @now = SYSUTCDATETIME(); + SET @enqueueTime = DATEADD(SECOND, @lockDuration, @now); + + DECLARE @ResultTable TABLE ( + TransportMessageId uniqueidentifier, + QueueId bigint, + Priority smallint, + MessageDeliveryId bigint, + ConsumerId uniqueidentifier, + LockId uniqueidentifier, + EnqueueTime datetime2, + ExpirationTime datetime2, + DeliveryCount int, + PartitionKey text, + RoutingKey text, + TransportHeaders nvarchar(max), + ContentType text, + MessageType text, + Body nvarchar(max), + BinaryBody varbinary(max), + MessageId uniqueidentifier, + CorrelationId uniqueidentifier, + ConversationId uniqueidentifier, + RequestId uniqueidentifier, + InitiatorId uniqueidentifier, + SourceAddress text, + DestinationAddress text, + ResponseAddress text, + FaultAddress text, + SentTime datetime2, + Headers nvarchar(max), + Host nvarchar(max) + ); + + WITH ready AS (SELECT mdx.MessageDeliveryId, + mdx.EnqueueTime, + mdx.LockId, + mdx.Priority, + row_number() over (partition by mdx.PartitionKey order by mdx.Priority, mdx.EnqueueTime, mdx.MessageDeliveryId) as row_normal, + row_number() over (partition by mdx.PartitionKey order by mdx.Priority, mdx.MessageDeliveryId, mdx.EnqueueTime) as row_ordered, + first_value(CASE WHEN mdx.EnqueueTime > @now THEN mdx.ConsumerId END) over (partition by mdx.PartitionKey + order by mdx.EnqueueTime DESC, mdx.MessageDeliveryId DESC) as ConsumerId, + sum(CASE WHEN mdx.EnqueueTime > @now AND mdx.ConsumerId = @consumerId AND mdx.LockId IS NOT NULL THEN 1 END) + over (partition by mdx.PartitionKey + order by mdx.EnqueueTime DESC, mdx.MessageDeliveryId DESC) as ActiveCount + FROM {0}.MessageDelivery mdx WITH (ROWLOCK, READPAST, UPDLOCK) + WHERE mdx.QueueId = @queueId + AND mdx.DeliveryCount < mdx.MaxDeliveryCount), + so_ready as (SELECT ready.MessageDeliveryId + FROM ready + WHERE ( ( @ordered = 0 AND ready.row_normal <= @concurrentCount) OR ( @ordered = 1 AND ready.row_ordered <= @concurrentCount ) ) + AND (ready.ConsumerId IS NULL OR ready.ConsumerId = @consumerId) + AND (ActiveCount < @concurrentCount OR ActiveCount IS NULL) + AND ready.EnqueueTime <= @now + ORDER BY ready.Priority, ready.EnqueueTime, ready.MessageDeliveryId + OFFSET 0 ROWS FETCH NEXT @fetchCount ROWS ONLY), + msgs AS (SELECT md.* + FROM {0}.MessageDelivery md + WITH (ROWLOCK, READPAST, UPDLOCK) + WHERE md.MessageDeliveryId IN (SELECT MessageDeliveryId FROM so_ready)) + UPDATE dm + SET + DeliveryCount = dm.DeliveryCount + 1, + LastDelivered = @now, + ConsumerId = @consumerId, + LockId = @lockId, + EnqueueTime = @enqueueTime + OUTPUT + inserted.TransportMessageId, + inserted.QueueId, + inserted.Priority, + inserted.MessageDeliveryId, + inserted.ConsumerId, + inserted.LockId, + inserted.EnqueueTime, + inserted.ExpirationTime, + inserted.DeliveryCount, + inserted.PartitionKey, + inserted.RoutingKey, + inserted.TransportHeaders, + m.ContentType, + m.MessageType, + m.Body, + m.BinaryBody, + m.MessageId, + m.CorrelationId, + m.ConversationId, + m.RequestId, + m.InitiatorId, + m.SourceAddress, + m.DestinationAddress, + m.ResponseAddress, + m.FaultAddress, + m.SentTime, + m.Headers, + m.Host + INTO + @ResultTable ( + TransportMessageId , + QueueId , + Priority , + MessageDeliveryId , + ConsumerId , + LockId , + EnqueueTime , + ExpirationTime , + DeliveryCount , + PartitionKey , + RoutingKey , + TransportHeaders, + ContentType , + MessageType , + Body, + BinaryBody, + MessageId , + CorrelationId , + ConversationId , + RequestId , + InitiatorId , + SourceAddress , + DestinationAddress , + ResponseAddress , + FaultAddress , + SentTime , + Headers, + Host + ) + FROM + {0}.MessageDelivery dm + INNER JOIN msgs ON dm.MessageDeliveryId = msgs.MessageDeliveryId + INNER JOIN {0}.Message m ON msgs.TransportMessageId = m.TransportMessageId; + + SELECT * FROM @ResultTable; +END"; + + const string SqlFnDeleteMessage = @" +CREATE OR ALTER PROCEDURE {0}.DeleteMessage + @messageDeliveryId bigint, + @lockId uniqueidentifier +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @outMessageDeliveryId bigint; + DECLARE @outTransportMessageId uniqueidentifier; + DECLARE @outQueueId bigint; + + DECLARE @DeletedMessages TABLE ( + MessageDeliveryId bigint, + TransportMessageId uniqueidentifier, + QueueId bigint + ); + + DELETE + FROM {0}.MessageDelivery + OUTPUT deleted.MessageDeliveryId, deleted.TransportMessageId, deleted.QueueId + INTO @DeletedMessages + WHERE MessageDeliveryId = @messageDeliveryId + AND LockId = @lockId; + + SELECT TOP 1 @outMessageDeliveryId = MessageDeliveryId, @outTransportMessageId = TransportMessageId, @outQueueId = QueueId + FROM @DeletedMessages; + + IF @outTransportMessageId IS NOT NULL + BEGIN + DELETE m + FROM {0}.Message m + WHERE m.TransportMessageId = @outTransportMessageId + AND NOT EXISTS (SELECT 1 FROM {0}.MessageDelivery md WHERE md.TransportMessageId = @outTransportMessageId); + + INSERT INTO {0}.QueueMetricCapture (Captured, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + VALUES (GETUTCDATE(), @outQueueId, 1, 0, 0); + END; + + RETURN @outMessageDeliveryId; +END"; + + const string SqlFnTouchQueue = @" +CREATE OR ALTER PROCEDURE {0}.TouchQueue + @queueName varchar(256) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @queueId bigint + SELECT @queueId = q.Id + FROM {0}.Queue q + WHERE q.Name = @queueName AND q.Type = 1; + + IF @queueId IS NULL + BEGIN + THROW 50000, 'Queue not found', 1; + END; + + INSERT INTO {0}.QueueMetricCapture (Captured, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + VALUES (GETUTCDATE(), @queueId, 0, 0, 0); + +END"; + + const string SqlFnPurgeQueue = @" +CREATE OR ALTER PROCEDURE {0}.PurgeQueue + @queueName varchar(256) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @DeletedMessages TABLE ( + TransportMessageId uniqueidentifier INDEX DMIDX CLUSTERED + ); + + DELETE FROM {0}.MessageDelivery + OUTPUT deleted.TransportMessageId + INTO @DeletedMessages + FROM {0}.MessageDelivery mdx + INNER JOIN {0}.queue q on mdx.queueid = q.Id + WHERE q.name = @queueName + + DELETE FROM {0}.Message + FROM {0}.Message m + INNER JOIN @DeletedMessages dm ON m.TransportMessageId = dm.TransportMessageId + WHERE NOT EXISTS (SELECT 1 FROM {0}.MessageDelivery md WHERE md.TransportMessageId = m.TransportMessageId); + + SELECT COUNT(*) FROM @DeletedMessages +END"; + + const string SqlFnDeleteScheduledMessage = @" +CREATE OR ALTER PROCEDURE {0}.DeleteScheduledMessage + @tokenId uniqueidentifier +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @DeletedMessages TABLE ( + TransportMessageId uniqueidentifier + ); + + DELETE m + OUTPUT deleted.TransportMessageId + INTO @DeletedMessages (TransportMessageId) + FROM {0}.Message m + LEFT JOIN {0}.MessageDelivery md ON md.TransportMessageId = m.TransportMessageId + WHERE m.SchedulingTokenId = @tokenId + AND md.DeliveryCount = 0 + AND md.LockId IS NULL; + + SELECT TransportMessageId + FROM @DeletedMessages; +END +"; + + const string SqlFnRenewMessageLock = @" +CREATE OR ALTER PROCEDURE {0}.RenewMessageLock + @messageDeliveryId bigint, + @lockId uniqueidentifier, + @duration int +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @enqueueTime datetime2; + SET @enqueueTime = DATEADD(SECOND, @duration, SYSUTCDATETIME()); + + DECLARE @updatedMessages TABLE ( + MessageDeliveryId bigint, + QueueId bigint + ); + + UPDATE md + SET EnqueueTime = @enqueueTime + OUTPUT inserted.MessageDeliveryId, inserted.QueueId INTO @updatedMessages + FROM {0}.MessageDelivery md + WHERE md.MessageDeliveryId = @messageDeliveryId AND md.LockId = @lockId; + + DECLARE @queueId bigint + SELECT TOP 1 @queueId = QueueID FROM @updatedMessages; + + IF @queueId IS NOT NULL + BEGIN + INSERT INTO {0}.QueueMetricCapture (Captured, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + VALUES (GETUTCDATE(), @queueId, 0, 0, 0); + END; + + SELECT MessageDeliveryId FROM @updatedMessages; +END"; + + const string SqlFnUnlockMessage = @" +CREATE OR ALTER PROCEDURE {0}.UnlockMessage + @messageDeliveryId bigint, + @lockId uniqueidentifier, + @delay int, + @headers nvarchar(max) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @enqueueTime datetime2; + SET @enqueueTime = DATEADD(SECOND, @delay, SYSUTCDATETIME()); + + DECLARE @updatedMessages TABLE ( + MessageDeliveryId bigint, + QueueId bigint + ); + + UPDATE md + SET EnqueueTime = @enqueueTime, LockId = NULL, ConsumerId = NULL, TransportHeaders = @headers + OUTPUT inserted.MessageDeliveryId, inserted.QueueId INTO @updatedMessages + FROM {0}.MessageDelivery md + WHERE md.MessageDeliveryId = @messageDeliveryId AND md.LockId = @lockId; + + DECLARE @queueId bigint + SELECT TOP 1 @queueId = QueueID FROM @updatedMessages; + + IF @queueId IS NOT NULL + BEGIN + INSERT INTO {0}.QueueMetricCapture (Captured, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + VALUES (GETUTCDATE(), @queueId, 0, 0, 0); + END; + + SELECT MessageDeliveryId FROM @updatedMessages; +END"; + + const string SqlFnMoveMessage = @" +CREATE OR ALTER PROCEDURE {0}.MoveMessage + @messageDeliveryId bigint, + @lockId uniqueidentifier, + @queueName nvarchar(256), + @queueType int, + @headers nvarchar(max) +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @queueId bigint + SELECT @queueId = q.Id + FROM {0}.Queue q + WHERE q.Name = @queueName AND q.Type = @queueType; + + IF @queueId IS NULL + BEGIN + THROW 50000, 'Queue not found', 1; + END; + + DECLARE @updatedMessages TABLE ( + MessageDeliveryId bigint, + QueueId bigint + ); + + UPDATE md + SET EnqueueTime = SYSUTCDATETIME(), QueueId = @queueId, LockId = NULL, ConsumerId = NULL, TransportHeaders = @headers + OUTPUT inserted.MessageDeliveryId, inserted.QueueId INTO @updatedMessages + FROM {0}.MessageDelivery md + WHERE md.MessageDeliveryId = @messageDeliveryId AND md.LockId = @lockId; + + DECLARE @outQueueId bigint + SELECT TOP 1 @outQueueId = QueueID FROM @updatedMessages; + + IF @outQueueId IS NOT NULL + BEGIN + INSERT INTO {0}.QueueMetricCapture (Captured, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + VALUES (GETUTCDATE(), @outQueueId, 0, CASE WHEN @queueType = 2 THEN 1 ELSE 0 END, CASE WHEN @queueType = 3 THEN 1 ELSE 0 END); + END; + + SELECT MessageDeliveryId FROM @updatedMessages; +END"; + + const string SqlFnRequeueMessages = @" +CREATE OR ALTER PROCEDURE {0}.RequeueMessages + @queueName nvarchar(256), + @sourceQueueType int, + @targetQueueType int, + @messageCount int, + @delay int = 0, + @redeliveryCount int = 10 +AS +BEGIN + SET NOCOUNT ON; + + IF NOT @sourceQueueType BETWEEN 1 AND 3 + BEGIN + THROW 50000, 'Invalid source queue type', 1; + END; + + IF NOT @targetQueueType BETWEEN 1 AND 3 + BEGIN + THROW 50000, 'Invalid target queue type', 1; + END; + + IF @sourceQueueType = @targetQueueType + BEGIN + THROW 50000, 'Source and target queue type must not be the same', 1; + END; + + DECLARE @sourceQueueId bigint + SELECT @sourceQueueId = q.Id + FROM {0}.Queue q + WHERE q.Name = @queueName AND q.Type = @sourceQueueType; + + IF @sourceQueueId IS NULL + BEGIN + THROW 50000, 'Source queue not found', 1; + END; + + DECLARE @targetQueueId bigint + SELECT @targetQueueId = q.Id + FROM {0}.Queue q + WHERE q.Name = @queueName AND q.Type = @targetQueueType; + + IF @targetQueueId IS NULL + BEGIN + THROW 50000, 'Target queue not found', 1; + END; + + DECLARE @enqueueTime datetime2; + SET @enqueueTime = DATEADD(SECOND, @delay, SYSUTCDATETIME()); + + UPDATE {0}.MessageDelivery + SET EnqueueTime = @enqueueTime, + QueueId = @targetQueueId, + MaxDeliveryCount = MessageDelivery.DeliveryCount + @redeliveryCount + FROM (SELECT mdx.MessageDeliveryId + FROM {0}.MessageDelivery mdx WITH (ROWLOCK, UPDLOCK) + WHERE mdx.QueueId = @sourceQueueId + AND mdx.LockId IS NULL + AND mdx.ConsumerId IS NULL + AND (mdx.ExpirationTime IS NULL OR mdx.ExpirationTime > @enqueueTime) + ORDER BY mdx.MessageDeliveryId OFFSET 0 ROWS + FETCH NEXT @messageCount ROWS ONLY) mdy + WHERE mdy.MessageDeliveryId = MessageDelivery.MessageDeliveryId; + + RETURN @@ROWCOUNT +END"; + + const string SqlFnRequeueMessage = @" +CREATE OR ALTER PROCEDURE {0}.RequeueMessage @messageDeliveryId bigint, + @targetQueueType int, + @delay int = 0, + @redeliveryCount int = 10 +AS +BEGIN + SET NOCOUNT ON; + + IF NOT @targetQueueType BETWEEN 1 AND 3 + BEGIN + THROW 50000, 'Invalid target queue type', 1; + END; + + DECLARE @sourceQueueId bigint; + SELECT @sourceQueueId = md.QueueId + FROM {0}.MessageDelivery md + WHERE md.MessageDeliveryId = @messageDeliveryId; + + IF @sourceQueueId IS NULL + BEGIN + THROW 50000, 'Message delivery not found', 1; + END; + + DECLARE @sourceQueueName nvarchar(256); + DECLARE @sourceQueueType int; + SELECT @sourceQueueName = q.Name, @sourceQueueType = q.Type + FROM {0}.Queue q + WHERE q.Id = @sourceQueueId; + + IF @sourceQueueName IS NULL + BEGIN + THROW 50000, 'Queue not found', 1; + END; + + IF @sourceQueueType = @targetQueueType + BEGIN + THROW 50000, 'Source and target queue type must not be the same', 1; + END; + + DECLARE @targetQueueId bigint; + SELECT @targetQueueId = q.Id + FROM {0}.Queue q + WHERE q.Name = @sourceQueueName + AND q.Type = @targetQueueType; + + IF @targetQueueId IS NULL + BEGIN + THROW 50000, 'Queue type not found', 1; + END; + + DECLARE @enqueueTime datetime2; + SET @enqueueTime = DATEADD(SECOND, @delay, SYSUTCDATETIME()); + + UPDATE {0}.MessageDelivery + SET EnqueueTime = @enqueueTime, + QueueId = @targetQueueId, + MaxDeliveryCount = MessageDelivery.DeliveryCount + @redeliveryCount + FROM (SELECT mdx.MessageDeliveryId + FROM {0}.MessageDelivery mdx WITH (ROWLOCK, UPDLOCK) + WHERE mdx.QueueId = @sourceQueueId + AND mdx.LockId IS NULL + AND mdx.ConsumerId IS NULL + AND (mdx.ExpirationTime IS NULL OR mdx.ExpirationTime > @enqueueTime) + AND mdx.MessageDeliveryId = @messageDeliveryId) mdy + WHERE mdy.MessageDeliveryId = MessageDelivery.MessageDeliveryId; + + RETURN @@ROWCOUNT; +END +"; + + const string SqlFnProcessMetrics = @" +CREATE OR ALTER PROCEDURE {0}.ProcessMetrics + @rowLimit int +AS +BEGIN + SET NOCOUNT ON; + + DECLARE @DeletedMetrics TABLE + ( + StartTime datetime2 not null, + Duration int not null, + QueueId bigint not null, + ConsumeCount bigint not null, + ErrorCount bigint not null, + DeadLetterCount bigint not null + ); + + DELETE + FROM {0}.QueueMetricCapture + OUTPUT CONVERT(DATETIME, CONVERT(VARCHAR(16), deleted.Captured, 120) + ':00'), 60, deleted.QueueId, + deleted.ConsumeCount, + deleted.ErrorCount, + deleted.DeadLetterCount + INTO @DeletedMetrics + WHERE Id < COALESCE((SELECT MIN(id) FROM {0}.queuemetriccapture), 0) + @rowLimit; + + MERGE INTO {0}.QueueMetric WITH (ROWLOCK) AS target + USING (SELECT m.StartTime, + m.Duration, + m.QueueId, + sum(m.ConsumeCount), + sum(m.ErrorCount), + sum(m.DeadLetterCount) + FROM @DeletedMetrics m + GROUP BY StartTime, m.Duration, m.QueueId) as source + (StartTime, Duration, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + ON target.StartTime = source.starttime AND target.Duration = source.duration AND + target.QueueId = source.QueueId + WHEN MATCHED THEN + UPDATE + SET ConsumeCount = source.ConsumeCount + target.ConsumeCount, + ErrorCount = source.ErrorCount + target.ErrorCount, + DeadLetterCount = source.DeadLetterCount + target.DeadLetterCount + WHEN NOT MATCHED THEN + INSERT (starttime, duration, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + values (source.starttime, source.duration, source.QueueId, source.ConsumeCount, source.ErrorCount, + source.DeadLetterCount); + + DELETE + FROM @DeletedMetrics; + + DELETE + FROM {0}.QueueMetric + OUTPUT CONVERT(DATETIME, CONVERT(VARCHAR(13), deleted.StartTime, 120) + ':00:00'), 3600, deleted.QueueId, + deleted.ConsumeCount, + deleted.ErrorCount, + deleted.DeadLetterCount + INTO @DeletedMetrics + WHERE Duration = 60 + AND StartTime < DATEADD(HOUR, -8, GETUTCDATE()) + + MERGE INTO {0}.QueueMetric WITH (ROWLOCK) AS target + USING (SELECT m.StartTime, + m.Duration, + m.QueueId, + sum(m.ConsumeCount), + sum(m.ErrorCount), + sum(m.DeadLetterCount) + FROM @DeletedMetrics m + GROUP BY StartTime, m.Duration, m.QueueId) as source + (StartTime, Duration, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + ON target.StartTime = source.starttime AND target.Duration = source.duration AND + target.QueueId = source.QueueId + WHEN MATCHED THEN + UPDATE + SET ConsumeCount = source.ConsumeCount + target.ConsumeCount, + ErrorCount = source.ErrorCount + target.ErrorCount, + DeadLetterCount = source.DeadLetterCount + target.DeadLetterCount + WHEN NOT MATCHED THEN + INSERT (starttime, duration, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + values (source.starttime, source.duration, source.QueueId, source.ConsumeCount, source.ErrorCount, + source.DeadLetterCount); + + DELETE + FROM @DeletedMetrics; + + DELETE + FROM {0}.QueueMetric + OUTPUT CONVERT(DATETIME, CONVERT(VARCHAR(10), deleted.StartTime, 120)), 86400, deleted.QueueId, + deleted.ConsumeCount, + deleted.ErrorCount, + deleted.DeadLetterCount + INTO @DeletedMetrics + WHERE Duration = 3600 + AND StartTime < DATEADD(HOUR, -48, GETUTCDATE()) + + MERGE INTO {0}.QueueMetric WITH (ROWLOCK) AS target + USING (SELECT m.StartTime, + m.Duration, + m.QueueId, + sum(m.ConsumeCount), + sum(m.ErrorCount), + sum(m.DeadLetterCount) + FROM @DeletedMetrics m + GROUP BY StartTime, m.Duration, m.QueueId) as source + (StartTime, Duration, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + ON target.StartTime = source.starttime AND target.Duration = source.duration AND + target.QueueId = source.QueueId + WHEN MATCHED THEN + UPDATE + SET ConsumeCount = source.ConsumeCount + target.ConsumeCount, + ErrorCount = source.ErrorCount + target.ErrorCount, + DeadLetterCount = source.DeadLetterCount + target.DeadLetterCount + WHEN NOT MATCHED THEN + INSERT (starttime, duration, QueueId, ConsumeCount, ErrorCount, DeadLetterCount) + values (source.starttime, source.duration, source.QueueId, source.ConsumeCount, source.ErrorCount, + source.DeadLetterCount); + + DELETE + FROM {0}.QueueMetric + WHERE StartTime < DATEADD(DAY, -90, GETUTCDATE()); +END +"; + + const string SqlFnPurgeTopology = @" +CREATE OR ALTER PROCEDURE {0}.PurgeTopology +AS +BEGIN + WITH expired AS (SELECT q.Id, q.name, DATEADD(second, -q.autodelete, GETUTCDATE()) as expires_at + FROM {0}.Queue q + WHERE q.Type = 1 AND q.AutoDelete IS NOT NULL AND DATEADD(second, -q.AutoDelete, GETUTCDATE()) > Updated), + metrics AS (SELECT qm.queueid, MAX(starttime) as start_time + FROM {0}.queuemetric qm + INNER JOIN expired q2 on q2.Id = qm.QueueId + WHERE DATEADD(second, duration, starttime) > q2.expires_at + GROUP BY qm.queueid) + DELETE FROM {0}.Queue + FROM {0}.Queue qd + INNER JOIN expired qdx ON qdx.Name = qd.Name + WHERE qdx.Id NOT IN (SELECT QueueId FROM metrics); + +END +"; + + const string SqlFnQueuesView = """ + CREATE OR ALTER VIEW {0}.Queues + AS + SELECT x.QueueName, + MAX(x.QueueAutoDelete) AS QueueAutoDelete, + SUM(x.MessageReady) AS Ready, + SUM(x.MessageScheduled) AS Scheduled, + SUM(x.MessageError) AS Errored, + SUM(x.MessageDeadLetter) AS DeadLettered, + SUM(x.MessageLocked) AS Locked, + ISNULL(MAX(x.ConsumeCount), 0) AS ConsumeCount, + ISNULL(MAX(x.ErrorCount), 0) AS ErrorCount, + ISNULL(MAX(x.DeadLetterCount), 0) AS DeadLetterCount, + MAX(x.StartTime) AS CountStartTime, + ISNULL(MAX(x.Duration), 0) AS CountDuration + FROM (SELECT q.Name AS QueueName, + q.AutoDelete AS QueueAutoDelete, + qm.ConsumeCount, + qm.ErrorCount, + qm.DeadLetterCount, + qm.StartTime, + qm.Duration, + + IIF(q.Type = 1 + AND md.MessageDeliveryId IS NOT NULL + AND md.EnqueueTime <= GETUTCDATE(), 1, 0) AS MessageReady, + IIF(q.Type = 1 + AND md.MessageDeliveryId IS NOT NULL + AND md.LockId IS NULL + AND md.EnqueueTime > GETUTCDATE(), 1, 0) AS MessageScheduled, + IIF(q.Type = 1 + AND md.MessageDeliveryId IS NOT NULL + AND md.LockId IS NOT NULL + AND md.DeliveryCount >= 1 + AND md.EnqueueTime > GETUTCDATE(), 1, 0) AS MessageLocked, + IIF(q.Type = 2 + AND md.MessageDeliveryId IS NOT NULL, 1, 0) AS MessageError, + IIF(q.Type = 3 + AND md.MessageDeliveryId IS NOT NULL, 1, 0) AS MessageDeadLetter + FROM {0}.Queue q + LEFT JOIN {0}.MessageDelivery md ON q.Id = md.QueueId + LEFT JOIN (SELECT qm.QueueId, + qm.QueueName, + qm.ConsumeCount AS ConsumeCount, + qm.ErrorCount AS ErrorCount, + qm.DeadLetterCount AS DeadLetterCount, + qm.StartTime, + qm.Duration + FROM (SELECT qm.QueueId, + q2.Name as QueueName, + ROW_NUMBER() OVER (PARTITION BY qm.QueueId ORDER BY qm.StartTime DESC) AS RowNum, + qm.ConsumeCount, + qm.ErrorCount, + qm.DeadLetterCount, + qm.StartTime, + qm.Duration + FROM {0}.QueueMetric qm + INNER JOIN {0}.Queue q2 ON qm.QueueId = q2.Id + WHERE q2.Type = 1 + AND qm.StartTime >= DATEADD(MINUTE, -5, GETUTCDATE())) qm + WHERE qm.RowNum = 1) qm ON qm.QueueId = q.Id) x + GROUP BY x.QueueName; + """; + + const string SqlFnSubscriptionsView = """ + CREATE OR ALTER VIEW {0}.Subscriptions + AS + SELECT t.name as TopicName, 'topic' as DestinationType, t2.name as DestinationName, ts.SubType as SubscriptionType, ts.RoutingKey + FROM {0}.topic t + JOIN {0}.TopicSubscription ts ON t.id = ts.sourceid + JOIN {0}.topic t2 on t2.id = ts.destinationid + UNION + SELECT t.name as TopicName, 'queue' as DestinationType, q.name as DestinationName, qs.SubType as SubscriptionType, qs.RoutingKey + FROM {0}.queuesubscription qs + LEFT JOIN {0}.queue q on qs.destinationid = q.id + LEFT JOIN {0}.topic t on qs.sourceid = t.id; + """; + + readonly ILogger _logger; + + public SqlServerDatabaseMigrator(ILogger logger) + { + _logger = logger; + } + + public async Task CreateDatabase(SqlTransportOptions options, CancellationToken cancellationToken) + { + await CreateDatabaseIfNotExist(options, cancellationToken).ConfigureAwait(false); + } + + public async Task DeleteDatabase(SqlTransportOptions options, CancellationToken cancellationToken) + { + await using var connection = SqlServerSqlTransportConnection.GetSystemDatabaseConnection(options); + await connection.Open(cancellationToken).ConfigureAwait(false); + + var result = await connection.Connection.ExecuteScalarAsync(string.Format(DbExistsSql, options.Database)).ConfigureAwait(false); + if (result > 0) + { + await connection.Connection.ExecuteScalarAsync(string.Format(DropSql, options.Database)).ConfigureAwait(false); + + _logger.LogInformation("Database {Database} deleted", options.Database); + } + } + + public async Task CreateInfrastructure(SqlTransportOptions options, CancellationToken cancellationToken) + { + await CreateSchemaIfNotExist(options, cancellationToken).ConfigureAwait(false); + + await using var connection = SqlServerSqlTransportConnection.GetDatabaseConnection(options); + await connection.Open(cancellationToken).ConfigureAwait(false); + + try + { + await connection.Connection.ExecuteScalarAsync(string.Format(CreateInfrastructureSql, options.Schema)).ConfigureAwait(false); + + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnCreateQueue, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnCreateTopic, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnCreateTopicSubscription, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnCreateQueueSubscription, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnPurgeQueue, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnPublish, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnSend, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnFetchMessages, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnFetchMessagesPartitioned, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnDeleteMessage, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnDeleteScheduledMessage, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnRenewMessageLock, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnUnlockMessage, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnMoveMessage, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnRequeueMessage, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnRequeueMessages, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnProcessMetrics, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnPurgeTopology, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnTouchQueue, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnQueuesView, options.Schema)).ConfigureAwait(false); + await connection.Connection.ExecuteScalarAsync(string.Format(SqlFnSubscriptionsView, options.Schema)).ConfigureAwait(false); + + _logger.LogDebug("Transport infrastructure in schema {Schema} created (or updated)", options.Schema); + } + finally + { + await connection.Close().ConfigureAwait(false); + } + } + + async Task CreateDatabaseIfNotExist(SqlTransportOptions options, CancellationToken cancellationToken) + { + await using var connection = SqlServerSqlTransportConnection.GetSystemDatabaseConnection(options); + await connection.Open(cancellationToken); + + try + { + var result = await connection.Connection.ExecuteScalarAsync(string.Format(DbExistsSql, options.Database)).ConfigureAwait(false); + if (result > 0) + _logger.LogDebug("Database {Database} already exists", options.Database); + else + { + await connection.Connection.ExecuteScalarAsync(string.Format(DbCreateSql, options.Database)).ConfigureAwait(false); + + _logger.LogInformation("Database {Database} created", options.Database); + } + + result = await connection.Connection.ExecuteScalarAsync(string.Format(LoginExistsSql, options.Username)).ConfigureAwait(false); + if (!result.HasValue) + { + await connection.Connection.ExecuteScalarAsync(string.Format(CreateLoginSql, options.Username, options.Password)) + .ConfigureAwait(false); + + _logger.LogDebug("Login {Username} created", options.Username); + } + } + finally + { + await connection.Close(); + } + } + + async Task CreateSchemaIfNotExist(SqlTransportOptions options, CancellationToken cancellationToken) + { + await using var connection = SqlServerSqlTransportConnection.GetDatabaseAdminConnection(options); + await connection.Open(cancellationToken).ConfigureAwait(false); + + try + { + await connection.Connection.ExecuteScalarAsync(string.Format(SchemaCreateSql, options.Database, options.Schema)).ConfigureAwait(false); + + _logger.LogDebug("Schema {Schema} created", options.Schema); + + await GrantAccess(connection, options).ConfigureAwait(false); + } + finally + { + await connection.Close().ConfigureAwait(false); + } + } + + async Task GrantAccess(ISqlServerSqlTransportConnection connection, SqlTransportOptions options) + { + if (string.IsNullOrWhiteSpace(options.Role)) + throw new ArgumentException("The SQL transport migrator requires a valid Role, but Role was not specified", nameof(options)); + + var result = await connection.Connection.ExecuteScalarAsync(string.Format(RoleExistsSql, options.Role)).ConfigureAwait(false); + if (!result.HasValue) + { + await connection.Connection.ExecuteScalarAsync(string.Format(CreateRoleSql, options.Role)).ConfigureAwait(false); + + _logger.LogDebug("Role {Role} created", options.Role); + } + + await connection.Connection.ExecuteScalarAsync(string.Format(GrantRoleSql, options.Role, options.Schema)).ConfigureAwait(false); + + _logger.LogDebug("Role {Role} granted access to schema {Schema}", options.Role, options.Schema); + + if (string.IsNullOrWhiteSpace(options.Username)) + throw new ArgumentException("The SQL transport migrator requires a valid Username, but Username was not specified", nameof(options)); + + result = await connection.Connection.ExecuteScalarAsync(string.Format(RoleExistsSql, options.Username)).ConfigureAwait(false); + if (!result.HasValue) + { + result = await connection.Connection + .ExecuteScalarAsync(string.Format(CreateUserSql, options.Schema, options.Username)) + .ConfigureAwait(false); + + if (result is 1) + _logger.LogDebug("User {Username} created", options.Username); + } + + result = await connection.Connection.ExecuteScalarAsync(string.Format(IsRoleMemberSql, options.Role, options.Username)).ConfigureAwait(false); + if (result is null or 0) + { + await connection.Connection + .ExecuteScalarAsync(string.Format(AddRoleMemberSql, options.Database, options.Username, options.Role)) + .ConfigureAwait(false); + + _logger.LogDebug("User {Username} added to role {Role}", options.Username, options.Role); + } + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerDbConnectionContext.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerDbConnectionContext.cs new file mode 100644 index 00000000000..35b7c008291 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerDbConnectionContext.cs @@ -0,0 +1,234 @@ +namespace MassTransit.SqlTransport.SqlServer; + +using System; +using System.Data; +using System.Threading; +using System.Threading.Tasks; +using Configuration; +using Dapper; +using Logging; +using MassTransit.Middleware; +using Microsoft.Data.SqlClient; +using RetryPolicies; +using Transports; +using Util; + + +public class SqlServerDbConnectionContext : + BasePipeContext, + ConnectionContext, + IAsyncDisposable +{ + readonly TaskExecutor _executor; + readonly ISqlHostConfiguration _hostConfiguration; + readonly SqlServerSqlHostSettings _hostSettings; + readonly IRetryPolicy _retryPolicy; + + static SqlServerDbConnectionContext() + { + DefaultTypeMap.MatchNamesWithUnderscores = true; + SqlMapper.AddTypeHandler(new UriTypeHandler()); + } + + public SqlServerDbConnectionContext(ISqlHostConfiguration hostConfiguration, ITransportSupervisor supervisor) + : base(supervisor.Stopped) + { + _hostConfiguration = hostConfiguration; + + _hostSettings = hostConfiguration.Settings as SqlServerSqlHostSettings + ?? throw new ConfigurationException("The host settings were not of the expected type"); + + _retryPolicy = Retry.CreatePolicy(x => x.Immediate(10).Handle(ex => IsTransient(ex))); + + Topology = hostConfiguration.Topology; + + supervisor.AddConsumeAgent(new MaintenanceAgent(this, hostConfiguration)); + + _executor = new TaskExecutor(hostConfiguration.Settings.ConnectionLimit); + } + + public ISqlBusTopology Topology { get; } + + public IsolationLevel IsolationLevel => _hostSettings.IsolationLevel; + + public Uri HostAddress => _hostConfiguration.HostAddress; + + public string? Schema => _hostSettings.Schema; + + public ClientContext CreateClientContext(CancellationToken cancellationToken) + { + return new SqlServerClientContext(this, cancellationToken); + } + + async Task ConnectionContext.CreateConnection(CancellationToken cancellationToken) + { + return await CreateConnection(cancellationToken).ConfigureAwait(false); + } + + public Task Query(Func> callback, CancellationToken cancellationToken) + { + return _executor.Run(() => + { + return _retryPolicy.Retry(async () => + { + await using var connection = await CreateConnection(cancellationToken).ConfigureAwait(false); + + #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + await using var transaction = await connection.Connection.BeginTransactionAsync(_hostSettings.IsolationLevel, cancellationToken) + .ConfigureAwait(false); + #else + using var transaction = connection.Connection.BeginTransaction(_hostSettings.IsolationLevel); + #endif + + var result = await callback(connection.Connection, transaction).ConfigureAwait(false); + + #if NET6_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER + await transaction.CommitAsync(cancellationToken).ConfigureAwait(false); + #else + transaction.Commit(); + #endif + + return result; + }, false, cancellationToken); + }, cancellationToken); + } + + public Task DelayUntilMessageReady(long queueId, TimeSpan timeout, CancellationToken cancellationToken) + { + async Task WaitAsync() + { + var delayTask = Task.Delay(timeout, cancellationToken); + await Task.WhenAny(delayTask).ConfigureAwait(false); + } + + return WaitAsync(); + } + + public async ValueTask DisposeAsync() + { + TransportLogMessages.DisconnectedHost(_hostConfiguration.HostAddress.ToString()); + } + + public async Task CreateConnection(CancellationToken cancellationToken) + { + var connection = new SqlServerSqlTransportConnection(_hostSettings.GetConnectionString()); + + await connection.Open(cancellationToken).ConfigureAwait(false); + + return connection; + } + + bool IsTransient(SqlException exception) + { + return exception.Number switch + { + -2 => true, + 20 => true, + 64 => true, + 233 => true, + 1205 => true, + 10053 => true, + 10054 => true, + 10060 => true, + 10928 => true, + 10929 => true, + 40197 => true, + 40143 => true, + 40501 => true, + 40613 => true, + _ => false + }; + } + + + class MaintenanceAgent : + Agent + { + readonly SqlServerDbConnectionContext _context; + readonly ISqlHostConfiguration _hostConfiguration; + readonly ILogContext? _logContext; + + public MaintenanceAgent(SqlServerDbConnectionContext context, ISqlHostConfiguration hostConfiguration) + { + _context = context; + _hostConfiguration = hostConfiguration; + _logContext = hostConfiguration.LogContext; + + var runTask = Task.Run(() => PerformMaintenance(), Stopping); + + SetReady(runTask); + + SetCompleted(runTask); + } + + async Task PerformMaintenance() + { + LogContext.SetCurrentIfNull(_logContext); + + var processMetricsSql = $"{_context.Schema}.ProcessMetrics"; + var purgeTopologySql = $"{_context.Schema}.PurgeTopology"; + + var random = new Random(); + + var cleanupInterval = _hostConfiguration.Settings.QueueCleanupInterval + + TimeSpan.FromSeconds(random.Next(0, (int)(_hostConfiguration.Settings.QueueCleanupInterval.TotalSeconds / 10))); + + while (!Stopping.IsCancellationRequested) + { + DateTime? lastCleanup = null; + + try + { + var maintenanceInterval = _hostConfiguration.Settings.MaintenanceInterval + + TimeSpan.FromSeconds(random.Next(0, (int)(_hostConfiguration.Settings.MaintenanceInterval.TotalSeconds / 10))); + + try + { + await Task.Delay(maintenanceInterval, Stopping); + } + catch (OperationCanceledException) + { + await Execute(processMetricsSql, new + { + rowLimit = _hostConfiguration.Settings.MaintenanceBatchSize, + }, CancellationToken.None); + + if (lastCleanup == null) + await Execute(purgeTopologySql, new { }, CancellationToken.None); + } + + await _hostConfiguration.Retry(async () => + { + await Execute(processMetricsSql, new + { + rowLimit = _hostConfiguration.Settings.MaintenanceBatchSize, + }, Stopping); + + if (lastCleanup == null || lastCleanup < DateTime.UtcNow - cleanupInterval) + { + await Execute(purgeTopologySql, new { }, CancellationToken.None); + + lastCleanup = DateTime.UtcNow; + cleanupInterval = _hostConfiguration.Settings.QueueCleanupInterval + + TimeSpan.FromSeconds(random.Next(0, (int)(_hostConfiguration.Settings.QueueCleanupInterval.TotalSeconds / 10))); + } + }, Stopping, Stopping); + } + catch (OperationCanceledException) + { + } + catch (Exception exception) + { + LogContext.Debug?.Log(exception, "SQL Server Maintenance Faulted"); + } + } + } + + Task Execute(string functionName, object values, CancellationToken cancellationToken) + where T : struct + { + return _context.Query((connection, transaction) => connection + .ExecuteScalarAsync(functionName, values, transaction, commandType: CommandType.StoredProcedure), cancellationToken); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlHostConfigurator.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlHostConfigurator.cs new file mode 100644 index 00000000000..e81cd022bcb --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlHostConfigurator.cs @@ -0,0 +1,41 @@ +namespace MassTransit.SqlTransport.SqlServer +{ + using System; + using Configuration; + + + public class SqlServerSqlHostConfigurator : + SqlHostConfigurator, + ISqlServerSqlHostConfigurator + { + readonly SqlServerSqlHostSettings _settings; + + public SqlServerSqlHostConfigurator(SqlServerSqlHostSettings settings) + : base(settings) + { + _settings = settings; + } + + public SqlServerSqlHostConfigurator(Uri hostAddress) + : this(new SqlServerSqlHostSettings(hostAddress)) + { + } + + public SqlServerSqlHostConfigurator(SqlTransportOptions options) + : this(new SqlServerSqlHostSettings(options)) + { + } + + public SqlServerSqlHostConfigurator(string connectionString) + : this(new SqlServerSqlHostSettings(connectionString)) + { + } + + public SqlHostSettings Settings => _settings; + + public override string? ConnectionString + { + set => _settings.ConnectionString = value; + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlHostSettings.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlHostSettings.cs new file mode 100644 index 00000000000..0594223f15b --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlHostSettings.cs @@ -0,0 +1,136 @@ +namespace MassTransit.SqlTransport.SqlServer +{ + using System; + using System.Net; + using System.Text; + using Configuration; + using Microsoft.Data.SqlClient; + + + public class SqlServerSqlHostSettings : + ConfigurationSqlHostSettings + { + SqlConnectionStringBuilder? _builder; + + public SqlServerSqlHostSettings(Uri hostAddress) + : base(hostAddress) + { + var address = new SqlHostAddress(hostAddress); + + Host = address.Host; + InstanceName = address.InstanceName; + } + + public SqlServerSqlHostSettings(string connectionString) + { + ConnectionString = connectionString; + } + + public SqlServerSqlHostSettings(SqlTransportOptions options) + { + var builder = SqlServerSqlTransportConnection.CreateBuilder(options); + + ParseDataSource(builder.DataSource); + + Database = builder.InitialCatalog; + Schema = options.Schema; + + Username = builder.UserID; + Password = builder.Password; + + _builder = builder; + + if (options.ConnectionLimit.HasValue) + ConnectionLimit = options.ConnectionLimit.Value; + } + + public string? ConnectionString + { + set + { + var builder = new SqlConnectionStringBuilder(value); + + ParseDataSource(builder.DataSource); + + Username = builder.UserID; + Password = builder.Password; + + Database = builder.InitialCatalog; + + _builder = builder; + } + } + + public override ConnectionContextFactory CreateConnectionContextFactory(ISqlHostConfiguration hostConfiguration) + { + return new SqlServerConnectionContextFactory(hostConfiguration); + } + + public string GetConnectionString() + { + var builder = _builder ??= new SqlConnectionStringBuilder + { + DataSource = FormatDataSource(), + UserID = Username, + Password = Password, + InitialCatalog = Database, + TrustServerCertificate = true + }; + + return builder.ToString(); + } + + void ParseDataSource(string? source) + { + var split = source?.Split(','); + if (split?.Length == 2) + { + ParseHost(split[0].Trim()); + if (int.TryParse(split[1].Trim(), out var port)) + Port = port; + } + else + ParseHost(source); + } + + void ParseHost(string? host) + { + var hostSegments = host?.Split('\\'); + if (hostSegments?.Length == 2) + { + Host = TrimHost(hostSegments[0]); + InstanceName = hostSegments[1].Trim(); + } + else + Host = TrimHost(host); + } + + string? TrimHost(string? host) + { + if (host == null) + return null; + + if (IPAddress.TryParse(host, out var endpoint)) + return endpoint.ToString(); + + var hostSplit = host.Trim().Split(':'); + return hostSplit.Length == 1 ? hostSplit[0] : hostSplit[1]; + } + + string? FormatDataSource() + { + if (string.IsNullOrWhiteSpace(Host)) + return null; + + var sb = new StringBuilder(); + sb.Append(Host); + if (!string.IsNullOrWhiteSpace(InstanceName)) + sb.Append('\\').Append(InstanceName); + + if (Port.HasValue) + sb.Append(',').Append(Port.Value); + + return sb.ToString(); + } + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlTransportConnection.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlTransportConnection.cs new file mode 100644 index 00000000000..0a882323305 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlTransportConnection.cs @@ -0,0 +1,117 @@ +namespace MassTransit.SqlTransport.SqlServer; + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; + + +public class SqlServerSqlTransportConnection : + ISqlServerSqlTransportConnection +{ + public SqlServerSqlTransportConnection(string connectionString) + { + Connection = new SqlConnection(connectionString); + } + + public SqlConnection Connection { get; } + + public ValueTask DisposeAsync() + { + Connection.Dispose(); + + return default; + } + + public Task Open(CancellationToken cancellationToken = default) + { + return Connection.OpenAsync(cancellationToken); + } + + public Task Close() + { + Connection.Close(); + + return Task.CompletedTask; + } + + public static SqlServerSqlTransportConnection GetSystemDatabaseConnection(SqlTransportOptions options) + { + var builder = CreateBuilder(options); + + builder.InitialCatalog = "master"; + + if (!string.IsNullOrWhiteSpace(options.AdminUsername)) + builder.UserID = options.AdminUsername; + if (!string.IsNullOrWhiteSpace(options.AdminPassword)) + builder.Password = options.AdminPassword; + + return new SqlServerSqlTransportConnection(builder.ToString()); + } + + public static SqlServerSqlTransportConnection GetDatabaseAdminConnection(SqlTransportOptions options) + { + var builder = CreateBuilder(options); + + if (!string.IsNullOrWhiteSpace(options.AdminUsername)) + builder.UserID = options.AdminUsername; + if (!string.IsNullOrWhiteSpace(options.AdminPassword)) + builder.Password = options.AdminPassword; + + return new SqlServerSqlTransportConnection(builder.ToString()); + } + + public static SqlServerSqlTransportConnection GetDatabaseConnection(SqlTransportOptions options) + { + var builder = CreateBuilder(options); + + return new SqlServerSqlTransportConnection(builder.ToString()); + } + + public static SqlConnectionStringBuilder CreateBuilder(SqlTransportOptions options) + { + var builder = new SqlConnectionStringBuilder(options.ConnectionString) { TrustServerCertificate = true }; + + if (!string.IsNullOrWhiteSpace(options.Host)) + builder.DataSource = options.FormatDataSource(); + else if (!string.IsNullOrWhiteSpace(builder.DataSource)) + (options.Host, options.Port) = ParseDataSource(builder.DataSource); + + if (!string.IsNullOrWhiteSpace(options.Database)) + builder.InitialCatalog = options.Database; + else if (!string.IsNullOrWhiteSpace(builder.InitialCatalog)) + options.Database = builder.InitialCatalog; + + if (!string.IsNullOrWhiteSpace(options.Username)) + builder.UserID = options.Username; + else if (!string.IsNullOrWhiteSpace(builder.UserID)) + options.Username = builder.UserID; + if (!string.IsNullOrWhiteSpace(options.Password)) + builder.Password = options.Password; + else if (!string.IsNullOrWhiteSpace(builder.Password)) + options.Password = builder.Password; + + if (string.IsNullOrWhiteSpace(options.Schema)) + options.Schema = "transport"; + + if (string.IsNullOrWhiteSpace(options.Role)) + options.Role = "transport"; + + return builder; + } + + static (string? host, int? port) ParseDataSource(string? source) + { + var split = source?.Split(','); + if (split?.Length == 2) + { + var host = split[0].Trim(); + + if (int.TryParse(split[1].Trim(), out var port)) + return (host, port); + + return (host, null); + } + + return (source?.Trim(), null); + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlTransportOptionsExtensions.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlTransportOptionsExtensions.cs new file mode 100644 index 00000000000..22e105ce9ff --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/SqlServerSqlTransportOptionsExtensions.cs @@ -0,0 +1,9 @@ +namespace MassTransit.SqlTransport.SqlServer; + +public static class SqlServerSqlTransportOptionsExtensions +{ + public static string? FormatDataSource(this SqlTransportOptions options) + { + return options.Port.HasValue ? $"{options.Host},{options.Port}" : options.Host; + } +} diff --git a/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/UriTypeHandler.cs b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/UriTypeHandler.cs new file mode 100644 index 00000000000..6316e26c161 --- /dev/null +++ b/src/Transports/MassTransit.SqlTransport.SqlServer/SqlTransport/SqlServer/UriTypeHandler.cs @@ -0,0 +1,24 @@ +namespace MassTransit.SqlTransport.SqlServer +{ + using System; + using System.Data; + using Dapper; + + + public class UriTypeHandler : SqlMapper.TypeHandler + { + public override void SetValue(IDbDataParameter parameter, Uri? value) + { + parameter.DbType = DbType.String; + parameter.Value = value != null ? value.ToString() : DBNull.Value; + } + + public override Uri Parse(object value) + { + if (value is string text && !string.IsNullOrWhiteSpace(text)) + return new Uri(text); + + return null!; + } + } +} diff --git a/src/Transports/MassTransit.WebJobs.EventHubsIntegration/EventHubIntegration/EventDataHeaderProvider.cs b/src/Transports/MassTransit.WebJobs.EventHubsIntegration/EventHubIntegration/EventDataHeaderProvider.cs index 3c9c85e5a02..0392fcf0c53 100644 --- a/src/Transports/MassTransit.WebJobs.EventHubsIntegration/EventHubIntegration/EventDataHeaderProvider.cs +++ b/src/Transports/MassTransit.WebJobs.EventHubsIntegration/EventHubIntegration/EventDataHeaderProvider.cs @@ -57,6 +57,12 @@ public bool TryGetHeader(string key, out object value) return !string.IsNullOrWhiteSpace(_eventData.CorrelationId); } + if (MessageHeaders.TransportSentTime.Equals(key, StringComparison.OrdinalIgnoreCase)) + { + value = _eventData.EnqueuedTime.UtcDateTime; + return true; + } + if (MessageHeaders.ContentType.Equals(key, StringComparison.OrdinalIgnoreCase)) { value = _eventData.ContentType; diff --git a/src/Transports/MassTransit.WebJobs.EventHubsIntegration/EventHubIntegration/EventDataReceiver.cs b/src/Transports/MassTransit.WebJobs.EventHubsIntegration/EventHubIntegration/EventDataReceiver.cs index 83bcbb99ae3..a4a83b33e9d 100644 --- a/src/Transports/MassTransit.WebJobs.EventHubsIntegration/EventHubIntegration/EventDataReceiver.cs +++ b/src/Transports/MassTransit.WebJobs.EventHubsIntegration/EventHubIntegration/EventDataReceiver.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using Azure.Messaging.EventHubs; + using Context; using Transports; @@ -52,11 +53,10 @@ public async Task Handle(EventData message, CancellationToken cancellationToken, try { - await _dispatcher.Dispatch(context).ConfigureAwait(false); + await _dispatcher.Dispatch(context, NoLockReceiveContext.Instance).ConfigureAwait(false); } finally { - // ReSharper disable once MethodHasAsyncOverload registration.Dispose(); context.Dispose(); diff --git a/src/Transports/MassTransit.WebJobs.EventHubsIntegration/MassTransit.WebJobs.EventHubsIntegration.csproj b/src/Transports/MassTransit.WebJobs.EventHubsIntegration/MassTransit.WebJobs.EventHubsIntegration.csproj index 2c2e0c80c76..06fc469c66c 100644 --- a/src/Transports/MassTransit.WebJobs.EventHubsIntegration/MassTransit.WebJobs.EventHubsIntegration.csproj +++ b/src/Transports/MassTransit.WebJobs.EventHubsIntegration/MassTransit.WebJobs.EventHubsIntegration.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -22,7 +22,6 @@ - diff --git a/src/Transports/MassTransit.WebJobs.ServiceBusIntegration/Configuration/AzureFunctionsBusConfigurationExtensions.cs b/src/Transports/MassTransit.WebJobs.ServiceBusIntegration/Configuration/AzureFunctionsBusConfigurationExtensions.cs index 98e4f3ad106..3fc6cc4f97c 100644 --- a/src/Transports/MassTransit.WebJobs.ServiceBusIntegration/Configuration/AzureFunctionsBusConfigurationExtensions.cs +++ b/src/Transports/MassTransit.WebJobs.ServiceBusIntegration/Configuration/AzureFunctionsBusConfigurationExtensions.cs @@ -51,8 +51,8 @@ public static IServiceCollection AddMassTransitForAzureFunctions(this IServiceCo if (string.IsNullOrWhiteSpace(connectionString)) { - var ns = config["ServiceBusConnection__fullyQualifiedNamespace"] - ?? config[$"{connectionStringConfigurationKey}__fullyQualifiedNamespace"]; + var ns = config["ServiceBusConnection:fullyQualifiedNamespace"] + ?? config[$"{connectionStringConfigurationKey}:fullyQualifiedNamespace"]; if (string.IsNullOrWhiteSpace(ns)) { throw new ArgumentNullException(connectionStringConfigurationKey, @@ -81,6 +81,8 @@ public static IServiceCollection AddMassTransitForAzureFunctions(this IServiceCo static bool IsMissingCredentials(string connectionString) { + if (!connectionString.Contains("=")) return true; + var properties = ServiceBusConnectionStringProperties.Parse(connectionString); return (string.IsNullOrWhiteSpace(properties.SharedAccessKeyName) || string.IsNullOrWhiteSpace(properties.SharedAccessKey)) diff --git a/src/Transports/MassTransit.WebJobs.ServiceBusIntegration/MassTransit.WebJobs.ServiceBusIntegration.csproj b/src/Transports/MassTransit.WebJobs.ServiceBusIntegration/MassTransit.WebJobs.ServiceBusIntegration.csproj index c7e172ca884..f66f265652c 100644 --- a/src/Transports/MassTransit.WebJobs.ServiceBusIntegration/MassTransit.WebJobs.ServiceBusIntegration.csproj +++ b/src/Transports/MassTransit.WebJobs.ServiceBusIntegration/MassTransit.WebJobs.ServiceBusIntegration.csproj @@ -2,11 +2,11 @@ - netstandard2.0;netstandard2.1;net6.0 + netstandard2.0;net6.0;net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 @@ -22,11 +22,10 @@ - - - - - - + + + + + diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props index 4a4dc1e9e9b..bde69c73839 100644 --- a/tests/Directory.Build.props +++ b/tests/Directory.Build.props @@ -3,7 +3,6 @@ - 8 4 $(NoWarn),CS0618 true diff --git a/tests/MassTransit.Abstractions.Tests/ExceptionFilter_Specs.cs b/tests/MassTransit.Abstractions.Tests/ExceptionFilter_Specs.cs new file mode 100644 index 00000000000..7bc1e2c0dba --- /dev/null +++ b/tests/MassTransit.Abstractions.Tests/ExceptionFilter_Specs.cs @@ -0,0 +1,69 @@ +namespace MassTransit.Abstractions.Tests; + +using System; +using Configuration; +using NUnit.Framework; + + +[TestFixture] +public class ExceptionFilter_Specs +{ + [Test] + public void Should_match_exception_type() + { + var filter = new Filter(); + + filter.Handle(); + + Assert.That(filter.Match(new Exception()), Is.True); + } + + [Test] + public void Should_match_exception_type_with_filter() + { + var filter = new Filter(); + + filter.Handle(x => x.Message.Contains("conflict")); + + Assert.That(filter.Match(new Exception("There was a conflict")), Is.True); + } + + [Test] + public void Should_not_match_exception_type_with_filter() + { + var filter = new Filter(); + + filter.Handle(x => x.Message.Contains("confusion")); + + Assert.That(filter.Match(new Exception("There was a conflict")), Is.False); + } + + [Test] + public void Should_match_base_exception_type_with_filter() + { + var filter = new Filter(); + + filter.Handle(x => x.Message.Contains("conflict")); + + Assert.That(filter.Match(new ArgumentNullException("There was a conflict")), Is.True); + } + + [Test] + public void Should_include_the_parameter_name_in_the_exception() + { + var exception = new ArgumentNullException("There was a conflict"); + + Assert.That(exception.Message, Does.Contain("conflict")); + } + + + class Filter : + ExceptionSpecification + { + public bool Match(T exception) + where T : Exception + { + return Filter.Match(exception); + } + } +} diff --git a/tests/MassTransit.Abstractions.Tests/MassTransit.Abstractions.Tests.csproj b/tests/MassTransit.Abstractions.Tests/MassTransit.Abstractions.Tests.csproj index d5546c2a95a..650fae4ae95 100644 --- a/tests/MassTransit.Abstractions.Tests/MassTransit.Abstractions.Tests.csproj +++ b/tests/MassTransit.Abstractions.Tests/MassTransit.Abstractions.Tests.csproj @@ -1,18 +1,22 @@  - net6.0 + net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/tests/MassTransit.Abstractions.Tests/NewId/Formatter_Specs.cs b/tests/MassTransit.Abstractions.Tests/NewId/Formatter_Specs.cs index 9a3fa273053..ed963802a1b 100644 --- a/tests/MassTransit.Abstractions.Tests/NewId/Formatter_Specs.cs +++ b/tests/MassTransit.Abstractions.Tests/NewId/Formatter_Specs.cs @@ -7,63 +7,66 @@ using NewIdParsers; using NUnit.Framework; + [TestFixture] public class Using_the_newid_formatters { - private readonly Dictionary _testValues; - public Using_the_newid_formatters() - { - var directory = AppDomain.CurrentDomain.BaseDirectory; - var textsFileName = Path.Combine(directory, "NewId", "texts.txt"); - var fileText = File.ReadAllText(textsFileName); - - _testValues = System.Text.Json.JsonSerializer.Deserialize>(fileText); - } - // Base32 [Test] public void Should_compare_known_conversions_Base32Lower() => CompareKnownEncoding("Base32Lower", new Base32Formatter()); + [Test] public void Should_compare_known_conversions_Base32Upper() => CompareKnownEncoding("Base32Upper", new Base32Formatter(true)); - [Test] - public void Should_compare_known_conversions_CustomBase32() => CompareKnownEncoding("CustomBase32", new Base32Formatter("0123456789ABCDEFGHIJKLMNOPQRSTUV")); - // ZBase32 [Test] - public void Should_compare_known_conversions_ZBase32Lower() => CompareKnownEncoding("ZBase32Lower", new ZBase32Formatter()); - [Test] - public void Should_compare_known_conversions_ZBase32Upper() => CompareKnownEncoding("ZBase32Upper", new ZBase32Formatter(true)); + public void Should_compare_known_conversions_CustomBase32() + { + CompareKnownEncoding("CustomBase32", new Base32Formatter("0123456789ABCDEFGHIJKLMNOPQRSTUV")); + } - // Hex [Test] - public void Should_compare_known_conversions_HexBase16Lower() => CompareKnownEncoding("HexBase16Lower", new HexFormatter()); + public void Should_compare_known_conversions_DashedHexBase16BracketsLower() + { + CompareKnownEncoding("DashedHexBase16BracketsLower", new DashedHexFormatter('{', '}')); + } + [Test] - public void Should_compare_known_conversions_HexBase16Upper() => CompareKnownEncoding("HexBase16Upper", new HexFormatter(true)); + public void Should_compare_known_conversions_DashedHexBase16BracketsUpper() + { + CompareKnownEncoding("DashedHexBase16BracketsUpper", new DashedHexFormatter('{', '}', true)); + } // DashedHex [Test] - public void Should_compare_known_conversions_DashedHexBase16Lower() => CompareKnownEncoding("DashedHexBase16Lower", new DashedHexFormatter()); - [Test] - public void Should_compare_known_conversions_DashedHexBase16Upper() => CompareKnownEncoding("DashedHexBase16Upper", new DashedHexFormatter(upperCase: true)); + public void Should_compare_known_conversions_DashedHexBase16Lower() + { + CompareKnownEncoding("DashedHexBase16Lower", new DashedHexFormatter()); + } + [Test] - public void Should_compare_known_conversions_DashedHexBase16BracketsLower() => CompareKnownEncoding("DashedHexBase16BracketsLower", new DashedHexFormatter('{', '}')); + public void Should_compare_known_conversions_DashedHexBase16Upper() + { + CompareKnownEncoding("DashedHexBase16Upper", new DashedHexFormatter(upperCase: true)); + } + + // Hex [Test] - public void Should_compare_known_conversions_DashedHexBase16BracketsUpper() => CompareKnownEncoding("DashedHexBase16BracketsUpper", new DashedHexFormatter('{', '}', upperCase: true)); + public void Should_compare_known_conversions_HexBase16Lower() => CompareKnownEncoding("HexBase16Lower", new HexFormatter()); + [Test] + public void Should_compare_known_conversions_HexBase16Upper() => CompareKnownEncoding("HexBase16Upper", new HexFormatter(true)); - public void CompareKnownEncoding(string name, INewIdFormatter formatter) + // ZBase32 + [Test] + public void Should_compare_known_conversions_ZBase32Lower() { - var guids = _testValues["Guids"]; - var expectedValues = _testValues[name]; - Assert.AreEqual(guids.Length, expectedValues.Length); + CompareKnownEncoding("ZBase32Lower", new ZBase32Formatter()); + } - for (var i = 0; i < guids.Length; i++) - { - var newId = new NewId(guids[i]); - var text = newId.ToString(formatter); - Assert.AreEqual(expectedValues[i], text); - } - Console.WriteLine("Compared {0} equal conversions", guids.Length); + [Test] + public void Should_compare_known_conversions_ZBase32Upper() + { + CompareKnownEncoding("ZBase32Upper", new ZBase32Formatter(true)); } [Test] @@ -79,7 +82,7 @@ public void Should_convert_back_using_parser() var newId = parser.Parse(ns); - Assert.AreEqual(n, newId); + Assert.That(newId, Is.EqualTo(n)); } [Test] @@ -95,7 +98,7 @@ public void Should_convert_back_using_standard_parser() var newId = parser.Parse(ns); - Assert.AreEqual(n, newId); + Assert.That(newId, Is.EqualTo(n)); } [Test] @@ -107,7 +110,7 @@ public void Should_convert_using_custom_base32_formatting_characters() var ns = n.ToString(formatter); - Assert.AreEqual("UQP7OV4AN129HB4N79GGF8GJ10", ns); + Assert.That(ns, Is.EqualTo("UQP7OV4AN129HB4N79GGF8GJ10")); } [Test] @@ -119,7 +122,7 @@ public void Should_convert_using_standard_base32_formatting_characters() var ns = n.ToString(formatter); - Assert.AreEqual("62ZHY7EKXBCJRLEXHJQQPIQTBA", ns); + Assert.That(ns, Is.EqualTo("62ZHY7EKXBCJRLEXHJQQPIQTBA")); } [Test] @@ -131,7 +134,7 @@ public void Should_convert_using_the_optimized_human_readable_formatter() var ns = n.ToString(formatter); - Assert.AreEqual("6438A9RKZBNJTMRZ8JOOXEOUBY", ns); + Assert.That(ns, Is.EqualTo("6438A9RKZBNJTMRZ8JOOXEOUBY")); } [Test] @@ -145,7 +148,34 @@ public void Should_translate_often_transposed_characters_to_proper_values() var newId = parser.Parse(ns); - Assert.AreEqual(n, newId); + Assert.That(newId, Is.EqualTo(n)); + } + + readonly Dictionary _testValues; + + public Using_the_newid_formatters() + { + var directory = AppDomain.CurrentDomain.BaseDirectory; + var textsFileName = Path.Combine(directory, "NewId", "texts.txt"); + var fileText = File.ReadAllText(textsFileName); + + _testValues = System.Text.Json.JsonSerializer.Deserialize>(fileText); + } + + public void CompareKnownEncoding(string name, INewIdFormatter formatter) + { + var guids = _testValues["Guids"]; + var expectedValues = _testValues[name]; + Assert.That(expectedValues, Has.Length.EqualTo(guids.Length)); + + for (var i = 0; i < guids.Length; i++) + { + var newId = new NewId(guids[i]); + var text = newId.ToString(formatter); + Assert.That(text, Is.EqualTo(expectedValues[i])); + } + + Console.WriteLine("Compared {0} equal conversions", guids.Length); } } } diff --git a/tests/MassTransit.Abstractions.Tests/NewId/Generator_Specs.cs b/tests/MassTransit.Abstractions.Tests/NewId/Generator_Specs.cs index 9104b979121..f6f72cd76c4 100644 --- a/tests/MassTransit.Abstractions.Tests/NewId/Generator_Specs.cs +++ b/tests/MassTransit.Abstractions.Tests/NewId/Generator_Specs.cs @@ -9,67 +9,56 @@ public class When_generating_id { [Test] - public void Should_match_when_all_providers_equal() + public void Should_generate_known_guid() { - // Arrange - var generator1 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); - var generator2 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); - - // Act - var id1 = generator1.Next(); - var id2 = generator2.Next(); - - // Assert - Assert.AreEqual(id1, id2); - } + var expected = Guid.Parse("437f0b01-bf34-7d81-3cf0-74b719ec7596"); + var tickProvider = new MockTickProvider(8410219332513447152); + var networkProvider = new MockNetworkProvider(BitConverter.GetBytes(6857996259202924925)); + var generator = new NewIdGenerator(tickProvider, networkProvider); - [Test] - public void Should_match_when_all_providers_equal_with_guid_method() - { - // Arrange - var generator1 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); - var generator2 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); - generator1.Next().ToGuid(); - generator2.NextGuid(); + for (var i = 0; i < 267; i++) + generator.NextGuid(); - // Act - var id1 = generator1.Next().ToGuid(); - var id2 = generator2.NextGuid(); + var guid = generator.NextGuid(); - // Assert - Assert.AreEqual(id1, id2); + Assert.That(guid, Is.EqualTo(expected)); } [Test] - public void Should_not_match_when_generated_from_two_processes() + public void Should_generate_known_guid_batch() { - // Arrange - var generator1 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); + var exp = new string[] { "74b719ec-7596-3cf0-7d81-bf34437f0b01", "74b719ec-7596-3cf0-7d81-bf34437f0c01", "74b719ec-7596-3cf0-7d81-bf34437f0d01" }; + var tickProvider = new MockTickProvider(8410219332513447152); + var networkProvider = new MockNetworkProvider(BitConverter.GetBytes(6857996259202924925)); + var generator = new NewIdGenerator(tickProvider, networkProvider); - _processIdProvider = new MockProcessIdProvider(BitConverter.GetBytes(11)); - var generator2 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); + for (var i = 0; i < 267; i++) + generator.NextGuid(); - // Act - var id1 = generator1.Next(); - var id2 = generator2.Next(); + var batch = new Guid[3]; + generator.NextSequentialGuid(batch, 0, batch.Length); - // Assert - Assert.AreNotEqual(id1, id2); + for (var i = 0; i < exp.Length; i++) + { + var guid = Guid.Parse(exp[i]); + Assert.That(batch[i], Is.EqualTo(guid)); + } } [Test] - public void Should_not_match_when_processor_id_provided() + public void Should_generate_known_sequential_guid() { - // Arrange - var generator1 = new NewIdGenerator(_tickProvider, _workerIdProvider); - var generator2 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); + var expected = Guid.Parse("74b719ec-7596-3cf0-7d81-bf34437f0b01"); + var tickProvider = new MockTickProvider(8410219332513447152); + var networkProvider = new MockNetworkProvider(BitConverter.GetBytes(6857996259202924925)); + var generator = new NewIdGenerator(tickProvider, networkProvider); - // Act - var id1 = generator1.Next(); - var id2 = generator2.Next(); + for (var i = 0; i < 267; i++) + generator.NextSequentialGuid(); - // Assert - Assert.AreNotEqual(id1, id2); + var guid = generator.NextSequentialGuid(); + + Assert.That(guid, Is.EqualTo(expected)); } [Test] @@ -81,17 +70,19 @@ public void Should_match_sequentially() var id2 = generator.NextGuid(); var id3 = generator.NextGuid(); - Assert.AreNotEqual(id1, id2); - Assert.AreNotEqual(id2, id3); - Assert.Greater(id2, id1); + Assert.Multiple(() => + { + Assert.That(id2, Is.Not.EqualTo(id1)); + Assert.That(id3, Is.Not.EqualTo(id2)); + }); + Assert.That(id2, Is.GreaterThan(id1)); Console.WriteLine(id1); Console.WriteLine(id2); Console.WriteLine(id3); - NewId nid1 = id1.ToNewId(); - NewId nid2 = id2.ToNewId(); - + var nid1 = id1.ToNewId(); + var nid2 = id2.ToNewId(); } [Test] @@ -104,75 +95,85 @@ public void Should_match_sequentially_with_sequential_guid() var id2 = generator.NextSequentialGuid(); var id3 = generator.NextSequentialGuid(); - Assert.AreNotEqual(id1, id2); - Assert.AreNotEqual(id2, id3); - Assert.Greater(id2, id1); + Assert.Multiple(() => + { + Assert.That(id2, Is.Not.EqualTo(id1)); + Assert.That(id3, Is.Not.EqualTo(id2)); + }); + Assert.That(id2, Is.GreaterThan(id1)); Console.WriteLine(id1); Console.WriteLine(id2); Console.WriteLine(id3); - NewId nid1 = id1.ToNewIdFromSequential(); - NewId nid2 = id2.ToNewIdFromSequential(); + var nid1 = id1.ToNewIdFromSequential(); + var nid2 = id2.ToNewIdFromSequential(); - Assert.AreEqual(nid, nid1); + Assert.That(nid1, Is.EqualTo(nid)); } [Test] - public void Should_generate_known_sequential_guid() + public void Should_match_when_all_providers_equal() { - var expected = Guid.Parse("74b719ec-7596-3cf0-7d81-bf34437f0b01"); - var tickProvider = new MockTickProvider(8410219332513447152); - var networkProvider = new MockNetworkProvider(BitConverter.GetBytes(6857996259202924925)); - var generator = new NewIdGenerator(tickProvider, networkProvider); + // Arrange + var generator1 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); + var generator2 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); - for (int i = 0; i < 267; i++) - { - generator.NextSequentialGuid(); - } - var guid = generator.NextSequentialGuid(); + // Act + var id1 = generator1.Next(); + var id2 = generator2.Next(); - Assert.AreEqual(expected, guid); + // Assert + Assert.That(id2, Is.EqualTo(id1)); } [Test] - public void Should_generate_known_guid() + public void Should_match_when_all_providers_equal_with_guid_method() { - var expected = Guid.Parse("437f0b01-bf34-7d81-3cf0-74b719ec7596"); - var tickProvider = new MockTickProvider(8410219332513447152); - var networkProvider = new MockNetworkProvider(BitConverter.GetBytes(6857996259202924925)); - var generator = new NewIdGenerator(tickProvider, networkProvider); + // Arrange + var generator1 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); + var generator2 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); + generator1.Next().ToGuid(); + generator2.NextGuid(); - for (int i = 0; i < 267; i++) - { - generator.NextGuid(); - } - var guid = generator.NextGuid(); + // Act + var id1 = generator1.Next().ToGuid(); + var id2 = generator2.NextGuid(); - Assert.AreEqual(expected, guid); + // Assert + Assert.That(id2, Is.EqualTo(id1)); } [Test] - public void Should_generate_known_guid_batch() + public void Should_not_match_when_generated_from_two_processes() { - var exp = new string[] { "74b719ec-7596-3cf0-7d81-bf34437f0b01", "74b719ec-7596-3cf0-7d81-bf34437f0c01", "74b719ec-7596-3cf0-7d81-bf34437f0d01" }; - var tickProvider = new MockTickProvider(8410219332513447152); - var networkProvider = new MockNetworkProvider(BitConverter.GetBytes(6857996259202924925)); - var generator = new NewIdGenerator(tickProvider, networkProvider); + // Arrange + var generator1 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); - for (int i = 0; i < 267; i++) - { - generator.NextGuid(); - } + _processIdProvider = new MockProcessIdProvider(BitConverter.GetBytes(11)); + var generator2 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); - var batch = new Guid[3]; - generator.NextSequentialGuid(batch, 0, batch.Length); + // Act + var id1 = generator1.Next(); + var id2 = generator2.Next(); - for (int i = 0; i < exp.Length; i++) - { - var guid = Guid.Parse(exp[i]); - Assert.AreEqual(guid, batch[i]); - } + // Assert + Assert.That(id2, Is.Not.EqualTo(id1)); + } + + [Test] + public void Should_not_match_when_processor_id_provided() + { + // Arrange + var generator1 = new NewIdGenerator(_tickProvider, _workerIdProvider); + var generator2 = new NewIdGenerator(_tickProvider, _workerIdProvider, _processIdProvider); + + // Act + var id1 = generator1.Next(); + var id2 = generator2.Next(); + + // Assert + Assert.That(id2, Is.Not.EqualTo(id1)); } [SetUp] diff --git a/tests/MassTransit.Abstractions.Tests/NewId/GuidInterop_Specs.cs b/tests/MassTransit.Abstractions.Tests/NewId/GuidInterop_Specs.cs index ee64390e812..ed234399ea6 100644 --- a/tests/MassTransit.Abstractions.Tests/NewId/GuidInterop_Specs.cs +++ b/tests/MassTransit.Abstractions.Tests/NewId/GuidInterop_Specs.cs @@ -17,7 +17,7 @@ public void Should_convert_from_a_guid_quickly() var ns = n.ToString(); var gs = g.ToString(); - Assert.AreEqual(ns, gs); + Assert.That(gs, Is.EqualTo(ns)); } [Test] @@ -30,7 +30,7 @@ public void Should_convert_to_guid_quickly() var ns = n.ToString(); var gs = g.ToString(); - Assert.AreEqual(ns, gs); + Assert.That(gs, Is.EqualTo(ns)); } [Test] @@ -41,7 +41,7 @@ public void Should_convert_to_guid_quickly_from_guid() var ns = g.ToNewId().ToString(); var gs = g.ToString(); - Assert.AreEqual(ns, gs); + Assert.That(gs, Is.EqualTo(ns)); } [Test] @@ -61,7 +61,7 @@ public void Should_make_the_round_trip_successfully_via_bytes() var gn = new Guid(n.ToByteArray()); - Assert.AreEqual(g, gn); + Assert.That(gn, Is.EqualTo(g)); } [Test] @@ -73,7 +73,7 @@ public void Should_make_the_round_trip_successfully_via_guid() var gn = n.ToGuid(); - Assert.AreEqual(g, gn); + Assert.That(gn, Is.EqualTo(g)); } [Test] @@ -85,125 +85,159 @@ public void Should_make_the_round_trip_successfully_via_sequential_guid() var gn = n.ToSequentialGuid(); - Assert.AreEqual(g, gn); + Assert.That(gn, Is.EqualTo(g)); } [Test] - public void Should_match_string_output_b() + public void Should_match_parsed_guid_bs() { - var bytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; - var g = new Guid(bytes); var n = new NewId(bytes); - var gs = g.ToString("B"); - var ns = n.ToString("B"); + var ns = n.ToString("Bs"); + + var gsn = Guid.Parse(ns); + var g = n.ToSequentialGuid(); - Assert.AreEqual(gs, ns); + Assert.That(gsn, Is.EqualTo(g)); } [Test] - public void Should_match_string_output_d() + public void Should_match_parsed_guid_ds() { - var bytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; - var g = new Guid(bytes); var n = new NewId(bytes); - var gs = g.ToString("d"); - var ns = n.ToString("d"); + var ns = n.ToString("Ds"); + + var gsn = Guid.Parse(ns); + var g = n.ToSequentialGuid(); - Assert.AreEqual(gs, ns); + Assert.Multiple(() => + { + Assert.That(gsn, Is.EqualTo(g)); + Assert.That(n.ToGuid(), Is.Not.EqualTo(gsn)); + }); } [Test] - public void Should_match_string_output_n() + public void Should_match_parsed_guid_ns() { - var bytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; - var g = new Guid(bytes); var n = new NewId(bytes); - var gs = g.ToString("N"); - var ns = n.ToString("N"); + var ns = n.ToString("Ns"); - Assert.AreEqual(gs, ns); + var gsn = Guid.Parse(ns); + var g = n.ToSequentialGuid(); + + Assert.That(gsn, Is.EqualTo(g)); } [Test] - public void Should_match_string_output_p() + public void Should_match_parsed_guid_ps() { - var bytes = new byte[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15}; + var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; - var g = new Guid(bytes); var n = new NewId(bytes); - var gs = g.ToString("P"); - var ns = n.ToString("P"); + var ns = n.ToString("Ps"); - Assert.AreEqual(gs, ns); + var gsn = Guid.Parse(ns); + var g = n.ToSequentialGuid(); + + Assert.That(gsn, Is.EqualTo(g)); } [Test] - public void Should_match_parsed_guid_bs() + public void Should_match_string_output_b() { var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + var g = new Guid(bytes); var n = new NewId(bytes); - var ns = n.ToString("Bs"); - - var gsn = Guid.Parse(ns); - var g = n.ToSequentialGuid(); + var gs = g.ToString("B"); + var ns = n.ToString("B"); - Assert.AreEqual(g, gsn); + Assert.That(ns, Is.EqualTo(gs)); } [Test] - public void Should_match_parsed_guid_ds() + public void Should_match_string_output_d() { var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + var g = new Guid(bytes); var n = new NewId(bytes); - var ns = n.ToString("Ds"); - - var gsn = Guid.Parse(ns); - var g = n.ToSequentialGuid(); + var gs = g.ToString("d"); + var ns = n.ToString("d"); - Assert.AreEqual(g, gsn); - Assert.AreNotEqual(gsn, n.ToGuid()); + Assert.That(ns, Is.EqualTo(gs)); } [Test] - public void Should_match_parsed_guid_ns() + public void Should_match_string_output_n() { var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + var g = new Guid(bytes); var n = new NewId(bytes); - var ns = n.ToString("Ns"); - - var gsn = Guid.Parse(ns); - var g = n.ToSequentialGuid(); + var gs = g.ToString("N"); + var ns = n.ToString("N"); - Assert.AreEqual(g, gsn); + Assert.That(ns, Is.EqualTo(gs)); } [Test] - public void Should_match_parsed_guid_ps() + public void Should_match_string_output_p() { var bytes = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 }; + var g = new Guid(bytes); var n = new NewId(bytes); - var ns = n.ToString("Ps"); + var gs = g.ToString("P"); + var ns = n.ToString("P"); - var gsn = Guid.Parse(ns); + Assert.That(ns, Is.EqualTo(gs)); + } + + [Test] + public void Should_parse_newid_guid_as_newid() + { + var n = NewId.Next(2)[1]; + var g = n.ToGuid(); + + var ng = NewId.FromGuid(g); + + Assert.That(ng, Is.EqualTo(n)); + + // Also checks to see if this would throw + Assert.That(ng.Timestamp, Is.Not.EqualTo(default)); + } + + [Test] + public void Should_parse_sequential_guid_as_newid() + { + var n = NewId.Next(2)[1]; + + var nn = n.ToGuid(); var g = n.ToSequentialGuid(); - Assert.AreEqual(g, gsn); + var ng = NewId.FromSequentialGuid(g); + + Assert.That(ng, Is.EqualTo(n)); + + // Also checks to see if this would throw + Assert.That(ng.Timestamp, Is.Not.EqualTo(default)); } + [Test] public void Should_properly_handle_string_passthrough() { @@ -215,7 +249,7 @@ public void Should_properly_handle_string_passthrough() var nn = new NewId(g.ToString("D")); - Assert.AreEqual(n, nn); + Assert.That(nn, Is.EqualTo(n)); } [Test] @@ -225,7 +259,7 @@ public void Should_support_the_same_constructor() var guid = new Guid(0x01020304, 0x0506, 0x0708, 9, 10, 11, 12, 13, 14, 15, 16); var newid = new NewId(0x01020304, 0x0506, 0x0708, 9, 10, 11, 12, 13, 14, 15, 16); - Assert.AreEqual(guid.ToString(), newid.ToString()); + Assert.That(newid.ToString(), Is.EqualTo(guid.ToString())); } [Test] @@ -239,38 +273,7 @@ public void Should_work_from_newid_to_guid_to_newid() Console.WriteLine(g.ToString("D")); - Assert.AreEqual(n, ng); - } - - - [Test] - public void Should_parse_newid_guid_as_newid() - { - NewId n = NewId.Next(2)[1]; - var g = n.ToGuid(); - - var ng = NewId.FromGuid(g); - - Assert.AreEqual(n, ng); - - // Also checks to see if this would throw - Assert.IsTrue(ng.Timestamp != default); - } - - [Test] - public void Should_parse_sequential_guid_as_newid() - { - NewId n = NewId.Next(2)[1]; - - var nn = n.ToGuid(); - var g = n.ToSequentialGuid(); - - var ng = NewId.FromSequentialGuid(g); - - Assert.AreEqual(n, ng); - - // Also checks to see if this would throw - Assert.IsTrue(ng.Timestamp != default); + Assert.That(ng, Is.EqualTo(n)); } } } diff --git a/tests/MassTransit.Abstractions.Tests/NewId/LongTerm_Specs.cs b/tests/MassTransit.Abstractions.Tests/NewId/LongTerm_Specs.cs index a11df6ba1b7..2987811efcb 100644 --- a/tests/MassTransit.Abstractions.Tests/NewId/LongTerm_Specs.cs +++ b/tests/MassTransit.Abstractions.Tests/NewId/LongTerm_Specs.cs @@ -23,11 +23,11 @@ public void Should_keep_them_ordered_for_sql_server() for (var i = 0; i < limit - 1; i++) { - Assert.AreNotEqual(ids[i], ids[i + 1]); + Assert.That(ids[i + 1], Is.Not.EqualTo(ids[i])); SqlGuid left = ids[i].ToGuid(); SqlGuid right = ids[i + 1].ToGuid(); - Assert.Less(left, right); + Assert.That(left, Is.LessThan(right)); if (i % 128 == 0) Console.WriteLine("Normal: {0} Sql: {1}", left, ids[i].ToSequentialGuid()); } diff --git a/tests/MassTransit.Abstractions.Tests/NewId/NetworkAddress_Specs.cs b/tests/MassTransit.Abstractions.Tests/NewId/NetworkAddress_Specs.cs index a2db4610256..5731888f086 100644 --- a/tests/MassTransit.Abstractions.Tests/NewId/NetworkAddress_Specs.cs +++ b/tests/MassTransit.Abstractions.Tests/NewId/NetworkAddress_Specs.cs @@ -30,8 +30,8 @@ public void Should_pull_the_network_adapter_mac_address() var networkId = networkIdProvider.GetWorkerId(0); - Assert.IsNotNull(networkId); - Assert.AreEqual(6, networkId.Length); + Assert.That(networkId, Is.Not.Null); + Assert.That(networkId, Has.Length.EqualTo(6)); } [Test] @@ -41,8 +41,8 @@ public void Should_pull_using_host_name() var networkId = networkIdProvider.GetWorkerId(0); - Assert.IsNotNull(networkId); - Assert.AreEqual(6, networkId.Length); + Assert.That(networkId, Is.Not.Null); + Assert.That(networkId, Has.Length.EqualTo(6)); } [Test] diff --git a/tests/MassTransit.Abstractions.Tests/NewId/NewId_Specs.cs b/tests/MassTransit.Abstractions.Tests/NewId/NewId_Specs.cs index 26a7b289e5c..fbcf9c8450d 100644 --- a/tests/MassTransit.Abstractions.Tests/NewId/NewId_Specs.cs +++ b/tests/MassTransit.Abstractions.Tests/NewId/NewId_Specs.cs @@ -17,7 +17,7 @@ public void Should_be_able_to_determine_equal_ids() var id1 = new NewId("fc070000-9565-3668-e000-08d5893343c6"); var id2 = new NewId("fc070000-9565-3668-e000-08d5893343c6"); - Assert.IsTrue(id1 == id2); + Assert.That(id1, Is.EqualTo(id2)); } [Test] @@ -26,7 +26,7 @@ public void Should_be_able_to_determine_greater_id() var lowerId = new NewId("fc070000-9565-3668-e000-08d5893343c6"); var greaterId = new NewId("fc070000-9565-3668-9180-08d589338b38"); - Assert.IsTrue(lowerId < greaterId); + Assert.That(lowerId, Is.LessThan(greaterId)); } [Test] @@ -35,7 +35,7 @@ public void Should_be_able_to_determine_lower_id() var lowerId = new NewId("fc070000-9565-3668-e000-08d5893343c6"); var greaterId = new NewId("fc070000-9565-3668-9180-08d589338b38"); - Assert.IsFalse(lowerId > greaterId); + Assert.That(lowerId, Is.LessThanOrEqualTo(greaterId)); } [Test] @@ -53,7 +53,7 @@ public void Should_be_able_to_extract_timestamp() if (difference < TimeSpan.Zero) difference = difference.Negate(); - Assert.LessOrEqual(difference, TimeSpan.FromMinutes(1)); + Assert.That(difference, Is.LessThanOrEqualTo(TimeSpan.FromMinutes(1))); } [Test] @@ -72,7 +72,7 @@ public void Should_be_able_to_extract_timestamp_with_process_id() if (difference < TimeSpan.Zero) difference = difference.Negate(); - Assert.LessOrEqual(difference, TimeSpan.FromMinutes(1)); + Assert.That(difference, Is.LessThanOrEqualTo(TimeSpan.FromMinutes(1))); } [Test] @@ -161,7 +161,7 @@ public void Should_generate_sequential_ids_quickly() for (var i = 0; i < limit - 1; i++) { - Assert.AreNotEqual(ids[i], ids[i + 1]); + Assert.That(ids[i + 1], Is.Not.EqualTo(ids[i])); Console.WriteLine(ids[i]); } } @@ -184,7 +184,7 @@ public void Should_generate_unique_identifiers_with_each_invocation() for (var i = 0; i < limit - 1; i++) { - Assert.AreNotEqual(ids[i], ids[i + 1]); + Assert.That(ids[i + 1], Is.Not.EqualTo(ids[i])); var end = ids[i].ToString().Substring(32, 4); if (end == "0000") Console.WriteLine("{0}", ids[i].ToString()); diff --git a/tests/MassTransit.Abstractions.Tests/NewId/Order_Specs.cs b/tests/MassTransit.Abstractions.Tests/NewId/Order_Specs.cs index f92ab35658f..76cf0489c74 100644 --- a/tests/MassTransit.Abstractions.Tests/NewId/Order_Specs.cs +++ b/tests/MassTransit.Abstractions.Tests/NewId/Order_Specs.cs @@ -22,11 +22,11 @@ public void Should_keep_them_ordered_for_sql_server_when_using_array_call() for (var i = 0; i < limit - 1; i++) { - Assert.AreNotEqual(ids[i], ids[i + 1]); + Assert.That(ids[i + 1], Is.Not.EqualTo(ids[i])); SqlGuid left = ids[i].ToGuid(); SqlGuid right = ids[i + 1].ToGuid(); - Assert.Less(left, right); + Assert.That(left, Is.LessThan(right)); if (i % 128 == 0) Console.WriteLine("Normal: {0} Sql: {1}", left, ids[i].ToSequentialGuid()); } @@ -45,11 +45,11 @@ public void Should_keep_them_ordered_for_ToSequentialGuid_when_using_array_call( for (var i = 0; i < limit - 1; i++) { - Assert.AreNotEqual(ids[i], ids[i + 1]); + Assert.That(ids[i + 1], Is.Not.EqualTo(ids[i])); var left = ids[i].ToSequentialGuid(); var right = ids[i + 1].ToSequentialGuid(); - Assert.Less(left, right); + Assert.That(left, Is.LessThan(right)); if (i % 128 == 0) Console.WriteLine("Sql: {0}", left); } diff --git a/tests/MassTransit.Abstractions.Tests/NewId/Usage_Specs.cs b/tests/MassTransit.Abstractions.Tests/NewId/Usage_Specs.cs index 942f1bdbfd7..d0cb26ecb65 100644 --- a/tests/MassTransit.Abstractions.Tests/NewId/Usage_Specs.cs +++ b/tests/MassTransit.Abstractions.Tests/NewId/Usage_Specs.cs @@ -12,7 +12,7 @@ public void Should_format_just_like_a_default_guid_formatter() { var newId = new NewId(); - Assert.AreEqual("00000000-0000-0000-0000-000000000000", newId.ToString()); + Assert.That(newId.ToString(), Is.EqualTo("00000000-0000-0000-0000-000000000000")); } [Test] @@ -20,7 +20,7 @@ public void Should_format_just_like_a_fancy_guid_formatter() { var newId = new NewId(); - Assert.AreEqual("{00000000-0000-0000-0000-000000000000}", newId.ToString("B")); + Assert.That(newId.ToString("B"), Is.EqualTo("{00000000-0000-0000-0000-000000000000}")); } [Test] @@ -28,7 +28,7 @@ public void Should_format_just_like_a_narrow_guid_formatter() { var newId = new NewId(); - Assert.AreEqual("00000000000000000000000000000000", newId.ToString("N")); + Assert.That(newId.ToString("N"), Is.EqualTo("00000000000000000000000000000000")); } [Test] @@ -36,7 +36,7 @@ public void Should_format_just_like_a_parenthesis_guid_formatter() { var newId = new NewId(); - Assert.AreEqual("(00000000-0000-0000-0000-000000000000)", newId.ToString("P")); + Assert.That(newId.ToString("P"), Is.EqualTo("(00000000-0000-0000-0000-000000000000)")); } [Test] @@ -49,7 +49,7 @@ public void Should_work_from_guid_to_newid_to_guid() var gs = g.ToString("d"); var ns = n.ToString("d"); - Assert.AreEqual(gs, ns); + Assert.That(ns, Is.EqualTo(gs)); } } } diff --git a/tests/MassTransit.Abstractions.Tests/TypeExtensions_Specs.cs b/tests/MassTransit.Abstractions.Tests/TypeExtensions_Specs.cs index 36223570cb1..f2f25b08b47 100644 --- a/tests/MassTransit.Abstractions.Tests/TypeExtensions_Specs.cs +++ b/tests/MassTransit.Abstractions.Tests/TypeExtensions_Specs.cs @@ -13,15 +13,15 @@ public class Using_the_type_extensions [Test] public void Should_accept_multiple_this_operators() { - var typeInfo = typeof(TypeWithMultipleThisOperators).GetTypeInfo(); - Assert.That(() => typeInfo.GetAllProperties().ToList(), Throws.Nothing); + var type = typeof(TypeWithMultipleThisOperators); + Assert.That(() => type.GetAllProperties().ToList(), Throws.Nothing); } [Test] public void Should_accept_multiple_this_operators_subclass() { - var typeInfo = typeof(SubclassWithThisOperator).GetTypeInfo(); - Assert.That(() => typeInfo.GetAllProperties().ToList(), Throws.Nothing); + var type = typeof(SubclassWithThisOperator); + Assert.That(() => type.GetAllProperties().ToList(), Throws.Nothing); } } diff --git a/tests/MassTransit.Abstractions.Tests/Usage/OrderDeliverySaga.cs b/tests/MassTransit.Abstractions.Tests/Usage/OrderDeliverySaga.cs index d21f1d3fb66..253333d123d 100644 --- a/tests/MassTransit.Abstractions.Tests/Usage/OrderDeliverySaga.cs +++ b/tests/MassTransit.Abstractions.Tests/Usage/OrderDeliverySaga.cs @@ -25,7 +25,8 @@ public async Task Consume(ConsumeContext context) public class OrderDeliverySagaDefinition : SagaDefinition { - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) { } } diff --git a/tests/MassTransit.Abstractions.Tests/Usage/ProcessOrderActivity.cs b/tests/MassTransit.Abstractions.Tests/Usage/ProcessOrderActivity.cs index a57c2ef0043..9bfc983d206 100644 --- a/tests/MassTransit.Abstractions.Tests/Usage/ProcessOrderActivity.cs +++ b/tests/MassTransit.Abstractions.Tests/Usage/ProcessOrderActivity.cs @@ -31,12 +31,14 @@ public ProcessOrderActivityDefinition() } protected override void ConfigureExecuteActivity(IReceiveEndpointConfigurator endpointConfigurator, - IExecuteActivityConfigurator executeActivityConfigurator) + IExecuteActivityConfigurator executeActivityConfigurator, + IRegistrationContext context) { } protected override void ConfigureCompensateActivity(IReceiveEndpointConfigurator endpointConfigurator, - ICompensateActivityConfigurator compensateActivityConfigurator) + ICompensateActivityConfigurator compensateActivityConfigurator, + IRegistrationContext context) { } } diff --git a/tests/MassTransit.Abstractions.Tests/Usage/SubmitOrderConsumer.cs b/tests/MassTransit.Abstractions.Tests/Usage/SubmitOrderConsumer.cs index 7e4b4c89f28..579ed2c660f 100644 --- a/tests/MassTransit.Abstractions.Tests/Usage/SubmitOrderConsumer.cs +++ b/tests/MassTransit.Abstractions.Tests/Usage/SubmitOrderConsumer.cs @@ -33,7 +33,8 @@ public SubmitOrderConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } diff --git a/tests/MassTransit.ActiveMqTransport.Tests/Configure_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/Configure_Specs.cs index 4248cba7c3f..8d77c93054a 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/Configure_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/Configure_Specs.cs @@ -6,9 +6,9 @@ namespace MassTransit.ActiveMqTransport.Tests using System.Threading.Tasks; using Configuration; using Internals; - using MassTransit.Testing; using NUnit.Framework; using TestFramework.Messages; + using Testing; using Topology; using Util; @@ -111,11 +111,12 @@ public void Failover_should_take_precendence_in_uri_construction() var settings = new ConfigurationHostSettings(new Uri("activemq://fake-host")) { Port = 61616, - FailoverHosts = new[] { "failover1", "failover2" } + FailoverHosts = new[] { "failover1", "failover2" }, }; + settings.TransportOptions.Add("reconnectAttempts", "-1"); Assert.That(settings.BrokerAddress, Is.EqualTo(new Uri( - "activemq:failover:(tcp://failover1:61616/,tcp://failover2:61616/)?wireFormat.tightEncodingEnabled=true&nms.AsyncSend=true"))); + "activemq:failover:(tcp://failover1:61616/?wireFormat.tightEncodingEnabled=true,tcp://failover2:61616/?wireFormat.tightEncodingEnabled=true)?transport.reconnectAttempts=-1"))); } [Test] diff --git a/tests/MassTransit.ActiveMqTransport.Tests/ConsumerEntity_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/ConsumerEntity_Specs.cs new file mode 100644 index 00000000000..88835330985 --- /dev/null +++ b/tests/MassTransit.ActiveMqTransport.Tests/ConsumerEntity_Specs.cs @@ -0,0 +1,61 @@ +namespace MassTransit.ActiveMqTransport.Tests +{ + using System.Collections; + using NUnit.Framework; + using Topology; + + + [TestFixture] + public class ConsumerEntity_Specs + { + [Test] + [TestCaseSource(nameof(CreateCasesForEntityComparerEquals))] + public bool ConsumerEntityComparer_Equal_should_return_expected(ConsumerEntity left, ConsumerEntity right) + { + // Act + return ConsumerEntity.EntityComparer.Equals(left, right); + } + + [Test] + [TestCaseSource(nameof(CreateCasesForNameComparerEquals))] + public bool NameComparer_Equal_should_return_expected(ConsumerEntity left, ConsumerEntity right) + { + // Act + return ConsumerEntity.NameComparer.Equals(left, right); + } + + public static IEnumerable CreateCasesForEntityComparerEquals() + { + var topic = new TopicEntity(1, "topic", false, true); + var queue = new QueueEntity(1, "queue", false, true); + + yield return new TestCaseData(new ConsumerEntity(1, topic, queue, "selector"), new ConsumerEntity(1, topic, queue, "selector")).Returns(true); + yield return new TestCaseData(new ConsumerEntity(1, topic, queue, "selector"), null).Returns(false); + yield return new TestCaseData(null, new ConsumerEntity(1, topic, queue, "selector")).Returns(false); + yield return new TestCaseData(new ConsumerEntity(1, topic, null, "selector"), new ConsumerEntity(1, topic, queue, "selector")).Returns(false); + yield return new TestCaseData(new ConsumerEntity(1, topic, queue, "selector"), new ConsumerEntity(1, topic, null, "selector")).Returns(false); + yield return new TestCaseData(new ConsumerEntity(1, topic, null, "selector"), new ConsumerEntity(1, topic, null, "selector")).Returns(true); + yield return new TestCaseData(null, null).Returns(true); + } + + public static IEnumerable CreateCasesForNameComparerEquals() + { + var leftTopic = new TopicEntity(1, "topic", false, true); + var leftQueue = new QueueEntity(1, "queue", false, true); + var rightTopic = new TopicEntity(1, "topic", false, true); + var rightQueue = new QueueEntity(1, "queue", false, true); + + yield return new TestCaseData(new ConsumerEntity(1, leftTopic, leftQueue, "selector"), new ConsumerEntity(1, rightTopic, rightQueue, "selector")) + .Returns(true); + yield return new TestCaseData(new ConsumerEntity(1, leftTopic, leftQueue, "selector"), null).Returns(false); + yield return new TestCaseData(null, new ConsumerEntity(1, rightTopic, rightQueue, "selector")).Returns(false); + yield return new TestCaseData(new ConsumerEntity(1, leftTopic, null, "selector"), new ConsumerEntity(1, rightTopic, rightQueue, "selector")) + .Returns(false); + yield return new TestCaseData(new ConsumerEntity(1, leftTopic, leftQueue, "selector"), new ConsumerEntity(1, rightTopic, null, "selector")) + .Returns(false); + yield return new TestCaseData(new ConsumerEntity(1, leftTopic, null, "selector"), new ConsumerEntity(1, rightTopic, null, "selector")) + .Returns(true); + yield return new TestCaseData(null, null).Returns(true); + } + } +} diff --git a/tests/MassTransit.ActiveMqTransport.Tests/DelayRetry_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/DelayRetry_Specs.cs index 5d2385e3bca..28f44baf5b9 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/DelayRetry_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/DelayRetry_Specs.cs @@ -21,7 +21,7 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _received.Task; - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); } TaskCompletionSource> _received; @@ -131,7 +131,7 @@ protected override void ConfigureActiveMqReceiveEndpoint(IActiveMqReceiveEndpoin Interlocked.Increment(ref _count); throw new IntentionalTestException(); - }, x => x.UseRetry(r => r.Intervals(100, 200))); + }, x => x.UseMessageRetry(r => r.Intervals(100, 200))); } } @@ -156,8 +156,11 @@ public async Task Should_retry_each_message_type() ConsumeContext> pingFaultContext = await pingFault; ConsumeContext> pongFaultContext = await pongFault; - Assert.That(_consumer.PingCount, Is.EqualTo(3)); - Assert.That(_consumer.PongCount, Is.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(_consumer.PingCount, Is.EqualTo(3)); + Assert.That(_consumer.PongCount, Is.EqualTo(3)); + }); } Consumer _consumer; @@ -212,8 +215,6 @@ public class TwoMessage [TestFixture] public class Delayed_redelivery { - Consumer _consumer; - [Test] [Category("Flaky")] [TestCase("activemq")] @@ -275,12 +276,17 @@ public async Task Should_properly_redeliver(string flavor) ConsumeContext> pingFaultContext = await pingFault; ConsumeContext> pongFaultContext = await pongFault; - Assert.That(_consumer.PingCount, Is.EqualTo(3)); - Assert.That(_consumer.PongCount, Is.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(_consumer.PingCount, Is.EqualTo(3)); + Assert.That(_consumer.PongCount, Is.EqualTo(3)); + }); await busControl.StopAsync(); } + Consumer _consumer; + public Task> SubscribeHandler(IBus bus, Func, bool> filter) where T : class { @@ -438,7 +444,7 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _received.Task; - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); } TaskCompletionSource> _received; @@ -490,8 +496,11 @@ public async Task Should_execute_callback_during_defer_the_message_delivery() ConsumeContext context = await _received.Task; - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); - Assert.IsTrue(_hit); + Assert.Multiple(() => + { + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); + Assert.That(_hit, Is.True); + }); } TaskCompletionSource> _received; diff --git a/tests/MassTransit.ActiveMqTransport.Tests/EndpointConfiguration_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/EndpointConfiguration_Specs.cs index eb3a5bd83a8..796cc03773e 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/EndpointConfiguration_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/EndpointConfiguration_Specs.cs @@ -42,9 +42,12 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_overrid var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -73,9 +76,12 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_specifi var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -104,9 +110,12 @@ public async Task Should_include_concurrency_filter_if_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -135,8 +144,11 @@ public async Task Should_include_nothing_if_not_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -157,8 +169,11 @@ public void Should_override_bus_setting_if_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); } [Test] @@ -190,8 +205,11 @@ public void Should_use_bus_setting_if_not_specified() var probe = JObject.Parse(busControl.GetProbeResult().ToJsonString()); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); } @@ -204,7 +222,8 @@ public PingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -223,7 +242,8 @@ public EndpointPingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -233,7 +253,8 @@ class EmptyPingConsumerDefinition : ConsumerDefinition { protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } diff --git a/tests/MassTransit.ActiveMqTransport.Tests/InMemoryOutboxRedelivery_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/InMemoryOutboxRedelivery_Specs.cs index 94fc8a3e0fb..9f2412c3108 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/InMemoryOutboxRedelivery_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/InMemoryOutboxRedelivery_Specs.cs @@ -181,7 +181,7 @@ protected override void ConfigureActiveMqBus(IActiveMqBusFactoryConfigurator con protected override void ConfigureActiveMqReceiveEndpoint(IActiveMqReceiveEndpointConfigurator configurator) { configurator.UseDelayedRedelivery(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); - configurator.UseRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); + configurator.UseMessageRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); configurator.UseInMemoryOutbox(); configurator.Consumer(); diff --git a/tests/MassTransit.ActiveMqTransport.Tests/InvalidMessage_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/InvalidMessage_Specs.cs index b9313f7b219..7d49ad2ae27 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/InvalidMessage_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/InvalidMessage_Specs.cs @@ -43,9 +43,9 @@ async Task ProduceInvalidMessage() message.NMSCorrelationID = "AB76E632-8550-49B9-A119-BBEB84D53355"; - producer.Send(message); + await producer.SendAsync(message); - producer.Close(); + await producer.CloseAsync(); session.Close(); } finally diff --git a/tests/MassTransit.ActiveMqTransport.Tests/JobConsumer_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/JobConsumer_Specs.cs new file mode 100644 index 00000000000..31e5963b107 --- /dev/null +++ b/tests/MassTransit.ActiveMqTransport.Tests/JobConsumer_Specs.cs @@ -0,0 +1,354 @@ +namespace MassTransit.ActiveMqTransport.Tests +{ + using System; + using System.Threading.Tasks; + using Contracts.JobService; + using JobConsumerTests; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + namespace JobConsumerTests + { + using System; + using System.Threading.Tasks; + using Contracts.JobService; + + + public interface OddJob + { + TimeSpan Duration { get; } + } + + + public class OddJobConsumer : + IJobConsumer + { + public async Task Run(JobContext context) + { + if (context.RetryAttempt == 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + } + } + + + public class OddJobCompletedConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + return Task.CompletedTask; + } + } + } + + + [TestFixture] + public class JobConsumer_Specs + { + [Test] + public async Task Should_cancel_the_job() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + Assert.That(await harness.Published.Any(), Is.True); + + await harness.Stop(); + } + + [Test] + public async Task Should_cancel_the_job_and_get_the_status() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + IRequestClient stateClient = harness.GetRequestClient(); + + Response jobState = await stateClient.GetResponse(new { JobId = jobId }); + + Assert.That(jobState.Message.CurrentState, Is.EqualTo("Started")); + + await harness.Bus.CancelJob(jobId); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Sent.Any(), Is.True); + }); + + jobState = await stateClient.GetResponse(new { JobId = jobId }); + + Assert.That(jobState.Message.CurrentState, Is.EqualTo("Canceled")); + + await harness.Stop(); + } + + [Test] + public async Task Should_cancel_the_job_and_retry_it() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Sent.Any(), Is.True); + }); + + await harness.Bus.RetryJob(jobId); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Stop(); + } + + [Test] + public async Task Should_cancel_the_job_while_waiting() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + await harness.Start(); + + var previousJobId = NewId.NextGuid(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = previousJobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + + response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Sent.Any(x => x.Context.Message.JobId == jobId), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + + await harness.Bus.CancelJob(previousJobId); + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + + await harness.Stop(); + } + + [Test] + public async Task Should_complete_the_job() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + await harness.Start(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(1) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Stop(); + } + + [Test] + public async Task Should_create_a_unique_job_id() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + await harness.Start(); + + await harness.Bus.Publish(new { Duration = TimeSpan.FromSeconds(1) }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + IPublishedMessage publishedMessage = await harness.Published.SelectAsync().First(); + Assert.That(publishedMessage.Context.Message.JobId, Is.Not.EqualTo(Guid.Empty)); + + await harness.Stop(); + } + + [Test] + public async Task Should_return_not_found() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + var jobId = NewId.NextGuid(); + + IRequestClient stateClient = harness.GetRequestClient(); + + var jobState = await stateClient.GetJobState(jobId); + + Assert.That(jobState.CurrentState, Is.EqualTo("NotFound")); + + await harness.Stop(); + } + + static ServiceProvider SetupServiceCollection() + { + var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); + + x.AddJobSagaStateMachines(); + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.UsingActiveMq((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + return provider; + } + } +} diff --git a/tests/MassTransit.ActiveMqTransport.Tests/KillSwitch_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/KillSwitch_Specs.cs index 5c6b7de98c1..301afc546f6 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/KillSwitch_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/KillSwitch_Specs.cs @@ -20,11 +20,14 @@ public async Task Should_be_degraded_after_too_many_exceptions() await Task.WhenAll(Enumerable.Range(0, 11).Select(x => Bus.Publish(new BadMessage()))); - Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Degraded, TimeSpan.FromSeconds(15)), Is.EqualTo(BusHealthStatus.Degraded)); + await Assert.MultipleAsync(async () => + { + Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Degraded, TimeSpan.FromSeconds(15)), Is.EqualTo(BusHealthStatus.Degraded)); - Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Healthy, TimeSpan.FromSeconds(10)), Is.EqualTo(BusHealthStatus.Healthy)); + Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Healthy, TimeSpan.FromSeconds(10)), Is.EqualTo(BusHealthStatus.Healthy)); - Assert.That(await ActiveMqTestHarness.Consumed.SelectAsync().Take(11).Count(), Is.EqualTo(11)); + Assert.That(await ActiveMqTestHarness.Consumed.SelectAsync().Take(11).Count(), Is.EqualTo(11)); + }); await Task.WhenAll(Enumerable.Range(0, 20).Select(x => Bus.Publish(new GoodMessage()))); diff --git a/tests/MassTransit.ActiveMqTransport.Tests/MassTransit.ActiveMqTransport.Tests.csproj b/tests/MassTransit.ActiveMqTransport.Tests/MassTransit.ActiveMqTransport.Tests.csproj index 784e7a222d7..2f226b98c0e 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/MassTransit.ActiveMqTransport.Tests.csproj +++ b/tests/MassTransit.ActiveMqTransport.Tests/MassTransit.ActiveMqTransport.Tests.csproj @@ -1,11 +1,7 @@  - net6.0 - - - - $(TargetFrameworks);net462 + net8.0 @@ -13,8 +9,13 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/tests/MassTransit.ActiveMqTransport.Tests/OpenTelemetry_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/OpenTelemetry_Specs.cs index c920963a4dd..0b03200f138 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/OpenTelemetry_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/OpenTelemetry_Specs.cs @@ -1,9 +1,9 @@ namespace MassTransit.ActiveMqTransport.Tests { - using System; using System.Diagnostics; using System.Threading.Tasks; using HarnessContracts; + using Logging; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using OpenTelemetry; @@ -14,6 +14,9 @@ namespace MassTransit.ActiveMqTransport.Tests namespace HarnessContracts { + using System; + + public interface SubmitOrder { Guid OrderId { get; } @@ -36,32 +39,30 @@ public class OpenTelemetry_Specs [Test] public async Task Should_report_telemetry_to_jaeger() { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("order-api")) - .AddSource("MassTransit") - .AddJaegerExporter(o => - { - o.AgentHost = "localhost"; - o.AgentPort = 6831; - - // Examples for the rest of the options, defaults unless otherwise specified - // Omitting Process Tags example as Resource API is recommended for additional tags - o.MaxPayloadSizeInBytes = 4096; - - // Using Batch Exporter (which is default) - // The other option is ExportProcessorType.Simple - o.ExportProcessorType = ExportProcessorType.Batch; - o.BatchExportProcessorOptions = new BatchExportProcessorOptions - { - MaxQueueSize = 2048, - ScheduledDelayMilliseconds = 5000, - ExporterTimeoutMilliseconds = 30000, - MaxExportBatchSize = 512, - }; - }) - .Build(); - var services = new ServiceCollection(); + services.AddOpenTelemetry() + .WithTracing(t => t.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("order-api")) + .AddSource(DiagnosticHeaders.DefaultListenerName) + .AddJaegerExporter(o => + { + o.AgentHost = "localhost"; + o.AgentPort = 6831; + + // Examples for the rest of the options, defaults unless otherwise specified + // Omitting Process Tags example as Resource API is recommended for additional tags + o.MaxPayloadSizeInBytes = 4096; + + // Using Batch Exporter (which is default) + // The other option is ExportProcessorType.Simple + o.ExportProcessorType = ExportProcessorType.Batch; + o.BatchExportProcessorOptions = new BatchExportProcessorOptions + { + MaxQueueSize = 2048, + ScheduledDelayMilliseconds = 5000, + ExporterTimeoutMilliseconds = 30000, + MaxExportBatchSize = 512 + }; + })); await using var provider = services .AddMassTransitTestHarness(x => @@ -87,9 +88,12 @@ await client.GetResponse(new OrderNumber = "123" }); - Assert.IsTrue(await harness.Sent.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Sent.Any(), Is.True); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); + }); } diff --git a/tests/MassTransit.ActiveMqTransport.Tests/Reconnecting_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/Reconnecting_Specs.cs index a57ab33c124..8c575047a16 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/Reconnecting_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/Reconnecting_Specs.cs @@ -3,9 +3,9 @@ namespace MassTransit.ActiveMqTransport.Tests using System; using System.Linq; using System.Threading.Tasks; - using MassTransit.Testing; using NUnit.Framework; using TestFramework.Messages; + using Testing; using Transports; @@ -20,7 +20,7 @@ public async Task Should_fault_nicely() await Bus.Publish(new ReconnectMessage { Value = "Before" }); var beforeFound = await Task.Run(() => _consumer.Received.Select(x => x.Context.Message.Value == "Before").Any()); - Assert.IsTrue(beforeFound); + Assert.That(beforeFound, Is.True); Console.WriteLine("Okay, restart ActiveMQ"); @@ -46,7 +46,7 @@ public async Task Should_fault_nicely() await Bus.Publish(new ReconnectMessage { Value = "After" }); var afterFound = await Task.Run(() => _consumer.Received.Select(x => x.Context.Message.Value == "After").Any()); - Assert.IsTrue(afterFound); + Assert.That(afterFound, Is.True); } public Reconnecting_Specs() diff --git a/tests/MassTransit.ActiveMqTransport.Tests/RequestReply_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/RequestReply_Specs.cs index c8bbddd982c..0f711da0feb 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/RequestReply_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/RequestReply_Specs.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using NUnit.Framework; using NUnit.Framework.Internal; - using Serialization; using TestFramework.Messages; @@ -17,25 +16,35 @@ public class Request_reply_use_temporary_queue_name_envelope public async Task Should_use_temporary_replyAddress() { var clientFactory = Bus.CreateClientFactory(); - RequestHandle request = clientFactory.CreateRequest(new PingMessage()); + RequestHandle request = clientFactory.CreateRequest(new PingMessage(_pingId)); Response response = await request.GetResponse(); TestExecutionContext.CurrentContext.OutWriter.Flush(); - Assert.IsNotNull(response); - Assert.NotNull(_replyToAddress); - Assert.IsTrue(_replyAddressPattern.IsMatch(_replyToAddress?.ToString()), "Reply address '{0}' does not match desired pattern", _replyToAddress); + Assert.Multiple(() => + { + Assert.That(response, Is.Not.Null); + Assert.That(_replyToAddress, Is.Not.Null); + Assert.That(_replyAddressPattern.IsMatch(_replyToAddress?.ToString()), Is.True, + $"Reply address '{_replyToAddress}' does not match desired pattern"); + }); } Uri _replyToAddress; readonly Regex _replyAddressPattern = new Regex("ID:[^:]*:[^:]*:[^:]*", RegexOptions.Compiled); + Guid _pingId; protected override void ConfigureActiveMqReceiveEndpoint(IActiveMqReceiveEndpointConfigurator configurator) { + _pingId = NewId.NextGuid(); + TestTimeout = TimeSpan.FromMinutes(5); base.ConfigureActiveMqReceiveEndpoint(configurator); configurator.Handler(async context => { + if (context.Message.CorrelationId != _pingId) + return; + _replyToAddress = context.ReceiveContext.TryGetPayload(out var payload) ? payload.TransportMessage.NMSReplyTo.ToEndpointAddress() : context.ResponseAddress; @@ -54,22 +63,32 @@ public class Request_reply_use_temporary_queue_name_raw public async Task Should_use_temporary_replyAddress() { var clientFactory = Bus.CreateClientFactory(); - RequestHandle request = clientFactory.CreateRequest(new PingMessage()); + RequestHandle request = clientFactory.CreateRequest(new PingMessage(_pingId)); Response response = await request.GetResponse(); - Assert.IsNotNull(response); - Assert.NotNull(_replyToAddress); - Assert.IsTrue(_replyAddressPattern.IsMatch(_replyToAddress?.ToString()), "Reply address '{0}' does not match desired pattern", _replyToAddress); + Assert.Multiple(() => + { + Assert.That(response, Is.Not.Null); + Assert.That(_replyToAddress, Is.Not.Null); + Assert.That(_replyAddressPattern.IsMatch(_replyToAddress?.ToString()), Is.True, + $"Reply address '{_replyToAddress}' does not match desired pattern"); + }); } Uri _replyToAddress; readonly Regex _replyAddressPattern = new Regex("ID:[^:]*:[^:]*:[^:]*", RegexOptions.Compiled); + Guid _pingId; protected override void ConfigureActiveMqReceiveEndpoint(IActiveMqReceiveEndpointConfigurator configurator) { + _pingId = NewId.NextGuid(); + base.ConfigureActiveMqReceiveEndpoint(configurator); configurator.Handler(async context => { + if (context.Message.CorrelationId != _pingId) + return; + _replyToAddress = context.ReceiveContext.TryGetPayload(out var payload) ? payload.TransportMessage.NMSReplyTo.ToEndpointAddress() : context.ResponseAddress; @@ -82,7 +101,7 @@ protected override void ConfigureActiveMqBus(IActiveMqBusFactoryConfigurator con { base.ConfigureActiveMqBus(configurator); - configurator.UseRawJsonSerializer(RawSerializerOptions.AddTransportHeaders, true); + configurator.UseRawJsonSerializer(); } } } diff --git a/tests/MassTransit.ActiveMqTransport.Tests/TopicEndpoint_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/TopicEndpoint_Specs.cs index 948c6e3eca5..d0c804903bf 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/TopicEndpoint_Specs.cs +++ b/tests/MassTransit.ActiveMqTransport.Tests/TopicEndpoint_Specs.cs @@ -13,8 +13,8 @@ public class Sending_to_a_topic_endpoint : [Test] public async Task Should_succeed() { - var endpoint = await Bus.GetSendEndpoint(new Uri("topic:VirtualTopic.private")); - await endpoint.Send(new PrivateMessage {Value = "Hello"}); + var endpoint = await Bus.GetSendEndpoint(new Uri("topic:private")); + await endpoint.Send(new PrivateMessage { Value = "Hello" }); ConsumeContext context = await _handler; @@ -27,7 +27,7 @@ protected override void ConfigureActiveMqReceiveEndpoint(IActiveMqReceiveEndpoin { configurator.ConfigureConsumeTopology = false; - configurator.Bind("VirtualTopic.private"); + configurator.Bind("private"); _handler = Handled(configurator); diff --git a/tests/MassTransit.ActiveMqTransport.Tests/VirtualTopicEndpoint_Specs.cs b/tests/MassTransit.ActiveMqTransport.Tests/VirtualTopicEndpoint_Specs.cs new file mode 100644 index 00000000000..3fca15ee329 --- /dev/null +++ b/tests/MassTransit.ActiveMqTransport.Tests/VirtualTopicEndpoint_Specs.cs @@ -0,0 +1,43 @@ +namespace MassTransit.ActiveMqTransport.Tests +{ + using System; + using System.Threading.Tasks; + using NUnit.Framework; + using TestFramework.Messages; + + + [TestFixture] + public class Sending_to_a_virtual_topic_endpoint : + ActiveMqTestFixture + { + [Test] + public async Task Should_succeed() + { + var endpoint = await Bus.GetSendEndpoint(new Uri("topic:VirtualTopic.private")); + await endpoint.Send(new PrivateMessage { Value = "Hello" }); + + ConsumeContext context = await _handler; + + Assert.That(context.Message.Value, Is.EqualTo("Hello")); + } + + Task> _handler; + + protected override void ConfigureActiveMqReceiveEndpoint(IActiveMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + configurator.Bind("VirtualTopic.private"); + + _handler = Handled(configurator); + + Handled(configurator); + } + + + class PrivateMessage + { + public string Value { get; set; } + } + } +} diff --git a/tests/MassTransit.ActiveMqTransport.Tests/docker-compose.yml b/tests/MassTransit.ActiveMqTransport.Tests/docker-compose.yml index 4363738c75e..83eb137b437 100644 --- a/tests/MassTransit.ActiveMqTransport.Tests/docker-compose.yml +++ b/tests/MassTransit.ActiveMqTransport.Tests/docker-compose.yml @@ -9,9 +9,9 @@ services: - "ACTIVEMQ_OPTS=-Xms512m -Xms512m" - "ACTIVEMQ_CONFIG_SCHEDULERENABLED=true" ports: - - 8161:8161 - - 61616:61616 - - 61613:61613 + - "8161:8161" + - "61616:61616" + - "61613:61613" artemis: image: quay.io/artemiscloud/activemq-artemis-broker environment: diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/AmazonSqsAddress_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/AmazonSqsAddress_Specs.cs index 188ec726e1c..ee7f24b19a5 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/AmazonSqsAddress_Specs.cs +++ b/tests/MassTransit.AmazonSqsTransport.Tests/AmazonSqsAddress_Specs.cs @@ -13,8 +13,11 @@ public void Should_return_a_valid_address_for_a_full_address() var host = new Uri("amazonsqs://remote-host"); var address = new AmazonSqsHostAddress(host); - Assert.That(address.Scope, Is.EqualTo("/")); - Assert.That(address.Host, Is.EqualTo("remote-host")); + Assert.Multiple(() => + { + Assert.That(address.Scope, Is.EqualTo("/")); + Assert.That(address.Host, Is.EqualTo("remote-host")); + }); Uri uri = address; @@ -27,8 +30,11 @@ public void Should_return_a_valid_address_for_a_full_address_with_scope() var host = new Uri("amazonsqs://remote-host/production"); var address = new AmazonSqsHostAddress(host); - Assert.That(address.Scope, Is.EqualTo("production")); - Assert.That(address.Host, Is.EqualTo("remote-host")); + Assert.Multiple(() => + { + Assert.That(address.Scope, Is.EqualTo("production")); + Assert.That(address.Host, Is.EqualTo("remote-host")); + }); Uri uri = address; @@ -47,8 +53,11 @@ public void Should_return_a_valid_address_for_a_full_address() var address = new AmazonSqsEndpointAddress(hostAddress, new Uri("amazonsqs://remote-host/input-queue")); - Assert.That(address.Scope, Is.EqualTo("/")); - Assert.That(address.Name, Is.EqualTo("input-queue")); + Assert.Multiple(() => + { + Assert.That(address.Scope, Is.EqualTo("/")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + }); Uri uri = address; @@ -62,8 +71,11 @@ public void Should_return_a_valid_address_for_a_full_address_with_scope() var address = new AmazonSqsEndpointAddress(hostAddress, new Uri("amazonsqs://remote-host/production/input-queue")); - Assert.That(address.Scope, Is.EqualTo("production")); - Assert.That(address.Name, Is.EqualTo("input-queue")); + Assert.Multiple(() => + { + Assert.That(address.Scope, Is.EqualTo("production")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + }); Uri uri = address; @@ -107,8 +119,11 @@ public void Should_return_a_valid_address_for_a_temporary_topic(bool isTemporary var hostAddress = new Uri("amazonsqs://localhost/test"); var address = new AmazonSqsEndpointAddress(hostAddress, new Uri($"topic:input?temporary={isTemporary}")); - Assert.That(address.AutoDelete, Is.EqualTo(isTemporary)); - Assert.That(address.Durable, Is.Not.EqualTo(isTemporary)); + Assert.Multiple(() => + { + Assert.That(address.AutoDelete, Is.EqualTo(isTemporary)); + Assert.That(address.Durable, Is.Not.EqualTo(isTemporary)); + }); } [Theory] diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/Batching_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/Batching_Specs.cs deleted file mode 100644 index 0baf981e088..00000000000 --- a/tests/MassTransit.AmazonSqsTransport.Tests/Batching_Specs.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace MassTransit.AmazonSqsTransport.Tests -{ - using System; - using System.Linq; - using System.Threading.Tasks; - using NUnit.Framework; - using TestFramework.Messages; - - - namespace Batching - { - [TestFixture] - public class When_a_batch_limit_is_reached : - AmazonSqsTestFixture - { - [Test] - public async Task Should_receive_the_message_batch() - { - await Task.WhenAll(Enumerable.Range(0, 5).Select(x => InputQueueSendEndpoint.Send(new PingMessage()))); - - Batch batch = await _consumer[0]; - - Assert.That(batch.Length, Is.EqualTo(5)); - Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Size)); - } - - TestBatchConsumer _consumer; - - protected override void ConfigureAmazonSqsReceiveEndpoint(IAmazonSqsReceiveEndpointConfigurator configurator) - { - _consumer = new TestBatchConsumer(GetTask>()); - - configurator.PrefetchCount = 10; - - configurator.Batch(x => - { - x.MessageLimit = 5; - - x.Consumer(() => _consumer); - }); - } - } - - - [TestFixture] - public class When_a_batch_timeout_is_reached_due_to_prefetch : - AmazonSqsTestFixture - { - [Test] - public async Task Should_receive_the_message_batch() - { - for (var i = 0; i < 10; i++) - await InputQueueSendEndpoint.Send(new PingMessage()); - - Batch batch = await _consumer[0]; - - Assert.That(batch.Length, Is.EqualTo(5)); - Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Time)); - - batch = await _consumer[1]; - - Assert.That(batch.Length, Is.EqualTo(5)); - Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Time)); - } - - TestBatchConsumer _consumer; - - protected override void ConfigureAmazonSqsReceiveEndpoint(IAmazonSqsReceiveEndpointConfigurator configurator) - { - _consumer = new TestBatchConsumer(GetTask>(), GetTask>()); - - configurator.PrefetchCount = 5; - - configurator.Batch(x => - { - x.MessageLimit = 10; - x.TimeLimit = TimeSpan.FromSeconds(1); - - x.Consumer(() => _consumer); - }); - } - } - - - class TestBatchConsumer : - IConsumer> - { - readonly TaskCompletionSource>[] _messageTask; - - int _count; - - public TestBatchConsumer(params TaskCompletionSource>[] messageTask) - { - _messageTask = messageTask; - } - - public Task> this[int index] => _messageTask[index].Task; - - public Task Consume(ConsumeContext> context) - { - if (_count < _messageTask.Length) - _messageTask[_count++].TrySetResult(context.Message); - - return Task.CompletedTask; - } - } - } -} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/Configure_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/Configure_Specs.cs index a392ec116b8..240f3480715 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/Configure_Specs.cs +++ b/tests/MassTransit.AmazonSqsTransport.Tests/Configure_Specs.cs @@ -144,45 +144,6 @@ public async Task Should_connect_locally_with_test_harness() await harness.Stop(); } - [Test] - public async Task Should_connect_locally_with_test_harness_and_a_handler() - { - var harness = new AmazonSqsTestHarness(); - HandlerTestHarness handler = harness.Handler(async context => - { - }); - - await harness.Start(); - - await harness.InputQueueSendEndpoint.Send(new PingMessage()); - - await harness.Stop(); - } - - [Test] - public async Task Should_connect_locally_with_test_harness_and_a_publisher() - { - var harness = new AmazonSqsTestHarness(); - HandlerTestHarness handler = harness.Handler(); - HandlerTestHarness handler2 = harness.Handler(); - - await harness.Start(); - - await harness.Bus.Publish(new PingMessage()); - - Assert.That(handler.Consumed.Select().Any(), Is.True); - - // await Task.Delay(20000); - - await harness.Bus.Publish(new PongMessage()); - - Assert.That(handler2.Consumed.Select().Any(), Is.True); - - await harness.Stop().OrTimeout(s: 5); - - await harness.Stop(); - } - [Test] public async Task Should_connect_locally_with_test_harness_and_publish_without_consumer() { diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/DelayedRedelivery_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/DelayedRedelivery_Specs.cs index f2cda00312a..b117a2e4ae7 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/DelayedRedelivery_Specs.cs +++ b/tests/MassTransit.AmazonSqsTransport.Tests/DelayedRedelivery_Specs.cs @@ -26,13 +26,16 @@ public async Task Should_stop_after_limit_exceeded() ConsumeContext> handled = await handler; - Assert.That(handled.Headers.Get(MessageHeaders.FaultRedeliveryCount), Is.EqualTo(_limit)); + Assert.Multiple(() => + { + Assert.That(handled.Headers.Get(MessageHeaders.FaultRedeliveryCount), Is.EqualTo(_limit)); - Assert.That(handled.Headers.Get(MessageHeaders.FaultRetryCount), Is.EqualTo(1)); + Assert.That(handled.Headers.Get(MessageHeaders.FaultRetryCount), Is.EqualTo(1)); + }); await InactivityTask; - Assert.LessOrEqual(_attempts[pingId], (_limit + 1) * 2); + Assert.That(_attempts[pingId], Is.LessThanOrEqualTo((_limit + 1) * 2)); } readonly int _limit; diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/Encrypted_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/Encrypted_Specs.cs new file mode 100644 index 00000000000..8a879d30600 --- /dev/null +++ b/tests/MassTransit.AmazonSqsTransport.Tests/Encrypted_Specs.cs @@ -0,0 +1,96 @@ +namespace MassTransit.AmazonSqsTransport.Tests +{ + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + [TestFixture] + public class Using_an_encrypted_binary_serializer + { + [Test] + public async Task Should_properly_expand_the_base64_string() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.LocalstackHost(); + + cfg.UseEncryption(_key); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new EncryptedString { Text = "Howdy!" }); + + Assert.That(await harness.Consumed.Any(x => x.Exception == null), Is.True); + + ConsumeContext context = (await harness.Consumed.SelectAsync(x => x.Exception == null).FirstOrDefault())?.Context; + + Assert.That(context, Is.Not.Null); + + Assert.That(context.Message.Text, Is.EqualTo("Howdy!")); + } + + static readonly byte[] _key = + { + 31, + 182, + 254, + 29, + 98, + 114, + 85, + 168, + 176, + 48, + 113, + 206, + 198, + 176, + 181, + 125, + 106, + 134, + 98, + 217, + 113, + 158, + 88, + 75, + 118, + 223, + 117, + 160, + 224, + 1, + 47, + 162 + }; + + + public class EncryptedString + { + public string Text { get; set; } + } + + + class EncryptedConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + } +} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/EndpointConfiguration_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/EndpointConfiguration_Specs.cs index c2582a510eb..111764ac071 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/EndpointConfiguration_Specs.cs +++ b/tests/MassTransit.AmazonSqsTransport.Tests/EndpointConfiguration_Specs.cs @@ -5,7 +5,6 @@ namespace MassTransit.AmazonSqsTransport.Tests using System.Threading.Tasks; using Amazon.SimpleNotificationService; using Amazon.SQS; - using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -13,6 +12,7 @@ namespace MassTransit.AmazonSqsTransport.Tests using NUnit.Framework; using TestFramework; using TestFramework.Messages; + using Testing; [TestFixture] @@ -46,9 +46,12 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_overrid var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -79,23 +82,14 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_specifi var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); - - await provider.DisposeAsync(); - } - - static void ConfigureHost(IAmazonSqsBusFactoryConfigurator cfg) - { - cfg.Host(new Uri("amazonsqs://localhost:4576"), h => + Assert.Multiple(() => { - h.AccessKey("admin"); - h.SecretKey("admin"); - - h.Config(new AmazonSQSConfig {ServiceURL = "http://localhost:4566"}); - h.Config(new AmazonSimpleNotificationServiceConfig {ServiceURL = "http://localhost:4566"}); + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); }); + + await provider.DisposeAsync(); } [Test] @@ -124,9 +118,12 @@ public async Task Should_include_concurrency_filter_if_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -157,8 +154,11 @@ public async Task Should_include_nothing_if_not_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -181,8 +181,11 @@ public void Should_override_bus_setting_if_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); } [Test] @@ -218,8 +221,23 @@ public void Should_use_bus_setting_if_not_specified() var probe = JObject.Parse(busControl.GetProbeResult().ToJsonString()); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); + } + + static void ConfigureHost(IAmazonSqsBusFactoryConfigurator cfg) + { + cfg.Host(new Uri("amazonsqs://localhost:4576"), h => + { + h.AccessKey("admin"); + h.SecretKey("admin"); + + h.Config(new AmazonSQSConfig { ServiceURL = "http://localhost:4566" }); + h.Config(new AmazonSimpleNotificationServiceConfig { ServiceURL = "http://localhost:4566" }); + }); } @@ -232,7 +250,8 @@ public PingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -251,7 +270,8 @@ public EndpointPingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -261,7 +281,8 @@ class EmptyPingConsumerDefinition : ConsumerDefinition { protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/EntityNameLength_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/EntityNameLength_Specs.cs new file mode 100644 index 00000000000..30431dfb70a --- /dev/null +++ b/tests/MassTransit.AmazonSqsTransport.Tests/EntityNameLength_Specs.cs @@ -0,0 +1,44 @@ +namespace MassTransit.AmazonSqsTransport.Tests +{ + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using TestFramework.Messages; + using Testing; + + + [TestFixture] + public class Entity_name_validation + { + [Test] + public async Task Should_throw_on_queue_name_length_exceeded() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer() + .Endpoint(e => e.Name = new string('a', 81)); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.LocalstackHost(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + Assert.That(async () => await provider.StartTestHarness(), Throws.TypeOf()); + } + + + class DumbConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + } +} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/Fifo_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/Fifo_Specs.cs index 353d4189fb2..f7a6c853e64 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/Fifo_Specs.cs +++ b/tests/MassTransit.AmazonSqsTransport.Tests/Fifo_Specs.cs @@ -1,16 +1,19 @@ namespace MassTransit.AmazonSqsTransport.Tests { using System; + using System.Collections.Generic; + using System.Linq; using System.Threading.Tasks; using Amazon.SimpleNotificationService; using Amazon.SQS; - using MassTransit.Testing; + using Internals; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using NUnit.Framework; using TestFramework; using TestFramework.Messages; + using Testing; [TestFixture] @@ -26,7 +29,7 @@ public async Task Should_allow_it_to_complete() Value = "Hello" }; - await Bus.Publish(message); + await Bus.Publish(message, Pipe.Execute(ctx => ctx.SetGroupId(message.CorrelationId.ToString()))); await AmazonSqsTestHarness.Consumed.Any(x => x.Context.Message.CorrelationId == message.CorrelationId); @@ -77,9 +80,7 @@ public async Task Should_properly_fifo_the_things() x.AddConfigureEndpointsCallback((name, _) => { if (_ is IAmazonSqsReceiveEndpointConfigurator configurator) - { configurator.QueueAttributes[QueueAttributeName.ContentBasedDeduplication] = true; - } }); x.UsingAmazonSqs((context, cfg) => @@ -107,7 +108,8 @@ public async Task Should_properly_fifo_the_things() await busControl.StartAsync(TestCancellationToken); try { - await busControl.Publish(new MessageInOrder()); + var id = NewId.NextGuid(); + await busControl.Publish(new MessageInOrder(), Pipe.Execute(ctx => ctx.SetGroupId(id.ToString()))); var source = provider.GetRequiredService>>(); @@ -214,4 +216,152 @@ public string SanitizeName(string name) } } } + + + [TestFixture] + public class When_sending_a_bunch_of_messages_in_the_same_group + { + [Test] + public async Task Should_arrive_in_order() + { + await using var provider = new ServiceCollection() + .AddSingleton>, List>>() + .AddMassTransitTestHarness(x => + { + x.AddConsumer() + .Endpoint(e => + { + e.ConfigureConsumeTopology = false; + + e.Name = "in-order.fifo"; + }); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.LocalstackHost(); + + cfg.ConfigureEndpoints(context); + }); + }).BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var groupId = NewId.NextGuid().ToString(); + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:in-order.fifo")); + + const int limit = 20; + + for (var i = 0; i < limit; i++) + { + await endpoint.Send(new MessageInOrder + { + Track = true, + Index = i + }, x => + { + x.SetGroupId(groupId); + x.SetDeduplicationId(x.MessageId.ToString()); + }); + } + + await harness.Consumed.SelectAsync().Take(limit).ToListAsync(); + + var results = provider.GetRequiredService>>(); + + Assert.That(results.Select(x => x.Message.Index), Is.EqualTo(Enumerable.Range(0, limit))); + } + + [Test] + public async Task Should_handle_multiple_groups_in_order() + { + await using var provider = new ServiceCollection() + .AddSingleton>, List>>() + .AddMassTransitTestHarness(x => + { + x.AddConsumer() + .Endpoint(e => + { + e.ConfigureConsumeTopology = false; + + e.Name = "in-order-many.fifo"; + }); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.LocalstackHost(); + + cfg.ConfigureEndpoints(context); + }); + }).BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:in-order-many.fifo")); + + const int groupLimit = 5; + const int limit = 10; + + var groupIds = NewId.NextGuid(groupLimit).Select(x => x.ToString()).ToArray(); + + for (var i = 0; i < limit; i++) + { + for (var j = 0; j < groupLimit; j++) + { + await endpoint.Send(new MessageInOrder + { + Track = j == groupLimit - 1, + Index = i + }, x => + { + x.SetGroupId(groupIds[j]); + x.SetDeduplicationId(x.MessageId.ToString()); + }); + } + } + + await harness.Consumed.SelectAsync().Take(limit * groupLimit).ToListAsync(); + + var results = provider.GetRequiredService>>(); + + Assert.That(results.Select(x => x.Message.Index), Is.EqualTo(Enumerable.Range(0, limit))); + } + + + public class MessageInOrder + { + public bool Track { get; set; } + public int Index { get; set; } + } + + + class MessageInOrderConsumer : + IConsumer + { + readonly ILogger _logger; + readonly IList> _messages; + + public MessageInOrderConsumer(IList> messages, ILogger logger) + { + _messages = messages; + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + await Task.Delay(100); + + if (context.Message.Track) + { + lock (_messages) + _messages.Add(context); + } + } + } + } } diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/InMemoryOutboxRedelivery_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/InMemoryOutboxRedelivery_Specs.cs index d9b2e9e2921..d66b65d4706 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/InMemoryOutboxRedelivery_Specs.cs +++ b/tests/MassTransit.AmazonSqsTransport.Tests/InMemoryOutboxRedelivery_Specs.cs @@ -181,7 +181,7 @@ protected override void ConfigureAmazonSqsBus(IAmazonSqsBusFactoryConfigurator c protected override void ConfigureAmazonSqsReceiveEndpoint(IAmazonSqsReceiveEndpointConfigurator configurator) { configurator.UseDelayedRedelivery(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); - configurator.UseRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); + configurator.UseMessageRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); configurator.UseInMemoryOutbox(); configurator.Consumer(); diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/MassTransit.AmazonSqsTransport.Tests.csproj b/tests/MassTransit.AmazonSqsTransport.Tests/MassTransit.AmazonSqsTransport.Tests.csproj index aa46a802592..79ff44f0347 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/MassTransit.AmazonSqsTransport.Tests.csproj +++ b/tests/MassTransit.AmazonSqsTransport.Tests/MassTransit.AmazonSqsTransport.Tests.csproj @@ -1,21 +1,24 @@  - net6.0 - - - - $(TargetFrameworks);net462 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/MessageBody_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/MessageBody_Specs.cs new file mode 100644 index 00000000000..12181cb1bde --- /dev/null +++ b/tests/MassTransit.AmazonSqsTransport.Tests/MessageBody_Specs.cs @@ -0,0 +1,107 @@ +namespace MassTransit.AmazonSqsTransport.Tests +{ + using System.Text.Json; + using Amazon.SQS.Model; + using NUnit.Framework; + using Serialization; + + + [TestFixture] + public class MessageBody_Specs + { + [Test] + public void Should_handle_cross_region_messages() + { + var body = new SqsMessageBody(new Message { Body = CrossRegionMessage }); + + JsonElement? bodyElement = body.GetJsonElement(SystemTextJsonMessageSerializer.Options); + + var envelope = bodyElement?.Deserialize(SystemTextJsonMessageSerializer.Options); + + Assert.Multiple(() => + { + Assert.That(envelope, Is.Not.Null); + Assert.That(envelope.MessageId, Is.EqualTo("00ab0000-6ab3-f8b4-f78c-08db7c8365ff")); + }); + } + + [Test] + public void Should_handle_intra_region_messages() + { + var body = new SqsMessageBody(new Message { Body = InRegionMessage }); + + JsonElement? bodyElement = body.GetJsonElement(SystemTextJsonMessageSerializer.Options); + + var envelope = bodyElement?.Deserialize(SystemTextJsonMessageSerializer.Options); + + Assert.Multiple(() => + { + Assert.That(envelope, Is.Not.Null); + Assert.That(envelope.MessageId, Is.EqualTo("00ab0000-6ab3-f8b4-f78c-08db7c8365ff")); + }); + } + + const string InRegionMessage = """ + { + "messageId": "00ab0000-6ab3-f8b4-f78c-08db7c8365ff", + "requestId": null, + "correlationId": null, + "conversationId": "00ab0000-6ab3-f8b4-739e-08db7c83660f", + "initiatorId": null, + "sourceAddress": "amazonsqs://us-east-1/some-namespace/some_address?durable=false&autodelete=true", + "destinationAddress": "amazonsqs://us-east-1/some-namespace_TestMessage.fifo?type=topic", + "responseAddress": null, + "faultAddress": null, + "messageType": [ + "urn:message:TestSerialization:TestMessage" + ], + "message": { + "name": "Hello world!" + }, + "expirationTime": null, + "sentTime": "2023-07-04T11:39:59.689102Z", + "headers": {}, + "host": + { + "machineName": "XXXX", + "processName": "Publisher", + "processId": 43776, + "assembly": "Publisher", + "assemblyVersion": "1.0.0.0", + "frameworkVersion": "6.0.14", + "massTransitVersion": "8.0.16.0", + "operatingSystemVersion": "Microsoft Windows NT 10.0.19044.0" + } + } + """; + + const string CrossRegionMessage = """ + { + "Type": "Notification", + "MessageId": "00ab0000-6ab3-f8b4-f78c-08db7c8365ff", + "SequenceNumber": "10000000000000127000", + "TopicArn": "arn:aws:sns:eu-west-1:000696323999:some-namespace_TestMessage.fifo", + "Message": "{\r\n \"messageId\": \"00ab0000-6ab3-f8b4-f78c-08db7c8365ff\",\r\n \"requestId\": null,\r\n \"correlationId\": null,\r\n \"conversationId\": \"00ab0000-6ab3-f8b4-739e-08db7c83660f\",\r\n \"initiatorId\": null,\r\n \"sourceAddress\": \"amazonsqs://us-east-1/some-namespace/some_address?durable=false&autodelete=true\",\r\n \"destinationAddress\": \"amazonsqs://us-east-1/some-namespace_TestMessage.fifo?type=topic\",\r\n \"responseAddress\": null,\r\n \"faultAddress\": null,\r\n \"messageType\": [\r\n \"urn:message:TestSerialization:TestMessage\"\r\n ],\r\n \"message\": {\r\n \"name\": \"Hello world!\"\r\n },\r\n \"expirationTime\": null,\r\n \"sentTime\": \"2023-07-04T11:39:59.689102Z\",\r\n \"headers\": {},\r\n \"host\": {\r\n \"machineName\": \"XXXX\",\r\n \"processName\": \"Publisher\",\r\n \"processId\": 43776,\r\n \"assembly\": \"Publisher\",\r\n \"assemblyVersion\": \"1.0.0.0\",\r\n \"frameworkVersion\": \"6.0.14\",\r\n \"massTransitVersion\": \"8.0.16.0\",\r\n \"operatingSystemVersion\": \"Microsoft Windows NT 10.0.19044.0\"\r\n }\r\n}", + "Timestamp": "2023-07-04T06:49:35.120Z", + "UnsubscribeURL": "https://sns.eu-west-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:eu-west-1:000696323999:some-namespace_TestMessage.fifo:16dc9aa0-4cc9-43eb-b0a5-48e56d5a0565", + "MessageAttributes": + { + "Content-Type": + { + "Type": "String", + "Value": "application/vnd.masstransit+json" + } + } + } + """; + } +} + + +namespace TestSerialization +{ + public class TestMessage + { + public string Name { get; set; } + } +} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/MultiBus_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/MultiBus_Specs.cs new file mode 100644 index 00000000000..5d9ab113cd2 --- /dev/null +++ b/tests/MassTransit.AmazonSqsTransport.Tests/MultiBus_Specs.cs @@ -0,0 +1,118 @@ +namespace MassTransit.AmazonSqsTransport.Tests +{ + namespace MultiBusTests + { + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + public class MyMessageConsumer : + IConsumer + { + async Task IConsumer.Consume(ConsumeContext context) + { + Console.WriteLine($"Received Message from server {context.Message.Server}, msg: {context.Message.MsgData}"); + await Task.CompletedTask; + } + } + + + public class BusEnvironmentNameFormatter : IEntityNameFormatter + { + readonly string _prefix; + + public BusEnvironmentNameFormatter(string server) + { + _prefix = $"{server}-topic-"; + } + + public string FormatEntityName() + { + Console.WriteLine($"NameFormatter {_prefix}{typeof(T).Name}"); + return _prefix + typeof(T).Name; + } + } + + + public class MyMessage + { + public int Server { get; set; } + public string MsgData { get; set; } + } + + + public interface ISecondBus : IBus + { + } + + + public class Using_two_separate_entity_name_formatters_with_multiple_buses + { + [Test] + public async Task Should_work() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(cfg => cfg.ConcurrentMessageLimit = 1); + + x.SetEndpointNameFormatter(new KebabCaseEndpointNameFormatter("server1", false)); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.LocalstackHost(); + + cfg.MessageTopology.SetEntityNameFormatter(new BusEnvironmentNameFormatter("server1")); + + cfg.ConfigureEndpoints(context); + }); + }) + .AddMassTransit(x => + { + x.AddConsumer(cfg => cfg.ConcurrentMessageLimit = 1); + + x.SetEndpointNameFormatter(new KebabCaseEndpointNameFormatter("server2", false)); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.LocalstackHost(); + + cfg.MessageTopology.SetEntityNameFormatter(new BusEnvironmentNameFormatter("server2")); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var bus = harness.Bus; + + Assert.That(bus.Topology.Publish().TryGetPublishAddress(bus.Address, out var publishAddress), Is.True); + Assert.That(publishAddress, Is.EqualTo(new Uri(bus.Address, "server1-topic-MyMessage?type=topic"))); + + await bus.Publish(new MyMessage + { + Server = 1, + MsgData = "Hello", + }); + + bus = provider.GetRequiredService(); + + Assert.That(bus.Topology.Publish().TryGetPublishAddress(bus.Address, out publishAddress), Is.True); + Assert.That(publishAddress, Is.EqualTo(new Uri(bus.Address, "server2-topic-MyMessage?type=topic"))); + + await bus.Publish(new MyMessage + { + Server = 2, + MsgData = "Hello", + }); + + Assert.That(await harness.Consumed.Any(x => x.Context.Message.Server == 1), Is.True); + } + } + } +} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/OpenTelemetry_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/OpenTelemetry_Specs.cs index 7f92816b9c8..c31b3c9d7ec 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/OpenTelemetry_Specs.cs +++ b/tests/MassTransit.AmazonSqsTransport.Tests/OpenTelemetry_Specs.cs @@ -3,6 +3,7 @@ namespace MassTransit.AmazonSqsTransport.Tests using System.Diagnostics; using System.Threading.Tasks; using HarnessContracts; + using Logging; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using OpenTelemetry; @@ -38,32 +39,31 @@ public class OpenTelemetry_Specs [Test] public async Task Should_report_telemetry_to_jaeger() { - using var tracerProvider = Sdk.CreateTracerProviderBuilder() - .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("order-api")) - .AddSource("MassTransit") - .AddJaegerExporter(o => - { - o.AgentHost = "localhost"; - o.AgentPort = 6831; - - // Examples for the rest of the options, defaults unless otherwise specified - // Omitting Process Tags example as Resource API is recommended for additional tags - o.MaxPayloadSizeInBytes = 4096; - - // Using Batch Exporter (which is default) - // The other option is ExportProcessorType.Simple - o.ExportProcessorType = ExportProcessorType.Batch; - o.BatchExportProcessorOptions = new BatchExportProcessorOptions - { - MaxQueueSize = 2048, - ScheduledDelayMilliseconds = 5000, - ExporterTimeoutMilliseconds = 30000, - MaxExportBatchSize = 512, - }; - }) - .Build(); - var services = new ServiceCollection(); + services.AddOpenTelemetry() + .WithTracing(t => t.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService("order-api")) + .AddSource(DiagnosticHeaders.DefaultListenerName) + .AddJaegerExporter(o => + { + o.AgentHost = "localhost"; + o.AgentPort = 6831; + + // Examples for the rest of the options, defaults unless otherwise specified + // Omitting Process Tags example as Resource API is recommended for additional tags + o.MaxPayloadSizeInBytes = 4096; + + // Using Batch Exporter (which is default) + // The other option is ExportProcessorType.Simple + o.ExportProcessorType = ExportProcessorType.Batch; + o.BatchExportProcessorOptions = new BatchExportProcessorOptions + { + MaxQueueSize = 2048, + ScheduledDelayMilliseconds = 5000, + ExporterTimeoutMilliseconds = 30000, + MaxExportBatchSize = 512 + }; + }) + .Build()); await using var provider = services .AddMassTransitTestHarness(x => @@ -91,9 +91,12 @@ await client.GetResponse(new OrderNumber = "123" }); - Assert.IsTrue(await harness.Sent.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Sent.Any(), Is.True); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); + }); } diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/Persistence/EmptyConsumer.cs b/tests/MassTransit.AmazonSqsTransport.Tests/Persistence/EmptyConsumer.cs new file mode 100644 index 00000000000..f4fdbb0b0fa --- /dev/null +++ b/tests/MassTransit.AmazonSqsTransport.Tests/Persistence/EmptyConsumer.cs @@ -0,0 +1,13 @@ +namespace MassTransit.AmazonSqsTransport.Tests.Persistence +{ + using System.Threading.Tasks; + + + public class EmptyConsumer : IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } +} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/Persistence/SimpleMessage.cs b/tests/MassTransit.AmazonSqsTransport.Tests/Persistence/SimpleMessage.cs new file mode 100644 index 00000000000..3da91e01759 --- /dev/null +++ b/tests/MassTransit.AmazonSqsTransport.Tests/Persistence/SimpleMessage.cs @@ -0,0 +1,8 @@ +#nullable enable +namespace MassTransit.AmazonSqsTransport.Tests.Persistence +{ + public class SimpleMessage + { + public MessageData? BigData { get; set; } + } +} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/Persistence/Storage_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/Persistence/Storage_Specs.cs new file mode 100644 index 00000000000..f322f688363 --- /dev/null +++ b/tests/MassTransit.AmazonSqsTransport.Tests/Persistence/Storage_Specs.cs @@ -0,0 +1,74 @@ +#nullable enable +namespace MassTransit.AmazonSqsTransport.Tests.Persistence +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using Amazon.S3; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + [TestFixture] + public class Storage_Specs + { + [Test] + public async Task S3MessageDataTestLoadFetchAsync() + { + Environment.SetEnvironmentVariable("AWS_REGION", "us-east-1"); + Environment.SetEnvironmentVariable("AWS_ACCESS_KEY_ID", "admin"); + Environment.SetEnvironmentVariable("AWS_SECRET_ACCESS_KEY", "admin"); + + MessageDataDefaults.TimeToLive = TimeSpan.FromDays(2); + MessageDataDefaults.ExtraTimeToLive = TimeSpan.FromMinutes(5); + + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetKebabCaseEndpointNameFormatter(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); + + x.UsingInMemory((context, cfg) => + { + cfg.UseMessageData(d => d.AmazonS3("test", new AmazonS3Config + { + ForcePathStyle = true, + ServiceURL = "http://localhost:4566", + })); + cfg.ConfigureEndpoints(context); + }); + x.AddConsumer(); + }).BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + await harness.Start(); + + var client = harness.Scope.ServiceProvider.GetRequiredService(); + var randomText = GenerateRandomAlphanumericString(7000); + await client.Publish(new + { + BigData = randomText, + }); + + Assert.That(await harness.Consumed.Any(), Is.True, "Did not receive message"); + IReceivedMessage? receivedMessage = await harness.Consumed.SelectAsync().First(); + var message = receivedMessage.Context.Message; + Assert.That(message.BigData, Is.Not.Null); + var messageValue = await message.BigData!.Value; + Assert.That(messageValue, Is.EqualTo(randomText), "Message not retrieved"); + + await harness.Stop(); + } + + static string GenerateRandomAlphanumericString(int length = 10) + { + const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + var random = new Random(); + var randomString = new string(Enumerable.Repeat(chars, length) + .Select(s => s[random.Next(s.Length)]).ToArray()); + return randomString; + } + } +} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/RawJson_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/RawJson_Specs.cs index 0cb4d73674b..42e3063948c 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/RawJson_Specs.cs +++ b/tests/MassTransit.AmazonSqsTransport.Tests/RawJson_Specs.cs @@ -58,27 +58,30 @@ public async Task Should_return_the_header_value_from_the_transport() ConsumeContext context = await _handled; - Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonRawMessageSerializer.JsonContentType), - $"unexpected content-type {context.ReceiveContext.ContentType}"); + Assert.Multiple(() => + { + Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonRawMessageSerializer.JsonContentType), + $"unexpected content-type {context.ReceiveContext.ContentType}"); - Assert.That(context.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(context.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.That(context.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(context.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); - Assert.That(context.Headers.Get(headerName), Is.EqualTo(headerValue)); + Assert.That(context.Headers.Get(headerName), Is.EqualTo(headerValue)); - Assert.IsTrue(context.MessageId.HasValue); - Assert.IsTrue(context.ConversationId.HasValue); - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.IsTrue(context.SentTime.HasValue); - Assert.IsNotNull(context.DestinationAddress); - Assert.That(context.SupportedMessageTypes.Count(), Is.EqualTo(1)); + Assert.That(context.MessageId.HasValue, Is.True); + Assert.That(context.ConversationId.HasValue, Is.True); + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.SentTime.HasValue, Is.True); + Assert.That(context.DestinationAddress, Is.Not.Null); + Assert.That(context.SupportedMessageTypes.Count(), Is.EqualTo(1)); + }); } Task> _handled; protected override void ConfigureAmazonSqsBus(IAmazonSqsBusFactoryConfigurator configurator) { - configurator.UseRawJsonSerializer(); + configurator.UseRawJsonSerializer(RawSerializerOptions.All); } protected override void ConfigureAmazonSqsReceiveEndpoint(IAmazonSqsReceiveEndpointConfigurator configurator) @@ -118,12 +121,15 @@ await InputQueueSendEndpoint.Send(message, x => ConsumeContext context = await _handled; - Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonMessageSerializer.JsonContentType), - $"unexpected content-type {context.ReceiveContext.ContentType}"); + Assert.Multiple(() => + { + Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonMessageSerializer.JsonContentType), + $"unexpected content-type {context.ReceiveContext.ContentType}"); - Assert.That(context.Message.CorrelationId, Is.EqualTo(message.CommandId)); + Assert.That(context.Message.CorrelationId, Is.EqualTo(message.CommandId)); - Assert.That(context.Headers.Get(headerName), Is.EqualTo(default)); + Assert.That(context.Headers.Get(headerName), Is.EqualTo(default)); + }); } Task> _handled; @@ -147,7 +153,7 @@ protected override void ConfigureAmazonSqsBus(IAmazonSqsBusFactoryConfigurator c protected override void ConfigureAmazonSqsReceiveEndpoint(IAmazonSqsReceiveEndpointConfigurator configurator) { - configurator.UseRawJsonDeserializer(); + configurator.UseRawJsonDeserializer(RawSerializerOptions.AnyMessageType | RawSerializerOptions.AddTransportHeaders); TaskCompletionSource> handler = GetTask>(); _handler = handler.Task; @@ -192,12 +198,15 @@ await InputQueueSendEndpoint.Send(message, x => ConsumeContext context = await _handled; - Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonMessageSerializer.JsonContentType), - $"unexpected content-type {context.ReceiveContext.ContentType}"); + Assert.Multiple(() => + { + Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonMessageSerializer.JsonContentType), + $"unexpected content-type {context.ReceiveContext.ContentType}"); - Assert.That(context.Message.CorrelationId, Is.EqualTo(message.CommandId)); + Assert.That(context.Message.CorrelationId, Is.EqualTo(message.CommandId)); - Assert.That(context.Headers.Get(headerName), Is.EqualTo(headerValue)); + Assert.That(context.Headers.Get(headerName), Is.EqualTo(headerValue)); + }); } Task> _handled; diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/Redelivery_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/Redelivery_Specs.cs new file mode 100644 index 00000000000..4fb5a71ae50 --- /dev/null +++ b/tests/MassTransit.AmazonSqsTransport.Tests/Redelivery_Specs.cs @@ -0,0 +1,97 @@ +namespace MassTransit.AmazonSqsTransport.Tests; + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using Testing; + + +[TestFixture] +public class ReleaseLockContext_Specs +{ + [Test] + public async Task Should_release_subsequent_lock_contexts() + { + var services = new ServiceCollection(); + + await using var provider = services + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + x.AddConfigureEndpointsCallback((context, name, cfg) => + { + if (cfg is IAmazonSqsReceiveEndpointConfigurator sqs) + { + sqs.RethrowFaultedMessages(); + sqs.ThrowOnSkippedMessages(); + sqs.RedeliverVisibilityTimeout = 0; + } + }); + x.UsingAmazonSqs((context, cfg) => + { + cfg.LocalstackHost(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + try + { + var next = Random.Shared.Next(10000); + await harness.Bus.Publish(new TestLockContextMessage() { Id = next.ToString() }); + + Assert.That(await harness.Published.Any(x => x.Context.Message.Id == next.ToString())); + } + finally + { + await harness.Stop(); + } + } +} + + +public class TestLockContextConsumer : + IConsumer +{ + readonly ILogger _logger; + + public TestLockContextConsumer(ILogger logger) + { + _logger = logger; + } + + public async Task Consume(ConsumeContext context) + { + await Task.Delay(TimeSpan.FromSeconds(1)); + + var redelivered = context.ReceiveContext.Redelivered ? "redelivered" : ""; + _logger.LogInformation($"Got Message {context.Message.Id} {redelivered}"); + + if (context.ReceiveContext.Redelivered) + { + await context.Publish(new TestLockContextRedeliveredMessage() { Id = context.Message.Id }); + return; + } + + throw new Exception("This is intentional"); + } +} + + +public class TestLockContextMessage +{ + public string Id { get; set; } +} + + +public class TestLockContextRedeliveredMessage +{ + public string Id { get; set; } +} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/SentTime_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/SentTime_Specs.cs new file mode 100644 index 00000000000..67740cf0cc1 --- /dev/null +++ b/tests/MassTransit.AmazonSqsTransport.Tests/SentTime_Specs.cs @@ -0,0 +1,82 @@ +namespace MassTransit.AmazonSqsTransport.Tests +{ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Serialization; + using TestFramework.Messages; + using Testing; + + + [TestFixture] + public class SentTime_Specs + { + [Test] + public async Task Should_have_sent_time_header_in_utc() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddHandler(async (PingMessage _) => + { + }); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.LocalstackHost(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new PingMessage()); + + IReceivedMessage consumed = await harness.Consumed.SelectAsync().FirstOrDefault(); + Assert.That(consumed, Is.Not.Null); + + Assert.That(consumed.Context.SentTime.Value.Kind, Is.EqualTo(DateTimeKind.Utc)); + } + + [Test] + public async Task Should_have_sent_time_header_in_utc_using_raw_json() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddHandler(async (PongMessage _) => + { + }); + + x.UsingAmazonSqs((context, cfg) => + { + cfg.LocalstackHost(); + + cfg.UseRawJsonSerializer(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new PongMessage()); + + IReceivedMessage consumed = await harness.Consumed.SelectAsync().FirstOrDefault(); + Assert.That(consumed, Is.Not.Null); + + Assert.Multiple(() => + { + Assert.That(consumed.Context.SentTime.Value.Kind, Is.EqualTo(DateTimeKind.Utc)); + Assert.That(consumed.Context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonRawMessageSerializer.JsonContentType)); + + Assert.That(consumed.Context.ReceiveContext.GetSentTime(), Is.GreaterThanOrEqualTo(consumed.Context.SentTime - TimeSpan.FromSeconds(30))); + }); + Assert.That(consumed.Context.ReceiveContext.GetSentTime(), Is.LessThanOrEqualTo(consumed.Context.SentTime + TimeSpan.FromSeconds(30))); + } + } +} diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/StartStop_Specs.cs b/tests/MassTransit.AmazonSqsTransport.Tests/StartStop_Specs.cs index 13b258d24e2..3b7d8fb37b6 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/StartStop_Specs.cs +++ b/tests/MassTransit.AmazonSqsTransport.Tests/StartStop_Specs.cs @@ -4,10 +4,10 @@ namespace MassTransit.AmazonSqsTransport.Tests using System.Threading.Tasks; using Amazon.SimpleNotificationService; using Amazon.SQS; - using MassTransit.Testing; using NUnit.Framework; using TestFramework; using TestFramework.Messages; + using Testing; [TestFixture] @@ -20,12 +20,14 @@ public async Task Should_start_stop_and_start() { var bus = MassTransit.Bus.Factory.CreateUsingAmazonSqs(x => { + x.AutoStart = true; + x.Host(new Uri("amazonsqs://localhost:4566"), h => { h.AccessKey("admin"); h.SecretKey("admin"); - h.Config(new AmazonSimpleNotificationServiceConfig {ServiceURL = "http://localhost:4566"}); - h.Config(new AmazonSQSConfig {ServiceURL = "http://localhost:4566"}); + h.Config(new AmazonSimpleNotificationServiceConfig { ServiceURL = "http://localhost:4566" }); + h.Config(new AmazonSQSConfig { ServiceURL = "http://localhost:4566" }); }); ConfigureBusDiagnostics(x); diff --git a/tests/MassTransit.AmazonSqsTransport.Tests/docker-compose.yml b/tests/MassTransit.AmazonSqsTransport.Tests/docker-compose.yml index 8e19b93146a..7f2846db7dd 100644 --- a/tests/MassTransit.AmazonSqsTransport.Tests/docker-compose.yml +++ b/tests/MassTransit.AmazonSqsTransport.Tests/docker-compose.yml @@ -2,17 +2,7 @@ version: '2.1' services: localstack: - image: localstack/localstack:0.12.17.5 + image: localstack/localstack:3.0.2 ports: - "4566:4566" - "4571:4571" - - "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}" - environment: - - SERVICES=${SERVICES- } - - DEBUG=${DEBUG- } - - DATA_DIR=${DATA_DIR- } - - PORT_WEB_UI=${PORT_WEB_UI- } - - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- } - - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- } - - DOCKER_HOST=unix:///var/run/docker.sock - - HOST_TMP_FOLDER=${TMPDIR} diff --git a/tests/MassTransit.Analyzers.Tests/MassTransit.Analyzers.Tests.csproj b/tests/MassTransit.Analyzers.Tests/MassTransit.Analyzers.Tests.csproj index 896ee1804de..a57c6ad968c 100644 --- a/tests/MassTransit.Analyzers.Tests/MassTransit.Analyzers.Tests.csproj +++ b/tests/MassTransit.Analyzers.Tests/MassTransit.Analyzers.Tests.csproj @@ -12,6 +12,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/MassTransit.Analyzers.Tests/MessageContractAnalyzerUnitTests.cs b/tests/MassTransit.Analyzers.Tests/MessageContractAnalyzerUnitTests.cs index dfb1accb06e..9975a069c15 100644 --- a/tests/MassTransit.Analyzers.Tests/MessageContractAnalyzerUnitTests.cs +++ b/tests/MassTransit.Analyzers.Tests/MessageContractAnalyzerUnitTests.cs @@ -828,6 +828,76 @@ await bus.Publish(new VerifyCSharpFix(test, fixtest); } + [Test] + public void WhenMessageContractHasNullableAreStructurallyCompatibleAndMissingCaseNullableProperty_ShouldHaveDiagnosticAndCodeFix() + { + var test = Usings + @" +namespace ConsoleApplication1 +{ + public interface OrderSubmitted + { + Guid Id { get; } + int Quantity { get; } + decimal? Price { get; } + } + + class Program + { + static async Task Main() + { + var bus = Bus.Factory.CreateUsingInMemory(cfg => { }); + + await bus.Publish(new + { + Id = NewId.NextGuid(), + quantity = 10 + }); + } + } +} +"; + var expected = new DiagnosticResult + { + Id = "MCA0003", + Message = + "Anonymous type is missing properties that are in the message contract 'OrderSubmitted'. The following properties are missing: Price.", + Severity = DiagnosticSeverity.Info, + Locations = + new[] { new DiagnosticResultLocation("Test0.cs", 23, 47) } + }; + + VerifyCSharpDiagnostic(test, expected); + + var fixtest = Usings + @" +namespace ConsoleApplication1 +{ + public interface OrderSubmitted + { + Guid Id { get; } + int Quantity { get; } + decimal? Price { get; } + } + + class Program + { + static async Task Main() + { + var bus = Bus.Factory.CreateUsingInMemory(cfg => { }); + + await bus.Publish(new + { + Id = NewId.NextGuid(), + quantity = 10 +, + Price = default(decimal?) + }); + } + } +} +"; + VerifyCSharpFix(test, fixtest); + } + [Test] public void WhenPublishTypesAreStructurallyCompatibleAndMissingProperty_ShouldHaveDiagnostic() { diff --git a/tests/MassTransit.Analyzers.Tests/Verifiers/CodeFixVerifier.cs b/tests/MassTransit.Analyzers.Tests/Verifiers/CodeFixVerifier.cs index 9d237a9e641..db32d76fcab 100644 --- a/tests/MassTransit.Analyzers.Tests/Verifiers/CodeFixVerifier.cs +++ b/tests/MassTransit.Analyzers.Tests/Verifiers/CodeFixVerifier.cs @@ -84,7 +84,7 @@ void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider cod bool allowNewCompilerDiagnostics) { var document = CreateDocument(oldSource, language); - Diagnostic[] analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] {document}); + Diagnostic[] analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); IEnumerable compilerDiagnostics = GetCompilerDiagnostics(document); var attempts = analyzerDiagnostics.Length; @@ -104,7 +104,7 @@ void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider cod } document = ApplyFix(document, actions.ElementAt(0)); - analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] {document}); + analyzerDiagnostics = GetSortedDiagnosticsFromDocuments(analyzer, new[] { document }); IEnumerable newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); @@ -116,10 +116,8 @@ void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider cod document.Project.Solution.Workspace)); newCompilerDiagnostics = GetNewDiagnostics(compilerDiagnostics, GetCompilerDiagnostics(document)); - Assert.IsTrue(false, - string.Format("Fix introduced new compiler diagnostics:\r\n{0}\r\n\r\nNew document:\r\n{1}\r\n", - string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString())), - document.GetSyntaxRootAsync().Result.ToFullString())); + Assert.Fail( + $"Fix introduced new compiler diagnostics:\r\n{string.Join("\r\n", newCompilerDiagnostics.Select(d => d.ToString()))}\r\n\r\nNew document:\r\n{document.GetSyntaxRootAsync().Result.ToFullString()}\r\n"); } //check if there are analyzer diagnostics left after the code fix @@ -130,7 +128,7 @@ void VerifyFix(string language, DiagnosticAnalyzer analyzer, CodeFixProvider cod //after applying all of the code fixes, compare the resulting string to the inputted one var actual = GetStringFromDocument(document).Replace("\r\n", "\n"); - Assert.AreEqual(newSource, actual); + Assert.That(actual, Is.EqualTo(newSource)); } } } diff --git a/tests/MassTransit.Analyzers.Tests/Verifiers/DiagnosticVerifier.cs b/tests/MassTransit.Analyzers.Tests/Verifiers/DiagnosticVerifier.cs index f1c0033f7bf..3c1831defc9 100644 --- a/tests/MassTransit.Analyzers.Tests/Verifiers/DiagnosticVerifier.cs +++ b/tests/MassTransit.Analyzers.Tests/Verifiers/DiagnosticVerifier.cs @@ -39,7 +39,7 @@ static string FormatDiagnostics(DiagnosticAnalyzer analyzer, params Diagnostic[] builder.AppendFormat("GetGlobalResult({0}.{1})", analyzerType.Name, rule.Id); else { - Assert.IsTrue(location.IsInSource, + Assert.That(location.IsInSource, Is.True, $"Test base does not currently handle diagnostics in metadata locations. Diagnostic in metadata: {diagnostics[i]}\r\n"); var resultMethodName = diagnostics[i].Location.SourceTree.FilePath.EndsWith(".cs") ? "GetCSharpResultAt" : "GetBasicResultAt"; @@ -89,7 +89,7 @@ protected virtual DiagnosticAnalyzer GetBasicDiagnosticAnalyzer() /// DiagnosticResults that should appear after the analyzer is run on the source protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] expected) { - VerifyDiagnostics(new[] {source}, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); + VerifyDiagnostics(new[] { source }, LanguageNames.CSharp, GetCSharpDiagnosticAnalyzer(), expected); } /// @@ -100,7 +100,7 @@ protected void VerifyCSharpDiagnostic(string source, params DiagnosticResult[] e /// DiagnosticResults that should appear after the analyzer is run on the source protected void VerifyBasicDiagnostic(string source, params DiagnosticResult[] expected) { - VerifyDiagnostics(new[] {source}, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); + VerifyDiagnostics(new[] { source }, LanguageNames.VisualBasic, GetBasicDiagnosticAnalyzer(), expected); } /// @@ -128,7 +128,8 @@ protected void VerifyBasicDiagnostic(string[] sources, params DiagnosticResult[] protected void VerifyCSharpDiagnosticWithoutMassTransit(string source, params DiagnosticResult[] expected) { var analyzer = GetCSharpDiagnosticAnalyzer(); - Diagnostic[] diagnostics = GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(new []{ source }, LanguageNames.CSharp, includeMassTransit: false)); + Diagnostic[] diagnostics = + GetSortedDiagnosticsFromDocuments(analyzer, GetDocuments(new[] { source }, LanguageNames.CSharp, includeMassTransit: false)); VerifyDiagnosticResults(diagnostics, analyzer, expected); } @@ -164,9 +165,8 @@ static void VerifyDiagnosticResults(IEnumerable actualResults, Diagn { var diagnosticsOutput = actualResults.Any() ? FormatDiagnostics(analyzer, actualResults.ToArray()) : " NONE."; - Assert.IsTrue(false, - string.Format("Mismatch between number of diagnostics returned, expected \"{0}\" actual \"{1}\"\r\n\r\nDiagnostics:\r\n{2}\r\n", - expectedCount, actualCount, diagnosticsOutput)); + Assert.Fail( + $"Mismatch between number of diagnostics returned, expected \"{expectedCount}\" actual \"{actualCount}\"\r\n\r\nDiagnostics:\r\n{diagnosticsOutput}\r\n"); } for (var i = 0; i < expectedResults.Length; i++) @@ -178,9 +178,8 @@ static void VerifyDiagnosticResults(IEnumerable actualResults, Diagn { if (actual.Location != Location.None) { - Assert.IsTrue(false, - string.Format("Expected:\nA project diagnostic with No location\nActual:\n{0}", - FormatDiagnostics(analyzer, actual))); + Assert.Fail( + $"Expected:\nA project diagnostic with No location\nActual:\n{FormatDiagnostics(analyzer, actual)}"); } } else @@ -190,10 +189,8 @@ static void VerifyDiagnosticResults(IEnumerable actualResults, Diagn if (additionalLocations.Length != expected.Locations.Length - 1) { - Assert.IsTrue(false, - string.Format("Expected {0} additional locations but got {1} for Diagnostic:\r\n {2}\r\n", - expected.Locations.Length - 1, additionalLocations.Length, - FormatDiagnostics(analyzer, actual))); + Assert.Fail( + $"Expected {expected.Locations.Length - 1} additional locations but got {additionalLocations.Length} for Diagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n"); } for (var j = 0; j < additionalLocations.Length; ++j) @@ -202,23 +199,20 @@ static void VerifyDiagnosticResults(IEnumerable actualResults, Diagn if (actual.Id != expected.Id) { - Assert.IsTrue(false, - string.Format("Expected diagnostic id to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", - expected.Id, actual.Id, FormatDiagnostics(analyzer, actual))); + Assert.Fail( + $"Expected diagnostic id to be \"{expected.Id}\" was \"{actual.Id}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n"); } if (actual.Severity != expected.Severity) { - Assert.IsTrue(false, - string.Format("Expected diagnostic severity to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", - expected.Severity, actual.Severity, FormatDiagnostics(analyzer, actual))); + Assert.Fail( + $"Expected diagnostic severity to be \"{expected.Severity}\" was \"{actual.Severity}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n"); } if (actual.GetMessage() != expected.Message) { - Assert.IsTrue(false, - string.Format("Expected diagnostic message to be \"{0}\" was \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", - expected.Message, actual.GetMessage(), FormatDiagnostics(analyzer, actual))); + Assert.Fail( + $"Expected diagnostic message to be \"{expected.Message}\" was \"{actual.GetMessage()}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, actual)}\r\n"); } } } @@ -234,9 +228,9 @@ static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic dia { var actualSpan = actual.GetLineSpan(); - Assert.IsTrue(actualSpan.Path == expected.Path || actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test."), - string.Format("Expected diagnostic to be in file \"{0}\" was actually in file \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", - expected.Path, actualSpan.Path, FormatDiagnostics(analyzer, diagnostic))); + Assert.That(actualSpan.Path == expected.Path || actualSpan.Path != null && actualSpan.Path.Contains("Test0.") && expected.Path.Contains("Test."), + Is.True, + $"Expected diagnostic to be in file \"{expected.Path}\" was actually in file \"{actualSpan.Path}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, diagnostic)}\r\n"); var actualLinePosition = actualSpan.StartLinePosition; @@ -245,9 +239,8 @@ static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic dia { if (actualLinePosition.Line + 1 != expected.Line) { - Assert.IsTrue(false, - string.Format("Expected diagnostic to be on line \"{0}\" was actually on line \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", - expected.Line, actualLinePosition.Line + 1, FormatDiagnostics(analyzer, diagnostic))); + Assert.Fail( + $"Expected diagnostic to be on line \"{expected.Line}\" was actually on line \"{actualLinePosition.Line + 1}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, diagnostic)}\r\n"); } } @@ -256,9 +249,8 @@ static void VerifyDiagnosticLocation(DiagnosticAnalyzer analyzer, Diagnostic dia { if (actualLinePosition.Character + 1 != expected.Column) { - Assert.IsTrue(false, - string.Format("Expected diagnostic to start at column \"{0}\" was actually at column \"{1}\"\r\n\r\nDiagnostic:\r\n {2}\r\n", - expected.Column, actualLinePosition.Character + 1, FormatDiagnostics(analyzer, diagnostic))); + Assert.Fail( + $"Expected diagnostic to start at column \"{expected.Column}\" was actually at column \"{actualLinePosition.Character + 1}\"\r\n\r\nDiagnostic:\r\n {FormatDiagnostics(analyzer, diagnostic)}\r\n"); } } } diff --git a/tests/MassTransit.Azure.Cosmos.Tests/Container_Specs.cs b/tests/MassTransit.Azure.Cosmos.Tests/Container_Specs.cs index 8f307ba903a..6876f21d62c 100644 --- a/tests/MassTransit.Azure.Cosmos.Tests/Container_Specs.cs +++ b/tests/MassTransit.Azure.Cosmos.Tests/Container_Specs.cs @@ -108,10 +108,10 @@ await harness.Bus.Publish(new StartTest TestKey = "Unique" }); - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.CorrelationId == correlationId)); + Assert.That(await harness.Published.Any(x => x.Context.Message.CorrelationId == correlationId), Is.True); // For some reason, the LINQ provider for Cosmos does not properly resolve this query. - var sagaHarness = harness.GetSagaStateMachineHarness(); + var sagaHarness = harness.GetSagaStateMachineHarness(); // Assert.IsNotNull(await sagaHarness.Exists(correlationId, x => x.Active)); await harness.Bus.Publish(new UpdateTest @@ -120,7 +120,7 @@ await harness.Bus.Publish(new UpdateTest TestKey = "Unique" }); - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.CorrelationId == correlationId)); + Assert.That(await harness.Published.Any(x => x.Context.Message.CorrelationId == correlationId), Is.True); } } diff --git a/tests/MassTransit.Azure.Cosmos.Tests/JobConsumer_Specs.cs b/tests/MassTransit.Azure.Cosmos.Tests/JobConsumer_Specs.cs new file mode 100644 index 00000000000..14b8d3cd76e --- /dev/null +++ b/tests/MassTransit.Azure.Cosmos.Tests/JobConsumer_Specs.cs @@ -0,0 +1,157 @@ +namespace MassTransit.Azure.Cosmos.Tests +{ + using System; + using System.Threading.Tasks; + using Contracts.JobService; + using JobConsumerTests; + using Logging; + using Microsoft.Azure.Cosmos; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + namespace JobConsumerTests + { + using System; + using System.Threading.Tasks; + using Contracts.JobService; + + + public interface OddJob + { + TimeSpan Duration { get; } + } + + + public class OddJobConsumer : + IJobConsumer + { + public async Task Run(JobContext context) + { + if (context.RetryAttempt == 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + } + } + + + public class OddJobCompletedConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + return Task.CompletedTask; + } + } + } + + + public class Using_the_new_job_service_configuration + { + readonly string _collectionName; + readonly string _databaseName; + Container _container; + CosmosClient _cosmosClient; + + Database _database; + + public Using_the_new_job_service_configuration() + { + _databaseName = "jobService"; + _collectionName = "sagas"; + } + + [Test] + public async Task Should_complete_the_job() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => options.Disable("Microsoft")); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); + + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.AddJobSagaStateMachines() + .JobEndpoint(e => e.Name = "da-job") + .JobAttemptEndpoint(e => e.Name = "da_job-attempt") + .CosmosRepository(r => + { + r.AccountEndpoint = Configuration.AccountEndpoint; + r.AuthKeyOrResourceToken = Configuration.AccountKey; + + r.DatabaseId = _databaseName; + r.CollectionId = _collectionName; + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + try + { + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + MassTransit.Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(1) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + } + finally + { + await harness.Stop(); + } + } + + [OneTimeSetUp] + public async Task Setup() + { + _cosmosClient = new CosmosClient(Configuration.AccountEndpoint, Configuration.AccountKey); + + var databaseResponse = await _cosmosClient.CreateDatabaseIfNotExistsAsync(_databaseName).ConfigureAwait(false); + _database = databaseResponse.Database; + + var containerResponse = await _database.CreateContainerIfNotExistsAsync(_collectionName, "/id").ConfigureAwait(false); + _container = containerResponse.Container; + } + + [OneTimeTearDown] + public async Task Teardown() + { + await _container.DeleteContainerAsync().ConfigureAwait(false); + await _database.DeleteAsync().ConfigureAwait(false); + } + } +} diff --git a/tests/MassTransit.Azure.Cosmos.Tests/MassTransit.Azure.Cosmos.Tests.csproj b/tests/MassTransit.Azure.Cosmos.Tests/MassTransit.Azure.Cosmos.Tests.csproj index 6ff47f0eb47..7eb178c5974 100644 --- a/tests/MassTransit.Azure.Cosmos.Tests/MassTransit.Azure.Cosmos.Tests.csproj +++ b/tests/MassTransit.Azure.Cosmos.Tests/MassTransit.Azure.Cosmos.Tests.csproj @@ -1,13 +1,17 @@  - net6.0 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/MassTransit.Azure.Cosmos.Tests/MissingInstance_Specs.cs b/tests/MassTransit.Azure.Cosmos.Tests/MissingInstance_Specs.cs index d1e6cd4cf40..4c599a614c7 100644 --- a/tests/MassTransit.Azure.Cosmos.Tests/MissingInstance_Specs.cs +++ b/tests/MassTransit.Azure.Cosmos.Tests/MissingInstance_Specs.cs @@ -175,9 +175,12 @@ public async Task Should_work_as_expected() Response response = await statusClient.GetResponse(new CheckStatus("A")); - Assert.That(response.Is(out MassTransit.Response status), Is.True); + Assert.Multiple(() => + { + Assert.That(response.Is(out MassTransit.Response status), Is.True); - Assert.AreEqual("A", status.Message.ServiceName); + Assert.That(status.Message.ServiceName, Is.EqualTo("A")); + }); } readonly string _databaseName; @@ -237,7 +240,7 @@ public async Task Should_match_an_existing_instance() MassTransit.Response result = await status; - Assert.AreEqual("A", result.Message.ServiceName); + Assert.That(result.Message.ServiceName, Is.EqualTo("A")); Assert.That(async () => await notFound, Throws.TypeOf()); } @@ -254,7 +257,7 @@ public async Task Should_publish_the_event_of_the_missing_instance() MassTransit.Response result = await notFound; - Assert.AreEqual("Z", result.Message.ServiceName); + Assert.That(result.Message.ServiceName, Is.EqualTo("Z")); Assert.That(async () => await status, Throws.TypeOf()); } diff --git a/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmosConcurrencyNoRetry_Specs.cs b/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmosConcurrencyNoRetry_Specs.cs index 0059032b8fa..b1393723f89 100644 --- a/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmosConcurrencyNoRetry_Specs.cs +++ b/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmosConcurrencyNoRetry_Specs.cs @@ -25,7 +25,7 @@ public async Task Should_not_be_in_final_state() var saga = await GetSagaRetry(correlationId, TestTimeout); - Assert.IsNotNull(saga); + Assert.That(saga, Is.Not.Null); await Task.WhenAll( InputQueueSendEndpoint.Send(new Bass @@ -53,7 +53,7 @@ await Task.WhenAll( // Because concurrency exception's happened without retry middleware configured, we aren't in our final state/ var instance = await GetSaga(correlationId); - Assert.IsTrue(!instance.CurrentState.Equals("Harmony")); + Assert.That(instance.CurrentState, Is.Not.EqualTo("Harmony")); } [Test] @@ -74,7 +74,7 @@ public async Task Some_should_not_be_in_final_state_all() for (var i = 0; i < 20; i++) { var saga = await GetSagaRetry(sagaIds[i], TestTimeout); - Assert.IsNotNull(saga); + Assert.That(saga, Is.Not.Null); } for (var i = 0; i < 20; i++) @@ -116,7 +116,7 @@ public async Task Some_should_not_be_in_final_state_all() break; } - Assert.IsTrue(someNotInFinalState); + Assert.That(someNotInFinalState, Is.True); } ChoirStateMachine _machine; diff --git a/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmosConcurrencyOptimistic_Specs.cs b/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmosConcurrencyOptimistic_Specs.cs index f7ddd489532..e41ce86b9a7 100644 --- a/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmosConcurrencyOptimistic_Specs.cs +++ b/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmosConcurrencyOptimistic_Specs.cs @@ -36,7 +36,7 @@ public async Task Should_capture_all_events_many_sagas() for (var i = 0; i < 20; i++) { var saga = await GetSagaRetry(sagaIds[i], TestTimeout); - Assert.IsNotNull(saga); + Assert.That(saga, Is.Not.Null); } for (var i = 0; i < 20; i++) @@ -72,8 +72,8 @@ public async Task Should_capture_all_events_many_sagas() { var instance = await GetSagaRetry(sid, TestTimeout, x => x.CurrentState == _machine.Harmony.Name); - Assert.IsNotNull(instance); - Assert.IsTrue(instance.CurrentState.Equals("Harmony")); + Assert.That(instance, Is.Not.Null); + Assert.That(instance.CurrentState, Is.EqualTo("Harmony")); } } @@ -87,7 +87,7 @@ public async Task Should_capture_all_events_single_saga() var saga = await GetSagaRetry(correlationId, TestTimeout); - Assert.IsNotNull(saga); + Assert.That(saga, Is.Not.Null); await Task.WhenAll( InputQueueSendEndpoint.Send(new Bass @@ -114,12 +114,12 @@ await Task.WhenAll( saga = await GetSagaRetry(correlationId, TestTimeout, x => x.CurrentState == _machine.Harmony.Name); - Assert.IsNotNull(saga); - Assert.IsTrue(saga.CurrentState == _machine.Harmony.Name); + Assert.That(saga, Is.Not.Null); + Assert.That(saga.CurrentState, Is.EqualTo(_machine.Harmony.Name)); var instance = await GetSaga(correlationId); - Assert.IsTrue(instance.CurrentState.Equals("Harmony")); + Assert.That(instance.CurrentState, Is.EqualTo("Harmony")); } ChoirStateMachine _machine; @@ -134,7 +134,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin { _machine = new ChoirStateMachine(); - configurator.UseRetry(x => + configurator.UseMessageRetry(x => { x.Handle(); x.Interval(5, 300); diff --git a/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmos_Specs.cs b/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmos_Specs.cs index 6bbde87d094..1ea66240dcc 100644 --- a/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmos_Specs.cs +++ b/tests/MassTransit.Azure.Cosmos.Tests/UsingCosmos_Specs.cs @@ -23,12 +23,12 @@ public async Task Should_have_removed_the_state_machine() await InputQueueSendEndpoint.Send(new GirlfriendYelling { CorrelationId = correlationId }); var saga = await GetSagaRetry(correlationId, TestTimeout); - Assert.IsNotNull(saga); + Assert.That(saga, Is.Not.Null); await InputQueueSendEndpoint.Send(new SodOff { CorrelationId = correlationId }); saga = await GetNoSagaRetry(correlationId, TestTimeout); - Assert.IsNull(saga); + Assert.That(saga, Is.Null); } [Test] @@ -40,18 +40,18 @@ public async Task Should_have_the_state_machine() var saga = await GetSagaRetry(correlationId, TestTimeout); - Assert.IsNotNull(saga); + Assert.That(saga, Is.Not.Null); await InputQueueSendEndpoint.Send(new GotHitByACar { CorrelationId = correlationId }); saga = await GetSagaRetry(correlationId, TestTimeout, x => x.CurrentState == _machine.Dead.Name); - Assert.IsNotNull(saga); - Assert.IsTrue(saga.CurrentState == _machine.Dead.Name); + Assert.That(saga, Is.Not.Null); + Assert.That(saga.CurrentState, Is.EqualTo(_machine.Dead.Name)); var instance = await GetSaga(correlationId); - Assert.IsTrue(instance.Screwed); + Assert.That(instance.Screwed, Is.True); } SuperShopper _machine; @@ -66,7 +66,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin { _machine = new SuperShopper(); - configurator.UseRetry(x => + configurator.UseMessageRetry(x => { x.Immediate(5); }); diff --git a/tests/MassTransit.Azure.Cosmos.Tests/docker-compose.yml b/tests/MassTransit.Azure.Cosmos.Tests/docker-compose.yml new file mode 100644 index 00000000000..66ad9dcebd9 --- /dev/null +++ b/tests/MassTransit.Azure.Cosmos.Tests/docker-compose.yml @@ -0,0 +1,33 @@ +services: + cosmosdb: + container_name: "azure-cosmosdb-emulator" + hostname: "azurecosmosemulator" + image: 'mcr.microsoft.com/cosmosdb/linux/azure-cosmos-emulator:latest' + ports: + - '8081:8081' # Data Explorer + - '8900:8900' + - '8901:8901' + - '8902:8902' + - '10250:10250' + - '10251:10251' + - '10252:10252' + - '10253:10253' + - '10254:10254' + - '10255:10255' + - '10256:10256' + - '10350:10350' + healthcheck: + test: + [ + "CMD", + "curl", + "-f", + "-k", + "https://localhost:8081/_explorer/emulator.pem", + ] + interval: 30s + timeout: 10s + retries: 3 + environment: + - AZURE_COSMOS_EMULATOR_PARTITION_COUNT=3 + - AZURE_COSMOS_EMULATOR_ENABLE_DATA_PERSISTENCE=false diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Address_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Address_Specs.cs index 02ac6c5ad5f..126477f8ae2 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Address_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Address_Specs.cs @@ -1,8 +1,6 @@ namespace MassTransit.Azure.ServiceBus.Core.Tests { using System; - using System.Linq; - using AzureServiceBusTransport; using Internals; using NUnit.Framework; using TestFramework.Messages; @@ -13,64 +11,58 @@ public class An_address : AzureServiceBusTestFixture { [Test] - public void Should_get_the_bus_address() - { - var queueName = Bus.Address.AbsolutePath.Split('/').Last(); - - var address = Bus.GetServiceBusBusTopology().GetDestinationAddress(queueName, x => x.AutoDeleteOnIdle = Defaults.TemporaryAutoDeleteOnIdle); - - Assert.That(address, Is.EqualTo(Bus.Address)); - } - - [Test] - public void Should_get_the_queue_address() - { - var address = Bus.GetServiceBusBusTopology().GetDestinationAddress("input_queue"); - - Assert.That(address, Is.EqualTo(InputQueueAddress)); - } - - [Test] - public void Should_have_the_full_path() + public void Should_handle_a_topic_address() { - var address = new ServiceBusEndpointAddress(HostAddress, "input_queue"); + var address = new ServiceBusEndpointAddress(AzureServiceBusTestHarness.HostAddress, new Uri("topic:private-topic")); - Assert.That((Uri)address, Is.EqualTo(InputQueueAddress)); + Assert.That(address.Type, Is.EqualTo(ServiceBusEndpointAddress.AddressType.Topic)); - Assert.That(address.Name, Is.EqualTo("input_queue")); - Assert.That(address.Scope, Is.EqualTo(typeof(An_address).Namespace)); + Uri uri = address; + Assert.Multiple(() => + { + Assert.That(uri.TryGetValueFromQueryString("type", out var type), Is.True); + Assert.That(type, Is.EqualTo("topic")); - Assert.That(address.Path, Is.EqualTo(InputQueueAddress.AbsolutePath.Substring(1))); + Assert.That(uri.AbsolutePath, Is.EqualTo("/private-topic")); + }); } [Test] public void Should_handle_address_loopback() { - Assert.IsTrue(Bus.Topology.TryGetPublishAddress(out var address)); + Assert.Multiple(() => + { + Assert.That(Bus.Topology.TryGetPublishAddress(out var address), Is.True); - Assert.That(address.TryGetValueFromQueryString("type", out var type), Is.True); - Assert.That(type, Is.EqualTo("topic")); + Assert.That(address.TryGetValueFromQueryString("type", out var type), Is.True); + Assert.That(type, Is.EqualTo("topic")); - Uri normalizedAddress = new ServiceBusEndpointAddress(AzureServiceBusTestHarness.HostAddress, address); + Uri normalizedAddress = new ServiceBusEndpointAddress(AzureServiceBusTestHarness.HostAddress, address); - Assert.That(normalizedAddress.TryGetValueFromQueryString("type", out type), Is.True); - Assert.That(type, Is.EqualTo("topic")); + Assert.Multiple(() => + { + Assert.That(normalizedAddress.TryGetValueFromQueryString("type", out type), Is.True); + Assert.That(type, Is.EqualTo("topic")); - Assert.That(normalizedAddress.AbsolutePath, Is.EqualTo(address.AbsolutePath)); + Assert.That(normalizedAddress.AbsolutePath, Is.EqualTo(address.AbsolutePath)); + }); + }); } [Test] - public void Should_handle_a_topic_address() + public void Should_have_the_full_path() { - var address = new ServiceBusEndpointAddress(AzureServiceBusTestHarness.HostAddress, new Uri("topic:private-topic")); + var address = new ServiceBusEndpointAddress(HostAddress, "input_queue"); - Assert.That(address.Type, Is.EqualTo(ServiceBusEndpointAddress.AddressType.Topic)); + Assert.Multiple(() => + { + Assert.That((Uri)address, Is.EqualTo(InputQueueAddress)); - Uri uri = address; - Assert.That(uri.TryGetValueFromQueryString("type", out var type), Is.True); - Assert.That(type, Is.EqualTo("topic")); + Assert.That(address.Name, Is.EqualTo("input_queue")); + Assert.That(address.Scope, Is.EqualTo(typeof(An_address).Namespace)); - Assert.That(uri.AbsolutePath, Is.EqualTo("/private-topic")); + Assert.That(address.Path, Is.EqualTo(InputQueueAddress.AbsolutePath.Substring(1))); + }); } } } diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/BuildTopology_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/BuildTopology_Specs.cs index 03208bcbbda..f3e97b62f0e 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/BuildTopology_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/BuildTopology_Specs.cs @@ -50,8 +50,8 @@ public class Publish_with_complex_hierarchy : [Test] public async Task Should_be_received() { - await Bus.Publish(new {Value = "A"}); - await Bus.Publish(new {Value = "B"}); + await Bus.Publish(new { Value = "A" }); + await Bus.Publish(new { Value = "B" }); ConsumeContext received = await _receivedA; @@ -119,17 +119,25 @@ public void Should_include_a_binding_for_the_second_interface_only() var interfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)).ToString(); - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == interfaceName), Is.True); - Assert.That( - topology.QueueSubscriptions.Any(x => x.Source.CreateTopicOptions.Name == interfaceName && x.Destination.CreateQueueOptions.Name == _inputQueueName), - Is.True); + Assert.Multiple(() => + { + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == interfaceName), Is.True); + Assert.That( + topology.QueueSubscriptions.Any(x => + x.Source.CreateTopicOptions.Name == interfaceName && x.Destination.CreateQueueOptions.Name == _inputQueueName), + Is.True); + }); interfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)).ToString(); - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == interfaceName), Is.False); - Assert.That( - topology.QueueSubscriptions.Any(x => x.Source.CreateTopicOptions.Name == interfaceName && x.Destination.CreateQueueOptions.Name == _inputQueueName), - Is.False); + Assert.Multiple(() => + { + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == interfaceName), Is.False); + Assert.That( + topology.QueueSubscriptions.Any(x => + x.Source.CreateTopicOptions.Name == interfaceName && x.Destination.CreateQueueOptions.Name == _inputQueueName), + Is.False); + }); } [Test] @@ -144,11 +152,14 @@ public void Should_include_a_binding_for_the_single_interface() var singleInterfaceName = _nameFormatter.GetMessageName(typeof(SingleInterface)).ToString(); - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); - Assert.That( - topology.QueueSubscriptions.Any(x => - x.Source.CreateTopicOptions.Name == singleInterfaceName && x.Destination.CreateQueueOptions.Name == _inputQueueName), - Is.True); + Assert.Multiple(() => + { + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); + Assert.That( + topology.QueueSubscriptions.Any(x => + x.Source.CreateTopicOptions.Name == singleInterfaceName && x.Destination.CreateQueueOptions.Name == _inputQueueName), + Is.True); + }); } [SetUp] @@ -156,7 +167,7 @@ public void Setup() { _nameFormatter = new ServiceBusMessageNameFormatter(); _entityNameFormatter = new MessageNameFormatterEntityNameFormatter(_nameFormatter); - _consumeTopology = new ServiceBusConsumeTopology(AzureBusFactory.MessageTopology, new ServiceBusPublishTopology(AzureBusFactory.MessageTopology)); + _consumeTopology = new ServiceBusConsumeTopology(AzureBusFactory.CreateMessageTopology(), new ServiceBusPublishTopology(AzureBusFactory.CreateMessageTopology())); _builder = new ReceiveEndpointBrokerTopologyBuilder(); @@ -186,14 +197,20 @@ public void Should_include_a_binding_for_the_second_interface_only() var singleInterfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)).ToString(); var interfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)).ToString(); - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == interfaceName), Is.True); - Assert.That(topology.Topics.Length, Is.EqualTo(2)); - Assert.That(topology.TopicSubscriptions.Length, Is.EqualTo(1)); - Assert.That( - topology.TopicSubscriptions.Any(x => - x.Source.CreateTopicOptions.Name == interfaceName && x.Destination.CreateTopicOptions.Name == singleInterfaceName), Is.True); - - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); + Assert.Multiple(() => + { + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == interfaceName), Is.True); + Assert.That(topology.Topics, Has.Length.EqualTo(2)); + Assert.That(topology.TopicSubscriptions, Has.Length.EqualTo(1)); + }); + Assert.Multiple(() => + { + Assert.That( + topology.TopicSubscriptions.Any(x => + x.Source.CreateTopicOptions.Name == interfaceName && x.Destination.CreateTopicOptions.Name == singleInterfaceName), Is.True); + + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); + }); } [Test] @@ -206,9 +223,12 @@ public void Should_include_a_binding_for_the_single_interface() var singleInterfaceName = _nameFormatter.GetMessageName(typeof(SingleInterface)).ToString(); - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); - Assert.That(topology.Topics.Length, Is.EqualTo(1)); - Assert.That(topology.TopicSubscriptions.Length, Is.EqualTo(0)); + Assert.Multiple(() => + { + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); + Assert.That(topology.Topics, Has.Length.EqualTo(1)); + Assert.That(topology.TopicSubscriptions, Is.Empty); + }); } [SetUp] @@ -216,7 +236,7 @@ public void Setup() { _nameFormatter = new ServiceBusMessageNameFormatter(); _entityNameFormatter = new MessageNameFormatterEntityNameFormatter(_nameFormatter); - _publishTopology = new ServiceBusPublishTopology(AzureBusFactory.MessageTopology); + _publishTopology = new ServiceBusPublishTopology(AzureBusFactory.CreateMessageTopology()); _builder = new PublishEndpointBrokerTopologyBuilder(_publishTopology); } @@ -243,14 +263,20 @@ public void Should_include_a_binding_for_the_second_interface_only() var singleInterfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)).ToString(); var interfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)).ToString(); - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == interfaceName), Is.True); - Assert.That(topology.Topics.Length, Is.EqualTo(2)); - Assert.That(topology.TopicSubscriptions.Length, Is.EqualTo(1)); - Assert.That( - topology.TopicSubscriptions.Any(x => - x.Source.CreateTopicOptions.Name == interfaceName && x.Destination.CreateTopicOptions.Name == singleInterfaceName), Is.True); - - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); + Assert.Multiple(() => + { + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == interfaceName), Is.True); + Assert.That(topology.Topics, Has.Length.EqualTo(2)); + Assert.That(topology.TopicSubscriptions, Has.Length.EqualTo(1)); + }); + Assert.Multiple(() => + { + Assert.That( + topology.TopicSubscriptions.Any(x => + x.Source.CreateTopicOptions.Name == interfaceName && x.Destination.CreateTopicOptions.Name == singleInterfaceName), Is.True); + + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); + }); } [Test] @@ -264,9 +290,12 @@ public void Should_include_a_binding_for_the_single_interface() var singleInterfaceName = _nameFormatter.GetMessageName(typeof(SingleInterface)).ToString(); - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); - Assert.That(topology.Topics.Length, Is.EqualTo(1)); - Assert.That(topology.TopicSubscriptions.Length, Is.EqualTo(0)); + Assert.Multiple(() => + { + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == singleInterfaceName), Is.True); + Assert.That(topology.Topics, Has.Length.EqualTo(1)); + Assert.That(topology.TopicSubscriptions, Is.Empty); + }); } [Test] @@ -282,20 +311,26 @@ public void Should_include_a_binding_for_the_third_interface_as_well() var secondInterfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)).ToString(); var thirdInterfaceName = _nameFormatter.GetMessageName(typeof(ThirdInterface)).ToString(); - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == secondInterfaceName), Is.True); - Assert.That(topology.Topics.Length, Is.EqualTo(3)); - Assert.That(topology.TopicSubscriptions.Length, Is.EqualTo(2)); - Assert.That( - topology.TopicSubscriptions.Any(x => - x.Source.CreateTopicOptions.Name == secondInterfaceName && x.Destination.CreateTopicOptions.Name == firstInterfaceName), - Is.True); - - Assert.That( - topology.TopicSubscriptions.Any(x => - x.Source.CreateTopicOptions.Name == thirdInterfaceName && x.Destination.CreateTopicOptions.Name == secondInterfaceName), - Is.True); - - Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == firstInterfaceName), Is.True); + Assert.Multiple(() => + { + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == secondInterfaceName), Is.True); + Assert.That(topology.Topics, Has.Length.EqualTo(3)); + Assert.That(topology.TopicSubscriptions, Has.Length.EqualTo(2)); + }); + Assert.Multiple(() => + { + Assert.That( + topology.TopicSubscriptions.Any(x => + x.Source.CreateTopicOptions.Name == secondInterfaceName && x.Destination.CreateTopicOptions.Name == firstInterfaceName), + Is.True); + + Assert.That( + topology.TopicSubscriptions.Any(x => + x.Source.CreateTopicOptions.Name == thirdInterfaceName && x.Destination.CreateTopicOptions.Name == secondInterfaceName), + Is.True); + + Assert.That(topology.Topics.Any(x => x.CreateTopicOptions.Name == firstInterfaceName), Is.True); + }); } [SetUp] @@ -303,7 +338,7 @@ public void Setup() { _nameFormatter = new ServiceBusMessageNameFormatter(); _entityNameFormatter = new MessageNameFormatterEntityNameFormatter(_nameFormatter); - _publishTopology = new ServiceBusPublishTopology(AzureBusFactory.MessageTopology); + _publishTopology = new ServiceBusPublishTopology(AzureBusFactory.CreateMessageTopology()); _builder = new PublishEndpointBrokerTopologyBuilder(_publishTopology); } diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Conductor_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Conductor_Specs.cs deleted file mode 100644 index c51b811b84a..00000000000 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Conductor_Specs.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace MassTransit.Azure.ServiceBus.Core.Tests -{ - namespace ConductorTests - { - using System.Threading.Tasks; - using Contracts; - using NUnit.Framework; - - - namespace Contracts - { - using System; - - - public interface DeployHappiness - { - string Target { get; } - } - - - public interface DeployPayload - { - string Target { get; } - } - - - public interface PayloadDeployed - { - DateTime Timestamp { get; } - string Target { get; } - } - } - - - public class DeployPayloadConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return context.RespondAsync(new - { - InVar.Timestamp, - context.Message.Target - }); - } - } - - - [TestFixture] - public class Using_conductor_for_service_discovery : - AzureServiceBusTestFixture - { - [Test] - public async Task Should_connect_using_the_service_client() - { - IRequestClient requestClient = Bus.CreateRequestClient(); - - Response response = await requestClient.GetResponse(new {Target = "Bogey"}); - - Assert.That(response.Message.Target, Is.EqualTo("Bogey")); - } - - protected override void ConfigureServiceBusBus(IServiceBusBusFactoryConfigurator configurator) - { - configurator.ServiceInstance(instance => - { - var serviceEndpointName = KebabCaseEndpointNameFormatter.Instance.Consumer(); - - instance.ReceiveEndpoint(serviceEndpointName, x => - { - x.Consumer(); - }); - }); - } - } - } -} diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/ConfiguringAzure_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ConfiguringAzure_Specs.cs index df979ed39bc..c60ea8d5ad6 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/ConfiguringAzure_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ConfiguringAzure_Specs.cs @@ -111,7 +111,7 @@ public async Task Should_not_fail_when_slash_is_missing() // if an exception is thrown e.Handler(Handle, h => { - h.UseRetry(r => r.Interval(5, 100)); + h.UseMessageRetry(r => r.Interval(5, 100)); }); }); }); @@ -191,7 +191,7 @@ public async Task Should_support_the_new_syntax() // if an exception is thrown e.Handler(Handle, h => { - h.UseRetry(r => r.Interval(5, 100)); + h.UseMessageRetry(r => r.Interval(5, 100)); }); }); }); diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/DeadLetter_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/DeadLetter_Specs.cs index 5c7e22b5c1b..49bcf4923f8 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/DeadLetter_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/DeadLetter_Specs.cs @@ -180,7 +180,7 @@ public async Task Should_redeliver_with_all_headers_intact() protected override void ConfigureServiceBusBus(IServiceBusBusFactoryConfigurator configurator) { - configurator.UseRawJsonSerializer(RawSerializerOptions.AddTransportHeaders | RawSerializerOptions.CopyHeaders); + configurator.UseRawJsonSerializer(); configurator.ReceiveEndpoint("input_queue_dl_fault", x => { diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/DuplicateDelivery_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/DuplicateDelivery_Specs.cs new file mode 100644 index 00000000000..e448f0a6d7f --- /dev/null +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/DuplicateDelivery_Specs.cs @@ -0,0 +1,44 @@ +namespace MassTransit.Azure.ServiceBus.Core.Tests +{ + using System; + using System.Threading.Tasks; + using NUnit.Framework; + using TestFramework.Messages; + using Testing; + + + [TestFixture] + public class Receive_single_message_with_tiny_lock_timeout : + AzureServiceBusTestFixture + { + [Test] + public async Task Should_receive_single_message() + { + await InputQueueSendEndpoint.Send(new PingMessage()); + + await InactivityTask; + + var count = await BusTestHarness.Consumed.SelectAsync(x => x.Exception is null).Count(); + + Assert.That(count, Is.EqualTo(1)); + } + + protected override void ConfigureServiceBusReceiveEndpoint(IServiceBusReceiveEndpointConfigurator configurator) + { + configurator.MaxDeliveryCount = 5; + configurator.LockDuration = TimeSpan.FromSeconds(5); + configurator.MaxAutoRenewDuration = TimeSpan.FromSeconds(5); + configurator.Consumer(() => new TestConsumer()); + } + + + class TestConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.Delay(TimeSpan.FromSeconds(10)); + } + } + } +} diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/EndpointConfiguration_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/EndpointConfiguration_Specs.cs index 2a2acba8658..9b6ef7dd625 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/EndpointConfiguration_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/EndpointConfiguration_Specs.cs @@ -3,7 +3,6 @@ namespace MassTransit.Azure.ServiceBus.Core.Tests using System; using System.Linq; using System.Threading.Tasks; - using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -11,6 +10,7 @@ namespace MassTransit.Azure.ServiceBus.Core.Tests using NUnit.Framework; using TestFramework; using TestFramework.Messages; + using Testing; [TestFixture] @@ -42,9 +42,12 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_overrid var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -73,9 +76,12 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_specifi var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -104,9 +110,12 @@ public async Task Should_include_concurrency_filter_if_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -135,8 +144,11 @@ public async Task Should_include_nothing_if_not_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -157,8 +169,11 @@ public void Should_override_bus_setting_if_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); } [Test] @@ -190,8 +205,11 @@ public void Should_use_bus_setting_if_not_specified() var probe = JObject.Parse(busControl.GetProbeResult().ToJsonString()); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); } @@ -204,7 +222,8 @@ public PingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -223,7 +242,8 @@ public EndpointPingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -233,7 +253,8 @@ class EmptyPingConsumerDefinition : ConsumerDefinition { protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/ErrorQueue_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ErrorQueue_Specs.cs index 14289dda68a..259ec13ffac 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/ErrorQueue_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ErrorQueue_Specs.cs @@ -4,7 +4,6 @@ using System.Runtime.Serialization; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework.Messages; @@ -17,55 +16,49 @@ public async Task Should_have_the_correlation_id() { ConsumeContext context = await _errorHandler; - context.CorrelationId.ShouldBe(_correlationId); + Assert.That(context.CorrelationId, Is.EqualTo(_correlationId)); } [Test] public async Task Should_have_the_exception() { ConsumeContext context = await _errorHandler; - - context.ReceiveContext.TransportHeaders.Get("MT-Fault-Message", (string)null).ShouldBe("This is fine, forcing death"); + Assert.That(context.ReceiveContext.TransportHeaders.Get("MT-Fault-Message", (string)null), Is.EqualTo("This is fine, forcing death")); } [Test] public async Task Should_have_the_original_destination_address() { ConsumeContext context = await _errorHandler; - - context.DestinationAddress.ShouldBe(InputQueueAddress); + Assert.That(context.DestinationAddress, Is.EqualTo(InputQueueAddress)); } [Test] public async Task Should_have_the_original_fault_address() { ConsumeContext context = await _errorHandler; - - context.FaultAddress.ShouldBe(BusAddress); + Assert.That(context.FaultAddress, Is.EqualTo(BusAddress)); } [Test] public async Task Should_have_the_original_response_address() { ConsumeContext context = await _errorHandler; - - context.ResponseAddress.ShouldBe(BusAddress); + Assert.That(context.ResponseAddress, Is.EqualTo(BusAddress)); } [Test] public async Task Should_have_the_original_source_address() { ConsumeContext context = await _errorHandler; - - context.SourceAddress.ShouldBe(BusAddress); + Assert.That(context.SourceAddress, Is.EqualTo(BusAddress)); } [Test] public async Task Should_have_the_reason() { ConsumeContext context = await _errorHandler; - - context.ReceiveContext.TransportHeaders.Get("MT-Reason", (string)null).ShouldBe("fault"); + Assert.That(context.ReceiveContext.TransportHeaders.Get("MT-Reason", (string)null), Is.EqualTo("fault")); } [Test] diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/ExistingSubscription_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ExistingSubscription_Specs.cs new file mode 100644 index 00000000000..05fda42b535 --- /dev/null +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ExistingSubscription_Specs.cs @@ -0,0 +1,102 @@ +namespace MassTransit.Azure.ServiceBus.Core.Tests +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using global::Azure; + using global::Azure.Messaging.ServiceBus.Administration; + using NUnit.Framework; + using TestFramework; + using Testing; + + + [TestFixture] + [Explicit] + public class Specifying_an_existing_subscription : + AsyncTestFixture + { + [Test] + public async Task Should_not_update_when_no_changes_are_present() + { + var topicName = "not_updated_topic"; + var subscriptionName = "existing_subscription"; + var queueName = "existing_queue"; + var managementClient = Configuration.GetManagementClient(); + + if (await managementClient.TopicExistsAsync(topicName)) + await managementClient.DeleteTopicAsync(topicName); + + if (await managementClient.QueueExistsAsync(queueName)) + await managementClient.DeleteQueueAsync(queueName); + + ServiceBusTokenProviderSettings settings = new TestAzureServiceBusAccountSettings(); + + var serviceUri = AzureServiceBusEndpointUriCreator.Create(Configuration.ServiceNamespace); + + var bus = Bus.Factory.CreateUsingAzureServiceBus(x => + { + BusTestFixture.ConfigureBusDiagnostics(x); + x.Host(serviceUri, h => + { + h.NamedKey(s => + { + s.NamedKeyCredential = settings.NamedKeyCredential; + }); + }); + + x.ReceiveEndpoint(queueName, e => + { + e.Subscribe(topicName, subscriptionName); + }); + }); + + var busHandle = await bus.StartAsync(); + await busHandle.StopAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); + + Response response = await managementClient.GetSubscriptionAsync(topicName, subscriptionName); + + Assert.Multiple(() => + { + Assert.That(response?.Value, Is.Not.Null); + + Assert.That(Uri.IsWellFormedUriString(response.Value.ForwardTo, UriKind.Absolute)); + Assert.That(new Uri(response.Value.ForwardTo).AbsolutePath.TrimStart('/'), Is.EqualTo(queueName)); + }); + + bus = Bus.Factory.CreateUsingAzureServiceBus(x => + { + BusTestFixture.ConfigureBusDiagnostics(x); + x.Host(serviceUri, h => + { + h.NamedKey(s => + { + s.NamedKeyCredential = settings.NamedKeyCredential; + }); + }); + + x.ReceiveEndpoint(queueName, e => + { + e.Subscribe(topicName, subscriptionName); + }); + }); + + busHandle = await bus.StartAsync(); + await busHandle.StopAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); + + await managementClient.GetSubscriptionAsync(topicName, subscriptionName); + + Assert.Multiple(() => + { + Assert.That(response?.Value, Is.Not.Null); + + Assert.That(Uri.IsWellFormedUriString(response.Value.ForwardTo, UriKind.Absolute)); + Assert.That(new Uri(response.Value.ForwardTo).AbsolutePath.TrimStart('/'), Is.EqualTo(queueName)); + }); + } + + public Specifying_an_existing_subscription() + : base(new InMemoryTestHarness()) + { + } + } +} diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Future_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Future_Specs.cs index 146e84e87c2..08c9edf40f4 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Future_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Future_Specs.cs @@ -40,12 +40,13 @@ public Task OneTimeTearDown(IServiceProvider provider) public class AzureServiceBusFutureDefinition : DefaultFutureDefinition where TFuture : class, SagaStateMachine { - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) { if (endpointConfigurator is IServiceBusEndpointConfigurator configurator) configurator.RequiresSession = true; - base.ConfigureSaga(endpointConfigurator, sagaConfigurator); + base.ConfigureSaga(endpointConfigurator, sagaConfigurator, context); } } diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/InMemoryOutboxRedelivery_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/InMemoryOutboxRedelivery_Specs.cs index b8b9b049c14..0eee5dd8634 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/InMemoryOutboxRedelivery_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/InMemoryOutboxRedelivery_Specs.cs @@ -181,7 +181,7 @@ protected override void ConfigureServiceBusBus(IServiceBusBusFactoryConfigurator protected override void ConfigureServiceBusReceiveEndpoint(IServiceBusReceiveEndpointConfigurator configurator) { configurator.UseDelayedRedelivery(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); - configurator.UseRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); + configurator.UseMessageRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); configurator.UseInMemoryOutbox(); configurator.Consumer(); diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/LazyMessageType_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/LazyMessageType_Specs.cs new file mode 100644 index 00000000000..51bb609df8f --- /dev/null +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/LazyMessageType_Specs.cs @@ -0,0 +1,119 @@ +#nullable enable +namespace MassTransit.Azure.ServiceBus.Core.Tests +{ + using System; + using System.Threading.Tasks; + using LazySubjects; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + public class LazyMessageType_Specs + { + [Test] + public async Task Should_not_recursively_call_into_the_topology_types() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddServiceBusMessageScheduler(); + x.UsingTestAzureServiceBus((context, cfg) => + { + cfg.UseServiceBusMessageScheduler(); + + cfg.UseMessageRetry(retryConfiguration => + { + retryConfiguration.Exponential(5, TimeSpan.FromMilliseconds(100), TimeSpan.FromSeconds(5), TimeSpan.FromMilliseconds(500)); + }); + + cfg.Publish(typeof(IntegrationEvent), m => m.Exclude = true); + cfg.Publish(typeof(IIntegrationEvent), m => m.Exclude = true); + + cfg.SetNamespaceSeparatorTo("-"); + cfg.MessageTopology.SetEntityNameFormatter(new CustomEntityNameFormatter(cfg.MessageTopology.EntityNameFormatter)); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var scheduler = harness.Scope.ServiceProvider.GetRequiredService(); + + object sampleEvent = new SampleIntegrationEvent(NewId.NextGuid(), DateTimeOffset.UtcNow, "sample"); + object sampleEvent2 = new SampleIntegrationEvent2(NewId.NextGuid(), DateTimeOffset.UtcNow, "sample"); + + var task1 = scheduler.SchedulePublish(DateTime.UtcNow.AddSeconds(5), sampleEvent, harness.CancellationToken); + var task2 = scheduler.SchedulePublish(DateTime.UtcNow.AddSeconds(5), sampleEvent2, harness.CancellationToken); + var task3 = scheduler.SchedulePublish(DateTime.UtcNow.AddSeconds(5), sampleEvent, harness.CancellationToken); + var task4 = scheduler.SchedulePublish(DateTime.UtcNow.AddSeconds(5), sampleEvent2, harness.CancellationToken); + + await Task.WhenAll(task1, task2, task3, task4); + } + } + + + namespace LazySubjects + { + using System; + using System.Reflection; + + + public interface IIntegrationEvent + { + public Guid EventId { get; init; } + public DateTimeOffset EventDate { get; init; } + } + + + [Serializable] + public abstract record IntegrationEvent( + Guid EventId, + DateTimeOffset EventDate); + + + public sealed record SampleIntegrationEvent( + Guid EventId, + DateTimeOffset EventDate, + string ResourceId) : IntegrationEvent(EventId, EventDate), + IIntegrationEvent + { + public static string EventName() + { + return "my.sample-event"; + } + } + + public sealed record SampleIntegrationEvent2( + Guid EventId, + DateTimeOffset EventDate, + string ResourceId) : IntegrationEvent(EventId, EventDate), + IIntegrationEvent + { + public static string EventName() + { + return "my.sample-event.2"; + } + } + + + public sealed class CustomEntityNameFormatter : IEntityNameFormatter + { + readonly IEntityNameFormatter _original; + + public CustomEntityNameFormatter(IEntityNameFormatter original) + { + _original = original; + } + + public string FormatEntityName() + { + string? result = null; + if (typeof(T).GetInterface(nameof(IIntegrationEvent)) != null) + result = typeof(T).GetMethod("EventName", BindingFlags.Public | BindingFlags.Static)?.Invoke(null, null) as string; + + return result ?? _original.FormatEntityName(); + } + } + } +} diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/MassTransit.Azure.ServiceBus.Core.Tests.csproj b/tests/MassTransit.Azure.ServiceBus.Core.Tests/MassTransit.Azure.ServiceBus.Core.Tests.csproj index 04847856f17..f67f3724a13 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/MassTransit.Azure.ServiceBus.Core.Tests.csproj +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/MassTransit.Azure.ServiceBus.Core.Tests.csproj @@ -1,15 +1,17 @@  - net6.0 - 9 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/MessageData_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/MessageData_Specs.cs index c57f5660fe5..3789b150e03 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/MessageData_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/MessageData_Specs.cs @@ -25,9 +25,12 @@ public async Task Should_store_and_retrieve_the_message_data_from_blob_storage() ConsumeContext consumeContext = await _handler; - Assert.That(consumeContext.Message.Data.HasValue, Is.True); + Assert.Multiple(() => + { + Assert.That(consumeContext.Message.Data.HasValue, Is.True); - Assert.That(_data, Is.EqualTo(data)); + Assert.That(_data, Is.EqualTo(data)); + }); } Task> _handler; @@ -80,8 +83,11 @@ await InputQueueSendEndpoint.Send(new ConsumeContext consumeContext = await _handler; - Assert.That(consumeContext.Message.Dictionary.HasValue, Is.True); - Assert.That(_data.Values, Is.EqualTo(dataDictionary.Values)); + Assert.Multiple(() => + { + Assert.That(consumeContext.Message.Dictionary.HasValue, Is.True); + Assert.That(_data.Values, Is.EqualTo(dataDictionary.Values)); + }); } Task> _handler; @@ -138,8 +144,11 @@ await InputQueueSendEndpoint.Send(new ConsumeContext consumeContext = await _handler; - Assert.That(consumeContext.Message.Dictionary.HasValue, Is.True); - Assert.That(_data.Values, Is.EqualTo(dataDictionary.Values)); + Assert.Multiple(() => + { + Assert.That(consumeContext.Message.Dictionary.HasValue, Is.True); + Assert.That(_data.Values, Is.EqualTo(dataDictionary.Values)); + }); } Task> _handler; diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Publish_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Publish_Specs.cs index dd3ac076381..2b13a4b365d 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Publish_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Publish_Specs.cs @@ -4,11 +4,12 @@ namespace MassTransit.Azure.ServiceBus.Core.Tests using System.Threading; using System.Threading.Tasks; using Internals; - using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Serialization; using TestFramework; using TestFramework.Messages; + using Testing; [TestFixture] @@ -111,7 +112,7 @@ public async Task Should_succeed() ConsumeContext received = await _handler; - Assert.AreEqual(EncryptedMessageSerializerV2.EncryptedContentType, received.ReceiveContext.ContentType); + Assert.That(received.ReceiveContext.ContentType, Is.EqualTo(EncryptedMessageSerializerV2.EncryptedContentType)); } Task> _handler; @@ -174,12 +175,6 @@ protected override void ConfigureServiceBusBus(IServiceBusBusFactoryConfigurator public class Publishing_a_message_to_an_remove_subscriptions_endpoint : AsyncTestFixture { - public Publishing_a_message_to_an_remove_subscriptions_endpoint() - : base(new InMemoryTestHarness()) - { - TestTimeout = TimeSpan.FromMinutes(3); - } - [Test] public async Task Should_create_receive_endpoint_and_start() { @@ -225,5 +220,44 @@ public async Task Should_create_receive_endpoint_and_start() await busHandle.StopAsync(new CancellationTokenSource(TimeSpan.FromSeconds(10)).Token); } } + + public Publishing_a_message_to_an_remove_subscriptions_endpoint() + : base(new InMemoryTestHarness()) + { + TestTimeout = TimeSpan.FromMinutes(3); + } + } + + + [TestFixture] + public class MessageType_Specs + { + [Test] + public async Task Should_not_allow_array_message_types_but_does() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.UsingTestAzureServiceBus(); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new[] { new PingMessage(), new PingMessage(), new PingMessage() }); + + Assert.That(await harness.Consumed.Any()); + } + + + class ArrayConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } } } diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Receiver_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Receiver_Specs.cs index 6235bd35655..4e6e801f3a9 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Receiver_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Receiver_Specs.cs @@ -1,12 +1,7 @@ namespace MassTransit.Azure.ServiceBus.Core.Tests { - using System; - using System.Reflection; using System.Threading.Tasks; - using AzureServiceBusTransport; using FunctionComponents; - using global::Azure.Core.Amqp; - using global::Azure.Messaging.ServiceBus; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using TestFramework; @@ -20,11 +15,12 @@ public class When_a_consumer_faults_in_a_function_receiver public async Task Should_retry_if_configured() { await using var provider = new ServiceCollection() - .AddSingleton() - .AddSingleton() .AddMassTransitTestHarness(x => { + x.AddAzureFunctionsTestComponents(); + x.AddConsumer(); + x.UsingTestAzureServiceBus((context, cfg) => { cfg.UseRawJsonDeserializer(isDefault: true); @@ -34,32 +30,26 @@ public async Task Should_retry_if_configured() var harness = provider.GetTestHarness(); - var receiver = provider.GetRequiredService(); - - var messageBody = new AmqpMessageBody(new[] { new BinaryData("{}").ToMemory() }); - var annotatedMessage = new AmqpAnnotatedMessage(messageBody); - annotatedMessage.Header.DeliveryCount = 1; - annotatedMessage.Properties.MessageId = new AmqpMessageId(NewId.NextGuid().ToString()); - annotatedMessage.Properties.ContentType = "application/json"; - - var message = (ServiceBusReceivedMessage)typeof(ServiceBusReceivedMessage).GetConstructor( - BindingFlags.NonPublic | BindingFlags.Instance, - null, new[] { typeof(AmqpAnnotatedMessage) }, null).Invoke(new object[] { annotatedMessage }); - - Assert.That(async () => await receiver.HandleConsumer("input-queue", message, harness.CancellationToken), + Assert.That(async () => await harness.HandleConsumer(new FunctionMessage()), Throws.TypeOf()); IConsumerTestHarness consumerHarness = harness.GetConsumerHarness(); - Assert.That(await consumerHarness.Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await consumerHarness.Consumed.Any(), Is.True); - Assert.That(await harness.Published.Any>(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); } } namespace FunctionComponents { + using System; + + public class FunctionMessage { } @@ -79,7 +69,8 @@ public class FaultyFunctionConsumerDefinition : ConsumerDefinition { protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Interval(3, TimeSpan.FromMilliseconds(1))); } diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/RequestClient_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/RequestClient_Specs.cs index a67591e148e..654e91cc66d 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/RequestClient_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/RequestClient_Specs.cs @@ -2,7 +2,6 @@ { using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework.Messages; @@ -50,8 +49,7 @@ public async Task Should_receive_the_response() using RequestHandle requestHandle = _requestClient.Create(new PingMessage()); Response response = await requestHandle.GetResponse(); - - response.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(response.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } Task> _ping; diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/ScheduleMessage_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ScheduleMessage_Specs.cs index 8b321bc2d0b..06e16ef7d88 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/ScheduleMessage_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ScheduleMessage_Specs.cs @@ -144,6 +144,64 @@ public class SecondMessage } } + + [TestFixture] + public class Scheduling_a_message_using_quartz_with_partition_key : + AzureServiceBusTestFixture + { + [Test] + public async Task Should_get_the_message() + { + await InputQueueSendEndpoint.Send(new FirstMessage(), x => x.SetPartitionKey("2112")); + + await _first; + + ConsumeContext consumeContext = await _second; + + Assert.That(consumeContext.PartitionKey(), Is.EqualTo("2112")); + } + + TimeSpan _testOffset; + + public Scheduling_a_message_using_quartz_with_partition_key() + { + _testOffset = TimeSpan.Zero; + AzureServiceBusTestHarness.ConfigureMessageScheduler = false; + } + + Uri QuartzAddress { get; set; } + + Task> _second; + Task> _first; + + protected override void ConfigureServiceBusReceiveEndpoint(IServiceBusReceiveEndpointConfigurator configurator) + { + _first = Handler(configurator, async context => + { + await context.ScheduleSend(DateTime.Now, new SecondMessage(), + Pipe.Execute(x => x.SetPartitionKey(context.PartitionKey()))); + }); + + _second = Handled(configurator); + } + + protected override void ConfigureServiceBusBus(IServiceBusBusFactoryConfigurator configurator) + { + QuartzAddress = configurator.UseInMemoryScheduler(); + } + + + public class FirstMessage + { + } + + + public class SecondMessage + { + } + } + + [TestFixture] public class Scheduling_a_published_message_using_quartz : AzureServiceBusTestFixture diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/ScheduleTimeout_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ScheduleTimeout_Specs.cs index bcfbd0c169b..7815276d082 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/ScheduleTimeout_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/ScheduleTimeout_Specs.cs @@ -24,7 +24,7 @@ public async Task Should_cancel_when_the_order_is_submitted() Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, _machine.Active, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await InputQueueSendEndpoint.Send(new { MemberNumber = memberNumber }); @@ -54,7 +54,7 @@ public async Task Should_reschedule_the_timeout_when_items_are_added() Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, _machine.Active, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await InputQueueSendEndpoint.Send(new { MemberNumber = memberNumber }); diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/SendContext_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/SendContext_Specs.cs index 01c49c29165..2551575f597 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/SendContext_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/SendContext_Specs.cs @@ -4,7 +4,6 @@ using System.Diagnostics; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework.Messages; @@ -24,7 +23,7 @@ public async Task Should_succeed() timer.Stop(); - timer.Elapsed.ShouldBeGreaterThanOrEqualTo(TimeSpan.FromSeconds(10)); + Assert.That(timer.Elapsed, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(10))); } Task> _handler; diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Send_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Send_Specs.cs index ab47937bcb2..bcad7b775f3 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Send_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Send_Specs.cs @@ -14,7 +14,7 @@ public async Task Should_have_a_redelivery_flag_of_false() { ConsumeContext context = await _handler; - Assert.IsFalse(context.ReceiveContext.Redelivered); + Assert.That(context.ReceiveContext.Redelivered, Is.False); } [Test] @@ -81,7 +81,7 @@ public async Task Should_receive_the_response() { Response message = await _response; - Assert.AreEqual(message.Message.CorrelationId, _ping.Result.Message.CorrelationId); + Assert.That(message.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } Task> _ping; diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/SessionConcurrency_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/SessionConcurrency_Specs.cs index ac957777e62..b6745367dc5 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/SessionConcurrency_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/SessionConcurrency_Specs.cs @@ -7,7 +7,6 @@ namespace MassTransit.Azure.ServiceBus.Core.Tests using System.Threading; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -18,14 +17,14 @@ public class Sending_multiple_messages_to_a_session public async Task Should_have_seen_all_messages() { await _handler; - _seenMessages.ShouldBe(TotalMessages); + Assert.That(_seenMessages, Is.EqualTo(TotalMessages)); } [Test] public async Task Should_have_seen_all_sessions() { await _handler; - _result.Count.ShouldBe(SessionCount); + Assert.That(_result, Has.Count.EqualTo(SessionCount)); } [Test] @@ -33,7 +32,7 @@ public async Task Should_have_seen_message_in_send_order() { await _handler; var outOfOrderSessions = _result.Values.Count(mr => mr.ReceivedSeqNumber != mr.SentSeqNumber); - outOfOrderSessions.ShouldBe(0); + Assert.That(outOfOrderSessions, Is.EqualTo(0)); } public Sending_multiple_messages_to_a_session() diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/SessionLockLost_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/SessionLockLost_Specs.cs index 49f5349c025..3b7c121b716 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/SessionLockLost_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/SessionLockLost_Specs.cs @@ -5,7 +5,6 @@ namespace MassTransit.Azure.ServiceBus.Core.Tests using System.Threading; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -30,7 +29,7 @@ await InputQueueSendEndpoint.Send(message, context => await _seenAll; await _handler; - _seenMessages.ShouldBe(TotalMessages); + Assert.That(_seenMessages, Is.EqualTo(TotalMessages)); } public When_message_process_exceeds_maxAutoRenewDuration() diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Session_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Session_Specs.cs index 7c83577436c..9a3f35c30f0 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Session_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Session_Specs.cs @@ -15,7 +15,7 @@ public async Task Should_have_a_redelivery_flag_of_false() { ConsumeContext context = await _handler; - Assert.IsFalse(context.ReceiveContext.Redelivered); + Assert.That(context.ReceiveContext.Redelivered, Is.False); } [Test] diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/StateMachineRequest_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/StateMachineRequest_Specs.cs index 50cdcf3afc2..46820d74485 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/StateMachineRequest_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/StateMachineRequest_Specs.cs @@ -172,7 +172,8 @@ public async Task Consume(ConsumeContext context) public class OrderShipmentSagaDefinition : SagaDefinition { - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) { if (endpointConfigurator is IServiceBusReceiveEndpointConfigurator sb) sb.RequiresSession = true; diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Stopping_the_bus.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Stopping_the_bus.cs index a2e23aa50cc..0fa22f40fb8 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/Stopping_the_bus.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/Stopping_the_bus.cs @@ -4,6 +4,7 @@ namespace MassTransit.Azure.ServiceBus.Core.Tests using System.Threading.Tasks; using Internals; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; using NUnit.Framework; using Testing; @@ -45,13 +46,71 @@ public async Task Should_not_wait_forever_once_a_consumer_has_completed() await harness.Stop().OrTimeout(TimeSpan.FromSeconds(40)); } + [Test] + [Explicit] + public async Task Should_publish_a_message_after_being_stopped() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10), testTimeout: TimeSpan.FromSeconds(60)); + + x.AddConsumer(c => c.UseConcurrentMessageLimit(1)); + + x.UsingTestAzureServiceBus((context, cfg) => + { + cfg.PrefetchCount = 10; + }); + + x.AddOptions().Configure(options => + { + options.WaitUntilStarted = true; + options.StartTimeout = TimeSpan.FromSeconds(10); + options.StopTimeout = TimeSpan.FromSeconds(30); + options.ConsumerStopTimeout = TimeSpan.FromSeconds(10); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var consumerEndpoint = await harness.GetConsumerEndpoint(); + + await consumerEndpoint.Send(new SloMessage()); + + await Task.Delay(4000); + + await harness.Stop(); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.Any(), Is.True); + Assert.That(await harness.Published.Any(), Is.True); + }); + } + class SloRide : IConsumer { + readonly ILogger _logger; + + public SloRide(ILogger logger) + { + _logger = logger; + } + public async Task Consume(ConsumeContext context) { + _logger.LogInformation("Starting the slow ride"); + await Task.Delay(TimeSpan.FromSeconds(8), context.CancellationToken); + + await context.Publish(new SloResult()); + + _logger.LogInformation("Finished the slow ride"); } } } @@ -60,4 +119,9 @@ public async Task Consume(ConsumeContext context) public record SloMessage { } + + + public record SloResult + { + } } diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/TopologyCorrelationId_Specs.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/TopologyCorrelationId_Specs.cs index 6c3478d78b5..28bb9339082 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/TopologyCorrelationId_Specs.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/TopologyCorrelationId_Specs.cs @@ -14,12 +14,15 @@ public async Task Should_handle_base_event_class() { var transactionId = NewId.NextGuid(); - await InputQueueSendEndpoint.Send(new {TransactionId = transactionId}); + await InputQueueSendEndpoint.Send(new { TransactionId = transactionId }); ConsumeContext context = await _handled; - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); + }); } [Test] @@ -27,12 +30,15 @@ public async Task Should_handle_named_configured_legacy() { var transactionId = NewId.NextGuid(); - await InputQueueSendEndpoint.Send(new {TransactionId = transactionId}); + await InputQueueSendEndpoint.Send(new { TransactionId = transactionId }); ConsumeContext legacyContext = await _legacyHandled; - Assert.IsTrue(legacyContext.CorrelationId.HasValue); - Assert.That(legacyContext.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(legacyContext.CorrelationId.HasValue, Is.True); + Assert.That(legacyContext.CorrelationId.Value, Is.EqualTo(transactionId)); + }); } [Test] @@ -40,12 +46,15 @@ public async Task Should_handle_named_property() { var transactionId = NewId.NextGuid(); - await InputQueueSendEndpoint.Send(new {CorrelationId = transactionId}); + await InputQueueSendEndpoint.Send(new { CorrelationId = transactionId }); ConsumeContext otherContext = await _otherHandled; - Assert.IsTrue(otherContext.CorrelationId.HasValue); - Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(otherContext.CorrelationId.HasValue, Is.True); + Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + }); } Task> _handled; @@ -104,12 +113,15 @@ public async Task Should_handle_named_property() { var transactionId = NewId.NextGuid(); - await Bus.Publish(new {CorrelationId = transactionId}); + await Bus.Publish(new { CorrelationId = transactionId }); ConsumeContext otherContext = await _otherHandled; - Assert.IsTrue(otherContext.CorrelationId.HasValue); - Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(otherContext.CorrelationId.HasValue, Is.True); + Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + }); } Task> _otherHandled; @@ -155,12 +167,15 @@ public async Task Should_handle_named_property() { var transactionId = NewId.NextGuid(); - await Bus.Publish(new {CorrelationId = transactionId}); + await Bus.Publish(new { CorrelationId = transactionId }); ConsumeContext otherContext = await _otherHandled; - Assert.IsTrue(otherContext.CorrelationId.HasValue); - Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(otherContext.CorrelationId.HasValue, Is.True); + Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + }); } Task> _otherHandled; diff --git a/tests/MassTransit.Azure.ServiceBus.Core.Tests/TwoScopeAzureServiceBusTestFixture.cs b/tests/MassTransit.Azure.ServiceBus.Core.Tests/TwoScopeAzureServiceBusTestFixture.cs index 1fc206380cf..7e7db672f00 100644 --- a/tests/MassTransit.Azure.ServiceBus.Core.Tests/TwoScopeAzureServiceBusTestFixture.cs +++ b/tests/MassTransit.Azure.ServiceBus.Core.Tests/TwoScopeAzureServiceBusTestFixture.cs @@ -6,6 +6,7 @@ using NUnit.Framework; + [TestFixture] public class TwoScopeAzureServiceBusTestFixture : AzureServiceBusTestFixture { @@ -66,10 +67,8 @@ public async Task SetupSecondAzureServiceBusTestFixture() { try { - using (var tokenSource = new CancellationTokenSource(TestTimeout)) - { - await _secondBusHandle.StopAsync(tokenSource.Token); - } + using var tokenSource = new CancellationTokenSource(TestTimeout); + await _secondBusHandle.StopAsync(tokenSource.Token); } finally { @@ -86,10 +85,8 @@ public async Task TearDownTwoScopeTestFixture() { try { - using (var tokenSource = new CancellationTokenSource(TestTimeout)) - { - await _secondBusHandle?.StopAsync(tokenSource.Token); - } + using var tokenSource = new CancellationTokenSource(TestTimeout); + await _secondBusHandle?.StopAsync(tokenSource.Token); } catch (Exception ex) { diff --git a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_ConsumeRecords_Specs.cs b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_ConsumeRecords_Specs.cs index d5f82035abc..a1ba2211603 100644 --- a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_ConsumeRecords_Specs.cs +++ b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_ConsumeRecords_Specs.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using AzureTable; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -15,8 +14,8 @@ public class Saving_consume_audit_records_to_the_audit_store : [Test] public async Task Should_have_consume_audit_records() { - IEnumerable consumeRecords = GetRecords().Where(x => x.ContextType == "Consume"); - consumeRecords.Count().ShouldBe(2); + List consumeRecords = GetRecords().Where(x => x.ContextType == "Consume").ToList(); + Assert.That(consumeRecords, Has.Count.EqualTo(2)); } Task> _handledA; diff --git a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_Filter_Specs .cs b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_Filter_Specs .cs index 57828d05769..b3b5890b061 100644 --- a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_Filter_Specs .cs +++ b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_Filter_Specs .cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using AzureTable; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -16,15 +15,15 @@ public class Saving_send_records_with_filter : public async Task Should_Have_Audit_Records() { _records = GetRecords(); - _records.ShouldNotBeEmpty(); + Assert.That(_records, Is.Not.Empty); } [Test] public async Task Should_have_send_audit_record() { List sendRecords = _records.Where(x => x.ContextType == "Send").ToList(); - sendRecords.Count.ShouldBe(1); - sendRecords[0].MessageType.ShouldBe(typeof(A).FullName); + Assert.That(sendRecords, Has.Count.EqualTo(1)); + Assert.That(sendRecords[0].MessageType, Is.EqualTo(typeof(A).FullName)); } IEnumerable _records; diff --git a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_PartitionKey_Specs.cs b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_PartitionKey_Specs.cs index 53e54ed477d..cea457ccc38 100644 --- a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_PartitionKey_Specs.cs +++ b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_PartitionKey_Specs.cs @@ -5,9 +5,8 @@ using System.Linq; using System.Threading.Tasks; using AzureTable; - using Microsoft.Azure.Cosmos.Table; + using global::Azure.Data.Tables; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -18,11 +17,11 @@ public class Saving_audit_records_with_custom_partitionKey : public async Task Should_Have_Custom_PartitionKey() { _records = GetTableEntities().ToList(); - _records.Count.ShouldBe(1); - _records[0].PartitionKey.ShouldBe(PartitionKey); + Assert.That(_records, Has.Count.EqualTo(1)); + Assert.That(_records[0].PartitionKey, Is.EqualTo(PartitionKey)); } - List _records; + List _records; readonly string PartitionKey = "TestPartitionKey"; [OneTimeSetUp] diff --git a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_SendRecords_Specs.cs b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_SendRecords_Specs.cs index d3e7ec38710..8e55ab62316 100644 --- a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_SendRecords_Specs.cs +++ b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_SendRecords_Specs.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using AzureTable; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -16,14 +15,14 @@ public class Saving_send_audit_records_to_the_audit_store : public async Task Should_Have_Audit_Records() { _records = GetRecords(); - _records.ShouldNotBeEmpty(); + Assert.That(_records, Is.Not.Empty); } [Test] public async Task Should_have_send_audit_record() { - IEnumerable sendRecords = _records.Where(x => x.ContextType == "Send"); - sendRecords.Count().ShouldBe(2); + List sendRecords = _records.Where(x => x.ContextType == "Send").ToList(); + Assert.That(sendRecords, Has.Count.EqualTo(2)); } IEnumerable _records; diff --git a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_Specs.cs b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_Specs.cs index 65d77ff108a..c310f3b73ee 100644 --- a/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_Specs.cs +++ b/tests/MassTransit.Azure.Table.Tests/Audit/AuditStore_Specs.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using AzureTable; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -15,12 +14,11 @@ public class Saving_audit_records_to_the_audit_store : [Test] public async Task Should_Have_Audit_Records() { - _records = GetRecords(); - _records.ShouldNotBeEmpty(); - _records.Count().ShouldBe(4); + _records = GetRecords().ToList(); + Assert.That(_records, Has.Count.EqualTo(4)); } - IEnumerable _records; + List _records; Task> _handledA; Task> _handledB; diff --git a/tests/MassTransit.Azure.Table.Tests/Audit/Configure_audit_store_supply_storage_account.cs b/tests/MassTransit.Azure.Table.Tests/Audit/Configure_audit_store_supply_storage_account.cs index 99df8560a57..0c7a2360c41 100644 --- a/tests/MassTransit.Azure.Table.Tests/Audit/Configure_audit_store_supply_storage_account.cs +++ b/tests/MassTransit.Azure.Table.Tests/Audit/Configure_audit_store_supply_storage_account.cs @@ -5,9 +5,8 @@ using System.Linq; using System.Threading.Tasks; using AzureTable; - using Microsoft.Azure.Cosmos.Table; + using global::Azure.Data.Tables; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -17,8 +16,8 @@ public class Configure_audit_store_supply_storage_account : [Test] public async Task Should_have_send_audit_records() { - IEnumerable sendRecords = GetRecords().Where(x => x.ContextType == "Send"); - sendRecords.Count().ShouldBe(1); + List sendRecords = GetRecords().Where(x => x.ContextType == "Send").ToList(); + Assert.That(sendRecords, Has.Count.EqualTo(1)); } [OneTimeSetUp] @@ -29,7 +28,7 @@ public async Task SetUp() protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { - var storageAccount = CloudStorageAccount.Parse(ConnectionString); + var storageAccount = new TableServiceClient(ConnectionString); configurator.UseAzureTableAuditStore(storageAccount, TestTableName); base.ConfigureInMemoryBus(configurator); } diff --git a/tests/MassTransit.Azure.Table.Tests/Audit/Configure_audit_store_supply_table.cs b/tests/MassTransit.Azure.Table.Tests/Audit/Configure_audit_store_supply_table.cs index 88ba0fed883..7da7e780ebf 100644 --- a/tests/MassTransit.Azure.Table.Tests/Audit/Configure_audit_store_supply_table.cs +++ b/tests/MassTransit.Azure.Table.Tests/Audit/Configure_audit_store_supply_table.cs @@ -5,9 +5,10 @@ using System.Linq; using System.Threading.Tasks; using AzureTable; - using Microsoft.Azure.Cosmos.Table; + using global::Azure; + using global::Azure.Data.Tables; + using global::Azure.Data.Tables.Models; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -17,8 +18,8 @@ public class Configure_audit_store_supply_table : [Test] public async Task Should_have_send_audit_records() { - IEnumerable sendRecords = GetRecords().Where(x => x.ContextType == "Send"); - sendRecords.Count().ShouldBe(1); + List sendRecords = GetRecords().Where(x => x.ContextType == "Send").ToList(); + Assert.That(sendRecords, Has.Count.EqualTo(1)); } [OneTimeSetUp] @@ -29,10 +30,9 @@ public async Task SetUp() protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { - var storageAccount = CloudStorageAccount.Parse(ConnectionString); - var tableClient = storageAccount.CreateCloudTableClient(new TableClientConfiguration()); - var table = tableClient.GetTableReference(TestTableName); - configurator.UseAzureTableAuditStore(table); + var storageAccount = new TableServiceClient(ConnectionString); + Response tableClient = storageAccount.CreateTableIfNotExists(TestTableName); + configurator.UseAzureTableAuditStore(TestCloudTable); base.ConfigureInMemoryBus(configurator); } diff --git a/tests/MassTransit.Azure.Table.Tests/AzureTableInMemoryTestFixture.cs b/tests/MassTransit.Azure.Table.Tests/AzureTableInMemoryTestFixture.cs index 9d54ede0cea..62313043990 100644 --- a/tests/MassTransit.Azure.Table.Tests/AzureTableInMemoryTestFixture.cs +++ b/tests/MassTransit.Azure.Table.Tests/AzureTableInMemoryTestFixture.cs @@ -3,7 +3,7 @@ namespace MassTransit.Azure.Table.Tests using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - using Microsoft.Azure.Cosmos.Table; + using global::Azure.Data.Tables; using NUnit.Framework; using TestFramework; @@ -12,53 +12,51 @@ public class AzureTableInMemoryTestFixture : InMemoryTestFixture { protected readonly string ConnectionString; - protected readonly CloudTable TestCloudTable; + protected readonly TableClient TestCloudTable; protected readonly string TestTableName; public AzureTableInMemoryTestFixture() { ConnectionString = Configuration.StorageAccount; TestTableName = "azuretabletests"; - var storageAccount = CloudStorageAccount.Parse(ConnectionString); - var tableClient = storageAccount.CreateCloudTableClient(); - TestCloudTable = tableClient.GetTableReference(TestTableName); + + var tableServiceClient = new TableServiceClient(ConnectionString); + TestCloudTable = tableServiceClient.GetTableClient(TestTableName); } protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { configurator.UseDelayedMessageScheduler(); - base.ConfigureInMemoryBus(configurator); } public IEnumerable GetRecords() + where T : class, ITableEntity, new() { - IEnumerable entities = TestCloudTable.ExecuteQuery(new TableQuery()); - return entities.Select(e => TableEntity.ConvertBack(e.Properties, new OperationContext())); + return TestCloudTable.Query().ToList(); } - public IEnumerable GetTableEntities() + public IEnumerable GetTableEntities() { - return TestCloudTable.ExecuteQuery(new TableQuery()); + return TestCloudTable.Query(); } [OneTimeSetUp] public async Task Bring_it_up() { - TestCloudTable.CreateIfNotExists(); + await TestCloudTable.CreateIfNotExistsAsync(); - IEnumerable entities = GetTableEntities(); + IEnumerable entities = GetTableEntities(); + IEnumerable> groupedEntities = entities.GroupBy(e => e.PartitionKey); - foreach (IGrouping key in entities.GroupBy(x => x.PartitionKey)) + foreach (IGrouping group in groupedEntities) { - // Create the batch operation. - var batchDeleteOperation = new TableBatchOperation(); - - foreach (var row in key) - batchDeleteOperation.Delete(row); + List batchOperations = group + .Select(entity => new TableTransactionAction(TableTransactionActionType.Delete, entity)) + .ToList(); - // Execute the batch operation. - await TestCloudTable.ExecuteBatchAsync(batchDeleteOperation); + // Execute the batch transaction + await TestCloudTable.SubmitTransactionAsync(batchOperations); } } } diff --git a/tests/MassTransit.Azure.Table.Tests/Configuration.cs b/tests/MassTransit.Azure.Table.Tests/Configuration.cs index ccab40898d1..4d5a7148b6d 100644 --- a/tests/MassTransit.Azure.Table.Tests/Configuration.cs +++ b/tests/MassTransit.Azure.Table.Tests/Configuration.cs @@ -1,15 +1,7 @@ namespace MassTransit.Azure.Table.Tests { - using System; - using NUnit.Framework; - - public static class Configuration { - public static string StorageAccount => - TestContext.Parameters.Exists(nameof(StorageAccount)) - ? TestContext.Parameters.Get(nameof(StorageAccount)) - : Environment.GetEnvironmentVariable("MT_AZURE_STORAGE_ACCOUNT") - ?? "DefaultEndpointsProtocol=http;AccountName=localhost;AccountKey=C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==;TableEndpoint=https://localhost:8081/;"; + public static string StorageAccount => "UseDevelopmentStorage=true"; } } diff --git a/tests/MassTransit.Azure.Table.Tests/Future_Specs.cs b/tests/MassTransit.Azure.Table.Tests/Future_Specs.cs index 6e6c80178df..ce3722e433a 100644 --- a/tests/MassTransit.Azure.Table.Tests/Future_Specs.cs +++ b/tests/MassTransit.Azure.Table.Tests/Future_Specs.cs @@ -1,9 +1,11 @@ namespace MassTransit.Azure.Table.Tests { using System; + using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - using Microsoft.Azure.Cosmos.Table; + using global::Azure; + using global::Azure.Data.Tables; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using TestFramework; @@ -21,7 +23,7 @@ public void ConfigureFutureSagaRepository(IBusRegistrationConfigurator configura configurator.AddSagaRepository() .AzureTableRepository(r => { - r.ConnectionFactory(provider => provider.GetRequiredService().GetTableReference(TableName)); + r.ConnectionFactory(provider => provider.GetRequiredService().GetTableClient(TableName)); }); } @@ -30,42 +32,28 @@ public void ConfigureServices(IServiceCollection collection) collection.AddSingleton(provider => { var connectionString = Configuration.StorageAccount; - var storageAccount = CloudStorageAccount.Parse(connectionString); - - return storageAccount; + return new TableServiceClient(connectionString); }) - .AddSingleton(provider => - { - var storageAccount = provider.GetRequiredService(); - - var tableClient = storageAccount.CreateCloudTableClient(); - - return tableClient; - }); +; } public async Task OneTimeSetup(IServiceProvider provider) { - var table = provider.GetRequiredService().GetTableReference(TableName); + var table = provider.GetRequiredService().GetTableClient(TableName); await table.CreateIfNotExistsAsync(); - var query = new TableQuery(); - TableQuerySegment segment = await table.ExecuteQuerySegmentedAsync(query, null); - - while (segment.Results.Count > 0) + await foreach (Page page in table.QueryAsync().AsPages()) { - foreach (IGrouping key in segment.Results.GroupBy(x => x.PartitionKey)) + foreach (IGrouping group in page.Values.GroupBy(x => x.PartitionKey)) { - var batchDeleteOperation = new TableBatchOperation(); + var batchDeleteOperations = new List(); - foreach (var row in key) - batchDeleteOperation.Delete(row); + foreach (var entity in group) + batchDeleteOperations.Add(new TableTransactionAction(TableTransactionActionType.Delete, entity)); - await table.ExecuteBatchAsync(batchDeleteOperation); + await table.SubmitTransactionAsync(batchDeleteOperations); } - - segment = await table.ExecuteQuerySegmentedAsync(query, segment.ContinuationToken); } } diff --git a/tests/MassTransit.Azure.Table.Tests/JobConsumer_Specs.cs b/tests/MassTransit.Azure.Table.Tests/JobConsumer_Specs.cs new file mode 100644 index 00000000000..fc9ec05375a --- /dev/null +++ b/tests/MassTransit.Azure.Table.Tests/JobConsumer_Specs.cs @@ -0,0 +1,191 @@ +namespace MassTransit.Azure.Table.Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Contracts.JobService; + using global::Azure; + using global::Azure.Data.Tables; + using global::Azure.Data.Tables.Models; + using JobConsumerTests; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using NUnit.Framework; + using Testing; + + + namespace JobConsumerTests + { + using System; + using System.Threading.Tasks; + using Contracts.JobService; + + + public interface OddJob + { + TimeSpan Duration { get; } + } + + + public class OddJobConsumer : + IJobConsumer + { + public async Task Run(JobContext context) + { + if (context.RetryAttempt == 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + } + } + + + public class OddJobCompletedConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + return Task.CompletedTask; + } + } + } + + + public class Using_the_new_job_service_configuration + { + const string TableName = "jobservice"; + + [Test] + public async Task Should_complete_the_job() + { + await using var provider = new ServiceCollection() + .AddSingleton(provider => + { + var connectionString = Configuration.StorageAccount; + var storageAccount = new TableServiceClient(connectionString); + + return storageAccount; + }) + .AddSingleton(provider => + { + var storageAccount = provider.GetRequiredService(); + Response tableClient = storageAccount.CreateTableIfNotExists(TableName); + return tableClient; + }) + .AddHostedService() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); + + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.AddJobSagaStateMachines() + .AzureTableRepository(r => + { + r.ConnectionFactory(provider => provider.GetRequiredService().GetTableClient(TableName)); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + try + { + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + MassTransit.Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(1) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + } + finally + { + await harness.Stop(); + } + } + + [OneTimeSetUp] + public async Task Setup() + { + } + + [OneTimeTearDown] + public async Task Teardown() + { + } + + + public class CreateTableHostedService : + IHostedService + { + readonly ILogger _logger; + readonly IServiceProvider _provider; + + public CreateTableHostedService(IServiceProvider provider, ILogger logger) + { + _provider = provider; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Creating table configuration in Azure Table Storage"); + + var table = _provider.GetRequiredService().GetTableClient(TableName); + + await table.CreateIfNotExistsAsync(cancellationToken); + + AsyncPageable entities = table.QueryAsync(cancellationToken: cancellationToken); + + await foreach (Page page in table.QueryAsync().AsPages()) + { + foreach (IGrouping group in page.Values.GroupBy(x => x.PartitionKey)) + { + var batchDeleteOperations = new List(); + + foreach (var row in group) + batchDeleteOperations.Add(new TableTransactionAction(TableTransactionActionType.Delete, row)); + + await table.SubmitTransactionAsync(batchDeleteOperations, cancellationToken); + } + } + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + } + } + } +} diff --git a/tests/MassTransit.Azure.Table.Tests/MassTransit.Azure.Table.Tests.csproj b/tests/MassTransit.Azure.Table.Tests/MassTransit.Azure.Table.Tests.csproj index d9796553615..17228fc7886 100644 --- a/tests/MassTransit.Azure.Table.Tests/MassTransit.Azure.Table.Tests.csproj +++ b/tests/MassTransit.Azure.Table.Tests/MassTransit.Azure.Table.Tests.csproj @@ -1,13 +1,16 @@ - net6.0 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/tests/MassTransit.Azure.Table.Tests/Saga/LocatingAnExistingSaga.cs b/tests/MassTransit.Azure.Table.Tests/Saga/LocatingAnExistingSaga.cs index 217b06b5bc9..0da032d41d9 100644 --- a/tests/MassTransit.Azure.Table.Tests/Saga/LocatingAnExistingSaga.cs +++ b/tests/MassTransit.Azure.Table.Tests/Saga/LocatingAnExistingSaga.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using AzureTable.Saga; using NUnit.Framework; - using Shouldly; using Testing; @@ -23,19 +22,19 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); - var nextMessage = new CompleteSimpleSaga {CorrelationId = sagaId}; + var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; await InputQueueSendEndpoint.Send(nextMessage); found = await _sagaRepository.Value.ShouldContainSaga(sagaId, x => x != null && x.Moved, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); var retrieveRepository = _sagaRepository.Value as ILoadSagaRepository; var retrieved = await retrieveRepository.Load(sagaId); - retrieved.ShouldNotBeNull(); - retrieved.Moved.ShouldBeTrue(); + Assert.That(retrieved, Is.Not.Null); + Assert.That(retrieved.Moved, Is.True); } [Test] @@ -48,7 +47,7 @@ public async Task An_initiating_message_should_start_the_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); } readonly Lazy> _sagaRepository; diff --git a/tests/MassTransit.Azure.Table.Tests/Saga/ReadOnly_Specs.cs b/tests/MassTransit.Azure.Table.Tests/Saga/ReadOnly_Specs.cs index 25d4d851aec..e8f5b78ab6b 100644 --- a/tests/MassTransit.Azure.Table.Tests/Saga/ReadOnly_Specs.cs +++ b/tests/MassTransit.Azure.Table.Tests/Saga/ReadOnly_Specs.cs @@ -1,13 +1,11 @@ namespace MassTransit.Azure.Table.Tests.Saga { - using System; - using System.Threading.Tasks; - using NUnit.Framework; - - namespace ReadOnlyTests { + using System; + using System.Threading.Tasks; using AzureTable.Saga; + using NUnit.Framework; [TestFixture] @@ -22,15 +20,15 @@ public async Task Should_not_update_the_saga_repository() IRequestClient startClient = Bus.CreateRequestClient(InputQueueAddress, TestTimeout); - await startClient.GetResponse(new Start {CorrelationId = serviceId}, TestCancellationToken); + await startClient.GetResponse(new Start { CorrelationId = serviceId }, TestCancellationToken); IRequestClient requestClient = Bus.CreateRequestClient(InputQueueAddress, TestTimeout); - Response status = await requestClient.GetResponse(new CheckStatus {CorrelationId = serviceId}, TestCancellationToken); + Response status = await requestClient.GetResponse(new CheckStatus { CorrelationId = serviceId }, TestCancellationToken); Assert.That(status.Message.StatusText, Is.EqualTo("Started")); - status = await requestClient.GetResponse(new CheckStatus {CorrelationId = serviceId}, TestCancellationToken); + status = await requestClient.GetResponse(new CheckStatus { CorrelationId = serviceId }, TestCancellationToken); Assert.That(status.Message.StatusText, Is.EqualTo("Started")); } @@ -75,7 +73,7 @@ public ReadOnlyStateMachine() Initially( When(Started) .Then(context => context.Instance.StatusText = "Started") - .Respond(context => new StartupComplete {CorrelationId = context.Instance.CorrelationId}) + .Respond(context => new StartupComplete { CorrelationId = context.Instance.CorrelationId }) .TransitionTo(Running) ); diff --git a/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/DataAccess/SlowConcurrentSaga.cs b/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/DataAccess/SlowConcurrentSaga.cs index 3ebe31bb578..9e88b88fa15 100644 --- a/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/DataAccess/SlowConcurrentSaga.cs +++ b/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/DataAccess/SlowConcurrentSaga.cs @@ -3,7 +3,8 @@ namespace MassTransit.Azure.Table.Tests.SlowConcurrentSaga.DataAccess using System; - public class SlowConcurrentSaga : SagaStateMachineInstance + public class SlowConcurrentSaga : + SagaStateMachineInstance { public string CurrentState { get; set; } diff --git a/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/SlowConcurrentSagaStateMachine.cs b/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/SlowConcurrentSagaStateMachine.cs index 5afed3f1895..acfdf545613 100644 --- a/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/SlowConcurrentSagaStateMachine.cs +++ b/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/SlowConcurrentSagaStateMachine.cs @@ -5,7 +5,8 @@ namespace MassTransit.Azure.Table.Tests.SlowConcurrentSaga using Events; - public class SlowConcurrentSagaStateMachine : MassTransitStateMachine + public class SlowConcurrentSagaStateMachine : + MassTransitStateMachine { public SlowConcurrentSagaStateMachine() { @@ -23,8 +24,10 @@ public SlowConcurrentSagaStateMachine() When(IncrementCounterSlowly) .ThenAsync(async context => { - await Task.Delay(5000); + await Task.Delay(3000); context.Instance.Counter++; + + LogContext.Debug?.Log("Incremented Counter: {0}", context.Saga.Counter); })); } diff --git a/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/SlowConcurrentSaga_Specs.cs b/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/SlowConcurrentSaga_Specs.cs index fe35b960099..c308fd9101d 100644 --- a/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/SlowConcurrentSaga_Specs.cs +++ b/tests/MassTransit.Azure.Table.Tests/SlowConcurrentSaga/SlowConcurrentSaga_Specs.cs @@ -7,11 +7,11 @@ namespace MassTransit.Azure.Table.Tests.SlowConcurrentSaga using DataAccess; using Events; using NUnit.Framework; - using Shouldly; using Testing; - public class SlowConcurrentSaga_Specs : AzureTableInMemoryTestFixture + public class SlowConcurrentSaga_Specs : + AzureTableInMemoryTestFixture { readonly Lazy> _sagaRepository; readonly ISagaStateMachineTestHarness _sagaTestHarness; @@ -27,32 +27,32 @@ public SlowConcurrentSaga_Specs() [Test] public async Task Two_Initiating_Messages_Deadlock_Results_In_One_Instance() { - var activityMonitor = Bus.CreateBusActivityMonitor(TimeSpan.FromMilliseconds(3000)); - var sagaId = NewId.NextGuid(); var message = new Begin { CorrelationId = sagaId }; await InputQueueSendEndpoint.Send(message); - Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); + Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestInactivityTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); var slowMessage = new IncrementCounterSlowly { CorrelationId = sagaId }; await Task.WhenAll( Task.Run(() => InputQueueSendEndpoint.Send(slowMessage)), Task.Run(() => InputQueueSendEndpoint.Send(slowMessage))); - _sagaTestHarness.Consumed.Select().Take(2).ToList(); + _ = _sagaTestHarness.Consumed.Select().Take(2).ToList(); - await activityMonitor.AwaitBusInactivity(TestTimeout); + await InactivityTask; - await _sagaRepository.Value.ShouldContainSaga(sagaId, s => s.Counter == 3, TestTimeout); + Assert.That(await _sagaRepository.Value.ShouldContainSaga(sagaId, s => s.Counter == 3, TestInactivityTimeout), Is.Not.Null); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { configurator.ConcurrentMessageLimit = 16; + + configurator.UseMessageRetry(x => x.Interval(5, 100)); } } } diff --git a/tests/MassTransit.Azure.Table.Tests/Turnout/Canceled_Specs.cs b/tests/MassTransit.Azure.Table.Tests/Turnout/Canceled_Specs.cs deleted file mode 100644 index 77739a0131b..00000000000 --- a/tests/MassTransit.Azure.Table.Tests/Turnout/Canceled_Specs.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace MassTransit.Azure.Table.Tests.Turnout -{ - using System; - using System.Threading.Tasks; - using Contracts.JobService; - using NUnit.Framework; - - - [TestFixture] - [Category("Flaky")] - public class Submitting_a_job_to_turnout_that_is_cancelled : - AzureTableInMemoryTestFixture - { - [Test] - [Order(1)] - public async Task Should_get_the_job_accepted() - { - IRequestClient> requestClient = Bus.CreateRequestClient>(); - - Response response = await requestClient.GetResponse(new - { - JobId = _jobId, - Job = new { Duration = TimeSpan.FromSeconds(30) } - }); - - Assert.That(response.Message.JobId, Is.EqualTo(_jobId)); - - ConsumeContext started = await _started; - - await Bus.Publish(new - { - JobId = _jobId, - Reason = "I give up", - InVar.Timestamp - }); - - // just to capture all the test output in a single window - ConsumeContext cancelled = await _cancelled; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_cancelled_event() - { - ConsumeContext cancelled = await _cancelled; - } - - [Test] - [Order(3)] - public async Task Should_have_published_the_job_started_event() - { - ConsumeContext started = await _started; - } - - [Test] - [Order(2)] - public async Task Should_have_published_the_job_submitted_event() - { - ConsumeContext submitted = await _submitted; - } - - readonly Guid _jobId = NewId.NextGuid(); - Task> _cancelled; - Task> _submitted; - Task> _started; - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - base.ConfigureInMemoryBus(configurator); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(x => - { - x.UseAzureTableSagaRepository(() => TestCloudTable); - }); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.Consumer(() => new GrindTheGearsConsumer(), cfg => - { - cfg.Options>(jobOptions => jobOptions.SetJobTimeout(TimeSpan.FromSeconds(90))); - }); - }); - }); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - _submitted = Handled(configurator, context => context.Message.JobId == _jobId); - _started = Handled(configurator, context => context.Message.JobId == _jobId); - _cancelled = Handled(configurator, context => context.Message.JobId == _jobId); - } - - - public class GrindTheGearsConsumer : - IJobConsumer - { - public async Task Run(JobContext context) - { - await Task.Delay(context.Job.Duration, context.CancellationToken); - } - } - } -} diff --git a/tests/MassTransit.Azure.Table.Tests/Turnout/Complete_Specs.cs b/tests/MassTransit.Azure.Table.Tests/Turnout/Complete_Specs.cs deleted file mode 100644 index 84eb22c21db..00000000000 --- a/tests/MassTransit.Azure.Table.Tests/Turnout/Complete_Specs.cs +++ /dev/null @@ -1,111 +0,0 @@ -namespace MassTransit.Azure.Table.Tests.Turnout -{ - using System; - using System.Threading.Tasks; - using Contracts.JobService; - using NUnit.Framework; - - - public interface CrunchTheNumbers - { - TimeSpan Duration { get; } - } - - - public class CrunchTheNumbersConsumer : - IJobConsumer - { - public async Task Run(JobContext context) - { - await Task.Delay(context.Job.Duration); - } - } - - - [TestFixture] - [Category("Flaky")] - public class Submitting_a_job_to_turnout : - AzureTableInMemoryTestFixture - { - [Test] - [Order(1)] - public async Task Should_get_the_job_accepted() - { - IRequestClient> requestClient = Bus.CreateRequestClient>(); - - Response response = await requestClient.GetResponse(new - { - JobId = _jobId, - Job = new { Duration = TimeSpan.FromSeconds(1) } - }); - - Assert.That(response.Message.JobId, Is.EqualTo(_jobId)); - - // just to capture all the test output in a single window - ConsumeContext completed = await _completed; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_completed_event() - { - ConsumeContext completed = await _completed; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_completed_generic_event() - { - ConsumeContext> completed = await _completedT; - } - - [Test] - [Order(3)] - public async Task Should_have_published_the_job_started_event() - { - ConsumeContext started = await _started; - } - - [Test] - [Order(2)] - public async Task Should_have_published_the_job_submitted_event() - { - ConsumeContext submitted = await _submitted; - } - - readonly Guid _jobId = NewId.NextGuid(); - Task> _completed; - Task> _submitted; - Task> _started; - Task>> _completedT; - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - base.ConfigureInMemoryBus(configurator); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(x => - { - x.UseAzureTableSagaRepository(() => TestCloudTable); - }); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.Consumer(() => new CrunchTheNumbersConsumer()); - }); - }); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - _submitted = Handled(configurator, context => context.Message.JobId == _jobId); - _started = Handled(configurator, context => context.Message.JobId == _jobId); - _completed = Handled(configurator, context => context.Message.JobId == _jobId); - _completedT = Handled>(configurator, context => context.Message.JobId == _jobId); - } - } -} diff --git a/tests/MassTransit.Azure.Table.Tests/Turnout/Faulted_Specs.cs b/tests/MassTransit.Azure.Table.Tests/Turnout/Faulted_Specs.cs deleted file mode 100644 index c572b6f4559..00000000000 --- a/tests/MassTransit.Azure.Table.Tests/Turnout/Faulted_Specs.cs +++ /dev/null @@ -1,124 +0,0 @@ -namespace MassTransit.Azure.Table.Tests.Turnout -{ - using System; - using System.Threading.Tasks; - using Contracts.JobService; - using NUnit.Framework; - using TestFramework; - - - public interface GrindTheGears - { - Guid GearId { get; } - TimeSpan Duration { get; } - } - - - [TestFixture] - [Category("Flaky")] - public class Submitting_a_job_to_turnout_that_faults : - AzureTableInMemoryTestFixture - { - [Test] - [Order(1)] - public async Task Should_get_the_job_accepted() - { - IRequestClient> requestClient = Bus.CreateRequestClient>(); - - Response response = await requestClient.GetResponse(new - { - JobId = _jobId, - Job = new - { - GearId = _jobId, - Duration = TimeSpan.FromSeconds(1) - } - }); - - Assert.That(response.Message.JobId, Is.EqualTo(_jobId)); - - // just to capture all the test output in a single window - ConsumeContext faulted = await _faulted; - - await _fault; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_fault_event() - { - ConsumeContext> fault = await _fault; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_faulted_event() - { - ConsumeContext faulted = await _faulted; - } - - [Test] - [Order(3)] - public async Task Should_have_published_the_job_started_event() - { - ConsumeContext started = await _started; - } - - [Test] - [Order(2)] - public async Task Should_have_published_the_job_submitted_event() - { - ConsumeContext submitted = await _submitted; - } - - readonly Guid _jobId = NewId.NextGuid(); - Task> _faulted; - Task> _submitted; - Task> _started; - Task>> _fault; - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - base.ConfigureInMemoryBus(configurator); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(x => - { - x.UseAzureTableSagaRepository(() => TestCloudTable); - }); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.Consumer(() => new GrindTheGearsConsumer(), cfg => - { - cfg.Options>(jobOptions => jobOptions.SetJobTimeout(TimeSpan.FromSeconds(90))); - }); - }); - }); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - _submitted = Handled(configurator, context => context.Message.JobId == _jobId); - _started = Handled(configurator, context => context.Message.JobId == _jobId); - _faulted = Handled(configurator, context => context.Message.JobId == _jobId); - _fault = Handled>(configurator, context => context.Message.Message.GearId == _jobId); - } - - - public class GrindTheGearsConsumer : - IJobConsumer - { - public async Task Run(JobContext context) - { - await Task.Delay(context.Job.Duration); - - throw new IntentionalTestException("Grinding gears, dropped the transmission"); - } - } - } -} diff --git a/tests/MassTransit.Azure.Table.Tests/Turnout/Submitting_a_bunch_of_jobs.cs b/tests/MassTransit.Azure.Table.Tests/Turnout/Submitting_a_bunch_of_jobs.cs deleted file mode 100644 index eda8459ede9..00000000000 --- a/tests/MassTransit.Azure.Table.Tests/Turnout/Submitting_a_bunch_of_jobs.cs +++ /dev/null @@ -1,145 +0,0 @@ -namespace MassTransit.Azure.Table.Tests.Turnout -{ - using System; - using System.Linq; - using System.Threading.Tasks; - using Contracts.JobService; - using NUnit.Framework; - - - [TestFixture] - [Category("Flaky")] - public class Submitting_a_bunch_of_jobs : - AzureTableInMemoryTestFixture - { - [Test] - [Order(1)] - public async Task Should_get_the_job_accepted() - { - IRequestClient> requestClient = Bus.CreateRequestClient>(); - - for (var i = 0; i < Count; i++) - { - Response response = await requestClient.GetResponse(new - { - JobId = _jobIds[i], - Job = new { Duration = TimeSpan.FromSeconds(1) } - }); - } - - ConsumeContext[] completed = await Task.WhenAll(_completed.Select(x => x.Task)); - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_completed_event() - { - ConsumeContext[] completed = await Task.WhenAll(_completed.Select(x => x.Task)); - } - - [Test] - [Order(3)] - public async Task Should_have_published_the_job_started_event() - { - ConsumeContext[] started = await Task.WhenAll(_started.Select(x => x.Task)); - } - - [Test] - [Order(2)] - public async Task Should_have_published_the_job_submitted_event() - { - ConsumeContext[] submitted = await Task.WhenAll(_submitted.Select(x => x.Task)); - } - - Guid[] _jobIds; - TaskCompletionSource>[] _completed; - TaskCompletionSource>[] _submitted; - TaskCompletionSource>[] _started; - - const int Count = 10; - - [OneTimeSetUp] - public async Task Arrange() - { - _jobIds = new Guid[Count]; - for (var i = 0; i < _jobIds.Length; i++) - _jobIds[i] = NewId.NextGuid(); - } - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - base.ConfigureInMemoryBus(configurator); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(x => - { - x.SlotWaitTime = TimeSpan.FromSeconds(1); - x.SagaPartitionCount = 16; - x.FinalizeCompleted = true; - - x.UseAzureTableSagaRepository(() => TestCloudTable); - }); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.Consumer(() => new CrunchTheNumbersConsumer(), cfg => - { - cfg.Options>(o => o.SetJobTimeout(TimeSpan.FromSeconds(90)).SetConcurrentJobLimit(3)); - }); - e.UseMessageScheduler(e.InputAddress); - }); - }); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - _submitted = new TaskCompletionSource>[Count]; - _started = new TaskCompletionSource>[Count]; - _completed = new TaskCompletionSource>[Count]; - - for (var i = 0; i < Count; i++) - { - _submitted[i] = GetTask>(); - _started[i] = GetTask>(); - _completed[i] = GetTask>(); - } - - configurator.Handler(context => - { - for (var i = 0; i < Count; i++) - { - if (_jobIds[i] == context.Message.JobId) - _submitted[i].TrySetResult(context); - } - - return Task.CompletedTask; - }); - - configurator.Handler(context => - { - for (var i = 0; i < Count; i++) - { - if (_jobIds[i] == context.Message.JobId) - _started[i].TrySetResult(context); - } - - return Task.CompletedTask; - }); - - configurator.Handler(context => - { - for (var i = 0; i < Count; i++) - { - if (_jobIds[i] == context.Message.JobId) - _completed[i].TrySetResult(context); - } - - return Task.CompletedTask; - }); - } - } -} diff --git a/tests/MassTransit.Azure.Table.Tests/docker-compose.yml b/tests/MassTransit.Azure.Table.Tests/docker-compose.yml new file mode 100644 index 00000000000..fd1ea6043b9 --- /dev/null +++ b/tests/MassTransit.Azure.Table.Tests/docker-compose.yml @@ -0,0 +1,8 @@ +services: + azurite: + container_name: "azurite" + image: "mcr.microsoft.com/azure-storage/azurite:latest" + ports: + - "10000:10000" + - "10001:10001" + - "10002:10002" diff --git a/tests/MassTransit.Benchmark/BusOutbox/BusOutboxDbContext.cs b/tests/MassTransit.Benchmark/BusOutbox/BusOutboxDbContext.cs index 603eba12da7..566e3a02947 100644 --- a/tests/MassTransit.Benchmark/BusOutbox/BusOutboxDbContext.cs +++ b/tests/MassTransit.Benchmark/BusOutbox/BusOutboxDbContext.cs @@ -23,8 +23,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.AddInboxStateEntity(); - modelBuilder.AddOutboxMessageEntity(); - modelBuilder.AddOutboxStateEntity(); + modelBuilder.AddTransactionalOutboxEntities(); } } diff --git a/tests/MassTransit.Benchmark/Dockerfile b/tests/MassTransit.Benchmark/Dockerfile index 9e8e5074bbc..c37f920317f 100644 --- a/tests/MassTransit.Benchmark/Dockerfile +++ b/tests/MassTransit.Benchmark/Dockerfile @@ -1,9 +1,9 @@ #See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. -FROM mcr.microsoft.com/dotnet/runtime:6.0 AS base +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base WORKDIR /app -FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build WORKDIR /src COPY ["Directory.Build.props", "."] diff --git a/tests/MassTransit.Benchmark/GrpcOptionSet.cs b/tests/MassTransit.Benchmark/GrpcOptionSet.cs deleted file mode 100644 index 1859e2eff6b..00000000000 --- a/tests/MassTransit.Benchmark/GrpcOptionSet.cs +++ /dev/null @@ -1,50 +0,0 @@ -namespace MassTransitBenchmark -{ - using System; - using NDesk.Options; - - - public class GrpcOptionSet : - OptionSet - { - public GrpcOptionSet() - { - Add("h|host:", "The host name of the broker", value => Host = value); - Add("port:", "The virtual host to use", value => Port = value); - Add("split:", "Split into two bus instances to leverage separate connections", x => Split = x); - Add("lb:", "Load balance across both bus instances (only valid with split)", x => LoadBalance = x); - - Host = "127.0.0.1"; - Port = 19796; - } - - public string Host { get; set; } - public int Port { get; set; } - public bool Split { get; set; } - public bool LoadBalance { get; set; } - - public Uri HostAddress => - new UriBuilder - { - Scheme = "http", - Host = Host, - Port = Port - }.Uri; - - public Uri SecondHostAddress => - new UriBuilder - { - Scheme = "http", - Host = Host, - Port = Port + 10000 - }.Uri; - - public void ShowOptions() - { - Console.WriteLine("Host: {0}", HostAddress); - - if (Split) - Console.WriteLine("Second Host: {0}", SecondHostAddress); - } - } -} diff --git a/tests/MassTransit.Benchmark/Latency/GrpcMessageLatencyTransport.cs b/tests/MassTransit.Benchmark/Latency/GrpcMessageLatencyTransport.cs deleted file mode 100644 index 12a065aeebf..00000000000 --- a/tests/MassTransit.Benchmark/Latency/GrpcMessageLatencyTransport.cs +++ /dev/null @@ -1,93 +0,0 @@ -namespace MassTransitBenchmark.Latency -{ - using System; - using System.Threading.Tasks; - using MassTransit; - - - public class GrpcMessageLatencyTransport : - IMessageLatencyTransport - { - const string QueueName = "latency_consumer"; - - readonly GrpcOptionSet _options; - readonly IMessageLatencySettings _settings; - IBusControl _busControl; - IBusControl _outboundBus; - ISendEndpoint _targetEndpoint; - - public GrpcMessageLatencyTransport(GrpcOptionSet options, IMessageLatencySettings settings) - { - _options = options; - _settings = settings; - } - - public Task Send(LatencyTestMessage message) - { - return _targetEndpoint.Send(message); - } - - public async Task Start(Action callback, IReportConsumerMetric reportConsumerMetric) - { - _busControl = Bus.Factory.CreateUsingGrpc(x => - { - x.Host(_options.HostAddress); - - x.ReceiveEndpoint(QueueName, e => - { - e.PrefetchCount = _settings.PrefetchCount; - - if (_settings.ConcurrencyLimit > 0) - e.ConcurrentMessageLimit = _settings.ConcurrencyLimit; - - callback(e); - }); - }); - - await _busControl.StartAsync(); - - var targetAddress = new Uri($"exchange:{QueueName}"); - - if (_options.Split) - { - _outboundBus = Bus.Factory.CreateUsingGrpc(x => - { - x.Host(_options.SecondHostAddress, h => - { - h.AddServer(_options.HostAddress); - }); - - if (_options.LoadBalance) - { - x.ReceiveEndpoint(QueueName, e => - { - e.PrefetchCount = _settings.PrefetchCount; - - if (_settings.ConcurrencyLimit > 0) - e.ConcurrentMessageLimit = _settings.ConcurrencyLimit; - - callback(e); - }); - } - }); - - await Task.WhenAll(_busControl.StartAsync(), _outboundBus.StartAsync()); - - _targetEndpoint = await _outboundBus.GetSendEndpoint(targetAddress); - } - else - { - await _busControl.StartAsync(); - _targetEndpoint = await _busControl.GetSendEndpoint(targetAddress); - } - } - - public async ValueTask DisposeAsync() - { - await _busControl.StopAsync(); - - if (_outboundBus != null) - await _outboundBus.StopAsync(); - } - } -} diff --git a/tests/MassTransit.Benchmark/Latency/SqlMessageLatencyTransport.cs b/tests/MassTransit.Benchmark/Latency/SqlMessageLatencyTransport.cs new file mode 100644 index 00000000000..9b9aba458f2 --- /dev/null +++ b/tests/MassTransit.Benchmark/Latency/SqlMessageLatencyTransport.cs @@ -0,0 +1,84 @@ +namespace MassTransitBenchmark.Latency; + +using System; +using System.Threading.Tasks; +using BusOutbox; +using MassTransit; +using Microsoft.Extensions.DependencyInjection; + + +public class SqlMessageLatencyTransport : + IMessageLatencyTransport +{ + readonly SqlOptionSet _options; + readonly IMessageLatencySettings _settings; + ServiceProvider _provider; + AsyncServiceScope _scope; + Uri _targetAddress; + ISendEndpoint _targetEndpoint; + + public SqlMessageLatencyTransport(SqlOptionSet options, IMessageLatencySettings settings) + { + _options = options; + _settings = settings; + } + + public Task Send(LatencyTestMessage message) + { + return _targetEndpoint.Send(message); + } + + public async Task Start(Action callback, IReportConsumerMetric reportConsumerMetric) + { + _provider = new ServiceCollection() + .AddTextLogger(Console.Out) + .AddSingleton(reportConsumerMetric) + .AddPostgresMigrationHostedService(true, true) + .AddMassTransit(x => + { + x.AddOptions().Configure(options => + { + options.Host = _options.Host; + options.Database = _options.Database; + options.Schema = _options.Schema; + options.Role = _options.Role; + options.Username = "benchmark"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = _options.Username; + options.AdminPassword = _options.Password; + }); + + x.AddConsumer(); + + x.UsingPostgres((context, cfg) => + { + cfg.ReceiveEndpoint("latency_consumer" + (_settings.Durable ? "" : "_express"), e => + { + e.PurgeOnStartup = true; + e.PrefetchCount = _settings.PrefetchCount; + + if (_settings.ConcurrencyLimit > 0) + e.ConcurrentMessageLimit = _settings.ConcurrencyLimit; + + callback(e); + + _targetAddress = e.InputAddress; + }); + }); + }) + .BuildServiceProvider(true); + + await _provider.StartHostedServices(); + + _scope = _provider.CreateAsyncScope(); + + _targetEndpoint = await _scope.ServiceProvider.GetRequiredService().GetSendEndpoint(_targetAddress); + } + + public async ValueTask DisposeAsync() + { + await _scope.DisposeAsync(); + + await _provider.StopHostedServices(); + } +} diff --git a/tests/MassTransit.Benchmark/MassTransit.Benchmark.csproj b/tests/MassTransit.Benchmark/MassTransit.Benchmark.csproj index 50f2d41d655..52cfb804f16 100644 --- a/tests/MassTransit.Benchmark/MassTransit.Benchmark.csproj +++ b/tests/MassTransit.Benchmark/MassTransit.Benchmark.csproj @@ -4,21 +4,21 @@ true mtbench MassTransitBenchmark - net6.0 - latest + net8.0 - + + diff --git a/tests/MassTransit.Benchmark/Options.cs b/tests/MassTransit.Benchmark/Options.cs index 752c636a6bf..9ea0ab9623c 100644 --- a/tests/MassTransit.Benchmark/Options.cs +++ b/tests/MassTransit.Benchmark/Options.cs @@ -477,6 +477,9 @@ public OptionException(string message, string optionName, Exception innerExcepti option = optionName; } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected OptionException(SerializationInfo info, StreamingContext context) : base(info, context) { @@ -485,6 +488,9 @@ protected OptionException(SerializationInfo info, StreamingContext context) public string OptionName => option; +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif public override void GetObjectData(SerializationInfo info, StreamingContext context) { base.GetObjectData(info, context); diff --git a/tests/MassTransit.Benchmark/Program.cs b/tests/MassTransit.Benchmark/Program.cs index e1bb05e356b..7fb50d7a93f 100644 --- a/tests/MassTransit.Benchmark/Program.cs +++ b/tests/MassTransit.Benchmark/Program.cs @@ -8,7 +8,13 @@ using System.Threading.Tasks; using BusOutbox; using Latency; + using MassTransit.Logging; + using MassTransit.Monitoring; using NDesk.Options; + using OpenTelemetry; + using OpenTelemetry.Metrics; + using OpenTelemetry.Resources; + using OpenTelemetry.Trace; using RequestResponse; @@ -23,6 +29,7 @@ static async Task Main(string[] args) var optionSet = new ProgramOptionSet(); + var disposables = new List(); try { _remaining = optionSet.Parse(args); @@ -37,6 +44,24 @@ static async Task Main(string[] args) { } + if (optionSet.EnableMetrics) + { + disposables.Add(Sdk.CreateMeterProviderBuilder() + .AddMeter(InstrumentationOptions.MeterName) + .ConfigureResource(r => r.AddService("MassTransit.Benchmark")) + .AddOtlpExporter() + .Build()); + } + + if (optionSet.EnableTraces) + { + disposables.Add(Sdk.CreateTracerProviderBuilder() + .AddSource(DiagnosticHeaders.DefaultListenerName) + .ConfigureResource(r => r.AddService("MassTransit.Benchmark")) + .AddOtlpExporter() + .Build()); + } + optionSet.ShowOptions(); if (optionSet.Threads.HasValue) @@ -70,6 +95,10 @@ static async Task Main(string[] args) { Console.WriteLine("Crashed: {0}", ex.Message); } + finally + { + disposables.ForEach(x => x.Dispose()); + } } static async Task RunLatencyBenchmark(ProgramOptionSet optionSet) @@ -121,15 +150,6 @@ static async Task RunLatencyBenchmark(ProgramOptionSet optionSet) transport = new ActiveMqMessageLatencyTransport(activeMqOptionSet, settings); } - else if (optionSet.Transport == ProgramOptionSet.TransportOptions.Grpc) - { - var grpcOptionSet = new GrpcOptionSet(); - grpcOptionSet.Parse(_remaining); - - grpcOptionSet.ShowOptions(); - - transport = new GrpcMessageLatencyTransport(grpcOptionSet, settings); - } else if (optionSet.Transport == ProgramOptionSet.TransportOptions.Kafka) { var kafkaOptionSet = new KafkaOptionSet(); @@ -139,6 +159,15 @@ static async Task RunLatencyBenchmark(ProgramOptionSet optionSet) transport = new KafkaMessageLatencyTransport(kafkaOptionSet, settings); } + else if (optionSet.Transport == ProgramOptionSet.TransportOptions.Sql) + { + var options = new SqlOptionSet(); + options.Parse(_remaining); + + options.ShowOptions(); + + transport = new SqlMessageLatencyTransport(options, settings); + } else if (optionSet.Transport == ProgramOptionSet.TransportOptions.Mediator) transport = new MediatorMessageLatencyTransport(settings); else diff --git a/tests/MassTransit.Benchmark/ProgramOptionSet.cs b/tests/MassTransit.Benchmark/ProgramOptionSet.cs index 732f9144151..9ed0c73b9cc 100644 --- a/tests/MassTransit.Benchmark/ProgramOptionSet.cs +++ b/tests/MassTransit.Benchmark/ProgramOptionSet.cs @@ -25,8 +25,8 @@ public enum TransportOptions InMemory, AmazonSqs, ActiveMq, - Grpc, - Kafka + Kafka, + Sql } @@ -35,7 +35,10 @@ public ProgramOptionSet() Add("v|verbose", "Verbose output", x => Verbose = x != null); Add("?|help", "Display this help and exit", x => Help = x != null); Add("threads:", "The minimum number of thread pool threads", value => Threads = value); - Add("t|transport:", "Transport (RabbitMQ, AzureServiceBus, Mediator, AmazonSqs, InMemory, Grpc)", + Add("traces", "Enable traces capturing to OTel exporter", x => EnableTraces = x != null); + Add("metrics", "Enable metrics capturing to OTel exporter", x => EnableMetrics = x != null); + + Add("t|transport:", "Transport (RabbitMQ, AzureServiceBus, Mediator, AmazonSqs, InMemory)", value => Transport = value); Add("rabbitmq", "Use RabbitMQ", x => Transport = TransportOptions.RabbitMq); Add("kafka", "Use Kafka", x => Transport = TransportOptions.Kafka); @@ -44,7 +47,7 @@ public ProgramOptionSet() Add("sqs", "Use Amazon SQS", x => Transport = TransportOptions.AmazonSqs); Add("servicebus", "Use Azure Service Bus", x => Transport = TransportOptions.AzureServiceBus); Add("activemq", "Use ActiveMQ", x => Transport = TransportOptions.ActiveMq); - Add("grpc", "Use gRPC", x => Transport = TransportOptions.Grpc); + Add("sql", "Use SQL Transport", x => Transport = TransportOptions.Sql); Add("run:", "Run benchmark (All, Latency, RPC)", value => Benchmark = value); Add("rpc", "Run the RPC benchmark", x => Benchmark = BenchmarkOptions.Rpc); @@ -59,6 +62,8 @@ public ProgramOptionSet() public int? Threads { get; set; } public bool Verbose { get; set; } public bool Help { get; set; } + public bool EnableTraces { get; set; } + public bool EnableMetrics { get; set; } public TransportOptions Transport { get; private set; } diff --git a/tests/MassTransit.Benchmark/RabbitMqOptionSet.cs b/tests/MassTransit.Benchmark/RabbitMqOptionSet.cs index e625af393b6..5758642ea15 100644 --- a/tests/MassTransit.Benchmark/RabbitMqOptionSet.cs +++ b/tests/MassTransit.Benchmark/RabbitMqOptionSet.cs @@ -103,6 +103,8 @@ public RabbitMqOptionSet() public TimeSpan ContinuationTimeout => TimeSpan.FromSeconds(20); public uint? MaxMessageSize { get; set; } + public ICredentialsProvider CredentialsProvider { get; set; } + public ICredentialsRefresher CredentialsRefresher { get; set; } public Task Refresh(ConnectionFactory connectionFactory) { diff --git a/tests/MassTransit.Benchmark/RequestResponse/InMemoryRequestResponseTransport.cs b/tests/MassTransit.Benchmark/RequestResponse/InMemoryRequestResponseTransport.cs index b5ce367e1ca..b29eea3a674 100644 --- a/tests/MassTransit.Benchmark/RequestResponse/InMemoryRequestResponseTransport.cs +++ b/tests/MassTransit.Benchmark/RequestResponse/InMemoryRequestResponseTransport.cs @@ -11,7 +11,7 @@ public class InMemoryRequestResponseTransport : readonly InMemoryOptionSet _optionSet; IBusControl _busControl; - Task _clientFactory; + IClientFactory _clientFactory; IRequestResponseSettings _settings; Uri _targetEndpointAddress; @@ -44,9 +44,7 @@ public void GetBusControl(Action callback) public async Task> GetRequestClient(TimeSpan settingsRequestTimeout) where T : class { - var clientFactory = await _clientFactory; - - return clientFactory.CreateRequestClient(_targetEndpointAddress, settingsRequestTimeout); + return _clientFactory.CreateRequestClient(_targetEndpointAddress, settingsRequestTimeout); } public void Dispose() diff --git a/tests/MassTransit.Benchmark/RequestResponse/RabbitMqRequestResponseTransport.cs b/tests/MassTransit.Benchmark/RequestResponse/RabbitMqRequestResponseTransport.cs index cf4e091522d..61445dcfd75 100644 --- a/tests/MassTransit.Benchmark/RequestResponse/RabbitMqRequestResponseTransport.cs +++ b/tests/MassTransit.Benchmark/RequestResponse/RabbitMqRequestResponseTransport.cs @@ -11,7 +11,7 @@ public class RabbitMqRequestResponseTransport : readonly RabbitMqHostSettings _hostSettings; readonly IRequestResponseSettings _settings; IBusControl _busControl; - Task _clientFactory; + IClientFactory _clientFactory; Uri _targetEndpointAddress; public RabbitMqRequestResponseTransport(RabbitMqHostSettings hostSettings, IRequestResponseSettings settings) @@ -23,9 +23,7 @@ public RabbitMqRequestResponseTransport(RabbitMqHostSettings hostSettings, IRequ public async Task> GetRequestClient(TimeSpan settingsRequestTimeout) where T : class { - var clientFactory = await _clientFactory; - - return clientFactory.CreateRequestClient(_targetEndpointAddress, settingsRequestTimeout); + return _clientFactory.CreateRequestClient(_targetEndpointAddress, settingsRequestTimeout); } public void GetBusControl(Action callback) diff --git a/tests/MassTransit.Benchmark/SqlOptionSet.cs b/tests/MassTransit.Benchmark/SqlOptionSet.cs new file mode 100644 index 00000000000..12f39361e3a --- /dev/null +++ b/tests/MassTransit.Benchmark/SqlOptionSet.cs @@ -0,0 +1,43 @@ +namespace MassTransitBenchmark; + +using System; +using NDesk.Options; + + +public class SqlOptionSet : + OptionSet +{ + public SqlOptionSet() + { + Add("h|host:", "The host name of the broker", x => Host = x); + Add("u|username:", "Username (if using basic credentials)", value => Username = value); + Add("p|password:", "Password (if using basic credentials)", value => Password = value); + Add("db|database:", "Database to use", value => Database = value); + Add("role:", "Database role to use", value => Role = value); + Add("schema:", "Schema to use", value => Schema = value); + + Host = "localhost"; + Username = "postgres"; + Password = "Password12!"; + Database = "benchmark"; + Role = "transport"; + Schema = "transport"; + } + + public string Host { get; set; } + public string Username { get; set; } + public string Password { get; set; } + public string Role { get; set; } + public string Database { get; set; } + public string Schema { get; set; } + + public void ShowOptions() + { + Console.WriteLine("Host: {0}", Host); + Console.WriteLine("Username: {0}", Username); + Console.WriteLine("Password: {0}", new string('*', (Password ?? "default").Length)); + Console.WriteLine("Database: {0}", Database); + Console.WriteLine("Role: {0}", Role); + Console.WriteLine("Schema: {0}", Schema); + } +} diff --git a/tests/MassTransit.Benchmark/configs/otel-collector/otelcol-config-extras.yml b/tests/MassTransit.Benchmark/configs/otel-collector/otelcol-config-extras.yml new file mode 100644 index 00000000000..9b76acda52e --- /dev/null +++ b/tests/MassTransit.Benchmark/configs/otel-collector/otelcol-config-extras.yml @@ -0,0 +1,2 @@ +# extra settings to be merged into OpenTelemetry Collector configuration +# do not delete this file diff --git a/tests/MassTransit.Benchmark/configs/otel-collector/otelcol-config.yml b/tests/MassTransit.Benchmark/configs/otel-collector/otelcol-config.yml new file mode 100644 index 00000000000..1f254c11104 --- /dev/null +++ b/tests/MassTransit.Benchmark/configs/otel-collector/otelcol-config.yml @@ -0,0 +1,26 @@ +receivers: + otlp: + protocols: + grpc: + +exporters: + logging: + loglevel: debug + +processors: + batch: + +service: + pipelines: + traces: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ logging ] + metrics: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ logging ] + logs: + receivers: [ otlp ] + processors: [ batch ] + exporters: [ logging ] diff --git a/tests/MassTransit.Benchmark/docker-compose.yml b/tests/MassTransit.Benchmark/docker-compose.yml index 4e6642bf4cc..455d6b977e1 100644 --- a/tests/MassTransit.Benchmark/docker-compose.yml +++ b/tests/MassTransit.Benchmark/docker-compose.yml @@ -11,7 +11,16 @@ services: - "ACCEPT_EULA=Y" - "SA_PASSWORD=Password12!" ports: - - 1433:1433 + - "1433:1433" + + otel-collector: + image: otel/opentelemetry-collector + restart: always + command: [ "--config=/etc/otelcol-config.yml" ] + volumes: + - ./configs/otel-collector/otelcol-config.yml:/etc/otelcol-config.yml + ports: + - "4317:4317" # OTLP gRPC receiver redpanda: image: docker.redpanda.com/vectorized/redpanda:latest @@ -24,8 +33,8 @@ services: - --pandaproxy-addr 0.0.0.0:8082 - --advertise-pandaproxy-addr localhost:8082 ports: - - 8081:8081 - - 8082:8082 - - 9092:9092 - - 9644:9644 - - 29092:29092 + - "8081:8081" + - "8082:8082" + - "9092:9092" + - "9644:9644" + - "29092:29092" diff --git a/tests/MassTransit.BenchmarkConsole/Benchmarker.cs b/tests/MassTransit.BenchmarkConsole/Benchmarker.cs index 1189b82f23a..b2219dc5621 100644 --- a/tests/MassTransit.BenchmarkConsole/Benchmarker.cs +++ b/tests/MassTransit.BenchmarkConsole/Benchmarker.cs @@ -7,7 +7,7 @@ [SimpleJob(RuntimeMoniker.NetCoreApp31)] [SimpleJob(RuntimeMoniker.Net60)] - [SimpleJob(RuntimeMoniker.Net70)] + [SimpleJob(RuntimeMoniker.Net80)] [MemoryDiagnoser] [GcServer(true)] [GcForce] diff --git a/tests/MassTransit.BenchmarkConsole/ChannelBenchmark.cs b/tests/MassTransit.BenchmarkConsole/ChannelBenchmark.cs new file mode 100644 index 00000000000..b44b81a2daf --- /dev/null +++ b/tests/MassTransit.BenchmarkConsole/ChannelBenchmark.cs @@ -0,0 +1,147 @@ +namespace MassTransit.BenchmarkConsole; + +using System; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; +using Util; + + +[SimpleJob(RuntimeMoniker.Net80)] +[MemoryDiagnoser] +[GcServer(true)] +[GcForce] +public class ChannelBenchmark : + IAsyncDisposable +{ + readonly ChannelExecutor _singleChannel = new(1, 1); + readonly TaskExecutor _taskExecutor = new(); + + public ValueTask DisposeAsync() + { + return _singleChannel.DisposeAsync(); + } + + [Benchmark(Baseline = true, Description = "Regular Method")] + public async Task RegularMethod() + { + await SubjectMethod().ConfigureAwait(false); + } + + [Benchmark(Description = "Single Channel")] + public async Task SingleChannelExecutor() + { + await _singleChannel.Run(SubjectMethod).ConfigureAwait(false); + } + + [Benchmark(Description = "Super Channel")] + public async Task SuperChannelExecutor() + { + await _taskExecutor.Run(SubjectMethod).ConfigureAwait(false); + } + + static async Task SubjectMethod() + { + await SubjectChildMethod().ConfigureAwait(false); + } + + static async Task SubjectChildMethod() + { + } +} + + +[SimpleJob(RuntimeMoniker.Net80)] +[MemoryDiagnoser] +[GcServer(true)] +[GcForce] +public class ConcurrentChannelBenchmark : + IAsyncDisposable +{ + readonly ChannelExecutor _singleChannel = new(1, 10); + readonly TaskExecutor _taskExecutor = new(10); + + public async ValueTask DisposeAsync() + { + await _singleChannel.DisposeAsync(); + + await _taskExecutor.DisposeAsync(); + } + + [Benchmark(Baseline = true, Description = "Regular Method", OperationsPerInvoke = 10)] + public async Task RegularMethod() + { + await Parallel.ForAsync(0, 10, async (n, token) => await SubjectMethod()); + } + + [Benchmark(Description = "Single Channel", OperationsPerInvoke = 10)] + public async Task SingleChannelExecutor() + { + await Parallel.ForAsync(0, 10, async (n, token) => await _singleChannel.Run(SubjectMethod, token)); + } + + [Benchmark(Description = "Super Channel", OperationsPerInvoke = 10)] + public async Task SuperChannelExecutor() + { + await Parallel.ForAsync(0, 10, async (n, token) => await _taskExecutor.Run(SubjectMethod, token)); + } + + // [Benchmark(Description = "Super Channel (Value)", OperationsPerInvoke = 10)] + // public async Task SuperChannelExecutorValue() + // { + // await Parallel.ForAsync(0, 10, (n, token) => _taskExecutor.Run(SubjectMethodValue, token)); + // } + + static async Task SubjectMethod() + { + await SubjectChildMethod().ConfigureAwait(false); + } + + static async Task SubjectChildMethod() + { + } + + static async ValueTask SubjectMethodValue() + { + await SubjectChildMethodValue().ConfigureAwait(false); + } + + static async ValueTask SubjectChildMethodValue() + { + } +} + + +public class MessageTypeChannelReader : + ChannelReader> + where T : class +{ + readonly ChannelReader _source; + + public MessageTypeChannelReader(ChannelReader source) + { + _source = source; + } + + public override bool TryRead(out ConsumeContext item) + { + while (_source.TryRead(out var read)) + { + if (read.TryGetMessage(out ConsumeContext messageContext)) + { + item = messageContext; + return true; + } + } + + item = null; + return false; + } + + public override ValueTask WaitToReadAsync(CancellationToken cancellationToken = new()) + { + return _source.WaitToReadAsync(cancellationToken); + } +} diff --git a/tests/MassTransit.BenchmarkConsole/ConcurrencyBenchmark.cs b/tests/MassTransit.BenchmarkConsole/ConcurrencyBenchmark.cs new file mode 100644 index 00000000000..ff1b3fda44b --- /dev/null +++ b/tests/MassTransit.BenchmarkConsole/ConcurrencyBenchmark.cs @@ -0,0 +1,56 @@ +namespace MassTransit.BenchmarkConsole; + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Jobs; + + +[SimpleJob(RuntimeMoniker.Net80)] +[MemoryDiagnoser] +[GcServer(true)] +[GcForce] +public class ConcurrencyBenchmark +{ + static Pending _pending = new(); + static long _id; + + readonly Dictionary _dictionary = new(); + readonly ConcurrentDictionary _concurrentDictionary = new(); + + [Benchmark(Baseline = true, Description = "Dictionary (Lock)")] + public async Task DictionaryWithLock() + { + var nextId = Interlocked.Increment(ref _id); + lock (_dictionary) + _dictionary.Add(nextId, _pending); + + lock (_dictionary) + _dictionary.Remove(nextId); + } + + [Benchmark(Description = "Dictionary (NoLock)")] + public async Task DictionaryNoLock() + { + var nextId = Interlocked.Increment(ref _id); + _dictionary.Add(nextId, _pending); + + _dictionary.Remove(nextId); + } + + [Benchmark(Description = "ConcurrentDictionary")] + public async Task ConcurrentDictionaryNoLock() + { + var nextId = Interlocked.Increment(ref _id); + _concurrentDictionary.TryAdd(nextId, _pending); + + _concurrentDictionary.TryRemove(nextId, out _); + } + + + class Pending + { + } +} diff --git a/tests/MassTransit.BenchmarkConsole/DeserializationBenchmark.cs b/tests/MassTransit.BenchmarkConsole/DeserializationBenchmark.cs new file mode 100644 index 00000000000..0232abde7dc --- /dev/null +++ b/tests/MassTransit.BenchmarkConsole/DeserializationBenchmark.cs @@ -0,0 +1,74 @@ +namespace MassTransit.BenchmarkConsole; + +using System; +using BenchmarkDotNet.Attributes; +using Context; +using Serialization; + + +[MemoryDiagnoser] +public class DeserializationBenchmark +{ + readonly MessagePackMessageSerializer _messagepackSerializer; + readonly SystemTextJsonMessageSerializer _systemTextJsonSerializer; + + MessageBody _messagePackMessageBody; + MessageBody _systemTextJsonMessageBody; + + [Params(0, 4096, 16384)] + public int MessageBufferSize { get; set; } + + public DeserializationBenchmark() + { + _messagepackSerializer = new MessagePackMessageSerializer(); + _systemTextJsonSerializer = SystemTextJsonMessageSerializer.Instance; + } + + [GlobalSetup] + public void Setup() + { + var bufferContent = new byte[MessageBufferSize]; + Random.Shared.NextBytes(bufferContent); + + var initialMessage = new TypeToDeserialize + { + StringValue = "Hello, World!", + IntValue = 42, + GuidValue = Guid.NewGuid(), + DateTimeValue = DateTime.UtcNow, + ByteArrayValue = bufferContent + }; + + var sendContext = new MessageSendContext(initialMessage); + + _messagePackMessageBody = _messagepackSerializer.GetMessageBody(sendContext); + _systemTextJsonMessageBody = _systemTextJsonSerializer.GetMessageBody(sendContext); + + // Triggers any lazy serialization. + _ = _messagePackMessageBody.GetBytes(); + _ = _systemTextJsonMessageBody.GetBytes(); + } + + [Benchmark] + public void MessagePack_Deserialize() + { + var serializerContext = _messagepackSerializer.Deserialize(_messagePackMessageBody, null, null); + _ = serializerContext.TryGetMessage(out _); + } + + [Benchmark(Baseline = true)] + public void SystemTextJson_Deserialize() + { + var serializerContext = _systemTextJsonSerializer.Deserialize(_systemTextJsonMessageBody, null, null); + _ = serializerContext.TryGetMessage(out _); + } + + class TypeToDeserialize + { + public string StringValue { get; set; } + public int IntValue { get; set; } + public Guid GuidValue { get; set; } + public DateTime DateTimeValue { get; set; } + public byte[] ByteArrayValue { get; set; } + } +} diff --git a/tests/MassTransit.BenchmarkConsole/MassTransit.BenchmarkConsole.csproj b/tests/MassTransit.BenchmarkConsole/MassTransit.BenchmarkConsole.csproj index 4d9bb085790..aea106c852b 100644 --- a/tests/MassTransit.BenchmarkConsole/MassTransit.BenchmarkConsole.csproj +++ b/tests/MassTransit.BenchmarkConsole/MassTransit.BenchmarkConsole.csproj @@ -1,23 +1,22 @@  - net6.0;net7.0;netcoreapp3.1 + net8.0 true MassTransit.BenchmarkConsole Exe False - 8 Exe - False - + + diff --git a/tests/MassTransit.BenchmarkConsole/MediatorBenchmark.cs b/tests/MassTransit.BenchmarkConsole/MediatorBenchmark.cs index 8521c878794..8c77fa397e9 100644 --- a/tests/MassTransit.BenchmarkConsole/MediatorBenchmark.cs +++ b/tests/MassTransit.BenchmarkConsole/MediatorBenchmark.cs @@ -10,7 +10,7 @@ namespace MassTransit.BenchmarkConsole public class ExampleCommand : - IRequest + IRequest { public ExampleCommand(string arg1, int arg2) { @@ -37,7 +37,7 @@ public class MediatorBenchmark public void Setup() { var services = new ServiceCollection(); - services.AddMediatR(typeof(MediatorBenchmark)); + services.AddMediatR(cfg => cfg.RegisterServicesFromAssemblyContaining()); _mediator = Bus.Factory.CreateMediator(cfg => { @@ -123,7 +123,7 @@ public class ExampleResponse public class ExampleCommandHandler : - IRequestHandler, + IRequestHandler, IConsumer { public Task Consume(ConsumeContext context) diff --git a/tests/MassTransit.BenchmarkConsole/NewIdBenchmarks.cs b/tests/MassTransit.BenchmarkConsole/NewIdBenchmarks.cs index e4355448782..1ac967df6a9 100644 --- a/tests/MassTransit.BenchmarkConsole/NewIdBenchmarks.cs +++ b/tests/MassTransit.BenchmarkConsole/NewIdBenchmarks.cs @@ -14,10 +14,10 @@ public Config() { // Run with intrinsics disabled AddJob( - Job.Default.WithEnvironmentVariable(new EnvironmentVariable("COMPlus_EnableSSE2", "0")).WithRuntime(CoreRuntime.Core60).AsDefault()); + Job.Default.WithEnvironmentVariable(new EnvironmentVariable("DOTNET_EnableSSE2", "0")).WithRuntime(CoreRuntime.Core60).AsDefault()); AddJob( - Job.Default.WithEnvironmentVariable(new EnvironmentVariable("COMPlus_EnableSSE2", "0")).WithRuntime(CoreRuntime.Core70)); + Job.Default.WithEnvironmentVariable(new EnvironmentVariable("DOTNET_EnableSSE2", "0")).WithRuntime(CoreRuntime.Core70)); // Run with intrinsics AddJob( diff --git a/tests/MassTransit.BenchmarkConsole/Program.cs b/tests/MassTransit.BenchmarkConsole/Program.cs index 8d3f96e65fd..2721a9c5bd9 100644 --- a/tests/MassTransit.BenchmarkConsole/Program.cs +++ b/tests/MassTransit.BenchmarkConsole/Program.cs @@ -1,25 +1,7 @@ -namespace MassTransit.BenchmarkConsole -{ - using System; - using BenchmarkDotNet.Running; +using System.Reflection; +using BenchmarkDotNet.Running; - - class Program - { - static void Main(string[] args) - { - Console.WriteLine("MassTransit Benchmark"); - Console.WriteLine(); - - // BenchmarkRunner.Run(); - - // BenchmarkRunner.Run(); - - // BenchmarkRunner.Run(); - - // BenchmarkRunner.Run(); - - BenchmarkRunner.Run(); - } - } -} +var currentAssembly = Assembly.GetExecutingAssembly(); +BenchmarkSwitcher + .FromAssembly(currentAssembly) + .Run(args); diff --git a/tests/MassTransit.BenchmarkConsole/SerializationBenchmark.cs b/tests/MassTransit.BenchmarkConsole/SerializationBenchmark.cs new file mode 100644 index 00000000000..96169ae0418 --- /dev/null +++ b/tests/MassTransit.BenchmarkConsole/SerializationBenchmark.cs @@ -0,0 +1,65 @@ +namespace MassTransit.BenchmarkConsole; + +using System; +using BenchmarkDotNet.Attributes; +using Serialization; + + +[MemoryDiagnoser] +public class SerializationBenchmark +{ + readonly MessagePackMessageSerializer _messagepackSerializer; + readonly SystemTextJsonMessageSerializer _systemTextJsonSerializer; + TypeToSerialize _serializationSubject = null!; + + [Params(0, 4096)] + public int MessageBufferSize { get; set; } + + public SerializationBenchmark() + { + _messagepackSerializer = new MessagePackMessageSerializer(); + _systemTextJsonSerializer = SystemTextJsonMessageSerializer.Instance; + } + + [GlobalSetup] + public void Setup() + { + var bufferContent = new byte[MessageBufferSize]; + Random.Shared.NextBytes(bufferContent); + + _serializationSubject = new TypeToSerialize + { + StringValue = "Hello, World!", + IntValue = 42, + GuidValue = Guid.NewGuid(), + DateTimeValue = DateTime.UtcNow, + ByteArrayValue = bufferContent + }; + } + + [Benchmark] + public void MessagePack_SerializeObject() + { + var messageBody = _messagepackSerializer.SerializeObject(_serializationSubject); + + _ = messageBody.GetBytes(); + } + + [Benchmark(Baseline = true)] + public void SystemTextJson_SerializeObject() + { + var messageBody = _systemTextJsonSerializer.SerializeObject(_serializationSubject); + + _ = messageBody.GetBytes(); + } + + + class TypeToSerialize + { + public required string StringValue { get; set; } + public required int IntValue { get; set; } + public required Guid GuidValue { get; set; } + public required DateTime DateTimeValue { get; set; } + public required byte[] ByteArrayValue { get; set; } + } +} diff --git a/tests/MassTransit.Containers.Tests/Batch_Specs.cs b/tests/MassTransit.Containers.Tests/Batch_Specs.cs deleted file mode 100644 index c22ac4e64f5..00000000000 --- a/tests/MassTransit.Containers.Tests/Batch_Specs.cs +++ /dev/null @@ -1,497 +0,0 @@ -namespace MassTransit.Containers.Tests -{ - using System; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using Microsoft.Extensions.DependencyInjection; - using Middleware.InMemoryOutbox; - using NUnit.Framework; - using TestFramework; - using Testing; - - - [TestFixture] - public class When_batch_limit_is_reached - { - [Test] - public async Task Should_deliver_the_batch_to_the_consumer() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.Count == 2 && x.Context.Message.Mode == BatchCompletionMode.Time)); - } - } - - - [TestFixture] - public class When_retry_and_in_memory_outbox_are_used_with_batch_consumers - { - [Test] - public async Task Should_deliver_the_batch_to_the_consumer() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(); - - x.AddConfigureEndpointsCallback((_, cfg) => - { - cfg.UseMessageRetry(r => r.Immediate(2)); - cfg.UseInMemoryOutbox(); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.Count == 2 && x.Context.Message.Mode == BatchCompletionMode.Time)); - } - - [Test] - public async Task Should_deliver_the_batch_to_the_consumer_with_message() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(c => c.Message>(m => m.UseInMemoryOutbox())); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.Count == 2 && x.Context.Message.Mode == BatchCompletionMode.Time)); - } - - [Test] - public async Task Should_deliver_the_batch_to_the_consumer_after_retry() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(); - - x.AddConfigureEndpointsCallback((_, cfg) => - { - cfg.UseMessageRetry(r => r.Immediate(2)); - cfg.UseInMemoryOutbox(); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.Count == 2 && x.Context.Message.Mode == BatchCompletionMode.Time)); - } - - - class TestOutboxBatchConsumer : - IConsumer> - { - public Task Consume(ConsumeContext> context) - { - if (context.TryGetPayload(out _)) - { - context.Respond(new BatchResult - { - Count = context.Message.Length, - Mode = context.Message.Mode - }); - } - else - throw new InvalidOperationException("Outbox context is not available at this point"); - - return Task.CompletedTask; - } - } - - - class TestRetryOutboxBatchConsumer : - IConsumer> - { - public Task Consume(ConsumeContext> context) - { - if (context.TryGetPayload(out _)) - { - if (context.GetRetryCount() == 0) - throw new IntentionalTestException("First time is not the charm"); - - context.Respond(new BatchResult - { - Count = context.Message.Length, - Mode = context.Message.Mode - }); - } - else - throw new InvalidOperationException("Outbox context is not available at this point"); - - return Task.CompletedTask; - } - } - } - - - [TestFixture] - public class When_a_batch_limit_is_configured - { - [Test] - public async Task Should_deliver_the_batch_to_the_consumer() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(c => - c.Options(o => o.SetMessageLimit(5).SetTimeLimit(1000))) - .Endpoint(e => e.ConcurrentMessageLimit = 16); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(5).Count(), Is.EqualTo(5)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.Count == 5 && x.Context.Message.Mode == BatchCompletionMode.Size)); - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.Count == 1 && x.Context.Message.Mode == BatchCompletionMode.Time)); - } - } - - - [TestFixture] - public class When_a_big_batch_limit_is_configured - { - [Test] - public async Task Should_deliver_the_batch_to_the_consumer() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(c => - c.Options(o => o.SetMessageLimit(100).SetTimeLimit(10000))) - .Endpoint(e => e.ConcurrentMessageLimit = 101); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(Enumerable.Range(0, 100).Select(_ => new BatchItem())); - - Assert.That(await harness.Consumed.SelectAsync().Take(100).Count(), Is.EqualTo(100)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.Count == 100 && x.Context.Message.Mode == BatchCompletionMode.Size)); - } - } - - - [TestFixture] - public class When_a_batch_limit_is_configured_using_a_definition - { - [Test] - public async Task Should_deliver_the_batch_to_the_consumer() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer() - .Endpoint(e => e.ConcurrentMessageLimit = 16); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(5).Count(), Is.EqualTo(5)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.Count == 5 && x.Context.Message.Mode == BatchCompletionMode.Size)); - Assert.IsTrue(await harness.Published.Any(x => x.Context.Message.Count == 1 && x.Context.Message.Mode == BatchCompletionMode.Time)); - } - } - - - [TestFixture] - public class When_a_batch_consumer_faults - { - [Test] - public async Task Should_fault_once_for_each_message_in_the_batch() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); - } - } - - - [TestFixture] - public class When_a_batch_consumer_faults_and_retries - { - [Test] - public async Task Should_fault_once_for_each_message_in_the_batch() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); - - x.AddConfigureEndpointsCallback((_, cfg) => cfg.UseMessageRetry(r => r.Immediate(1))); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); - } - - [Test] - public async Task Should_fault_once_for_each_message_in_the_batch_with_in_memory_outbox() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); - - x.AddConfigureEndpointsCallback((_, cfg) => - { - cfg.UseMessageRetry(r => r.Immediate(2)); - cfg.UseInMemoryOutbox(); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); - } - - [Test] - public async Task Should_fault_once_for_each_message_in_the_batch_at_the_consumer_retry() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(c => - { - c.UseMessageRetry(r => r.Immediate(1)); - c.Options(o => o.SetMessageLimit(2)); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); - } - - [Test] - public async Task Should_fault_once_for_each_message_in_the_batch_with_delayed_redelivery() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); - - x.AddConfigureEndpointsCallback((_, cfg) => - { - cfg.UseDelayedRedelivery(r => r.Intervals(10)); - cfg.UseMessageRetry(r => r.Immediate(1)); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); - } - - [Test] - public async Task Should_fault_once_for_each_message_in_the_batch_with_scheduled_redelivery() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); - - x.AddConfigureEndpointsCallback((_, cfg) => - { - cfg.UseScheduledRedelivery(r => r.Intervals(10)); - cfg.UseMessageRetry(r => r.Immediate(1)); - }); - - x.UsingInMemory((context, cfg) => - { - cfg.UseDelayedMessageScheduler(); - - cfg.ConfigureEndpoints(context); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); - - Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); - - Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); - - Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); - } - } - - - public class BatchItem - { - } - - - public class BatchResult - { - public int Count { get; set; } - public BatchCompletionMode Mode { get; set; } - } - - - public class TestBatchConsumerDefinition : - ConsumerDefinition - { - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) - { - endpointConfigurator.UseInMemoryOutbox(); - consumerConfigurator.Options(o => o.SetMessageLimit(5).SetTimeLimit(1000)); - } - } - - - public class TestBatchConsumer : - IConsumer> - { - public Task Consume(ConsumeContext> context) - { - context.Respond(new BatchResult - { - Count = context.Message.Length, - Mode = context.Message.Mode - }); - - return Task.CompletedTask; - } - } - - - class FailingBatchConsumer : - IConsumer> - { - int _attempts; - - public int Attempts => _attempts; - - public Task Consume(ConsumeContext> context) - { - Interlocked.Increment(ref _attempts); - - throw new IntentionalTestException("Failing Batch Consumer"); - } - } -} diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ConsumeContext.cs b/tests/MassTransit.Containers.Tests/Common_Tests/Common_ConsumeContext.cs deleted file mode 100644 index 3db3b36dbe3..00000000000 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ConsumeContext.cs +++ /dev/null @@ -1,553 +0,0 @@ -namespace MassTransit.Containers.Tests.Common_Tests -{ - using System; - using System.Linq; - using System.Threading.Tasks; - using ConsumeContextTestSubjects; - using Context; - using Microsoft.Extensions.DependencyInjection; - using Middleware.InMemoryOutbox; - using NUnit.Framework; - using TestFramework; - using TestFramework.Messages; - using Testing; - using UnitOfWorkComponents; - - - public class Common_ConsumeContext : - InMemoryContainerTestFixture - { - [Test] - public async Task Should_provide_the_consume_context() - { - await InputQueueSendEndpoint.Send(new PingMessage()); - - var consumeContext = await ConsumeContext; - - Assert.That(consumeContext.TryGetPayload(out MessageConsumeContext _), "Is MessageConsumeContext"); - - var publishEndpoint = await PublishEndpoint; - var sendEndpointProvider = await SendEndpointProvider; - - Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); - Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(messageConsumeContext, sendEndpointProvider)"); - } - - Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; - Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; - Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; - - protected override IServiceCollection ConfigureServices(IServiceCollection collection) - { - TaskCompletionSource pingTask = GetTask(); - TaskCompletionSource sendEndpointProviderTask = GetTask(); - TaskCompletionSource publishEndpointTask = GetTask(); - - return collection.AddSingleton(pingTask) - .AddSingleton(sendEndpointProviderTask) - .AddSingleton(publishEndpointTask) - .AddScoped() - .AddScoped(); - } - - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) - { - configurator.AddConsumer(); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumers(BusRegistrationContext); - } - } - - - public class Common_ConsumeContext_Outbox : - InMemoryContainerTestFixture - { - [Test] - public async Task Should_provide_the_outbox() - { - Task>> fault = await ConnectPublishHandler>(); - - await InputQueueSendEndpoint.Send(new PingMessage()); - - var consumeContext = await ConsumeContext; - - Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext _), "Is ConsumerConsumeContext"); - - var publishEndpoint = await PublishEndpoint; - var sendEndpointProvider = await SendEndpointProvider; - - Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); - Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); - - await fault; - - Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); - } - - Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; - Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; - Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; - - protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) - { - harness.TestTimeout = TimeSpan.FromSeconds(3); - } - - protected override IServiceCollection ConfigureServices(IServiceCollection collection) - { - TaskCompletionSource pingTask = GetTask(); - TaskCompletionSource sendEndpointProviderTask = GetTask(); - TaskCompletionSource publishEndpointTask = GetTask(); - - return collection - .AddSingleton(pingTask) - .AddSingleton(sendEndpointProviderTask) - .AddSingleton(publishEndpointTask) - .AddScoped() - .AddScoped(); - } - - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) - { - configurator.AddConsumer(); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.UseInMemoryOutbox(); - - configurator.ConfigureConsumers(BusRegistrationContext); - } - } - - - public class Common_ConsumeContext_Outbox_Batch : - InMemoryContainerTestFixture - { - [Test] - public async Task Should_provide_the_outbox() - { - Task>> fault = await ConnectPublishHandler>(); - - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - - var consumeContext = await ConsumeContext; - - Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext> _), "Is ConsumerConsumeContext"); - - var publishEndpoint = await PublishEndpoint; - var sendEndpointProvider = await SendEndpointProvider; - - Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); - Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); - - await fault; - - Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); - } - - Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; - Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; - Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; - - protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) - { - harness.TestTimeout = TimeSpan.FromSeconds(3); - } - - protected override IServiceCollection ConfigureServices(IServiceCollection collection) - { - TaskCompletionSource pingTask = GetTask(); - TaskCompletionSource sendEndpointProviderTask = GetTask(); - TaskCompletionSource publishEndpointTask = GetTask(); - - return collection - .AddSingleton(pingTask) - .AddSingleton(sendEndpointProviderTask) - .AddSingleton(publishEndpointTask) - .AddScoped() - .AddScoped(); - } - - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) - { - configurator.AddConsumer(x => - x.Options(b => b.SetTimeLimit(200).SetMessageLimit(4))); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.UseDelayedRedelivery(r => r.None()); - configurator.UseMessageRetry(r => r.None()); - configurator.UseInMemoryOutbox(); - configurator.UseUnitOfWork(); - - configurator.ConfigureConsumers(BusRegistrationContext); - } - } - - - public class Common_ConsumeContext_Filter_Batch : - InMemoryContainerTestFixture - { - [Test] - public async Task Should_provide_the_outbox() - { - Task>> fault = await ConnectPublishHandler>(); - - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - - var consumeContext = await ConsumeContext; - - Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext> _), "Is ConsumerConsumeContext"); - - var publishEndpoint = await PublishEndpoint; - var sendEndpointProvider = await SendEndpointProvider; - - Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); - Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); - - await fault; - - Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); - } - - Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; - Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; - Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; - - protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) - { - harness.TestTimeout = TimeSpan.FromSeconds(3); - } - - protected override IServiceCollection ConfigureServices(IServiceCollection collection) - { - TaskCompletionSource pingTask = GetTask(); - TaskCompletionSource sendEndpointProviderTask = GetTask(); - TaskCompletionSource publishEndpointTask = GetTask(); - - return collection - .AddSingleton(pingTask) - .AddSingleton(sendEndpointProviderTask) - .AddSingleton(publishEndpointTask) - .AddScoped() - .AddScoped(); - } - - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) - { - configurator.AddConsumer(x => - x.Options(b => b.SetTimeLimit(200).SetMessageLimit(4))); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.UseInMemoryOutbox(); - configurator.UseUnitOfWork(); - - configurator.ConfigureConsumers(BusRegistrationContext); - } - } - - - public class Common_ConsumeContext_Outbox_Solo : - InMemoryContainerTestFixture - { - [Test] - public async Task Should_provide_the_outbox_to_the_consumer() - { - Task>> fault = await ConnectPublishHandler>(); - - await InputQueueSendEndpoint.Send(new PingMessage()); - - var consumeContext = await ConsumeContext; - - Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext _), "Is ConsumerConsumeContext"); - - var publishEndpoint = await PublishEndpoint; - var sendEndpointProvider = await SendEndpointProvider; - - Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); - Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); - - await fault; - - Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); - } - - Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; - Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; - Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; - - protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) - { - harness.TestTimeout = TimeSpan.FromSeconds(3); - } - - protected override IServiceCollection ConfigureServices(IServiceCollection collection) - { - TaskCompletionSource pingTask = GetTask(); - TaskCompletionSource sendEndpointProviderTask = GetTask(); - TaskCompletionSource publishEndpointTask = GetTask(); - - return collection - .AddSingleton(pingTask) - .AddSingleton(sendEndpointProviderTask) - .AddSingleton(publishEndpointTask) - .AddScoped() - .AddScoped(); - } - - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) - { - configurator.AddConsumer(); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.UseInMemoryOutbox(); - - configurator.ConfigureConsumers(BusRegistrationContext); - } - } - - - namespace ConsumeContextTestSubjects - { - using TestFramework.Messages; - - - class DependentConsumer : - IConsumer - { - readonly IAnotherService _anotherService; - readonly IService _service; - - public DependentConsumer(IService service, IAnotherService anotherService) - { - _service = service; - _anotherService = anotherService; - } - - public async Task Consume(ConsumeContext context) - { - await _service.DoIt(); - - _anotherService.Done(); - - throw new IntentionalTestException(); - } - } - - - class DependentBatchConsumer : - IConsumer> - { - readonly IService _service; - readonly UnitOfWork _unitOfWork; - - public DependentBatchConsumer(IService service, UnitOfWork unitOfWork) - { - _service = service; - _unitOfWork = unitOfWork; - } - - public async Task Consume(ConsumeContext> context) - { - await _service.DoIt(); - - _unitOfWork.Add(); - - throw new IntentionalTestException(); - } - } - - - class FlyingSoloConsumer : - IConsumer - { - readonly ConsumeContext _consumeContext; - readonly TaskCompletionSource _consumeContextTask; - readonly IPublishEndpoint _publishEndpoint; - - public FlyingSoloConsumer(IPublishEndpoint publishEndpoint, ISendEndpointProvider sendEndpointProvider, ConsumeContext consumeContext, - TaskCompletionSource consumeContextTask, - TaskCompletionSource publishEndpointTask, - TaskCompletionSource sendEndpointProviderTask) - { - _publishEndpoint = publishEndpoint; - _consumeContext = consumeContext; - _consumeContextTask = consumeContextTask; - publishEndpointTask.TrySetResult(publishEndpoint); - sendEndpointProviderTask.TrySetResult(sendEndpointProvider); - } - - public async Task Consume(ConsumeContext context) - { - await _publishEndpoint.Publish(new { }); - - _consumeContextTask.TrySetResult(_consumeContext); - - throw new IntentionalTestException(); - } - } - - - public interface ServiceDidIt - { - } - - - interface IService - { - Task DoIt(); - } - - - class Service : - IService - { - readonly IPublishEndpoint _publishEndpoint; - - public Service(IPublishEndpoint publishEndpoint, ISendEndpointProvider sendEndpointProvider, - TaskCompletionSource publishEndpointTask, - TaskCompletionSource sendEndpointProviderTask) - { - _publishEndpoint = publishEndpoint; - publishEndpointTask.TrySetResult(publishEndpoint); - sendEndpointProviderTask.TrySetResult(sendEndpointProvider); - } - - public async Task DoIt() - { - await _publishEndpoint.Publish(new { }); - } - } - - - interface IAnotherService - { - void Done(); - } - - - class AnotherService : - IAnotherService - { - readonly TaskCompletionSource _consumeContextTask; - readonly ConsumeContext _context; - - public AnotherService(ConsumeContext context, TaskCompletionSource consumeContextTask) - { - _context = context; - _consumeContextTask = consumeContextTask; - } - - public void Done() - { - _consumeContextTask.TrySetResult(_context); - } - } - - - public class UnitOfWork - { - readonly TaskCompletionSource _consumeContextTask; - readonly ConsumeContext _context; - - public UnitOfWork(ConsumeContext context, TaskCompletionSource consumeContextTask) - { - _context = context; - _consumeContextTask = consumeContextTask; - } - - public void Add() - { - _consumeContextTask.TrySetResult(_context); - } - } - } - - - namespace UnitOfWorkComponents - { - using Configuration; - - - public class UnitOfWorkFilter : - IFilter> - where TMessage : class - { - public void Probe(ProbeContext context) - { - context.CreateFilterScope("uow"); - } - - public async Task Send(ConsumeContext context, IPipe> next) - { - var provider = context.GetPayload(); - var unitOfWork = provider.GetRequiredService(); - - await next.Send(context); - } - } - - - public class UnitOfWorkFilter : - IFilter - where TConsumer : class - where TContext : class, ConsumerConsumeContext - { - public void Probe(ProbeContext context) - { - context.CreateFilterScope("uow"); - } - - public async Task Send(TContext context, IPipe next) - { - var provider = context.GetPayload(); - var unitOfWork = provider.GetRequiredService(); - - await next.Send(context); - } - } - - - public class UnitOfWorkConfigurationObserver : - IConsumerConfigurationObserver - { - public void ConsumerConfigured(IConsumerConfigurator configurator) - where TConsumer : class - { - var filter = new UnitOfWorkFilter, TConsumer>(); - var specification = new FilterPipeSpecification>(filter); - configurator.AddPipeSpecification(specification); - } - - public void ConsumerMessageConfigured(IConsumerMessageConfigurator configurator) - where TConsumer : class - where TMessage : class - { - } - } - - - public static class UnitOfWorkMiddlewareConfiguratorExtensions - { - public static void UseUnitOfWork(this IConsumePipeConfigurator configurator) - { - configurator.ConnectConsumerConfigurationObserver(new UnitOfWorkConfigurationObserver()); - } - } - } -} diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Saga.cs b/tests/MassTransit.Containers.Tests/Common_Tests/Common_Saga.cs deleted file mode 100644 index 215b0a36eef..00000000000 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Saga.cs +++ /dev/null @@ -1,132 +0,0 @@ -namespace MassTransit.Containers.Tests.Common_Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using Scenarios; - using Shouldly; - using TestFramework; - using Testing; - - - public class Common_Saga : - InMemoryContainerTestFixture - { - [Test] - public async Task Should_handle_first_message() - { - var sagaId = NewId.NextGuid(); - - var message = new FirstSagaMessage { CorrelationId = sagaId }; - - await InputQueueSendEndpoint.Send(message); - - Guid? foundId = await GetSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); - - foundId.HasValue.ShouldBe(true); - } - - [Test] - public async Task Should_handle_second_message() - { - var sagaId = NewId.NextGuid(); - - var message = new FirstSagaMessage { CorrelationId = sagaId }; - - await InputQueueSendEndpoint.Send(message); - - Guid? foundId = await GetSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); - - foundId.HasValue.ShouldBe(true); - - var nextMessage = new SecondSagaMessage { CorrelationId = sagaId }; - - await InputQueueSendEndpoint.Send(nextMessage); - - foundId = await GetSagaRepository().ShouldContainSaga(x => x.CorrelationId == sagaId && x.Second.IsCompleted, TestTimeout); - - foundId.HasValue.ShouldBe(true); - } - - [Test] - public async Task Should_handle_third_message() - { - var sagaId = NewId.NextGuid(); - - var message = new FirstSagaMessage { CorrelationId = sagaId }; - - await InputQueueSendEndpoint.Send(message); - - Guid? foundId = await GetSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); - - foundId.HasValue.ShouldBe(true); - - var nextMessage = new ThirdSagaMessage { CorrelationId = sagaId }; - - await InputQueueSendEndpoint.Send(nextMessage); - - foundId = await GetSagaRepository().ShouldContainSaga(x => x.CorrelationId == sagaId && x.Third.IsCompleted, TestTimeout); - - foundId.HasValue.ShouldBe(true); - } - - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) - { - configurator.AddSaga() - .InMemoryRepository(); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.ConfigureSaga(BusRegistrationContext); - } - } - - - public class Common_Saga_Endpoint : - InMemoryContainerTestFixture - { - [Test] - public async Task Should_handle_the_message() - { - var sagaId = NewId.NextGuid(); - - var message = new FirstSagaMessage { CorrelationId = sagaId }; - - var sendEndpoint = await Bus.GetSendEndpoint(new Uri("loopback://localhost/custom-endpoint-name")); - - await sendEndpoint.Send(message); - - Guid? foundId = await GetSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); - - foundId.HasValue.ShouldBe(true); - } - - [Test] - public async Task Should_use_custom_endpoint_and_definition_together() - { - var sagaId = NewId.NextGuid(); - - var message = new FirstSagaMessage { CorrelationId = sagaId }; - - var sendEndpoint = await Bus.GetSendEndpoint(new Uri("loopback://localhost/custom-second-saga")); - - await sendEndpoint.Send(message); - - Guid? foundId = await GetSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); - - foundId.HasValue.ShouldBe(true); - } - - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) - { - configurator.AddSaga() - .Endpoint(e => e.Name = "custom-endpoint-name") - .InMemoryRepository(); - - configurator.AddSaga(typeof(SecondSimpleSagaDefinition)) - .Endpoint(e => e.Temporary = true) - .InMemoryRepository(); - } - } -} diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/MultiBus_Specs.cs b/tests/MassTransit.Containers.Tests/Common_Tests/MultiBus_Specs.cs deleted file mode 100644 index 41cdd979bae..00000000000 --- a/tests/MassTransit.Containers.Tests/Common_Tests/MultiBus_Specs.cs +++ /dev/null @@ -1,263 +0,0 @@ -namespace MassTransit.Containers.Tests.Common_Tests -{ - using System; - using System.Collections.Generic; - using System.Linq; - using System.Threading.Tasks; - using Contracts; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Hosting; - using NUnit.Framework; - using Scenarios; - using TestFramework; - using TestFramework.Messages; - using Transports; - - - public class Using_MultiBus : - InMemoryContainerTestFixture - { - [Test] - public async Task Should_receive() - { - await One.Publish(new SimpleMessageClass("abc")); - - await Task1.Task; - await Task2.Task; - } - - [Test] - public void Should_resolve_bus_declaration() - { - Assert.NotNull(ServiceProvider.GetService()); - Assert.NotNull(ServiceProvider.GetService()); - } - - [Test] - public void Should_resolve_bus_instance() - { - Assert.NotNull(ServiceProvider.GetService>()); - Assert.NotNull(ServiceProvider.GetService>()); - } - - [Test] - public async Task Should_support_request_client_on_bus_one() - { - IRequestClient client = GetRequestClient(); - - await client.GetResponse(new OneRequest()); - } - - [Test] - public async Task Should_support_request_client_on_bus_two() - { - IRequestClient client = GetRequestClient(); - - await client.GetResponse(new TwoRequest()); - } - - [Test] - public async Task Should_support_request_client_on_default_bus() - { - IRequestClient client = GetRequestClient(); - - await client.GetResponse(new Request()); - } - - TaskCompletionSource> Task1 { get; } - TaskCompletionSource> Task2 { get; } - - IBusOne One => ServiceProvider.GetService(); - IEnumerable HostedServices => ServiceProvider.GetService>(); - - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) - { - configurator.AddConsumer(); - configurator.AddRequestClient(); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumer(BusRegistrationContext); - } - - protected override IServiceCollection ConfigureServices(IServiceCollection collection) - { - collection = base.ConfigureServices(collection); - - collection.AddSingleton(Task1); - collection.AddSingleton(Task2); - - collection.AddMassTransit(ConfigureOne); - collection.AddMassTransit(ConfigureTwo); - - return collection; - } - - static void ConfigureOne(IBusRegistrationConfigurator configurator) - { - configurator.AddConsumer(); - configurator.AddConsumer(); - configurator.UsingInMemory((context, cfg) => - { - cfg.Host(new Uri("loopback://bus-one/")); - cfg.ConfigureEndpoints(context); - }); - configurator.AddRequestClient(); - } - - static void ConfigureTwo(IBusRegistrationConfigurator configurator) - { - configurator.AddConsumer(); - configurator.AddConsumer(); - configurator.UsingInMemory((context, cfg) => - { - cfg.Host(new Uri("loopback://bus-two/")); - cfg.ConfigureEndpoints(context); - }); - configurator.AddRequestClient(); - } - - public Using_MultiBus() - { - Task1 = GetTask>(); - Task2 = GetTask>(); - } - - [OneTimeSetUp] - public async Task Setup() - { - await Task.WhenAll(HostedServices.Select(x => x.StartAsync(InMemoryTestHarness.TestCancellationToken))); - } - - [OneTimeTearDown] - public async Task TearDown() - { - await Task.WhenAll(HostedServices.Select(x => x.StopAsync(InMemoryTestHarness.TestCancellationToken))); - } - - - class Consumer1 : - IConsumer - { - readonly IPublishEndpoint _publishEndpoint; - readonly TaskCompletionSource> _taskCompletionSource; - - public Consumer1(IPublishEndpoint publishEndpointDefault, IBusTwo publishEndpoint, - TaskCompletionSource> taskCompletionSource) - { - _publishEndpoint = publishEndpoint; - _taskCompletionSource = taskCompletionSource; - } - - public async Task Consume(ConsumeContext context) - { - _taskCompletionSource.TrySetResult(context); - await _publishEndpoint.Publish(new PingMessage()); - } - } - - - class Consumer2 : - IConsumer - { - readonly TaskCompletionSource> _taskCompletionSource; - - public Consumer2(IPublishEndpoint publishEndpoint, TaskCompletionSource> taskCompletionSource) - { - _taskCompletionSource = taskCompletionSource; - } - - public async Task Consume(ConsumeContext context) - { - _taskCompletionSource.TrySetResult(context); - } - } - - - class RequestConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return context.RespondAsync(new Response()); - } - } - - - class OneRequestConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return context.RespondAsync(new OneResponse()); - } - } - - - class TwoRequestConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return context.RespondAsync(new TwoResponse()); - } - } - } - - - namespace Contracts - { - public class Request - { - } - - - public class Response - { - } - - - public class OneRequest - { - } - - - public class OneResponse - { - } - - - public class TwoRequest - { - } - - - public class TwoResponse - { - } - - - public interface IBusOne : - IBus - { - } - - - public class BusOne : - BusInstance, - IBusOne - { - public BusOne(IBusControl busControl) - : base(busControl) - { - } - } - - - public interface IBusTwo : - IBus - { - } - } -} diff --git a/tests/MassTransit.Containers.Tests/Dispatcher_Specs.cs b/tests/MassTransit.Containers.Tests/Dispatcher_Specs.cs deleted file mode 100644 index 19d012852b3..00000000000 --- a/tests/MassTransit.Containers.Tests/Dispatcher_Specs.cs +++ /dev/null @@ -1,126 +0,0 @@ -namespace MassTransit.Containers.Tests -{ - using System.Collections.Generic; - using System.Threading.Tasks; - using Context; - using Internals; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; - using Microsoft.Extensions.Hosting; - using Microsoft.Extensions.Logging; - using NUnit.Framework; - using Serialization; - using TestFramework; - using Testing; - using Transports; - - - [TestFixture] - public class Dispatching_a_string : - AsyncTestFixture - { - [Test] - public async Task Should_be_handled_by_the_consumer() - { - var services = new ServiceCollection(); - - services.AddSingleton(BusTestFixture.LoggerFactory); - services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); - - services.AddMassTransit(x => - { - x.AddConsumer(); - x.AddConsumer(); - - x.UsingInMemory((context, cfg) => - { - cfg.ConfigureEndpoints(context, filter => filter.Include()); - }); - - x.AddConfigureEndpointsCallback((name, cfg) => cfg.UseRawJsonSerializer()); - }); - - await using var provider = services - .BuildServiceProvider(true); - - await provider.GetRequiredService().StartAsync(TestCancellationToken); - try - { - var receiver = provider.GetRequiredService>(); - - (var bytes, Dictionary headers) = Serialize(new SimpleCommand { Value = "Hello" }); - - await receiver.Dispatch(bytes, headers, TestCancellationToken); - - await SimpleEventConsumer.Completed.OrCanceled(TestCancellationToken); - } - finally - { - await provider.GetRequiredService().StopAsync(TestCancellationToken); - } - } - - static (byte[], Dictionary) Serialize(T obj) - where T : class - { - var serializer = new SystemTextJsonRawMessageSerializer(); - - var sendContext = new MessageSendContext(obj); - - var bytes = serializer.GetMessageBody(sendContext).GetBytes(); - - var headers = new Dictionary - { - { MessageHeaders.ContentType, SystemTextJsonRawMessageSerializer.JsonContentType }, - { MessageHeaders.MessageId, sendContext.MessageId } - }; - - headers.Set(sendContext.Headers); - - return (bytes, headers); - } - - public Dispatching_a_string() - : base(new InMemoryTestHarness()) - { - } - - - class SimpleCommandConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return context.Publish(new SimpleEvent { Value = context.Message.Value }); - } - } - - - class SimpleEventConsumer : - IConsumer - { - static readonly TaskCompletionSource> _source = new TaskCompletionSource>(); - - public static Task> Completed => _source.Task; - - public Task Consume(ConsumeContext context) - { - _source.TrySetResult(context); - - return Task.CompletedTask; - } - } - - - class SimpleCommand - { - public string Value { get; set; } - } - - - class SimpleEvent - { - public string Value { get; set; } - } - } -} diff --git a/tests/MassTransit.Containers.Tests/MassTransit.Containers.Tests.csproj b/tests/MassTransit.Containers.Tests/MassTransit.Containers.Tests.csproj deleted file mode 100644 index 5c2d73e9aa3..00000000000 --- a/tests/MassTransit.Containers.Tests/MassTransit.Containers.Tests.csproj +++ /dev/null @@ -1,21 +0,0 @@ - - - - net6.0 - - - - 9 - - - - - - - - - - - - - diff --git a/tests/MassTransit.Containers.Tests/Scenarios/AnotherMessageConsumer.cs b/tests/MassTransit.Containers.Tests/Scenarios/AnotherMessageConsumer.cs deleted file mode 100644 index f1d1491c1ac..00000000000 --- a/tests/MassTransit.Containers.Tests/Scenarios/AnotherMessageConsumer.cs +++ /dev/null @@ -1,8 +0,0 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - public interface AnotherMessageConsumer : - IConsumer - { - AnotherMessageInterface Last { get; } - } -} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/AnotherMessageInterface.cs b/tests/MassTransit.Containers.Tests/Scenarios/AnotherMessageInterface.cs deleted file mode 100644 index d867deebff8..00000000000 --- a/tests/MassTransit.Containers.Tests/Scenarios/AnotherMessageInterface.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - public interface AnotherMessageInterface - { - string Name { get; } - } -} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/FirstSagaMessage.cs b/tests/MassTransit.Containers.Tests/Scenarios/FirstSagaMessage.cs deleted file mode 100644 index 38abfd07747..00000000000 --- a/tests/MassTransit.Containers.Tests/Scenarios/FirstSagaMessage.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - using System; - - - public class FirstSagaMessage : - CorrelatedBy - { - public Guid CorrelationId { get; set; } - } -} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/SecondSagaMessage.cs b/tests/MassTransit.Containers.Tests/Scenarios/SecondSagaMessage.cs deleted file mode 100644 index 415ae1667bc..00000000000 --- a/tests/MassTransit.Containers.Tests/Scenarios/SecondSagaMessage.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - using System; - - - public class SecondSagaMessage : - CorrelatedBy - { - public Guid CorrelationId { get; set; } - } -} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/SimpleMessageInterface.cs b/tests/MassTransit.Containers.Tests/Scenarios/SimpleMessageInterface.cs deleted file mode 100644 index 467399b1872..00000000000 --- a/tests/MassTransit.Containers.Tests/Scenarios/SimpleMessageInterface.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - public interface SimpleMessageInterface - { - string Name { get; } - } -} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/SimplePublishedInterface.cs b/tests/MassTransit.Containers.Tests/Scenarios/SimplePublishedInterface.cs deleted file mode 100644 index aea203b6b5b..00000000000 --- a/tests/MassTransit.Containers.Tests/Scenarios/SimplePublishedInterface.cs +++ /dev/null @@ -1,7 +0,0 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - public interface SimplePublishedInterface - { - string Name { get; } - } -} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/ThirdSagaMessage.cs b/tests/MassTransit.Containers.Tests/Scenarios/ThirdSagaMessage.cs deleted file mode 100644 index 934d450fa4e..00000000000 --- a/tests/MassTransit.Containers.Tests/Scenarios/ThirdSagaMessage.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - using System; - - - public class ThirdSagaMessage - { - public Guid CorrelationId { get; set; } - } -} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/When_registering_a_consumer.cs b/tests/MassTransit.Containers.Tests/Scenarios/When_registering_a_consumer.cs deleted file mode 100644 index 10ccf836817..00000000000 --- a/tests/MassTransit.Containers.Tests/Scenarios/When_registering_a_consumer.cs +++ /dev/null @@ -1,63 +0,0 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - using System.Threading.Tasks; - using NUnit.Framework; - using Shouldly; - using TestFramework; - - - [TestFixture] - public abstract class When_registering_a_consumer : - InMemoryTestFixture - { - [Test] - public async Task Should_receive_using_the_first_consumer() - { - const string name = "Joe"; - - await InputQueueSendEndpoint.Send(new SimpleMessageClass(name)); - - var lastConsumer = await SimpleConsumer.LastConsumer; - lastConsumer.ShouldNotBe(null); - - var last = await lastConsumer.Last; - last.Name - .ShouldBe(name); - - var wasDisposed = await lastConsumer.Dependency.WasDisposed; - wasDisposed - .ShouldBe(true); //Dependency was not disposed"); - - lastConsumer.Dependency.SomethingDone - .ShouldBe(true); //Dependency was disposed before consumer executed"); - } - } - - - [TestFixture] - public abstract class When_registering_a_consumer_by_interface : - InMemoryTestFixture - { - [Test] - public async Task Should_receive_using_the_first_consumer() - { - const string name = "Joe"; - - await InputQueueSendEndpoint.Send(new SimpleMessageClass(name)); - - var lastConsumer = await SimpleConsumer.LastConsumer; - lastConsumer.ShouldNotBe(null); - - var last = await lastConsumer.Last; - last.Name - .ShouldBe(name); - - var wasDisposed = await lastConsumer.Dependency.WasDisposed; - wasDisposed - .ShouldBe(true); //Dependency was not disposed"); - - lastConsumer.Dependency.SomethingDone - .ShouldBe(true); //Dependency was disposed before consumer executed"); - } - } -} diff --git a/tests/MassTransit.Containers.Tests/Scheduler_Specs.cs b/tests/MassTransit.Containers.Tests/Scheduler_Specs.cs deleted file mode 100644 index 035d6dbe7fc..00000000000 --- a/tests/MassTransit.Containers.Tests/Scheduler_Specs.cs +++ /dev/null @@ -1,179 +0,0 @@ -namespace MassTransit.Containers.Tests -{ - using System; - using System.Threading.Tasks; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using NUnit.Framework; - using Testing; - - - [TestFixture] - public class When_the_scheduler_is_used_with_a_scoped_filter - { - [Test] - public async Task Should_use_same_scope_for_send() - { - await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddDelayedMessageScheduler(); - - x.AddSagaStateMachine() - .InMemoryRepository(); - - x.UsingInMemory((context, cfg) => - { - cfg.UseDelayedMessageScheduler(); - cfg.UseSendFilter(typeof(SendFilter<>), context); - - cfg.ConfigureEndpoints(context); - }); - }) - .AddScoped() - .AddScoped(typeof(SendFilter<>)) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - await harness.Bus.Publish(new { InVar.CorrelationId }); - - Assert.That(await harness.Consumed.Any(), Is.True); - Assert.That(await harness.Sent.Any(), Is.True); - - ISentMessage message = await harness.Sent.SelectAsync().FirstOrDefault(); - Assert.That(message, Is.Not.Null); - - Assert.That(message.Context.Headers.TryGetHeader("Scoped-Value", out var value), Is.True); - Assert.That(value, Is.EqualTo("Correct scoped value")); - } - - - public class TestStateMachine : - MassTransitStateMachine - { - public TestStateMachine() - { - InstanceState(x => x.CurrentState, Created); - - Event(() => InitiateEvent, x => x.CorrelateById(context => context.Message.CorrelationId)); - Schedule(() => ExpiredTimeout, x => x.ExpiredTimeoutToken, x => x.Received = r => r.CorrelateById(s => s.Message.CorrelationId)); - - Initially( - When(InitiateEvent) - .Activity(selector => selector.OfType()) - .SendAsync(_ => new Uri("queue:another-queue"), x => x.Init(x.Saga)) - .Schedule(ExpiredTimeout, x => x.Init(x.Saga), _ => TimeSpan.FromSeconds(10)) - .Finalize() - ); - - SetCompletedWhenFinalized(); - } - - public State Created { get; private set; } - - public Event InitiateEvent { get; private set; } - public Schedule ExpiredTimeout { get; set; } = null!; - } - - - public class SendFilter : - IFilter> - where TMessage : class - { - readonly ILogger _logger; - readonly ScopedService _scopedService; - - public SendFilter(ScopedService scopedService, ILogger> logger) - { - _scopedService = scopedService; - _logger = logger; - } - - public Task Send(SendContext context, IPipe> next) - { - _logger.LogInformation("Scoped value for {Message} is : {Value}", typeof(TMessage), _scopedService.ScopedValue); - - context.Headers.Set("Scoped-Value", _scopedService.ScopedValue ?? "NULL"); - - return next.Send(context); - } - - public void Probe(ProbeContext context) - { - } - } - - - public class ScopedService - { - public string ScopedValue { get; set; } - } - - - public class SetScopedValueActivity : - IStateMachineActivity - { - readonly ILogger _logger; - readonly ScopedService _scopedService; - - public SetScopedValueActivity(ScopedService scopedService, ILogger logger) - { - _scopedService = scopedService; - _logger = logger; - } - - public async Task Execute(BehaviorContext context, IBehavior next) - { - _logger.LogInformation("Setting scoped value for {CorrelationId}", context.CorrelationId); - - _scopedService.ScopedValue = "Correct scoped value"; - - await next.Execute(context); - } - - public void Probe(ProbeContext context) - { - } - - public void Accept(StateMachineVisitor visitor) - { - } - - public Task Faulted(BehaviorExceptionContext context, IBehavior next) - where TException : Exception - { - return next.Faulted(context); - } - } - - - public class TestSaga : - SagaStateMachineInstance - { - public int CurrentState { get; set; } - public Guid? ExpiredTimeoutToken { get; set; } - public Guid CorrelationId { get; set; } - } - - - public interface ExpiredEvent : - CorrelatedBy - { - } - - - public interface InitiateEvent : - CorrelatedBy - { - } - - - public interface OutgoingEvent : - CorrelatedBy - { - } - } -} diff --git a/tests/MassTransit.Containers.Tests/TenantScope_Specs.cs b/tests/MassTransit.Containers.Tests/TenantScope_Specs.cs deleted file mode 100644 index 86bf43c293a..00000000000 --- a/tests/MassTransit.Containers.Tests/TenantScope_Specs.cs +++ /dev/null @@ -1,363 +0,0 @@ -namespace MassTransit.Containers.Tests -{ - using System; - using System.Threading.Tasks; - using Courier.Contracts; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.Logging; - using NUnit.Framework; - using TestFramework; - using Testing; - - - [TestFixture] - public class When_specifying_a_scoped_filter - { - [Test] - public async Task It_should_be_resolved_prior_to_resolving_the_consumer() - { - await using var provider = new ServiceCollection() - .AddScoped() - .AddScoped() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(); - - x.AddConfigureEndpointsCallback((provider, name, cfg) => - { - cfg.UseConsumeFilter(typeof(TenantConsumeContextFilter<>), provider); - }); - - x.UsingInMemory((context, cfg) => - { - cfg.UsePublishFilter(typeof(PublishTenantHeaderFilter<>), context); - - cfg.ConfigureEndpoints(context); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - IRequestClient client = harness.GetRequestClient(); - - await client.GetResponse(new TenantRequest()); - } - - [Test] - public async Task It_should_be_resolved_prior_to_resolving_the_consumer_with_send() - { - await using var provider = new ServiceCollection() - .AddScoped() - .AddScoped() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(); - x.AddRequestClient(new Uri($"queue:{DefaultEndpointNameFormatter.Instance.Consumer()}")); - - x.AddConfigureEndpointsCallback((provider, name, cfg) => - { - cfg.UseConsumeFilter(typeof(TenantConsumeContextFilter<>), provider); - }); - - x.UsingInMemory((context, cfg) => - { - cfg.UseSendFilter(typeof(SendTenantHeaderFilter<>), context); - - cfg.ConfigureEndpoints(context); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - IRequestClient client = harness.GetRequestClient(); - - await client.GetResponse(new TenantRequest()); - } - - [Test] - public async Task It_should_be_resolved_after_the_message_retry_filter() - { - await using var provider = new ServiceCollection() - .AddScoped() - .AddScoped() - .AddMassTransitTestHarness(x => - { - x.AddConsumer(); - x.AddRequestClient(new Uri($"queue:{DefaultEndpointNameFormatter.Instance.Consumer()}")); - - x.AddConfigureEndpointsCallback((provider, name, cfg) => - { - cfg.UseConsumeFilter(typeof(TenantConsumeContextFilter<>), provider); - }); - - x.UsingInMemory((context, cfg) => - { - cfg.UseMessageRetry(r => r.Immediate(10)); - cfg.UseSendFilter(typeof(SendTenantHeaderFilter<>), context); - - cfg.ConfigureEndpoints(context); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - IRequestClient client = harness.GetRequestClient(); - - await client.GetResponse(new TenantRequest() { FailureCount = 2 }); - } - - [Test] - public async Task It_should_be_resolved_prior_to_resolving_the_activity() - { - await using var provider = new ServiceCollection() - .AddScoped() - .AddScoped() - .AddMassTransitTestHarness(x => - { - x.AddExecuteActivity(); - - x.AddConfigureEndpointsCallback((provider, name, cfg) => - { - cfg.UseExecuteActivityFilter(typeof(TenantExecuteContextFilter<>), provider); - }); - - x.UsingInMemory((context, cfg) => - { - cfg.UseSendFilter(typeof(SendTenantHeaderFilter<>), context); - - cfg.ConfigureEndpoints(context); - }); - }) - .BuildServiceProvider(true); - - var harness = provider.GetTestHarness(); - - await harness.Start(); - - var builder = new RoutingSlipBuilder(NewId.NextGuid()); - builder.AddActivity("tenant", new Uri($"queue:{DefaultEndpointNameFormatter.Instance.ExecuteActivity()}")); - - await harness.Bus.Execute(builder.Build()); - - Assert.IsTrue(await harness.Published.Any()); - } - - - class TenantConsumer : - IConsumer - { - readonly FakeTenantDbContext _dbContext; - readonly TenantContext _tenantContext; - - public TenantConsumer(FakeTenantDbContext dbContext, TenantContext tenantContext, ILogger logger) - { - _dbContext = dbContext; - _tenantContext = tenantContext; - - logger.LogInformation($"{tenantContext.TenantId} - {dbContext.TenantId} Creating TenantConsumer"); - } - - public Task Consume(ConsumeContext context) - { - if (string.IsNullOrWhiteSpace(_tenantContext.TenantId)) - throw new InvalidOperationException("The tenantId was not present"); - - if (_tenantContext.TenantId != _dbContext.TenantId) - throw new InvalidOperationException("The tenantId was not properly initialized prior to consumer resolution"); - - if (context.Message.FailureCount > 0) - { - if (context.GetRetryCount() < context.Message.FailureCount) - throw new IntentionalTestException("Not yet, not yet."); - } - - return context.RespondAsync(new TenantResponse()); - } - } - - - public interface TenantArguments - { - } - - - class TenantActivity : - IExecuteActivity - { - readonly FakeTenantDbContext _dbContext; - readonly TenantContext _tenantContext; - - public TenantActivity(FakeTenantDbContext dbContext, TenantContext tenantContext, ILogger logger) - { - _dbContext = dbContext; - _tenantContext = tenantContext; - - logger.LogInformation($"{tenantContext.TenantId} - {dbContext.TenantId} Creating TenantActivity"); - } - - public async Task Execute(ExecuteContext context) - { - if (string.IsNullOrWhiteSpace(_tenantContext.TenantId)) - throw new InvalidOperationException("The tenantId was not present"); - - if (_tenantContext.TenantId != _dbContext.TenantId) - throw new InvalidOperationException("The tenantId was not properly initialized prior to consumer resolution"); - - return context.Completed(); - } - } - - - class TenantContext - { - public string TenantId { get; set; } - } - - - class FakeTenantDbContext - { - public FakeTenantDbContext(TenantContext tenantContext, ILogger logger) - { - logger.LogInformation($"{tenantContext.TenantId} Creating FakeTenantDbContext"); - - TenantId = tenantContext.TenantId; - } - - public string TenantId { get; } - } - - - class PublishTenantHeaderFilter : - IFilter> - where T : class - { - readonly ILogger> _logger; - - public PublishTenantHeaderFilter(ILogger> logger) - { - _logger = logger; - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("PublishTenantHeaderFilter"); - } - - public Task Send(PublishContext context, IPipe> next) - { - var tenantId = NewId.NextGuid().ToString(); - _logger.LogInformation($"{tenantId} Setting header for tenant"); - context.Headers.Set("X-TenantId", tenantId); - - return next.Send(context); - } - } - - - class SendTenantHeaderFilter : - IFilter> - where T : class - { - readonly ILogger> _logger; - - public SendTenantHeaderFilter(ILogger> logger) - { - _logger = logger; - } - - public void Probe(ProbeContext context) - { - context.CreateFilterScope("SendTenantHeaderFilter"); - } - - public Task Send(SendContext context, IPipe> next) - { - var tenantId = NewId.NextGuid().ToString(); - _logger.LogInformation($"{tenantId} Setting header for tenant"); - context.Headers.Set("X-TenantId", tenantId); - - return next.Send(context); - } - } - - - class TenantConsumeContextFilter : - IFilter> - where T : class - { - readonly ILogger> _logger; - readonly TenantContext _tenantContext; - - public TenantConsumeContextFilter(TenantContext tenantContext, ILogger> logger) - { - _tenantContext = tenantContext; - _logger = logger; - _logger.LogInformation("Creating TenantContextFilter"); - } - - public void Probe(ProbeContext context) - { - context.CreateScope("TenantConsumeContextFilter"); - } - - public Task Send(ConsumeContext context, IPipe> next) - { - var tenantId = context.Headers.Get("X-TenantId", string.Empty); - _tenantContext.TenantId = tenantId; - _logger.LogInformation($"{tenantId} Reading header for tenant"); - return next.Send(context); - } - } - - - class TenantExecuteContextFilter : - IFilter> - where T : class - { - readonly ILogger> _logger; - readonly TenantContext _tenantContext; - - public TenantExecuteContextFilter(TenantContext tenantContext, ILogger> logger) - { - _tenantContext = tenantContext; - _logger = logger; - _logger.LogInformation("Creating TenantContextFilter"); - } - - public void Probe(ProbeContext context) - { - context.CreateScope("TenantExecuteContextFilter"); - } - - public Task Send(ExecuteContext context, IPipe> next) - { - var tenantId = context.Headers.Get("X-TenantId", string.Empty); - _tenantContext.TenantId = tenantId; - _logger.LogInformation($"{tenantId} Reading header for tenant"); - return next.Send(context); - } - } - - - [Serializable] - class TenantRequest - { - public int? FailureCount { get; set; } - } - - - [Serializable] - class TenantResponse - { - } - } -} diff --git a/tests/MassTransit.DapperIntegration.Tests/Container_Specs.cs b/tests/MassTransit.DapperIntegration.Tests/Container_Specs.cs index 450e6cebf3d..999bea7e5cc 100644 --- a/tests/MassTransit.DapperIntegration.Tests/Container_Specs.cs +++ b/tests/MassTransit.DapperIntegration.Tests/Container_Specs.cs @@ -6,6 +6,7 @@ namespace ContainerTests using System.Threading.Tasks; using Dapper; using Dapper.Contrib.Extensions; + using MassTransit.Tests; using Microsoft.Data.SqlClient; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; diff --git a/tests/MassTransit.DapperIntegration.Tests/DapperSagaRepositoryTests.cs b/tests/MassTransit.DapperIntegration.Tests/DapperSagaRepositoryTests.cs index 76973baabd6..8bdec38b5b8 100644 --- a/tests/MassTransit.DapperIntegration.Tests/DapperSagaRepositoryTests.cs +++ b/tests/MassTransit.DapperIntegration.Tests/DapperSagaRepositoryTests.cs @@ -3,10 +3,10 @@ using System; using System.Threading.Tasks; using Dapper; + using MassTransit.Tests; using MassTransit.Tests.Saga.Messages; using Microsoft.Data.SqlClient; using NUnit.Framework; - using Shouldly; using TestFramework; using Testing; @@ -26,7 +26,7 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId.HasValue, Is.True); var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; @@ -34,7 +34,7 @@ public async Task A_correlated_message_should_find_the_correct_saga() foundId = await _sagaRepository.Value.ShouldContainSaga(x => x.CorrelationId == sagaId && x.Completed, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId.HasValue, Is.True); } [Test] @@ -47,9 +47,9 @@ public async Task An_initiating_message_should_start_the_saga() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId.HasValue, Is.True); } - + [Test] public async Task An_observed_message_should_find_and_update_the_correct_saga() { @@ -60,14 +60,14 @@ public async Task An_observed_message_should_find_and_update_the_correct_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldBe(sagaId); + Assert.That(found, Is.EqualTo(sagaId)); var nextMessage = new ObservableSagaMessage { Name = "MySimpleSaga" }; await InputQueueSendEndpoint.Send(nextMessage); found = await _sagaRepository.Value.ShouldContainSaga(x => x.CorrelationId == sagaId && x.Observed, TestTimeout); - found.ShouldBe(sagaId); + Assert.That(found, Is.EqualTo(sagaId)); } [OneTimeSetUp] diff --git a/tests/MassTransit.DapperIntegration.Tests/MassTransit.DapperIntegration.Tests.csproj b/tests/MassTransit.DapperIntegration.Tests/MassTransit.DapperIntegration.Tests.csproj index fda14bbeca3..58656bbd9fc 100644 --- a/tests/MassTransit.DapperIntegration.Tests/MassTransit.DapperIntegration.Tests.csproj +++ b/tests/MassTransit.DapperIntegration.Tests/MassTransit.DapperIntegration.Tests.csproj @@ -1,10 +1,14 @@  - net6.0 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/MassTransit.DapperIntegration.Tests/SqlExpressionVisitorTests.cs b/tests/MassTransit.DapperIntegration.Tests/SqlExpressionVisitorTests.cs index e4d61bc223f..2ccbdd78fe4 100644 --- a/tests/MassTransit.DapperIntegration.Tests/SqlExpressionVisitorTests.cs +++ b/tests/MassTransit.DapperIntegration.Tests/SqlExpressionVisitorTests.cs @@ -19,9 +19,12 @@ public void CreateFromExpression_CanHandleEqualNodes_WithConstantValues() // Act var result = SqlExpressionVisitor.CreateFromExpression(filter).Single(); - // Assert - Assert.That(result.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelateBySomething))); - Assert.That(result.Item2, Is.EqualTo("Fiskbullar")); + Assert.Multiple(() => + { + // Assert + Assert.That(result.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelateBySomething))); + Assert.That(result.Item2, Is.EqualTo("Fiskbullar")); + }); } [Test] @@ -33,9 +36,12 @@ public void CreateFromExpression_CanHandleEqualNodes_WithBool() // Act var result = SqlExpressionVisitor.CreateFromExpression(filter).Single(); - // Assert - Assert.That(result.Item1, Is.EqualTo(nameof(SimpleSaga.Completed))); - Assert.That(result.Item2, Is.True); + Assert.Multiple(() => + { + // Assert + Assert.That(result.Item1, Is.EqualTo(nameof(SimpleSaga.Completed))); + Assert.That(result.Item2, Is.True); + }); } [Test] @@ -48,9 +54,12 @@ public void CreateFromExpression_CanHandleEqualNodes_WithNonConstantValues() // Act var result = SqlExpressionVisitor.CreateFromExpression(filter).Single(); - // Assert - Assert.That(result.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelationId))); - Assert.That(result.Item2, Is.EqualTo(sagaId)); + Assert.Multiple(() => + { + // Assert + Assert.That(result.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelationId))); + Assert.That(result.Item2, Is.EqualTo(sagaId)); + }); } [Test] @@ -64,15 +73,21 @@ public void CreateFromExpression_CanHandleAndAlsoNodes_WithNonConstantValues_And List<(string, object)> result = SqlExpressionVisitor.CreateFromExpression(filter); // Assert - Assert.That(result.Count, Is.EqualTo(2)); + Assert.That(result, Has.Count.EqualTo(2)); var first = result.First(); - Assert.That(first.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelationId))); - Assert.That(first.Item2, Is.EqualTo(sagaId)); + Assert.Multiple(() => + { + Assert.That(first.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelationId))); + Assert.That(first.Item2, Is.EqualTo(sagaId)); + }); var last = result.Last(); - Assert.That(last.Item1, Is.EqualTo(nameof(SimpleSaga.Completed))); - Assert.That(last.Item2, Is.True); + Assert.Multiple(() => + { + Assert.That(last.Item1, Is.EqualTo(nameof(SimpleSaga.Completed))); + Assert.That(last.Item2, Is.True); + }); } [Test] @@ -86,19 +101,28 @@ public void CreateFromExpression_CanHandleAndAlsoNodes_WithNestedAndAlso() List<(string, object)> result = SqlExpressionVisitor.CreateFromExpression(filter); // Assert - Assert.That(result.Count, Is.EqualTo(3)); + Assert.That(result, Has.Count.EqualTo(3)); var first = result[0]; - Assert.That(first.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelationId))); - Assert.That(first.Item2, Is.EqualTo(sagaId)); + Assert.Multiple(() => + { + Assert.That(first.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelationId))); + Assert.That(first.Item2, Is.EqualTo(sagaId)); + }); var second = result[1]; - Assert.That(second.Item1, Is.EqualTo(nameof(SimpleSaga.Completed))); - Assert.That(second.Item2, Is.True); + Assert.Multiple(() => + { + Assert.That(second.Item1, Is.EqualTo(nameof(SimpleSaga.Completed))); + Assert.That(second.Item2, Is.True); + }); var third = result[2]; - Assert.That(third.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelateBySomething))); - Assert.That(third.Item2, Is.EqualTo("Kebabsvarv")); + Assert.Multiple(() => + { + Assert.That(third.Item1, Is.EqualTo(nameof(SimpleSaga.CorrelateBySomething))); + Assert.That(third.Item2, Is.EqualTo("Kebabsvarv")); + }); } } } diff --git a/tests/MassTransit.DynamoDbIntegration.Tests/MassTransit.DynamoDbIntegration.Tests.csproj b/tests/MassTransit.DynamoDbIntegration.Tests/MassTransit.DynamoDbIntegration.Tests.csproj index f7d1d6ca633..b4c5b6434f5 100644 --- a/tests/MassTransit.DynamoDbIntegration.Tests/MassTransit.DynamoDbIntegration.Tests.csproj +++ b/tests/MassTransit.DynamoDbIntegration.Tests/MassTransit.DynamoDbIntegration.Tests.csproj @@ -1,15 +1,18 @@ - net6.0 + net8.0 MassTransit.DynamoDb.Tests + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/tests/MassTransit.DynamoDbIntegration.Tests/SagaPersistenceTests.cs b/tests/MassTransit.DynamoDbIntegration.Tests/SagaPersistenceTests.cs index c6018d0d2fd..0fcc7009452 100644 --- a/tests/MassTransit.DynamoDbIntegration.Tests/SagaPersistenceTests.cs +++ b/tests/MassTransit.DynamoDbIntegration.Tests/SagaPersistenceTests.cs @@ -9,7 +9,6 @@ using Amazon.DynamoDBv2.Model; using DynamoDbIntegration.Saga; using NUnit.Framework; - using Shouldly; using TestFramework; using Testing; @@ -28,19 +27,19 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? found = await _sagaRepository.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; await InputQueueSendEndpoint.Send(nextMessage); found = await _sagaRepository.ShouldContainSaga(sagaId, x => x != null && x.Moved, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); var retrieveRepository = _sagaRepository as ILoadSagaRepository; var retrieved = await retrieveRepository.Load(sagaId); - retrieved.ShouldNotBeNull(); - retrieved.Moved.ShouldBeTrue(); + Assert.That(retrieved, Is.Not.Null); + Assert.That(retrieved.Moved, Is.True); } [Test] @@ -53,7 +52,7 @@ public async Task An_initiating_message_should_start_the_saga() Guid? found = await _sagaRepository.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); } [SetUp] diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/AuditStore/AuditStore_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/AuditStore/AuditStore_Specs.cs index 7698f35dce2..7ede1d8a9ac 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/AuditStore/AuditStore_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/AuditStore/AuditStore_Specs.cs @@ -7,7 +7,6 @@ using Microsoft.EntityFrameworkCore; using NUnit.Framework; using Shared; - using Shouldly; using Testing; @@ -23,7 +22,7 @@ public async Task Should_have_consume_audit_records() { var consumed = InMemoryTestHarness.Consumed; await Task.Delay(2000); - (await GetAuditRecords("Consume", consumed.Count(), TimeSpan.FromSeconds(10))).ShouldBe(consumed.Count()); + Assert.That(await GetAuditRecords("Consume", consumed.Count(), TimeSpan.FromSeconds(10)), Is.EqualTo(consumed.Count())); } [Test] @@ -31,7 +30,7 @@ public async Task Should_have_send_audit_record() { var sent = InMemoryTestHarness.Sent; await Task.Delay(2000); - (await GetAuditRecords("Send", sent.Count(), TimeSpan.FromSeconds(10))).ShouldBe(sent.Count()); + Assert.That(await GetAuditRecords("Send", sent.Count(), TimeSpan.FromSeconds(10)), Is.EqualTo(sent.Count())); } [SetUp] diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/FutureSagaDbContextFactory.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/FutureSagaDbContextFactory.cs index 1c094b0221a..81085f4e184 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/FutureSagaDbContextFactory.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/FutureSagaDbContextFactory.cs @@ -1,17 +1,17 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Tests { using System.Reflection; + using MassTransit.Tests; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using TestFramework; - public class FutureSagaDbContextFactory : IDesignTimeDbContextFactory { public FutureSagaDbContext CreateDbContext(string[] args) { - var builder = new DbContextOptionsBuilder(); + var builder = new DbContextOptionsBuilder(); Apply(builder); @@ -27,7 +27,7 @@ public static void Apply(DbContextOptionsBuilder builder) }); } - public FutureSagaDbContext CreateDbContext(DbContextOptionsBuilder optionsBuilder) + public FutureSagaDbContext CreateDbContext(DbContextOptionsBuilder optionsBuilder) { return new FutureSagaDbContext(optionsBuilder.Options); } diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/JobConsumer_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/JobConsumer_Specs.cs new file mode 100644 index 00000000000..2160dabbd8b --- /dev/null +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/JobConsumer_Specs.cs @@ -0,0 +1,153 @@ +namespace MassTransit.EntityFrameworkCoreIntegration.Tests +{ + using System; + using System.Threading.Tasks; + using Contracts.JobService; + using JobConsumerTests; + using Logging; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Shared; + using Testing; + using Turnout; + + + namespace JobConsumerTests + { + using System; + using System.Threading.Tasks; + using Contracts.JobService; + + + public interface OddJob + { + TimeSpan Duration { get; } + } + + + public class OddJobConsumer : + IJobConsumer + { + public async Task Run(JobContext context) + { + if (context.RetryAttempt == 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + } + } + + + public class OddJobCompletedConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + return Task.CompletedTask; + } + } + } + + + [Category("Flaky")] + [TestFixture(typeof(SqlServerTestDbParameters))] + [TestFixture(typeof(PostgresTestDbParameters))] + public class Using_the_new_job_service_configuration + where TTestDbParameters : ITestDbParameters, new() + { + [Test] + public async Task Should_complete_the_job() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => options.Disable("Microsoft")); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); + + x.SetInMemorySagaRepositoryProvider(); + + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.AddDbContext(db => _testDbParameters.Apply(typeof(JobServiceSagaDbContext), db)); + + x.AddJobSagaStateMachines() + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.LockStatementProvider = _testDbParameters.RawSqlLockStatements; + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + try + { + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(1) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + } + finally + { + await harness.Stop(); + } + } + + [OneTimeSetUp] + public async Task Arrange() + { + await using var context = new JobServiceSagaDbContextFactory().CreateDbContext(_testDbParameters.GetDbContextOptions()); + + await context.Database.EnsureDeletedAsync(); + await context.Database.EnsureCreatedAsync(); + } + + [OneTimeTearDown] + public async Task TearDown() + { + await using var context = new JobServiceSagaDbContextFactory().CreateDbContext(_testDbParameters.GetDbContextOptions()); + + await context.Database.EnsureDeletedAsync(); + } + + TTestDbParameters _testDbParameters; + + public Using_the_new_job_service_configuration() + { + _testDbParameters = new TTestDbParameters(); + } + } +} diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/MassTransit.EntityFrameworkCoreIntegration.Tests.csproj b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/MassTransit.EntityFrameworkCoreIntegration.Tests.csproj index 60a1f68aba9..e3bd2222abf 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/MassTransit.EntityFrameworkCoreIntegration.Tests.csproj +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/MassTransit.EntityFrameworkCoreIntegration.Tests.csproj @@ -1,13 +1,13 @@  - net6.0 + net8.0 - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -16,9 +16,12 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Migrations/20240912162558_UpdateJobConsumer.Designer.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Migrations/20240912162558_UpdateJobConsumer.Designer.cs new file mode 100644 index 00000000000..3572274ec22 --- /dev/null +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Migrations/20240912162558_UpdateJobConsumer.Designer.cs @@ -0,0 +1,171 @@ +// +using System; +using MassTransit.EntityFrameworkCoreIntegration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MassTransit.EntityFrameworkCoreIntegration.Tests.Migrations +{ + [DbContext(typeof(JobServiceSagaDbContext))] + [Migration("20240912162558_UpdateJobConsumer")] + partial class UpdateJobConsumer + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.JobAttemptSaga", b => + { + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentState") + .HasColumnType("int"); + + b.Property("Faulted") + .HasColumnType("datetime2"); + + b.Property("InstanceAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("JobId") + .HasColumnType("uniqueidentifier"); + + b.Property("RetryAttempt") + .HasColumnType("int"); + + b.Property("ServiceAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("Started") + .HasColumnType("datetime2"); + + b.Property("StatusCheckTokenId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CorrelationId"); + + b.HasIndex("JobId", "RetryAttempt") + .IsUnique(); + + b.ToTable("JobAttemptSaga"); + }); + + modelBuilder.Entity("MassTransit.JobSaga", b => + { + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptId") + .HasColumnType("uniqueidentifier"); + + b.Property("Completed") + .HasColumnType("datetime2"); + + b.Property("CronExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("CurrentState") + .HasColumnType("int"); + + b.Property("Duration") + .HasColumnType("time"); + + b.Property("EndDate") + .HasColumnType("datetimeoffset"); + + b.Property("Faulted") + .HasColumnType("datetime2"); + + b.Property("Job") + .HasColumnType("nvarchar(max)"); + + b.Property("JobRetryDelayToken") + .HasColumnType("uniqueidentifier"); + + b.Property("JobSlotWaitToken") + .HasColumnType("uniqueidentifier"); + + b.Property("JobTimeout") + .HasColumnType("time"); + + b.Property("JobTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("NextStartDate") + .HasColumnType("datetimeoffset"); + + b.Property("Reason") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryAttempt") + .HasColumnType("int"); + + b.Property("ServiceAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetimeoffset"); + + b.Property("Started") + .HasColumnType("datetime2"); + + b.Property("Submitted") + .HasColumnType("datetime2"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("CorrelationId"); + + b.ToTable("JobSaga"); + }); + + modelBuilder.Entity("MassTransit.JobTypeSaga", b => + { + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ActiveJobCount") + .HasColumnType("int"); + + b.Property("ActiveJobs") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrentJobLimit") + .HasColumnType("int"); + + b.Property("CurrentState") + .HasColumnType("int"); + + b.Property("Instances") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("OverrideJobLimit") + .HasColumnType("int"); + + b.Property("OverrideLimitExpiration") + .HasColumnType("datetime2"); + + b.HasKey("CorrelationId"); + + b.ToTable("JobTypeSaga"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Migrations/20240912162558_UpdateJobConsumer.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Migrations/20240912162558_UpdateJobConsumer.cs new file mode 100644 index 00000000000..38677418b92 --- /dev/null +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Migrations/20240912162558_UpdateJobConsumer.cs @@ -0,0 +1,103 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace MassTransit.EntityFrameworkCoreIntegration.Tests.Migrations +{ + /// + public partial class UpdateJobConsumer : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "JobAttemptSaga", + columns: table => new + { + CorrelationId = table.Column(type: "uniqueidentifier", nullable: false), + CurrentState = table.Column(type: "int", nullable: false), + JobId = table.Column(type: "uniqueidentifier", nullable: false), + RetryAttempt = table.Column(type: "int", nullable: false), + ServiceAddress = table.Column(type: "nvarchar(max)", nullable: true), + InstanceAddress = table.Column(type: "nvarchar(max)", nullable: true), + Started = table.Column(type: "datetime2", nullable: true), + Faulted = table.Column(type: "datetime2", nullable: true), + StatusCheckTokenId = table.Column(type: "uniqueidentifier", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_JobAttemptSaga", x => x.CorrelationId); + }); + + migrationBuilder.CreateTable( + name: "JobSaga", + columns: table => new + { + CorrelationId = table.Column(type: "uniqueidentifier", nullable: false), + CurrentState = table.Column(type: "int", nullable: false), + Submitted = table.Column(type: "datetime2", nullable: true), + ServiceAddress = table.Column(type: "nvarchar(max)", nullable: true), + JobTimeout = table.Column(type: "time", nullable: true), + Job = table.Column(type: "nvarchar(max)", nullable: true), + JobTypeId = table.Column(type: "uniqueidentifier", nullable: false), + AttemptId = table.Column(type: "uniqueidentifier", nullable: false), + RetryAttempt = table.Column(type: "int", nullable: false), + Started = table.Column(type: "datetime2", nullable: true), + Completed = table.Column(type: "datetime2", nullable: true), + Duration = table.Column(type: "time", nullable: true), + Faulted = table.Column(type: "datetime2", nullable: true), + Reason = table.Column(type: "nvarchar(max)", nullable: true), + JobSlotWaitToken = table.Column(type: "uniqueidentifier", nullable: true), + JobRetryDelayToken = table.Column(type: "uniqueidentifier", nullable: true), + CronExpression = table.Column(type: "nvarchar(max)", nullable: true), + TimeZoneId = table.Column(type: "nvarchar(max)", nullable: true), + StartDate = table.Column(type: "datetimeoffset", nullable: true), + EndDate = table.Column(type: "datetimeoffset", nullable: true), + NextStartDate = table.Column(type: "datetimeoffset", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_JobSaga", x => x.CorrelationId); + }); + + migrationBuilder.CreateTable( + name: "JobTypeSaga", + columns: table => new + { + CorrelationId = table.Column(type: "uniqueidentifier", nullable: false), + CurrentState = table.Column(type: "int", nullable: false), + ActiveJobCount = table.Column(type: "int", nullable: false), + ConcurrentJobLimit = table.Column(type: "int", nullable: false), + Name = table.Column(type: "nvarchar(max)", nullable: true), + OverrideJobLimit = table.Column(type: "int", nullable: true), + OverrideLimitExpiration = table.Column(type: "datetime2", nullable: true), + ActiveJobs = table.Column(type: "nvarchar(max)", nullable: true), + Instances = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_JobTypeSaga", x => x.CorrelationId); + }); + + migrationBuilder.CreateIndex( + name: "IX_JobAttemptSaga_JobId_RetryAttempt", + table: "JobAttemptSaga", + columns: new[] { "JobId", "RetryAttempt" }, + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "JobAttemptSaga"); + + migrationBuilder.DropTable( + name: "JobSaga"); + + migrationBuilder.DropTable( + name: "JobTypeSaga"); + } + } +} diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Migrations/JobServiceSagaDbContextModelSnapshot.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Migrations/JobServiceSagaDbContextModelSnapshot.cs new file mode 100644 index 00000000000..57b750a8bd9 --- /dev/null +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Migrations/JobServiceSagaDbContextModelSnapshot.cs @@ -0,0 +1,168 @@ +// +using System; +using MassTransit.EntityFrameworkCoreIntegration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace MassTransit.EntityFrameworkCoreIntegration.Tests.Migrations +{ + [DbContext(typeof(JobServiceSagaDbContext))] + partial class JobServiceSagaDbContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "8.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("MassTransit.JobAttemptSaga", b => + { + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("CurrentState") + .HasColumnType("int"); + + b.Property("Faulted") + .HasColumnType("datetime2"); + + b.Property("InstanceAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("JobId") + .HasColumnType("uniqueidentifier"); + + b.Property("RetryAttempt") + .HasColumnType("int"); + + b.Property("ServiceAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("Started") + .HasColumnType("datetime2"); + + b.Property("StatusCheckTokenId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CorrelationId"); + + b.HasIndex("JobId", "RetryAttempt") + .IsUnique(); + + b.ToTable("JobAttemptSaga"); + }); + + modelBuilder.Entity("MassTransit.JobSaga", b => + { + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptId") + .HasColumnType("uniqueidentifier"); + + b.Property("Completed") + .HasColumnType("datetime2"); + + b.Property("CronExpression") + .HasColumnType("nvarchar(max)"); + + b.Property("CurrentState") + .HasColumnType("int"); + + b.Property("Duration") + .HasColumnType("time"); + + b.Property("EndDate") + .HasColumnType("datetimeoffset"); + + b.Property("Faulted") + .HasColumnType("datetime2"); + + b.Property("Job") + .HasColumnType("nvarchar(max)"); + + b.Property("JobRetryDelayToken") + .HasColumnType("uniqueidentifier"); + + b.Property("JobSlotWaitToken") + .HasColumnType("uniqueidentifier"); + + b.Property("JobTimeout") + .HasColumnType("time"); + + b.Property("JobTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("NextStartDate") + .HasColumnType("datetimeoffset"); + + b.Property("Reason") + .HasColumnType("nvarchar(max)"); + + b.Property("RetryAttempt") + .HasColumnType("int"); + + b.Property("ServiceAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("StartDate") + .HasColumnType("datetimeoffset"); + + b.Property("Started") + .HasColumnType("datetime2"); + + b.Property("Submitted") + .HasColumnType("datetime2"); + + b.Property("TimeZoneId") + .HasColumnType("nvarchar(max)"); + + b.HasKey("CorrelationId"); + + b.ToTable("JobSaga"); + }); + + modelBuilder.Entity("MassTransit.JobTypeSaga", b => + { + b.Property("CorrelationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ActiveJobCount") + .HasColumnType("int"); + + b.Property("ActiveJobs") + .HasColumnType("nvarchar(max)"); + + b.Property("ConcurrentJobLimit") + .HasColumnType("int"); + + b.Property("CurrentState") + .HasColumnType("int"); + + b.Property("Instances") + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasColumnType("nvarchar(max)"); + + b.Property("OverrideJobLimit") + .HasColumnType("int"); + + b.Property("OverrideLimitExpiration") + .HasColumnType("datetime2"); + + b.HasKey("CorrelationId"); + + b.ToTable("JobTypeSaga"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/BusOutbox_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/BusOutbox_Specs.cs index ddb949ef8d9..4783fb5542e 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/BusOutbox_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/BusOutbox_Specs.cs @@ -1,6 +1,7 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Tests.ReliableMessaging { using System; + using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; @@ -25,6 +26,8 @@ public async Task Should_support_the_test_harness() .AddTelemetryListener() .AddMassTransitTestHarness(x => { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + x.AddEntityFrameworkOutbox(o => { o.QueryDelay = TimeSpan.FromSeconds(1); @@ -40,7 +43,6 @@ public async Task Should_support_the_test_harness() .BuildServiceProvider(true); var harness = provider.GetTestHarness(); - harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); await harness.Start(); @@ -67,6 +69,154 @@ public async Task Should_support_the_test_harness() } Assert.That(await consumerHarness.Consumed.Any(), Is.True); + + IReceivedMessage context = harness.Consumed.Select().Single(); + + Assert.Multiple(() => + { + Assert.That(context.Context.MessageId, Is.Not.Null); + Assert.That(context.Context.ConversationId, Is.Not.Null); + Assert.That(context.Context.DestinationAddress, Is.Not.Null); + Assert.That(context.Context.SourceAddress, Is.Not.Null); + }); + } + finally + { + await harness.Stop(); + } + } + + [Test] + public async Task Should_include_headers_when_using_raw_json() + { + using var tracerProvider = TraceConfig.CreateTraceProvider("ef-core-tests"); + + await using var provider = new ServiceCollection() + .AddBusOutboxServices() + .AddTelemetryListener() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + x.AddEntityFrameworkOutbox(o => + { + o.QueryDelay = TimeSpan.FromSeconds(1); + + o.UseBusOutbox(bo => + { + bo.MessageDeliveryLimit = 10; + }); + }); + + x.AddConsumer(); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.UsingInMemory((context, cfg) => + { + cfg.UseRawJsonSerializer(); + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + IConsumerTestHarness consumerHarness = harness.GetConsumerHarness(); + + try + { + { + await using var dbContext = harness.Scope.ServiceProvider.GetRequiredService(); + + var publishEndpoint = harness.Scope.ServiceProvider.GetRequiredService(); + + var activity = TraceConfig.Source.StartActivity(ActivityKind.Client); + + await publishEndpoint.Publish(new PingMessage(), x => x.Headers.Set("Test-Header", "Test-Value")); + + await dbContext.SaveChangesAsync(harness.CancellationToken); + + activity.Stop(); + } + + Assert.That(await consumerHarness.Consumed.Any(), Is.True); + + IReceivedMessage context = await consumerHarness.Consumed.SelectAsync().FirstOrDefault(); + + Assert.Multiple(() => + { + Assert.That(context.Context.Headers.TryGetHeader("Test-Header", out var header), Is.True); + + Assert.That(header, Is.EqualTo("Test-Value")); + + Assert.That(context.Context.MessageId, Is.Not.Null); + Assert.That(context.Context.ConversationId, Is.Not.Null); + Assert.That(context.Context.DestinationAddress, Is.Not.Null); + Assert.That(context.Context.SourceAddress, Is.Not.Null); + }); + } + finally + { + await harness.Stop(); + } + } + + [Test] + public async Task Should_support_baggage_in_telemetry() + { + using var tracerProvider = TraceConfig.CreateTraceProvider("ef-core-tests"); + + await using var provider = new ServiceCollection() + .AddBusOutboxServices() + .AddTelemetryListener() + .AddMassTransitTestHarness(x => + { + x.AddEntityFrameworkOutbox(o => + { + o.QueryDelay = TimeSpan.FromSeconds(1); + + o.UseBusOutbox(bo => + { + bo.MessageDeliveryLimit = 10; + }); + }); + + x.AddTaskCompletionSource(); + x.AddConsumer(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + await harness.Start(); + + IConsumerTestHarness consumerHarness = harness.GetConsumerHarness(); + + try + { + { + await using var dbContext = harness.Scope.ServiceProvider.GetRequiredService(); + + var publishEndpoint = harness.Scope.ServiceProvider.GetRequiredService(); + + var activity = TraceConfig.Source.StartActivity(ActivityKind.Client); + + activity.AddBaggage("Suitcase", "Full of cash"); + + await publishEndpoint.Publish(new PingMessage()); + + await dbContext.SaveChangesAsync(harness.CancellationToken); + + activity.Stop(); + } + + Assert.That(await consumerHarness.Consumed.Any(), Is.True); + + var source = provider.GetRequiredService>(); + Assert.That(await source.Task, Is.EqualTo("Full of cash")); } finally { @@ -87,8 +237,6 @@ public async Task Should_support_multiple_save_changes() x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); x.AddEntityFrameworkOutbox(o => { - o.QueryDelay = TimeSpan.FromMinutes(10); - o.UseBusOutbox(bo => { bo.MessageDeliveryLimit = 10; @@ -236,7 +384,7 @@ public async Task Fill_up_the_outbox() var totalTimer = Stopwatch.StartNew(); var sendTimer = Stopwatch.StartNew(); - const int loopCount = 100; + const int loopCount = 400; const int messagesPerLoop = 3; await Task.WhenAll(Enumerable.Range(0, loopCount).Select(async n => { @@ -378,6 +526,27 @@ public async Task Should_work_without_starting_the_bus() } + class PingBaggageConsumer : + IConsumer + { + readonly TaskCompletionSource _baggage; + + public PingBaggageConsumer(TaskCompletionSource baggage) + { + _baggage = baggage; + } + + public Task Consume(ConsumeContext context) + { + KeyValuePair? pair = Activity.Current?.Baggage.FirstOrDefault(x => x.Key.Equals("Suitcase")); + if (pair != null) + _baggage.TrySetResult(pair.Value.Value); + + return Task.CompletedTask; + } + } + + class PingConsumer : IConsumer { diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/InboxLock_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/InboxLock_Specs.cs index 033b8c91e55..9db15654661 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/InboxLock_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/InboxLock_Specs.cs @@ -47,7 +47,7 @@ public async Task Should_block_subsequent_consumers_by_lock() var events = provider.GetRequiredService>(); - Assert.That(events.Count, Is.EqualTo(100)); + Assert.That(events, Has.Count.EqualTo(100)); } } @@ -75,7 +75,7 @@ public static IServiceCollection AddEntityFrameworkInMemoryTestHarness(this ISer x.AddConsumer(); }); - services.AddOptions().Configure(options => options.Disable("Microsoft")); + services.AddOptions().Configure(options => options.Disable("Microsoft")); return services; } @@ -84,19 +84,12 @@ public static IServiceCollection AddEntityFrameworkInMemoryTestHarness(this ISer public class InboxLockEntityFrameworkConsumerDefinition : ConsumerDefinition { - readonly IServiceProvider _provider; - - public InboxLockEntityFrameworkConsumerDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(10, 50, 100, 100, 100, 100, 100, 100)); - endpointConfigurator.UseEntityFrameworkOutbox(_provider); + endpointConfigurator.UseEntityFrameworkOutbox(context); } } } diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/OutboxScopedFilter_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/OutboxScopedFilter_Specs.cs index 085c079acc2..bd6362e16db 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/OutboxScopedFilter_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/OutboxScopedFilter_Specs.cs @@ -132,7 +132,7 @@ public async Task Should_call_the_scoped_filter_on_send() var myId = harness.Scope.ServiceProvider.GetRequiredService(); var result = await taskCompletionSource.Task; - Assert.AreEqual(myId, result); + Assert.That(result, Is.EqualTo(myId)); } finally { @@ -215,17 +215,10 @@ public async Task Consume(ConsumeContext message) class SimplerConsumerDefinition : ConsumerDefinition { - readonly IServiceProvider _provider; - - public SimplerConsumerDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { - endpointConfigurator.UseEntityFrameworkOutbox(_provider); + endpointConfigurator.UseEntityFrameworkOutbox(context); } } diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/OutboxTransactionFault_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/OutboxTransactionFault_Specs.cs new file mode 100644 index 00000000000..a1577b67cda --- /dev/null +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/OutboxTransactionFault_Specs.cs @@ -0,0 +1,153 @@ +namespace MassTransit.EntityFrameworkCoreIntegration.Tests.ReliableMessaging; + +using System; +using System.Reflection; +using System.Threading.Tasks; +using Logging; +using MassTransit.Tests; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NUnit.Framework; +using Testing; + + +public class OutboxTransactionFault_Specs +{ + [Test] + public async Task Should_throw_typed_exception() + { + var services = new ServiceCollection(); + + services + .AddDbContext(builder => + { + builder.UseSqlServer(LocalDbConnectionStringProvider.GetLocalDbConnectionString(), options => + { + options.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); + options.MigrationsHistoryTable($"__{nameof(TestDbContext)}"); + + options.MinBatchSize(1); + }); + + builder.EnableSensitiveDataLogging(); + }); + + services + .AddHostedService>() + .AddMassTransitTestHarness(x => + { + x.AddEntityFrameworkOutbox(); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.AddConsumer(); + x.AddConsumer(); + x.AddConsumer(); + + + x.AddConfigureEndpointsCallback((context, name, cfg) => + { + cfg.UseEntityFrameworkOutbox(context); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }); + + services.AddOptions() + .Configure(options => options.Disable("Microsoft")); + + await using var provider = services.BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var testEntityId = Guid.NewGuid(); + + await harness.Bus.Publish(new TestMessage + { + TestId = testEntityId, + Key = "A", + ThrowInConsumer = false + }); + + Assert.That(await harness.Consumed.Any(x => x.Context.Message.Key == "A")); + + await harness.Bus.Publish(new TestMessage + { + TestId = testEntityId, + Key = "C", + ThrowInConsumer = false + }); + + Assert.That(await harness.Consumed.Any(x => x.Context.Message.Key == "C")); + + Assert.That(await harness.Published.Any>(x => x.Context.Message.Message.Key == "C")); + + await harness.Stop(); + } + + + public class TestMessage + { + public Guid TestId { get; set; } + public string Key { get; set; } + + public bool ThrowInConsumer { get; set; } + } + + + class TestDbContext(DbContextOptions options) : + DbContext(options) + { + public DbSet TestEntities { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.AddTransactionalOutboxEntities(); + } + } + + + class TestEntity + { + public Guid Id { get; set; } + } + + + class TestMessageConsumer(TestDbContext testDbContext) : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + if (context.Message.ThrowInConsumer) + throw new InvalidOperationException("Throw requested by messaged inside consumer"); + + await testDbContext.AddAsync(new TestEntity { Id = context.Message.TestId }); + } + } + + + class TestFaultTypedMessageConsumer(ILogger logger) : IConsumer> + { + public Task Consume(ConsumeContext> context) + { + logger.LogInformation("Consumed typed FAULT for Key {testId}", context.Message.Message.Key); + return Task.CompletedTask; + } + } + + + class TestFaultNotTypedMessageConsumer(ILogger logger) : IConsumer + { + public Task Consume(ConsumeContext context) + { + logger.LogInformation("Consumed NOTTYPED FAULT"); + return Task.CompletedTask; + } + } +} diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/Outbox_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/Outbox_Specs.cs index 739694497d6..835b4b5e7d0 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/Outbox_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/Outbox_Specs.cs @@ -204,10 +204,10 @@ static ServiceProvider CreateServiceProvider(Action { - readonly IServiceProvider _provider; - - public ResponsibleStateDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, - ISagaConfigurator consumerConfigurator) + ISagaConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(10, 50, 100, 100, 100, 100, 100, 100)); - endpointConfigurator.UseEntityFrameworkOutbox(_provider); - endpointConfigurator.UseSendFilter(typeof(SendFilter<>), _provider); + endpointConfigurator.UseEntityFrameworkOutbox(context); + endpointConfigurator.UseSendFilter(typeof(SendFilter<>), context); } } @@ -372,6 +365,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) modelBuilder.AddInboxStateEntity(); modelBuilder.AddOutboxMessageEntity(); + modelBuilder.AddOutboxStateEntity(); } } diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/QuartzOutbox_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/QuartzOutbox_Specs.cs index c4d41135074..abd4c92c2d9 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/QuartzOutbox_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/QuartzOutbox_Specs.cs @@ -17,9 +17,8 @@ public class Using_the_quartz_scheduler_with_the_outbox public async Task Should_delay_message_scheduling_until_the_outbox_messages_are_delivered() { await using var provider = new ServiceCollection() - .AddQuartz(q => + .AddQuartz(_ => { - q.UseMicrosoftDependencyInjectionJobFactory(); }) .AddBusOutboxServices() .AddMassTransitTestHarness(x => @@ -56,9 +55,12 @@ public async Task Should_delay_message_scheduling_until_the_outbox_messages_are_ { await harness.Bus.Publish(new { }); - Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); - Assert.That(await harness.Consumed.Any(), Is.True); + Assert.That(await harness.Consumed.Any(), Is.True); + }); await adjustment.AdvanceTime(TimeSpan.FromSeconds(10)); @@ -84,19 +86,12 @@ public async Task Consume(ConsumeContext context) public class FirstMessageConsumerDefinition : ConsumerDefinition { - readonly IServiceProvider _provider; - - public FirstMessageConsumerDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 100, 100)); - endpointConfigurator.UseEntityFrameworkOutbox(_provider); + endpointConfigurator.UseEntityFrameworkOutbox(context); } } diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableConsumerDefinition.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableConsumerDefinition.cs index a1875e211a4..5433840f369 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableConsumerDefinition.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableConsumerDefinition.cs @@ -1,25 +1,17 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Tests.ReliableMessaging { - using System; using MassTransit.Tests.ReliableMessaging; public class ReliableConsumerDefinition : ConsumerDefinition { - readonly IServiceProvider _provider; - - public ReliableConsumerDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(10, 50, 100, 100, 100, 100, 100, 100)); - endpointConfigurator.UseEntityFrameworkOutbox(_provider); + endpointConfigurator.UseEntityFrameworkOutbox(context); } } } diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableDbContext.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableDbContext.cs index 2bedf32fe5c..04569c788ed 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableDbContext.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableDbContext.cs @@ -21,9 +21,7 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - modelBuilder.AddInboxStateEntity(); - modelBuilder.AddOutboxMessageEntity(); - modelBuilder.AddOutboxStateEntity(); + modelBuilder.AddTransactionalOutboxEntities(); } } } diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableDbContextFactory.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableDbContextFactory.cs index 4c6947c6dd3..1df256c26dc 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableDbContextFactory.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableDbContextFactory.cs @@ -1,6 +1,7 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Tests.ReliableMessaging { using System.Reflection; + using MassTransit.Tests; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Design; using TestFramework; diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableStateDefinition.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableStateDefinition.cs index 3c14d349caa..63d7a805f5d 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableStateDefinition.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/ReliableStateDefinition.cs @@ -1,26 +1,18 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Tests.ReliableMessaging { - using System; using MassTransit.Tests.ReliableMessaging; public class ReliableStateDefinition : SagaDefinition { - readonly IServiceProvider _provider; - - public ReliableStateDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, - ISagaConfigurator consumerConfigurator) + ISagaConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(10, 50, 100, 100, 100, 100, 100, 100)); - endpointConfigurator.UseMessageScope(_provider); - endpointConfigurator.UseEntityFrameworkOutbox(_provider); + endpointConfigurator.UseMessageScope(context); + endpointConfigurator.UseEntityFrameworkOutbox(context); } } } diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/Reliable_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/Reliable_Specs.cs index cb893857dec..7d93962936d 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/Reliable_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/ReliableMessaging/Reliable_Specs.cs @@ -109,9 +109,12 @@ public async Task Should_handle_the_saga_successfully() ISagaStateMachineTestHarness? sagaHarness = harness.GetSagaStateMachineHarness(); - Assert.That(await sagaHarness.Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any(), Is.True); - Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + }); } finally { @@ -145,9 +148,12 @@ await harness.Bus.Publish(new CreateState ISagaStateMachineTestHarness? sagaHarness = harness.GetSagaStateMachineHarness(); - Assert.That(await sagaHarness.Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any(), Is.True); - Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + }); } finally { @@ -181,9 +187,12 @@ await harness.Bus.Publish(new CreateState ISagaStateMachineTestHarness? sagaHarness = harness.GetSagaStateMachineHarness(); - Assert.That(await sagaHarness.Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any(), Is.True); - Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + }); } finally { diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SagaWithDependency/Using_custom_include_in_repository.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SagaWithDependency/Using_custom_include_in_repository.cs index 99ac3b7eea8..33f28c40098 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SagaWithDependency/Using_custom_include_in_repository.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SagaWithDependency/Using_custom_include_in_repository.cs @@ -8,7 +8,6 @@ using Microsoft.EntityFrameworkCore; using NUnit.Framework; using Shared; - using Shouldly; using Testing; @@ -29,7 +28,7 @@ public async Task A_correlated_message_should_update_inner_saga_dependency() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); var propertyValue = "expected saga property value"; var updateInnerProperty = new UpdateSagaDependency(sagaId, propertyValue); @@ -39,7 +38,7 @@ public async Task A_correlated_message_should_update_inner_saga_dependency() foundId = await _sagaRepository.Value.ShouldContainSaga( x => x.CorrelationId == sagaId && x.Completed && x.Dependency.SagaInnerDependency.Name == propertyValue, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); } [Test] @@ -53,7 +52,7 @@ public async Task An_initiating_message_should_start_the_saga() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout).ConfigureAwait(false); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); } readonly Lazy> _sagaRepository; diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Shared/SqlServerResiliencyTestDbParameters.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Shared/SqlServerResiliencyTestDbParameters.cs index 001de69a85b..8484c9e7fb1 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Shared/SqlServerResiliencyTestDbParameters.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Shared/SqlServerResiliencyTestDbParameters.cs @@ -2,6 +2,7 @@ { using System; using System.Reflection; + using MassTransit.Tests; using Microsoft.EntityFrameworkCore; using TestFramework; diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Shared/SqlServerTestDbParameters.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Shared/SqlServerTestDbParameters.cs index 8a5988fd870..0b2e8232186 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Shared/SqlServerTestDbParameters.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Shared/SqlServerTestDbParameters.cs @@ -2,6 +2,7 @@ { using System; using System.Reflection; + using MassTransit.Tests; using Microsoft.EntityFrameworkCore; using TestFramework; diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SimpleSaga/SagaLocator_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SimpleSaga/SagaLocator_Specs.cs index 0ebe3fd29e5..3e9b3849600 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SimpleSaga/SagaLocator_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SimpleSaga/SagaLocator_Specs.cs @@ -7,7 +7,6 @@ using MassTransit.Tests.Saga.Messages; using NUnit.Framework; using Shared; - using Shouldly; using Testing; @@ -28,15 +27,15 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); - var nextMessage = new CompleteSimpleSaga {CorrelationId = sagaId}; + var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; await InputQueueSendEndpoint.Send(nextMessage); foundId = await _sagaRepository.Value.ShouldContainSaga(x => x.CorrelationId == sagaId && x.Completed, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); } [Test] @@ -49,7 +48,7 @@ public async Task An_initiating_message_should_start_the_saga() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); } readonly Lazy> _sagaRepository; diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SlowConcurrentSaga/SlowConcurrentSaga_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SlowConcurrentSaga/SlowConcurrentSaga_Specs.cs index 9a2021da43e..39de7a33803 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SlowConcurrentSaga/SlowConcurrentSaga_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/SlowConcurrentSaga/SlowConcurrentSaga_Specs.cs @@ -8,7 +8,6 @@ namespace MassTransit.EntityFrameworkCoreIntegration.Tests.SlowConcurrentSaga using Events; using NUnit.Framework; using Shared; - using Shouldly; using Testing; @@ -22,8 +21,6 @@ public class SlowConcurrentSaga_Specs : [Test] public async Task Two_Initiating_Messages_Deadlock_Results_In_One_Instance() { - var activityMonitor = Bus.CreateBusActivityMonitor(TimeSpan.FromMilliseconds(3000)); - var sagaId = NewId.NextGuid(); var message = new Begin { CorrelationId = sagaId }; @@ -31,7 +28,7 @@ public async Task Two_Initiating_Messages_Deadlock_Results_In_One_Instance() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); var slowMessage = new IncrementCounterSlowly { CorrelationId = sagaId }; await Task.WhenAll( @@ -40,7 +37,7 @@ await Task.WhenAll( _sagaTestHarness.Consumed.Select().Take(2).ToList(); - await activityMonitor.AwaitBusInactivity(TestTimeout); + await InactivityTask; await _sagaRepository.Value.ShouldContainSagaInState(s => s.CorrelationId == sagaId && s.Counter == 2, _machine, _machine.DidIncrement, TestTimeout); @@ -52,6 +49,8 @@ await _sagaRepository.Value.ShouldContainSagaInState(s => s.CorrelationId == sag public SlowConcurrentSaga_Specs() { + TestInactivityTimeout = TimeSpan.FromSeconds(3); + // rowlock statements that don't work to cause a deadlock. var notWorkingRowLockStatements = new SqlLockStatementProvider("dbo", new NoLockStatementFormatter()); diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/TransactionalBusOutbox_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/TransactionalBusOutbox_Specs.cs index 63acdffc748..360eae6930d 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/TransactionalBusOutbox_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/TransactionalBusOutbox_Specs.cs @@ -4,6 +4,7 @@ using System.Threading.Tasks; using System.Transactions; using Internals; + using MassTransit.Tests; using MassTransit.Tests.Saga.Messages; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.ChangeTracking; @@ -40,7 +41,7 @@ public async Task Should_not_publish_properly() await using (var dbContext = GetDbContext()) { - Assert.IsFalse(await dbContext.Products.AnyAsync(x => x.Id == product.Id)); + Assert.That(await dbContext.Products.AnyAsync(x => x.Id == product.Id), Is.False); } } @@ -70,7 +71,7 @@ public async Task Should_publish_after_db_create() await using (var dbContext = GetDbContext()) { - Assert.IsTrue(await dbContext.Products.AnyAsync(x => x.Id == product.Id)); + Assert.That(await dbContext.Products.AnyAsync(x => x.Id == product.Id), Is.True); } } @@ -100,7 +101,7 @@ public async Task Should_publish_after_db_create_outbox_bus() await using (var dbContext = GetDbContext()) { - Assert.IsTrue(await dbContext.Products.AnyAsync(x => x.Id == product.Id)); + Assert.That(await dbContext.Products.AnyAsync(x => x.Id == product.Id), Is.True); } } diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Turnout/Canceled_Specs.cs b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Turnout/Canceled_Specs.cs index a3c6774b8d8..cb5bc1b8d87 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Turnout/Canceled_Specs.cs +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/Turnout/Canceled_Specs.cs @@ -30,12 +30,7 @@ public async Task Should_get_the_job_accepted() ConsumeContext started = await _started; - await Bus.Publish(new - { - JobId = _jobId, - Reason = "I give up", - InVar.Timestamp - }); + await Bus.CancelJob(_jobId, "I give up"); // just to capture all the test output in a single window ConsumeContext cancelled = await _cancelled; diff --git a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/docker-compose.yml b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/docker-compose.yml index fa066f7b508..1ac74386d9c 100644 --- a/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/docker-compose.yml +++ b/tests/MassTransit.EntityFrameworkCoreIntegration.Tests/docker-compose.yml @@ -1,7 +1,4 @@ -# this compose file will start local services the same as those running on appveyor CI for testing. - -version: '2.3' -services: +services: mssql: image: "mcr.microsoft.com/azure-sql-edge" environment: diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/AuditStore_Specs.cs b/tests/MassTransit.EntityFrameworkIntegration.Tests/AuditStore_Specs.cs index 9b566b2024f..c247e221fd2 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/AuditStore_Specs.cs +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/AuditStore_Specs.cs @@ -4,9 +4,8 @@ using System.Linq; using System.Threading.Tasks; using Audit; + using MassTransit.Tests; using NUnit.Framework; - using Shouldly; - using TestFramework; using Testing; @@ -18,7 +17,7 @@ public async Task Should_have_consume_audit_records() { var consumed = _harness.Consumed; await Task.Delay(500); - (await GetAuditRecords("Consume")).ShouldBe(consumed.Count()); + Assert.That(await GetAuditRecords("Consume"), Is.EqualTo(consumed.Count())); } [Test] @@ -26,7 +25,7 @@ public async Task Should_have_send_audit_record() { var sent = _harness.Sent; await Task.Delay(500); - (await GetAuditRecords("Send")).ShouldBe(sent.Count()); + Assert.That(await GetAuditRecords("Send"), Is.EqualTo(sent.Count())); } [SetUp] diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/Container_Specs.cs b/tests/MassTransit.EntityFrameworkIntegration.Tests/Container_Specs.cs index 8aa34786c56..cf229200d7d 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/Container_Specs.cs +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/Container_Specs.cs @@ -7,6 +7,7 @@ namespace ContainerTests using System.Data.Entity; using System.Data.Entity.ModelConfiguration; using System.Threading.Tasks; + using MassTransit.Tests; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using TestFramework; diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/DiscardEvent_Specs.cs b/tests/MassTransit.EntityFrameworkIntegration.Tests/DiscardEvent_Specs.cs index 19434c61485..66a8a301883 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/DiscardEvent_Specs.cs +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/DiscardEvent_Specs.cs @@ -6,6 +6,7 @@ using System.Data.Entity.ModelConfiguration; using System.Linq; using System.Threading.Tasks; + using MassTransit.Tests; using NUnit.Framework; using Saga; using TestFramework; @@ -30,14 +31,14 @@ await Bus.Publish(new var wasDiscarded = await _discarded.Task; - Assert.IsTrue(wasDiscarded); + Assert.That(wasDiscarded, Is.True); using (var dbContext = _sagaDbContextFactory.Create()) { var result = dbContext.Set().FirstOrDefault(x => x.CorrelationId == sagaId); // THE PROBLEM : the missing instance is not discarded and is persisted to the repository // This test fails - Assert.IsNull(result); + Assert.That(result, Is.Null); } } @@ -61,7 +62,7 @@ await Bus.Publish(new using (var dbContext = _sagaDbContextFactory.Create()) { var result = dbContext.Set().FirstOrDefault(x => x.CorrelationId == sagaId); - Assert.IsNotNull(result); + Assert.That(result, Is.Not.Null); } } diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/MassTransit.EntityFrameworkIntegration.Tests.csproj b/tests/MassTransit.EntityFrameworkIntegration.Tests/MassTransit.EntityFrameworkIntegration.Tests.csproj index a4f3260cbc0..89623a13322 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/MassTransit.EntityFrameworkIntegration.Tests.csproj +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/MassTransit.EntityFrameworkIntegration.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 true true @@ -9,8 +9,11 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/PreInsert_Specs.cs b/tests/MassTransit.EntityFrameworkIntegration.Tests/PreInsert_Specs.cs index 4cfbf2cc599..3f2e09a17f7 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/PreInsert_Specs.cs +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/PreInsert_Specs.cs @@ -7,6 +7,7 @@ namespace PreInsert using System.Data.Entity; using System.Data.Entity.ModelConfiguration; using System.Threading.Tasks; + using MassTransit.Tests; using NUnit.Framework; using Saga; using TestFramework; @@ -119,17 +120,20 @@ public async Task Should_receive_the_published_message() ConsumeContext received = await messageReceived; - Assert.AreEqual(message.CorrelationId, received.Message.TransactionId); + Assert.Multiple(() => + { + Assert.That(received.Message.TransactionId, Is.EqualTo(message.CorrelationId)); - Assert.IsTrue(received.InitiatorId.HasValue, "The initiator should be copied from the CorrelationId"); + Assert.That(received.InitiatorId.HasValue, Is.True, "The initiator should be copied from the CorrelationId"); - Assert.AreEqual(received.InitiatorId.Value, message.CorrelationId, "The initiator should be the saga CorrelationId"); + Assert.That(received.InitiatorId.Value, Is.EqualTo(message.CorrelationId), "The initiator should be the saga CorrelationId"); - Assert.AreEqual(received.SourceAddress, InputQueueAddress, "The published message should have the input queue source address"); + Assert.That(received.SourceAddress, Is.EqualTo(InputQueueAddress), "The published message should have the input queue source address"); + }); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, _machine.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } public When_pre_inserting_the_state_machine_instance_using_ef() @@ -172,7 +176,7 @@ public TestStateMachine() Initially( When(Started) - .Publish(context => new StartupComplete {TransactionId = context.Data.CorrelationId}) + .Publish(context => new StartupComplete { TransactionId = context.Data.CorrelationId }) .TransitionTo(Running)); } @@ -208,17 +212,20 @@ public async Task Should_receive_the_published_message() ConsumeContext received = await messageReceived; - Assert.AreEqual(sagaId, received.Message.TransactionId); + Assert.Multiple(() => + { + Assert.That(received.Message.TransactionId, Is.EqualTo(sagaId)); - Assert.IsTrue(received.InitiatorId.HasValue, "The initiator should be copied from the CorrelationId"); + Assert.That(received.InitiatorId.HasValue, Is.True, "The initiator should be copied from the CorrelationId"); - Assert.AreEqual(received.InitiatorId.Value, message.CorrelationId, "The initiator should be the saga CorrelationId"); + Assert.That(received.InitiatorId.Value, Is.EqualTo(message.CorrelationId), "The initiator should be the saga CorrelationId"); - Assert.AreEqual(received.SourceAddress, InputQueueAddress, "The published message should have the input queue source address"); + Assert.That(received.SourceAddress, Is.EqualTo(InputQueueAddress), "The published message should have the input queue source address"); + }); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, _machine.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } public When_pre_inserting_in_an_invalid_state_using_ef() @@ -266,7 +273,7 @@ public TestStateMachine() { }), When(Started) - .Publish(context => new StartupComplete {TransactionId = context.Data.CorrelationId}) + .Publish(context => new StartupComplete { TransactionId = context.Data.CorrelationId }) .TransitionTo(Running)); } diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/SagaLocator_Specs.cs b/tests/MassTransit.EntityFrameworkIntegration.Tests/SagaLocator_Specs.cs index 01720a6a1de..469b1b40822 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/SagaLocator_Specs.cs +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/SagaLocator_Specs.cs @@ -2,11 +2,11 @@ { using System; using System.Threading.Tasks; + using MassTransit.Tests; using MassTransit.Tests.Saga; using MassTransit.Tests.Saga.Messages; using NUnit.Framework; using Saga; - using Shouldly; using TestFramework; using Testing; @@ -26,7 +26,7 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; @@ -34,7 +34,7 @@ public async Task A_correlated_message_should_find_the_correct_saga() foundId = await _sagaRepository.Value.ShouldContainSaga(x => x.CorrelationId == sagaId && x.Completed, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); } [Test] @@ -47,7 +47,7 @@ public async Task An_initiating_message_should_start_the_saga() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); } readonly Lazy> _sagaRepository; diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyFail_Specs.cs b/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyFail_Specs.cs index e370f1d48b6..8dac5902d4e 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyFail_Specs.cs +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyFail_Specs.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Data.Entity; using System.Threading.Tasks; + using MassTransit.Tests; using NUnit.Framework; using Saga; using TestFramework; @@ -34,7 +35,7 @@ public async Task Should_not_capture_all_events_many_sagas() for (var i = 0; i < 20; i++) { Guid? sagaId = await _repository.Value.ShouldContainSaga(sagaIds[i], TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); } for (var i = 0; i < 20; i++) @@ -68,7 +69,7 @@ public async Task Should_not_capture_all_events_many_sagas() { Guid? sagaId = await _repository.Value.ShouldContainSagaInState(sid, _machine, _machine.Warmup, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); } } @@ -81,7 +82,7 @@ public async Task Should_not_capture_all_events_single_saga() Guid? sagaId = await _repository.Value.ShouldContainSaga(correlationId, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); await Task.WhenAll( InputQueueSendEndpoint.Send(new Bass @@ -108,7 +109,7 @@ await Task.WhenAll( sagaId = await _repository.Value.ShouldContainSagaInState(correlationId, _machine, _machine.Warmup, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); } ChoirStateMachine _machine; diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyOptimistic_Specs.cs b/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyOptimistic_Specs.cs index 894e3d87b03..4ac2c361e5b 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyOptimistic_Specs.cs +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyOptimistic_Specs.cs @@ -5,6 +5,7 @@ using System.Data.Entity; using System.Data.Entity.Infrastructure; using System.Threading.Tasks; + using MassTransit.Tests; using NUnit.Framework; using Saga; using TestFramework; @@ -34,7 +35,7 @@ public async Task Should_capture_all_events_many_sagas() for (var i = 0; i < 20; i++) { Guid? sagaId = await _repository.Value.ShouldContainSaga(sagaIds[i], TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); } for (var i = 0; i < 20; i++) @@ -68,7 +69,7 @@ public async Task Should_capture_all_events_many_sagas() { Guid? sagaId = await _repository.Value.ShouldContainSagaInState(sid, _machine, _machine.Harmony, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); } } @@ -81,7 +82,7 @@ public async Task Should_capture_all_events_single_saga() Guid? sagaId = await _repository.Value.ShouldContainSaga(correlationId, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); await Task.WhenAll( InputQueueSendEndpoint.Send(new Bass @@ -108,11 +109,11 @@ await Task.WhenAll( sagaId = await _repository.Value.ShouldContainSagaInState(correlationId, _machine, _machine.Harmony, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); var instance = await GetSaga(correlationId); - Assert.IsTrue(instance.CurrentState.Equals("Harmony")); + Assert.That(instance.CurrentState, Is.EqualTo("Harmony")); } ChoirStateMachine _machine; @@ -123,7 +124,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin { _machine = new ChoirStateMachine(); - configurator.UseRetry(x => + configurator.UseMessageRetry(x => { x.Handle(); x.Immediate(5); diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyPessimistic_Specs.cs b/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyPessimistic_Specs.cs index bd9b4bef318..a805f043579 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyPessimistic_Specs.cs +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFrameworkConcurrencyPessimistic_Specs.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Data.Entity; using System.Threading.Tasks; + using MassTransit.Tests; using NUnit.Framework; using Saga; using TestFramework; @@ -33,7 +34,7 @@ public async Task Should_capture_all_events_many_sagas() for (var i = 0; i < 20; i++) { Guid? sagaId = await _repository.Value.ShouldContainSaga(sagaIds[i], TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); } for (var i = 0; i < 20; i++) @@ -67,7 +68,7 @@ public async Task Should_capture_all_events_many_sagas() { Guid? sagaId = await _repository.Value.ShouldContainSagaInState(sid, _machine, _machine.Harmony, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); } } @@ -80,7 +81,7 @@ public async Task Should_capture_all_events_single_saga() Guid? sagaId = await _repository.Value.ShouldContainSaga(correlationId, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); await Task.WhenAll( InputQueueSendEndpoint.Send(new Bass @@ -107,11 +108,11 @@ await Task.WhenAll( sagaId = await _repository.Value.ShouldContainSagaInState(correlationId, _machine, _machine.Harmony, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); var instance = await GetSaga(correlationId); - Assert.IsTrue(instance.CurrentState.Equals("Harmony")); + Assert.That(instance.CurrentState, Is.EqualTo("Harmony")); } ChoirStatePessimisticMachine _machine; diff --git a/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFramework_Specs.cs b/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFramework_Specs.cs index 6de9cf42e0d..0f4abdef863 100644 --- a/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFramework_Specs.cs +++ b/tests/MassTransit.EntityFrameworkIntegration.Tests/UsingEntityFramework_Specs.cs @@ -7,6 +7,7 @@ using System.Data.Entity.ModelConfiguration; using System.Linq; using System.Threading.Tasks; + using MassTransit.Tests; using NUnit.Framework; using Saga; using TestFramework; @@ -39,7 +40,7 @@ public async Task Should_handle_the_big_load() for (var i = 0; i < 200; i++) { Guid? sagaId = await _repository.Value.ShouldContainSaga(sagaIds[i], TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); } } @@ -51,12 +52,12 @@ public async Task Should_have_removed_the_state_machine() await InputQueueSendEndpoint.Send(new GirlfriendYelling { CorrelationId = correlationId }); Guid? sagaId = await _repository.Value.ShouldContainSaga(correlationId, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); await InputQueueSendEndpoint.Send(new SodOff { CorrelationId = correlationId }); sagaId = await _repository.Value.ShouldNotContainSaga(correlationId, TestTimeout); - Assert.IsFalse(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.False); } [Test] @@ -68,17 +69,17 @@ public async Task Should_have_the_state_machine() Guid? sagaId = await _repository.Value.ShouldContainSaga(correlationId, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); await InputQueueSendEndpoint.Send(new GotHitByACar { CorrelationId = correlationId }); sagaId = await _repository.Value.ShouldContainSagaInState(correlationId, _machine, _machine.Dead, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); var instance = await GetSaga(correlationId); - Assert.IsTrue(instance.Screwed); + Assert.That(instance.Screwed, Is.True); } SuperShopper _machine; @@ -89,7 +90,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin { _machine = new SuperShopper(); - configurator.UseRetry(x => + configurator.UseMessageRetry(x => { x.Handle(); x.Immediate(5); diff --git a/tests/MassTransit.EventHubIntegration.Tests/BatchProducer_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/BatchProducer_Specs.cs index 455e0e6344c..6fba06d8ea6 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/BatchProducer_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/BatchProducer_Specs.cs @@ -17,6 +17,7 @@ public class BatchProducer_Specs : InMemoryTestFixture { const int Expected = 10; + const string EventHubName = "batch-eh"; [Test] public async Task Should_produce() @@ -39,7 +40,7 @@ public async Task Should_produce() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.Consumer(() => consumer); }); @@ -53,10 +54,10 @@ public async Task Should_produce() await busControl.StartAsync(TestCancellationToken); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); var producerProvider = serviceScope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); try { @@ -78,17 +79,20 @@ await producer.Produce(messages, Pipe.Execute(cont ConsumeContext result = await taskCompletionSource.Task; - Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); - Assert.That(result.DestinationAddress, - Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{Configuration.EventHubName}"))); - Assert.That(result.MessageId, Is.EqualTo(messageId)); - Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); - Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); - Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + Assert.Multiple(() => + { + Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); + Assert.That(result.DestinationAddress, + Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{EventHubName}"))); + Assert.That(result.MessageId, Is.EqualTo(messageId)); + Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); + Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); + Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + }); } finally { - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await busControl.StopAsync(TestCancellationToken); diff --git a/tests/MassTransit.EventHubIntegration.Tests/BatchReceive_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/BatchReceive_Specs.cs index fae7014c946..4113e281df2 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/BatchReceive_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/BatchReceive_Specs.cs @@ -14,6 +14,8 @@ namespace MassTransit.EventHubIntegration.Tests public class BatchReceive_Specs : InMemoryTestFixture { + const string EventHubName = "batch-eh"; + [Test] public async Task Should_receive_batch() { @@ -42,7 +44,7 @@ public async Task Should_receive_batch() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); @@ -60,10 +62,10 @@ public async Task Should_receive_batch() await busControl.StartAsync(TestCancellationToken); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); var producerProvider = serviceScope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); try { @@ -72,14 +74,14 @@ public async Task Should_receive_batch() ConsumeContext> result = await taskCompletionSource.Task; - Assert.AreEqual(batchSize, result.Message.Length); + Assert.That(result.Message, Has.Length.EqualTo(batchSize)); for (var i = 0; i < batchSize; i++) - Assert.AreEqual(i, result.Message[i].Message.Index); + Assert.That(result.Message[i].Message.Index, Is.EqualTo(i)); } finally { - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await busControl.StopAsync(TestCancellationToken); diff --git a/tests/MassTransit.EventHubIntegration.Tests/Configuration.cs b/tests/MassTransit.EventHubIntegration.Tests/Configuration.cs index ad1f7de35fa..b4cb6fa912f 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Configuration.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Configuration.cs @@ -1,24 +1,12 @@ namespace MassTransit.EventHubIntegration.Tests { - using System; - using NUnit.Framework; - - static class Configuration { - public static string EventHubNamespace => - TestContext.Parameters.Exists(nameof(EventHubNamespace)) - ? TestContext.Parameters.Get(nameof(EventHubNamespace)) - : Environment.GetEnvironmentVariable("MT_EH_NAMESPACE") ?? "MassTransitBuild"; + public static string ConsumerGroup = "cg1"; - public static string EventHubName => - TestContext.Parameters.Exists(nameof(EventHubName)) - ? TestContext.Parameters.Get(nameof(EventHubName)) - : Environment.GetEnvironmentVariable("MT_EH_NAME") ?? "masstransit-build"; + public static string EventHubNamespace => + "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"; - public static string StorageAccount => - TestContext.Parameters.Exists(nameof(StorageAccount)) - ? TestContext.Parameters.Get(nameof(StorageAccount)) - : Environment.GetEnvironmentVariable("MT_AZURE_STORAGE_ACCOUNT") ?? ""; + public static string StorageAccount => "UseDevelopmentStorage=true"; } } diff --git a/tests/MassTransit.EventHubIntegration.Tests/EndpointConnector_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/EndpointConnector_Specs.cs index bcccc8ff52e..c73a6a3cbee 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/EndpointConnector_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/EndpointConnector_Specs.cs @@ -13,6 +13,8 @@ namespace MassTransit.EventHubIntegration.Tests public class EndpointConnector_Specs : InMemoryTestFixture { + const string EventHubName = "default-eh"; + [Test] public async Task Should_produce() { @@ -44,11 +46,11 @@ public async Task Should_produce() await busControl.StartAsync(TestCancellationToken); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); var eventHubRider = provider.GetRequiredService(); var producerProvider = serviceScope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); try { @@ -70,7 +72,7 @@ await producer.Produce(new { Text = "text" }, Pipe.Execute + var connected = eventHubRider.ConnectEventHubEndpoint(EventHubName, Configuration.ConsumerGroup, (context, configurator) => { configurator.ConfigureConsumer(context); }); @@ -78,23 +80,29 @@ await producer.Produce(new { Text = "text" }, Pipe.Execute result = await taskCompletionSource.Task; - Assert.AreEqual("text", result.Message.Text); - Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); - Assert.That(result.DestinationAddress, - Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{Configuration.EventHubName}"))); - Assert.That(result.MessageId, Is.EqualTo(messageId)); - Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); - Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); - Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + Assert.Multiple(() => + { + Assert.That(result.Message.Text, Is.EqualTo("text")); + Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); + Assert.That(result.DestinationAddress, + Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{EventHubName}"))); + Assert.That(result.MessageId, Is.EqualTo(messageId)); + Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); + Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); + Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + }); var headerType = result.Headers.Get("Special"); Assert.That(headerType, Is.Not.Null); - Assert.That(headerType.Key, Is.EqualTo("Hello")); - Assert.That(headerType.Value, Is.EqualTo("World")); + Assert.Multiple(() => + { + Assert.That(headerType.Key, Is.EqualTo("Hello")); + Assert.That(headerType.Value, Is.EqualTo("World")); + }); } finally { - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await busControl.StopAsync(TestCancellationToken); diff --git a/tests/MassTransit.EventHubIntegration.Tests/Faults_Receive_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/Faults_Receive_Specs.cs index 32ae0c3b354..26cb791fd85 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Faults_Receive_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Faults_Receive_Specs.cs @@ -12,6 +12,8 @@ namespace MassTransit.EventHubIntegration.Tests public class Faults_Receive_Specs : InMemoryTestFixture { + const string EventHubName = "default-eh"; + [Test] public async Task Should_produce() { @@ -34,7 +36,7 @@ public async Task Should_produce() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -48,10 +50,10 @@ public async Task Should_produce() await busControl.StartAsync(TestCancellationToken); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); var producerProvider = serviceScope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); try { @@ -62,7 +64,7 @@ public async Task Should_produce() } finally { - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await busControl.StopAsync(TestCancellationToken); diff --git a/tests/MassTransit.EventHubIntegration.Tests/Filter_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/Filter_Specs.cs index 81a5d1ff7a6..33dc037d9f2 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Filter_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Filter_Specs.cs @@ -11,6 +11,8 @@ namespace MassTransit.EventHubIntegration.Tests public class Using_a_consumer_filter { + const string EventHubName = "receive-eh"; + static int _attempts; static int _lastAttempt; static int _lastCount; @@ -29,10 +31,11 @@ public async Task Should_produce() r.UsingEventHub((context, k) => { - k.Host(Configuration.EventHubNamespace); - k.Storage(Configuration.StorageAccount); + k.Host( + "Endpoint=sb://localhost;SharedAccessKeyName=RootManageSharedAccessKey;SharedAccessKey=SAS_KEY_VALUE;UseDevelopmentEmulator=true;"); + k.Storage("UseDevelopmentStorage=true"); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.UseMessageRetry(retry => retry.Immediate(3)); @@ -47,7 +50,7 @@ public async Task Should_produce() await harness.Start(); - var producer = await harness.GetProducer(Configuration.EventHubName); + var producer = await harness.GetProducer(EventHubName); var messageId = NewId.NextGuid(); @@ -58,9 +61,12 @@ await producer.Produce(new { Text = "text" }, Pipe.Execute result = await provider.GetRequiredService>>().Task; - Assert.That(_attempts, Is.EqualTo(4)); - Assert.That(_lastCount, Is.EqualTo(2)); - Assert.That(_lastAttempt, Is.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(_attempts, Is.EqualTo(4)); + Assert.That(_lastCount, Is.EqualTo(2)); + Assert.That(_lastAttempt, Is.EqualTo(3)); + }); } diff --git a/tests/MassTransit.EventHubIntegration.Tests/HealthCheck_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/HealthCheck_Specs.cs index 50ae5a0ffd1..5a971ff4a47 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/HealthCheck_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/HealthCheck_Specs.cs @@ -15,6 +15,8 @@ namespace MassTransit.EventHubIntegration.Tests public class HealthCheck_Specs : InMemoryTestFixture { + const string EventHubName = "default-eh"; + [Test] public async Task Should_be_healthy() { @@ -33,7 +35,7 @@ public async Task Should_be_healthy() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { }); }); diff --git a/tests/MassTransit.EventHubIntegration.Tests/Long_Receive_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/Long_Receive_Specs.cs index 183e7e9add6..b7e6906858a 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Long_Receive_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Long_Receive_Specs.cs @@ -13,6 +13,8 @@ namespace MassTransit.EventHubIntegration.Tests public class Long_Receive_Specs : InMemoryTestFixture { + const string EventHubName = "receive-eh"; + [Test] public async Task Should_receive() { @@ -35,7 +37,7 @@ public async Task Should_receive() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConcurrentMessageLimit = 1; c.CheckpointMessageCount = 1; @@ -50,12 +52,12 @@ public async Task Should_receive() var busControl = provider.GetRequiredService(); - var scope = provider.CreateScope(); + var scope = provider.CreateAsyncScope(); await busControl.StartAsync(TestCancellationToken); var producerProvider = scope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); try { @@ -65,7 +67,7 @@ await producer.Produce(new { Text = "" }, Pipe.Execute result = await taskCompletionSource.Task; - Assert.AreEqual(messageId, result.MessageId); + Assert.That(result.MessageId, Is.EqualTo(messageId)); } finally { diff --git a/tests/MassTransit.EventHubIntegration.Tests/MassTransit.EventHubIntegration.Tests.csproj b/tests/MassTransit.EventHubIntegration.Tests/MassTransit.EventHubIntegration.Tests.csproj index 8ce5d5efc8b..d7bfb4fa8d9 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/MassTransit.EventHubIntegration.Tests.csproj +++ b/tests/MassTransit.EventHubIntegration.Tests/MassTransit.EventHubIntegration.Tests.csproj @@ -1,13 +1,16 @@ - net6.0 - latest + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/MassTransit.EventHubIntegration.Tests/MultiBus_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/MultiBus_Specs.cs index 52068c27502..9ee49754a64 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/MultiBus_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/MultiBus_Specs.cs @@ -13,12 +13,16 @@ namespace MassTransit.EventHubIntegration.Tests using TestFramework; + [Category("Flaky")] public class MultiBus_Specs : InMemoryTestFixture { + const string EventHubNameOne = "multibus-eh1"; + const string EventHubNameTwo = "multibus-eh2"; + public MultiBus_Specs() { - TestTimeout = TimeSpan.FromMinutes(5); + TestTimeout = TimeSpan.FromMinutes(1); } [Test] @@ -40,7 +44,7 @@ public async Task Should_receive_in_both_buses() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubNameOne, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -60,7 +64,7 @@ public async Task Should_receive_in_both_buses() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubNameTwo, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -75,10 +79,10 @@ public async Task Should_receive_in_both_buses() await Task.WhenAll(hostedServices.Select(x => x.StartAsync(TestCancellationToken))); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); var producerProvider = serviceScope.ServiceProvider.GetRequiredService>().Value; - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubNameOne); await producer.Produce(new FirstBusMessage(), TestCancellationToken); @@ -86,7 +90,7 @@ public async Task Should_receive_in_both_buses() await provider.GetRequiredService>>().Task.OrCanceled(TestCancellationToken); - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await Task.WhenAll(hostedServices.Select(x => x.StopAsync(TestCancellationToken))); } @@ -129,7 +133,7 @@ public FirstBusMessageConsumer(TaskCompletionSource context) { - var producer = await _provider.GetProducer(Configuration.EventHubName); + var producer = await _provider.GetProducer(EventHubNameTwo); await producer.Produce(new SecondBusMessage()); _taskCompletionSource.TrySetResult(context); } diff --git a/tests/MassTransit.EventHubIntegration.Tests/Outbox_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/Outbox_Specs.cs index f2e066511c7..49381ff3f9d 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Outbox_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Outbox_Specs.cs @@ -12,6 +12,8 @@ namespace MassTransit.EventHubIntegration.Tests public class Publishing_a_message_to_the_bus_through_the_outbox : InMemoryTestFixture { + const string EventHubName = "publish-eh"; + [Test] public async Task Should_use_the_default_endpoint_serializer() { @@ -34,7 +36,7 @@ public async Task Should_use_the_default_endpoint_serializer() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.UseInMemoryInboxOutbox(context); @@ -48,7 +50,7 @@ public async Task Should_use_the_default_endpoint_serializer() await harness.Start(); - var producer = await harness.GetProducer(Configuration.EventHubName); + var producer = await harness.GetProducer(EventHubName); var correlationId = NewId.NextGuid(); var conversationId = NewId.NextGuid(); @@ -67,8 +69,11 @@ await producer.Produce(new { Test = "text" }, Pipe.Execute + { + Assert.That(message.Context.Message.OriginalMessageId, Is.EqualTo(messageId)); + Assert.That(message.Context.Message.OriginalCorrelationId, Is.EqualTo(correlationId)); + }); } diff --git a/tests/MassTransit.EventHubIntegration.Tests/ProducerPipe_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/ProducerPipe_Specs.cs index c9d86f2f6d9..1c4ddaeaf75 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/ProducerPipe_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/ProducerPipe_Specs.cs @@ -13,6 +13,8 @@ namespace MassTransit.EventHubIntegration.Tests public class ProducerPipe_Specs : InMemoryTestFixture { + const string EventHubName = "produce-eh"; + [Test] public async Task Should_produce() { @@ -36,7 +38,7 @@ public async Task Should_produce() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -51,10 +53,10 @@ public async Task Should_produce() await busControl.StartAsync(TestCancellationToken); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); var producerProvider = serviceScope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); try { @@ -62,16 +64,19 @@ public async Task Should_produce() var result = await sendFilterTaskCompletionSource.Task; - Assert.IsTrue(result.TryGetPayload(out _)); - Assert.IsTrue(result.TryGetPayload>(out _)); - Assert.That(result.DestinationAddress, - Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{Configuration.EventHubName}"))); + Assert.Multiple(() => + { + Assert.That(result.TryGetPayload(out _), Is.True); + Assert.That(result.TryGetPayload>(out _), Is.True); + Assert.That(result.DestinationAddress, + Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{EventHubName}"))); + }); await taskCompletionSource.Task; } finally { - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await busControl.StopAsync(TestCancellationToken); diff --git a/tests/MassTransit.EventHubIntegration.Tests/Producer_Saga_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/Producer_Saga_Specs.cs index 78280a1fcd8..b110b7da96b 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Producer_Saga_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Producer_Saga_Specs.cs @@ -14,6 +14,8 @@ namespace MassTransit.EventHubIntegration.Tests public class Saga_Producer_Specs : InMemoryTestFixture { + const string EventHubName = "produce-eh"; + [Test] public async Task Should_produce() { @@ -39,7 +41,7 @@ public async Task Should_produce() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -53,7 +55,7 @@ public async Task Should_produce() await busControl.StartAsync(TestCancellationToken); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); var publishEndpoint = serviceScope.ServiceProvider.GetRequiredService(); @@ -69,12 +71,15 @@ await publishEndpoint.Publish(new StartTest ConsumeContext result = await taskCompletionSource.Task; - Assert.AreEqual("Key: ABC123", result.Message.Text); - Assert.AreEqual(correlationId, result.InitiatorId); + Assert.Multiple(() => + { + Assert.That(result.Message.Text, Is.EqualTo("Key: ABC123")); + Assert.That(result.InitiatorId, Is.EqualTo(correlationId)); + }); } finally { - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await busControl.StopAsync(TestCancellationToken); @@ -119,7 +124,7 @@ public TestStateMachineSaga() Initially( When(Started) .Then(context => context.Instance.Key = context.Data.TestKey) - .Produce(x => Configuration.EventHubName, x => x.Init(new { Text = $"Key: {x.Data.TestKey}" })) + .Produce(x => EventHubName, x => x.Init(new { Text = $"Key: {x.Data.TestKey}" })) .TransitionTo(Active)); SetCompletedWhenFinalized(); diff --git a/tests/MassTransit.EventHubIntegration.Tests/Producer_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/Producer_Specs.cs index 0d62f50aed2..df5833eb5b8 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Producer_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Producer_Specs.cs @@ -13,6 +13,8 @@ namespace MassTransit.EventHubIntegration.Tests public class Producer_Specs : InMemoryTestFixture { + const string EventHubName = "produce-eh"; + [Test] public async Task Should_produce() { @@ -35,7 +37,7 @@ public async Task Should_produce() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -49,10 +51,10 @@ public async Task Should_produce() await busControl.StartAsync(TestCancellationToken); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); var producerProvider = serviceScope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); try { @@ -76,23 +78,29 @@ await producer.Produce(new { Text = "text" }, Pipe.Execute result = await taskCompletionSource.Task; - Assert.AreEqual("text", result.Message.Text); - Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); - Assert.That(result.DestinationAddress, - Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{Configuration.EventHubName}"))); - Assert.That(result.MessageId, Is.EqualTo(messageId)); - Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); - Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); - Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + Assert.Multiple(() => + { + Assert.That(result.Message.Text, Is.EqualTo("text")); + Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); + Assert.That(result.DestinationAddress, + Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{EventHubName}"))); + Assert.That(result.MessageId, Is.EqualTo(messageId)); + Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); + Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); + Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + }); var headerType = result.Headers.Get("Special"); Assert.That(headerType, Is.Not.Null); - Assert.That(headerType.Key, Is.EqualTo("Hello")); - Assert.That(headerType.Value, Is.EqualTo("World")); + Assert.Multiple(() => + { + Assert.That(headerType.Key, Is.EqualTo("Hello")); + Assert.That(headerType.Value, Is.EqualTo("World")); + }); } finally { - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await busControl.StopAsync(TestCancellationToken); @@ -129,6 +137,8 @@ public interface HeaderType public class ProducerObserver_Specs : InMemoryTestFixture { + const string EventHubName = "produce-eh"; + [Test] public async Task Should_use_bus_observers() { @@ -157,7 +167,7 @@ public async Task Should_use_bus_observers() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -171,10 +181,10 @@ public async Task Should_use_bus_observers() await busControl.StartAsync(TestCancellationToken); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); var producerProvider = serviceScope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); try { @@ -184,16 +194,19 @@ public async Task Should_use_bus_observers() ConsumeContext result = await taskCompletionSource.Task; - Assert.AreEqual("text", result.Message.Text); - Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); - Assert.That(result.DestinationAddress, - Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{Configuration.EventHubName}"))); + Assert.Multiple(() => + { + Assert.That(result.Message.Text, Is.EqualTo("text")); + Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); + Assert.That(result.DestinationAddress, + Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{EventHubName}"))); + }); await postSendCompletionSource.Task; } finally { - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await busControl.StopAsync(TestCancellationToken); diff --git a/tests/MassTransit.EventHubIntegration.Tests/Publish_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/Publish_Specs.cs index 2e785f73bea..682e89a6a3a 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Publish_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Publish_Specs.cs @@ -3,7 +3,6 @@ namespace MassTransit.EventHubIntegration.Tests using System; using System.Threading.Tasks; using Azure.Messaging.EventHubs; - using Azure.Messaging.EventHubs.Consumer; using Azure.Messaging.EventHubs.Producer; using Context; using Contracts; @@ -18,10 +17,11 @@ namespace MassTransit.EventHubIntegration.Tests public class Publish_Specs : InMemoryTestFixture { + const string EventHubName = "publish-eh"; + [Test] public async Task Should_receive() { - const string consumerGroup = EventHubConsumerClient.DefaultConsumerGroupName; TaskCompletionSource> taskCompletionSource = GetTask>(); TaskCompletionSource> pingTaskCompletionSource = GetTask>(); @@ -45,7 +45,7 @@ public async Task Should_receive() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, consumerGroup, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -61,7 +61,7 @@ public async Task Should_receive() try { - await using var producer = new EventHubProducerClient(Configuration.EventHubNamespace, Configuration.EventHubName); + await using var producer = new EventHubProducerClient(Configuration.EventHubNamespace, EventHubName); var message = new EventHubMessageClass("test"); var context = new MessageSendContext(message) @@ -76,11 +76,15 @@ public async Task Should_receive() ConsumeContext result = await taskCompletionSource.Task; ConsumeContext ping = await pingTaskCompletionSource.Task; - Assert.AreEqual(message.Text, result.Message.Text); + Assert.Multiple(() => + { + Assert.That(result.Message.Text, Is.EqualTo(message.Text)); - Assert.AreEqual(result.CorrelationId, ping.InitiatorId); - Assert.That(ping.SourceAddress, - Is.EqualTo(new Uri($"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{Configuration.EventHubName}/{consumerGroup}"))); + Assert.That(ping.InitiatorId, Is.EqualTo(result.CorrelationId)); + Assert.That(ping.SourceAddress, + Is.EqualTo(new Uri( + $"loopback://localhost/{EventHubEndpointAddress.PathPrefix}/{EventHubName}/{Configuration.ConsumerGroup}"))); + }); } finally { diff --git a/tests/MassTransit.EventHubIntegration.Tests/Receive_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/Receive_Specs.cs index ac833e4d7b7..e485ccbad8d 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Receive_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Receive_Specs.cs @@ -16,6 +16,8 @@ namespace MassTransit.EventHubIntegration.Tests public class Receive_Specs : InMemoryTestFixture { + const string EventHubName = "receive-eh"; + [Test] public async Task Should_receive() { @@ -38,7 +40,7 @@ public async Task Should_receive() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -54,7 +56,7 @@ public async Task Should_receive() try { - await using var producer = new EventHubProducerClient(Configuration.EventHubNamespace, Configuration.EventHubName); + await using var producer = new EventHubProducerClient(Configuration.EventHubNamespace, EventHubName); var message = new EventHubMessageClass("test"); var context = new MessageSendContext(message); @@ -64,7 +66,7 @@ public async Task Should_receive() ConsumeContext result = await taskCompletionSource.Task; - Assert.AreEqual(message.Text, result.Message.Text); + Assert.That(result.Message.Text, Is.EqualTo(message.Text)); } finally { @@ -104,9 +106,12 @@ public async Task Consume(ConsumeContext context) } } + public class ReceiveWithPayload_Specs : InMemoryTestFixture { + const string EventHubName = "receive-eh"; + [Test] public async Task Should_contains_payload() { @@ -129,7 +134,7 @@ public async Task Should_contains_payload() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -146,13 +151,13 @@ public async Task Should_contains_payload() try { var producerProvider = provider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); await producer.Produce(new { }, TestCancellationToken); ConsumeContext result = await taskCompletionSource.Task; - Assert.IsTrue(result.TryGetPayload(out EventHubConsumeContext _)); + Assert.That(result.TryGetPayload(out EventHubConsumeContext _), Is.True); } finally { diff --git a/tests/MassTransit.EventHubIntegration.Tests/Recycle_Specs.cs b/tests/MassTransit.EventHubIntegration.Tests/Recycle_Specs.cs index de802c5b64a..ea84f003456 100644 --- a/tests/MassTransit.EventHubIntegration.Tests/Recycle_Specs.cs +++ b/tests/MassTransit.EventHubIntegration.Tests/Recycle_Specs.cs @@ -14,9 +14,11 @@ namespace MassTransit.EventHubIntegration.Tests public class Recycled_Specs : InMemoryTestFixture { + const string EventHubName = "default-eh"; + public Recycled_Specs() { - TestTimeout = TimeSpan.FromMinutes(5); + TestTimeout = TimeSpan.FromMinutes(1); } [Test] @@ -41,7 +43,7 @@ public async Task Should_produce() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, c => { c.ConfigureConsumer(context); }); @@ -59,11 +61,11 @@ public async Task Should_produce() await busControl.StartAsync(TestCancellationToken); - var serviceScope = provider.CreateScope(); + var serviceScope = provider.CreateAsyncScope(); try { var producerProvider = serviceScope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); await producer.Produce(new { }, TestCancellationToken); await taskCompletionSource.Task.OrCanceled(TestCancellationToken); @@ -71,11 +73,35 @@ public async Task Should_produce() } finally { - serviceScope.Dispose(); + await serviceScope.DisposeAsync(); await provider.DisposeAsync(); } } + + class EventHubMessageConsumer : + IConsumer + { + readonly TaskCompletionSource> _taskCompletionSource; + + public EventHubMessageConsumer(TaskCompletionSource> taskCompletionSource) + { + _taskCompletionSource = taskCompletionSource; + } + + public async Task Consume(ConsumeContext context) + { + _taskCompletionSource.TrySetResult(context); + } + } + } + + + public class Recycled_Produce_Specs : + InMemoryTestFixture + { + const string EventHubName = "default-eh"; + [Test] public async Task Should_produce_after_recycle() { @@ -94,7 +120,8 @@ public async Task Should_produce_after_recycle() k.Host(Configuration.EventHubNamespace); k.Storage(Configuration.StorageAccount); - k.ReceiveEndpoint(Configuration.EventHubName, c => c.Handler(_ => Task.CompletedTask)); + k.ReceiveEndpoint(EventHubName, Configuration.ConsumerGroup, + c => c.Handler(_ => Task.CompletedTask)); }); }); }); @@ -105,10 +132,10 @@ public async Task Should_produce_after_recycle() try { await busControl.StartAsync(TestCancellationToken); - using (var scope = provider.CreateScope()) + using (var scope = provider.CreateAsyncScope()) { var producerProvider = scope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); await producer.Produce(new { }, TestCancellationToken); } @@ -118,10 +145,10 @@ public async Task Should_produce_after_recycle() await busControl.StartAsync(TestCancellationToken); - using (var scope = provider.CreateScope()) + await using (var scope = provider.CreateAsyncScope()) { var producerProvider = scope.ServiceProvider.GetRequiredService(); - var producer = await producerProvider.GetProducer(Configuration.EventHubName); + var producer = await producerProvider.GetProducer(EventHubName); await producer.Produce(new { }, TestCancellationToken); } @@ -132,22 +159,5 @@ public async Task Should_produce_after_recycle() await provider.DisposeAsync(); } } - - - class EventHubMessageConsumer : - IConsumer - { - readonly TaskCompletionSource> _taskCompletionSource; - - public EventHubMessageConsumer(TaskCompletionSource> taskCompletionSource) - { - _taskCompletionSource = taskCompletionSource; - } - - public async Task Consume(ConsumeContext context) - { - _taskCompletionSource.TrySetResult(context); - } - } } } diff --git a/tests/MassTransit.EventHubIntegration.Tests/config.json b/tests/MassTransit.EventHubIntegration.Tests/config.json new file mode 100644 index 00000000000..11aa6eef27e --- /dev/null +++ b/tests/MassTransit.EventHubIntegration.Tests/config.json @@ -0,0 +1,78 @@ +{ + "UserConfig": { + "NamespaceConfig": [ + { + "Type": "EventHub", + "Name": "emulatorNs1", + "Entities": [ + { + "Name": "default-eh", + "PartitionCount": "2", + "ConsumerGroups": [ + { + "Name": "cg1" + } + ] + }, + { + "Name": "batch-eh", + "PartitionCount": "2", + "ConsumerGroups": [ + { + "Name": "cg1" + } + ] + }, + { + "Name": "produce-eh", + "PartitionCount": "2", + "ConsumerGroups": [ + { + "Name": "cg1" + } + ] + }, + { + "Name": "publish-eh", + "PartitionCount": "2", + "ConsumerGroups": [ + { + "Name": "cg1" + } + ] + }, + { + "Name": "multibus-eh1", + "PartitionCount": "2", + "ConsumerGroups": [ + { + "Name": "cg1" + } + ] + }, + { + "Name": "multibus-eh2", + "PartitionCount": "2", + "ConsumerGroups": [ + { + "Name": "cg1" + } + ] + }, + { + "Name": "receive-eh", + "PartitionCount": "2", + "ConsumerGroups": [ + { + "Name": "cg1" + } + ] + } + ] + } + ], + "LoggingConfig": { + "Type": "File" + } + } +} diff --git a/tests/MassTransit.EventHubIntegration.Tests/docker-compose.yml b/tests/MassTransit.EventHubIntegration.Tests/docker-compose.yml new file mode 100644 index 00000000000..7339ac2edd8 --- /dev/null +++ b/tests/MassTransit.EventHubIntegration.Tests/docker-compose.yml @@ -0,0 +1,22 @@ +services: + emulator: + container_name: "eventhubs-emulator" + image: "mcr.microsoft.com/azure-messaging/eventhubs-emulator:latest" + volumes: + - "./config.json:/Eventhubs_Emulator/ConfigFiles/Config.json" + ports: + - "5672:5672" + environment: + BLOB_SERVER: azurite + METADATA_SERVER: azurite + ACCEPT_EULA: "Y" + depends_on: + - azurite + + azurite: + container_name: "azurite" + image: "mcr.microsoft.com/azure-storage/azurite:latest" + ports: + - "10000:10000" + - "10001:10001" + - "10002:10002" diff --git a/tests/MassTransit.GrpcTransport.Tests/Batching_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/Batching_Specs.cs deleted file mode 100644 index 62e518e937e..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/Batching_Specs.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - namespace Batching - { - using System; - using System.Linq; - using System.Threading.Tasks; - using NUnit.Framework; - using TestFramework.Messages; - - - [TestFixture] - public class When_a_batch_limit_is_reached : - GrpcTestFixture - { - [Test] - public async Task Should_receive_the_message_batch() - { - await Task.WhenAll(Enumerable.Range(0, 5).Select(x => InputQueueSendEndpoint.Send(new PingMessage()))); - - Batch batch = await _consumer[0]; - - Assert.That(batch.Length, Is.EqualTo(5)); - Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Size)); - } - - TestBatchConsumer _consumer; - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _consumer = new TestBatchConsumer(GetTask>()); - - configurator.PrefetchCount = 10; - - configurator.Batch(x => - { - x.MessageLimit = 5; - - x.Consumer(() => _consumer); - }); - } - } - - - [TestFixture] - public class When_a_batch_timeout_is_reached_due_to_prefetch : - GrpcTestFixture - { - [Test] - public async Task Should_receive_the_message_batch() - { - for (var i = 0; i < 10; i++) - await InputQueueSendEndpoint.Send(new PingMessage()); - - Batch batch = await _consumer[0]; - - Assert.That(batch.Length, Is.EqualTo(5)); - Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Time)); - - batch = await _consumer[1]; - - Assert.That(batch.Length, Is.EqualTo(5)); - Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Time)); - } - - TestBatchConsumer _consumer; - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _consumer = new TestBatchConsumer(GetTask>(), GetTask>()); - - configurator.ConcurrentMessageLimit = 5; - - configurator.Batch(x => - { - x.MessageLimit = 10; - x.TimeLimit = TimeSpan.FromSeconds(1); - - x.Consumer(() => _consumer); - }); - } - } - - - class TestBatchConsumer : - IConsumer> - { - readonly TaskCompletionSource>[] _messageTask; - - int _count; - - public TestBatchConsumer(params TaskCompletionSource>[] messageTask) - { - _messageTask = messageTask; - } - - public Task> this[int index] => _messageTask[index].Task; - - public Task Consume(ConsumeContext> context) - { - if (_count < _messageTask.Length) - _messageTask[_count++].TrySetResult(context.Message); - - return Task.CompletedTask; - } - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/ClientRequest_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/ClientRequest_Specs.cs deleted file mode 100644 index 64ee8dc65cb..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/ClientRequest_Specs.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using TestFramework.Messages; - - - [TestFixture] - public class Sending_a_request_from_the_client_bus : - GrpcClientTestFixture - { - public Sending_a_request_from_the_client_bus() - { - TestTimeout = TimeSpan.FromSeconds(5); - } - - [Test] - public async Task Should_receive_the_response() - { - IRequestClient client = CreateRequestClient(); - - Task> response = client.GetResponse(new PingMessage()); - - _ = await response; - } - - Task> _ping; - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _ping = Handler(configurator, async x => await x.RespondAsync(new PongMessage(x.Message.CorrelationId))); - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/ConsumerHarness_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/ConsumerHarness_Specs.cs deleted file mode 100644 index bf4560a1d84..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/ConsumerHarness_Specs.cs +++ /dev/null @@ -1,297 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System.Linq; - using System.Threading.Tasks; - using NUnit.Framework; - using Testing; - - - [TestFixture] - public class When_a_consumer_is_being_tested - { - [Test] - public void Should_have_called_the_consumer_method() - { - Assert.IsTrue(_consumer.Consumed.Select().Any()); - } - - [Test] - public void Should_have_sent_the_response_from_the_consumer() - { - Assert.IsTrue(_harness.Published.Select().Any()); - } - - [Test] - public void Should_receive_the_message_type_a() - { - Assert.IsTrue(_harness.Consumed.Select().Any()); - } - - [Test] - public void Should_send_the_initial_message_to_the_consumer() - { - Assert.IsTrue(_harness.Sent.Select().Any()); - } - - InMemoryTestHarness _harness; - ConsumerTestHarness _consumer; - - [OneTimeSetUp] - public async Task A_consumer_is_being_tested() - { - _harness = new InMemoryTestHarness(); - _consumer = _harness.Consumer(); - - await _harness.Start(); - - await _harness.InputQueueSendEndpoint.Send(new A()); - } - - [OneTimeTearDown] - public async Task Teardown() - { - await _harness.Stop(); - } - - - class TestConsumer : - IConsumer - { - public async Task Consume(ConsumeContext context) - { - await context.RespondAsync(new B()); - } - } - - - class A - { - } - - - class B - { - } - } - - - [TestFixture] - public class When_a_slow_consumer_is_being_tested - { - [Test] - public void Should_have_called_the_consumer_method() - { - Assert.That(async () => await _consumer.Consumed.Any(), Is.True); - } - - [Test] - public void Should_have_sent_the_response_from_the_consumer() - { - Assert.That(async () => await _harness.Published.Any(), Is.True); - } - - [Test] - public void Should_receive_the_message_type_a() - { - Assert.That(async () => await _harness.Consumed.Any(), Is.True); - } - - [Test] - public void Should_send_the_initial_message_to_the_consumer() - { - Assert.That(async () => await _harness.Sent.Any(), Is.True); - } - - InMemoryTestHarness _harness; - ConsumerTestHarness _consumer; - - [OneTimeSetUp] - public async Task A_consumer_is_being_tested() - { - _harness = new InMemoryTestHarness(); - _consumer = _harness.Consumer(); - - await _harness.Start(); - - await _harness.InputQueueSendEndpoint.Send(new A()); - } - - [OneTimeTearDown] - public async Task Teardown() - { - await _harness.Stop(); - } - - - class TestConsumer : - IConsumer - { - public async Task Consume(ConsumeContext context) - { - await Task.Delay(2000); - await context.RespondAsync(new B()); - } - } - - - class A - { - } - - - class B - { - } - } - - - [TestFixture] - public class When_a_consumer_of_interfaces_is_being_tested - { - [Test] - public void Should_have_called_the_consumer_method() - { - Assert.IsTrue(_consumer.Consumed.Select().Any()); - } - - [Test] - public void Should_have_sent_the_response_from_the_consumer() - { - Assert.IsTrue(_harness.Published.Select().Any()); - Assert.IsTrue(_harness.Published.Select().Any()); - } - - [Test] - public void Should_receive_the_message_type_a() - { - Assert.IsTrue(_harness.Consumed.Select().Any()); - } - - [Test] - public void Should_send_the_initial_message_to_the_consumer() - { - Assert.IsTrue(_harness.Sent.Select().Any()); - Assert.IsTrue(_harness.Sent.Select().Any()); - } - - InMemoryTestHarness _harness; - ConsumerTestHarness _consumer; - - [OneTimeSetUp] - public async Task A_consumer_is_being_tested() - { - _harness = new InMemoryTestHarness(); - _consumer = _harness.Consumer(); - - await _harness.Start(); - - await _harness.InputQueueSendEndpoint.Send(new A()); - } - - [OneTimeTearDown] - public async Task Teardown() - { - await _harness.Stop(); - } - - - class TestInterfaceConsumer : - IConsumer - { - public async Task Consume(ConsumeContext context) - { - await context.RespondAsync(new B()); - } - } - - - public interface IA - { - } - - - class A : - IA - { - } - - - public interface IB - { - } - - - class B : - IB - { - } - } - - - public class When_a_context_consumer_is_being_tested - { - ConsumerTestHarness _consumer; - InMemoryTestHarness _harness; - - [OneTimeSetUp] - public async Task A_consumer_is_being_tested() - { - _harness = new InMemoryTestHarness(); - _consumer = _harness.Consumer(); - - await _harness.Start(); - - await _harness.InputQueueSendEndpoint.Send(new A(), context => context.ResponseAddress = _harness.BusAddress); - } - - [OneTimeTearDown] - public async Task Teardown() - { - await _harness.Stop(); - } - - [Test] - public void Should_send_the_initial_message_to_the_consumer() - { - Assert.IsTrue(_harness.Sent.Select().Any()); - } - - [Test] - public void Should_have_sent_the_response_from_the_consumer() - { - Assert.IsTrue(_harness.Sent.Select().Any()); - } - - [Test] - public void Should_receive_the_message_type_a() - { - Assert.IsTrue(_harness.Consumed.Select().Any()); - } - - [Test] - public void Should_have_called_the_consumer_method() - { - Assert.IsTrue(_consumer.Consumed.Select().Any()); - } - - - class TestConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return context.RespondAsync(new B()); - } - } - - - class A - { - } - - - class B - { - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/Dispatcher_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/Dispatcher_Specs.cs deleted file mode 100644 index 5bf093d0f65..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/Dispatcher_Specs.cs +++ /dev/null @@ -1,127 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System.Collections.Generic; - using System.Threading.Tasks; - using Context; - using Internals; - using Microsoft.Extensions.DependencyInjection; - using Microsoft.Extensions.DependencyInjection.Extensions; - using Microsoft.Extensions.Logging; - using NUnit.Framework; - using Serialization; - using TestFramework; - using Testing; - using Transports; - - - [TestFixture] - public class Dispatching_a_string : - AsyncTestFixture - { - [Test] - public async Task Should_be_handled_by_the_consumer() - { - var services = new ServiceCollection(); - - services.AddSingleton(BusTestFixture.LoggerFactory); - services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); - - services.AddMassTransit(x => - { - x.AddConsumer(); - x.AddConsumer(); - - x.UsingGrpc((context, cfg) => - { - cfg.ConfigureEndpoints(context, filter => filter.Include()); - }); - - x.AddConfigureEndpointsCallback((name, cfg) => cfg.UseRawJsonSerializer()); - }); - - await using var provider = services - .BuildServiceProvider(true); - - await provider.GetRequiredService().Start(TestCancellationToken); - try - { - var receiver = provider.GetRequiredService>(); - - (var bytes, Dictionary headers) = Serialize(new SimpleCommand { Value = "Hello" }); - - await receiver.Dispatch(bytes, headers, TestCancellationToken); - - await SimpleEventConsumer.Completed.OrCanceled(TestCancellationToken); - } - finally - { - await provider.GetRequiredService().Stop(TestCancellationToken); - } - - await SimpleEventConsumer.Completed.OrCanceled(TestCancellationToken); - } - - static (byte[], Dictionary) Serialize(T obj) - where T : class - { - var serializer = new NewtonsoftRawJsonMessageSerializer(); - - var sendContext = new MessageSendContext(obj); - - var bytes = serializer.GetMessageBody(sendContext).GetBytes(); - - var headers = new Dictionary - { - { MessageHeaders.ContentType, NewtonsoftRawJsonMessageSerializer.ContentTypeHeaderValue }, - { MessageHeaders.MessageId, sendContext.MessageId } - }; - - headers.Set(sendContext.Headers); - - return (bytes, headers); - } - - public Dispatching_a_string() - : base(new InMemoryTestHarness()) - { - } - - - class SimpleCommandConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return context.Publish(new SimpleEvent { Value = context.Message.Value }); - } - } - - - class SimpleEventConsumer : - IConsumer - { - static readonly TaskCompletionSource> _source = new TaskCompletionSource>(); - - public static Task> Completed => _source.Task; - - public Task Consume(ConsumeContext context) - { - _source.TrySetResult(context); - - return Task.CompletedTask; - } - } - - - class SimpleCommand - { - public string Value { get; set; } - } - - - class SimpleEvent - { - public string Value { get; set; } - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/FaultPoly_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/FaultPoly_Specs.cs deleted file mode 100644 index 3b786c41821..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/FaultPoly_Specs.cs +++ /dev/null @@ -1,69 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System.Threading.Tasks; - using FaultMessages; - using NUnit.Framework; - using TestFramework; - - - [TestFixture] - public class Publishing_a_fault_message : - GrpcTestFixture - { - [Test] - public async Task Should_support_the_base_fault_type() - { - await InputQueueSendEndpoint.Send(new - { - MemberName = "Frank", - Address = "123 American Way" - }); - - await _handled; - } - - Task>> _handled; - - protected override void ConfigureGrpcBus(IGrpcBusFactoryConfigurator configurator) - { - configurator.ReceiveEndpoint(e => - { - _handled = Handled>(e); - }); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - configurator.Handler(async context => throw new IntentionalTestException()); - } - } - - - namespace FaultMessages - { - public interface MemberUpdateCommand - { - string MemberName { get; } - } - - - public interface UpdateMemberAddress : - MemberUpdateCommand - { - string Address { get; } - } - - - public class MemberUpdateEvent - { - public string MemberName { get; set; } - } - - - public class MemberAddressUpdated : - MemberUpdateEvent - { - public string Address { get; set; } - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/GrpcClientTestFixture.cs b/tests/MassTransit.GrpcTransport.Tests/GrpcClientTestFixture.cs deleted file mode 100644 index aede00dd41b..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/GrpcClientTestFixture.cs +++ /dev/null @@ -1,91 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using Testing; - - - public class GrpcClientTestFixture : - GrpcTestFixture - { - public GrpcClientTestFixture() - : base(new GrpcTestHarness(new Uri("http://127.0.0.1:29796"))) - { - GrpcClientTestHarness = new GrpcTestHarness(new Uri("http://127.0.0.1:29797"), "client-queue"); - GrpcClientTestHarness.OnConfigureGrpcBus += ConfigureGrpcClientBus; - GrpcClientTestHarness.OnConfigureGrpcHost += ConfigureGrpcClientHost; - GrpcClientTestHarness.OnConfigureGrpcReceiveEndpoint += ConfigureGrpcClientReceiveEndpoint; - } - - protected GrpcTestHarness GrpcClientTestHarness { get; } - - protected string ClientInputQueueName => GrpcClientTestHarness.InputQueueName; - - protected Uri ClientBaseAddress => GrpcClientTestHarness.BaseAddress; - - protected ISendEndpoint ClientInputQueueSendEndpoint => GrpcClientTestHarness.InputQueueSendEndpoint; - - protected ISendEndpoint ClientBusSendEndpoint => GrpcClientTestHarness.BusSendEndpoint; - - protected Uri ClientBusAddress => GrpcClientTestHarness.BusAddress; - - protected Uri ClientInputQueueAddress => GrpcClientTestHarness.InputQueueAddress; - - protected IBus ClientBus => GrpcClientTestHarness.Bus; - protected IBusControl ClientBusControl => GrpcClientTestHarness.BusControl; - - [OneTimeSetUp] - public async Task SetupGrpcClientTestFixture() - { - LoggerFactory.Current = FixtureContext; - - GrpcClientTestHarness.TestTimeout = TestTimeout; - GrpcClientTestHarness.TestInactivityTimeout = TestInactivityTimeout; - - await GrpcClientTestHarness.Start().ConfigureAwait(false); - } - - [OneTimeTearDown] - public async Task TearDownGrpcClientTestFixture() - { - LoggerFactory.Current = FixtureContext; - - await GrpcClientTestHarness.Stop().ConfigureAwait(false); - - GrpcClientTestHarness.Dispose(); - } - - protected override IRequestClient CreateRequestClient() - where TRequest : class - { - return GrpcClientTestHarness.CreateRequestClient(InputQueueAddress); - } - - protected override IRequestClient CreateRequestClient(Uri destinationAddress) - where TRequest : class - { - return GrpcClientTestHarness.CreateRequestClient(destinationAddress); - } - - protected override Task> ConnectRequestClient() - where TRequest : class - { - return GrpcClientTestHarness.ConnectRequestClient(); - } - - protected virtual void ConfigureGrpcClientHost(IGrpcHostConfigurator configurator) - { - configurator.AddServer(BaseAddress); - } - - protected virtual void ConfigureGrpcClientBus(IGrpcBusFactoryConfigurator configurator) - { - ConfigureBusDiagnostics(configurator); - } - - protected virtual void ConfigureGrpcClientReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/GrpcTestFixture.cs b/tests/MassTransit.GrpcTransport.Tests/GrpcTestFixture.cs deleted file mode 100644 index a44a89a02f5..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/GrpcTestFixture.cs +++ /dev/null @@ -1,112 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using NUnit.Framework.Internal; - using TestFramework; - using Testing; - - - public class GrpcTestFixture : - BusTestFixture - { - protected TestExecutionContext FixtureContext; - - public GrpcTestFixture() - : this(new GrpcTestHarness()) - { - } - - public GrpcTestFixture(GrpcTestHarness harness) - : base(harness) - { - GrpcTestHarness = harness; - GrpcTestHarness.OnConfigureGrpcBus += ConfigureGrpcBus; - GrpcTestHarness.OnConfigureGrpcReceiveEndpoint += ConfigureGrpcReceiveEndpoint; - } - - protected GrpcTestHarness GrpcTestHarness { get; } - - protected string InputQueueName => GrpcTestHarness.InputQueueName; - - protected Uri BaseAddress => GrpcTestHarness.BaseAddress; - - /// - /// The sending endpoint for the InputQueue - /// - protected ISendEndpoint InputQueueSendEndpoint => GrpcTestHarness.InputQueueSendEndpoint; - - /// - /// The sending endpoint for the Bus - /// - protected ISendEndpoint BusSendEndpoint => GrpcTestHarness.BusSendEndpoint; - - protected Uri BusAddress => GrpcTestHarness.BusAddress; - - protected Uri InputQueueAddress => GrpcTestHarness.InputQueueAddress; - - [SetUp] - public Task SetupGrpcTest() - { - return Task.CompletedTask; - } - - [TearDown] - public Task TearDownGrpcTest() - { - return Task.CompletedTask; - } - - protected virtual IRequestClient CreateRequestClient() - where TRequest : class - { - return GrpcTestHarness.CreateRequestClient(); - } - - protected virtual IRequestClient CreateRequestClient(Uri destinationAddress) - where TRequest : class - { - return GrpcTestHarness.CreateRequestClient(destinationAddress); - } - - protected virtual Task> ConnectRequestClient() - where TRequest : class - { - return GrpcTestHarness.ConnectRequestClient(); - } - - [OneTimeSetUp] - public Task SetupGrpcTestFixture() - { - FixtureContext = TestExecutionContext.CurrentContext; - - LoggerFactory.Current = FixtureContext; - - return GrpcTestHarness.Start(); - } - - protected Task GetSendEndpoint(Uri address) - { - return GrpcTestHarness.GetSendEndpoint(address); - } - - [OneTimeTearDown] - public async Task TearDownGrpcTestFixture() - { - LoggerFactory.Current = FixtureContext; - - await GrpcTestHarness.Stop().ConfigureAwait(false); - - GrpcTestHarness.Dispose(); - } - - protected virtual void ConfigureGrpcBus(IGrpcBusFactoryConfigurator configurator) - { - } - - protected virtual void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/GrpcTestFixture_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/GrpcTestFixture_Specs.cs deleted file mode 100644 index d1f4b1cdf50..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/GrpcTestFixture_Specs.cs +++ /dev/null @@ -1,70 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - - - [TestFixture] - public class Sending_a_message_to_the_endpoint : - GrpcTestFixture - { - [Test] - public async Task Should_be_received_by_the_handler() - { - await InputQueueSendEndpoint.Send(new A()); - - await _receivedA; - } - - [Test] - public void Should_start_the_handler_properly() - { - } - - Task> _receivedA; - - - class A - { - } - - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _receivedA = Handler(configurator, async context => Console.WriteLine("Hi")); - } - } - - - [TestFixture] - public class Starting_up_the_client_test_fixture : - GrpcClientTestFixture - { - [Test] - public void Should_be_successful() - { - } - - [Test] - public async Task Should_cross_the_border() - { - await ClientBus.Publish(new A()); - - await _receivedA; - } - - Task> _receivedA; - - - class A - { - } - - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _receivedA = Handler(configurator, async context => Console.WriteLine("Hi")); - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/JobServiceTests/EncodeVideo.cs b/tests/MassTransit.GrpcTransport.Tests/JobServiceTests/EncodeVideo.cs deleted file mode 100644 index 87eae35d785..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/JobServiceTests/EncodeVideo.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests.JobServiceTests -{ - using System; - - - public interface EncodeVideo - { - Guid VideoId { get; } - string Path { get; } - int Duration { get; } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/JobServiceTests/EncodeVideoConsumer.cs b/tests/MassTransit.GrpcTransport.Tests/JobServiceTests/EncodeVideoConsumer.cs deleted file mode 100644 index 6bd399bc4bd..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/JobServiceTests/EncodeVideoConsumer.cs +++ /dev/null @@ -1,25 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests.JobServiceTests -{ - using System; - using System.Threading.Tasks; - using Microsoft.Extensions.Logging; - - - public class EncodeVideoConsumer : - IJobConsumer - { - readonly ILogger _logger; - - public EncodeVideoConsumer(ILogger logger) - { - _logger = logger; - } - - public async Task Run(JobContext context) - { - _logger.LogInformation("Encoding Video: {VideoId} ({Path})", context.Job.VideoId, context.Job.Path); - - await Task.Delay(TimeSpan.FromSeconds(context.Job.Duration)); - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/JobServiceTests/JobService_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/JobServiceTests/JobService_Specs.cs deleted file mode 100644 index 790df099c01..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/JobServiceTests/JobService_Specs.cs +++ /dev/null @@ -1,296 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests.JobServiceTests -{ - using System; - using System.Linq; - using System.Threading.Tasks; - using MassTransit.Contracts.JobService; - using NUnit.Framework; - - - [TestFixture] - public class A_single_job_service_instance : - GrpcClientTestFixture - { - [Test] - [Order(1)] - public async Task Should_get_the_job_accepted() - { - IRequestClient> requestClient = ClientBus.CreateRequestClient>(); - - Response response = - await requestClient.GetResponse(new - { - JobId = _jobId, - Job = new - { - VideoId = _jobId, - Path = "C:\\Downloads\\RickRoll.mp4", - Duration = 1 - } - }); - - Assert.That(response.Message.JobId, Is.EqualTo(_jobId)); - - // just to capture all the test output in a single window - ConsumeContext completed = await _completed; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_completed_event() - { - ConsumeContext completed = await _completed; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_completed_generic_event() - { - ConsumeContext> completed = await _completedT; - } - - [Test] - [Order(3)] - public async Task Should_have_published_the_job_started_event() - { - ConsumeContext started = await _started; - } - - [Test] - [Order(2)] - public async Task Should_have_published_the_job_submitted_event() - { - ConsumeContext submitted = await _submitted; - } - - public A_single_job_service_instance() - { - TestTimeout = TimeSpan.FromSeconds(5); - _jobServiceOptions = new JobServiceOptions(); - } - - readonly Guid _jobId = NewId.NextGuid(); - readonly JobServiceOptions _jobServiceOptions; - - Task> _completed; - Task> _submitted; - Task> _started; - Task>> _completedT; - - protected override void ConfigureGrpcClientReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _submitted = Handled(configurator, context => context.Message.JobId == _jobId); - _started = Handled(configurator, context => context.Message.JobId == _jobId); - _completed = Handled(configurator, context => context.Message.JobId == _jobId); - _completedT = Handled>(configurator, context => context.Message.JobId == _jobId); - } - - protected override void ConfigureGrpcClientBus(IGrpcBusFactoryConfigurator configurator) - { - base.ConfigureGrpcClientBus(configurator); - - configurator.UseDelayedMessageScheduler(); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobService(_jobServiceOptions); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.Consumer(() => new EncodeVideoConsumer(LoggerFactory.CreateLogger("EncodeVideo")), x => - { - x.Options>(x => x.SetConcurrentJobLimit(2)); - }); - }); - }); - } - - protected override void ConfigureGrpcBus(IGrpcBusFactoryConfigurator configurator) - { - configurator.UseDelayedMessageScheduler(); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(_jobServiceOptions); - }); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - } - } - - - [TestFixture] - public class A_single_job_service_instance_running_multiple_jobs : - GrpcClientTestFixture - { - [Test] - [Order(1)] - public async Task Should_get_the_job_accepted() - { - IRequestClient> requestClient = ClientBus.CreateRequestClient>(); - - for (var i = 0; i < Count; i++) - { - Response response = - await requestClient.GetResponse(new - { - JobId = _jobIds[i], - Job = new - { - VideoId = _jobIds[i], - Path = "C:\\Downloads\\RickRoll.mp4", - Duration = 1 - } - }); - } - - ConsumeContext[] completed = await Task.WhenAll(_completed.Select(x => x.Task)); - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_completed_event() - { - ConsumeContext[] completed = await Task.WhenAll(_completed.Select(x => x.Task)); - } - - [Test] - [Order(3)] - public async Task Should_have_published_the_job_started_event() - { - ConsumeContext[] started = await Task.WhenAll(_started.Select(x => x.Task)); - } - - [Test] - [Order(2)] - public async Task Should_have_published_the_job_submitted_event() - { - ConsumeContext[] submitted = await Task.WhenAll(_submitted.Select(x => x.Task)); - } - - public A_single_job_service_instance_running_multiple_jobs() - { - TestTimeout = TimeSpan.FromSeconds(10); - _jobServiceOptions = new JobServiceOptions(); - } - - Guid[] _jobIds; - TaskCompletionSource>[] _completed; - TaskCompletionSource>[] _submitted; - TaskCompletionSource>[] _started; - - readonly JobServiceOptions _jobServiceOptions; - const int Count = 10; - - [OneTimeSetUp] - public async Task Arrange() - { - _jobIds = new Guid[Count]; - for (var i = 0; i < _jobIds.Length; i++) - _jobIds[i] = NewId.NextGuid(); - } - - protected override void ConfigureGrpcClientReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _submitted = new TaskCompletionSource>[Count]; - _started = new TaskCompletionSource>[Count]; - _completed = new TaskCompletionSource>[Count]; - - for (var i = 0; i < Count; i++) - { - _submitted[i] = GetTask>(); - _started[i] = GetTask>(); - _completed[i] = GetTask>(); - } - - configurator.Handler(context => - { - for (var i = 0; i < Count; i++) - { - if (_jobIds[i] == context.Message.JobId) - _submitted[i].TrySetResult(context); - } - - return Task.CompletedTask; - }); - - configurator.Handler(context => - { - for (var i = 0; i < Count; i++) - { - if (_jobIds[i] == context.Message.JobId) - _started[i].TrySetResult(context); - } - - return Task.CompletedTask; - }); - - configurator.Handler(context => - { - for (var i = 0; i < Count; i++) - { - if (_jobIds[i] == context.Message.JobId) - _completed[i].TrySetResult(context); - } - - return Task.CompletedTask; - }); - } - - protected override void ConfigureGrpcClientBus(IGrpcBusFactoryConfigurator configurator) - { - base.ConfigureGrpcClientBus(configurator); - - configurator.UseDelayedMessageScheduler(); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobService(_jobServiceOptions); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.Consumer(() => new EncodeVideoConsumer(LoggerFactory.CreateLogger("EncodeVideo")), x => - { - x.Options>(options => options.SetConcurrentJobLimit(5)); - }); - }); - }); - } - - protected override void ConfigureGrpcBus(IGrpcBusFactoryConfigurator configurator) - { - configurator.UseDelayedMessageScheduler(); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(_jobServiceOptions); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.Consumer(() => new EncodeVideoConsumer(LoggerFactory.CreateLogger("EncodeVideo")), x => - { - x.Options>(o => o.SetConcurrentJobLimit(5)); - }); - }); - }); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/MassTransit.GrpcTransport.Tests.csproj b/tests/MassTransit.GrpcTransport.Tests/MassTransit.GrpcTransport.Tests.csproj deleted file mode 100644 index 9375a3e12ef..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/MassTransit.GrpcTransport.Tests.csproj +++ /dev/null @@ -1,28 +0,0 @@ - - - - net6.0 - - - - $(TargetFrameworks);net462 - - - - 9 - false - - - - - - - - - - - - - - - diff --git a/tests/MassTransit.GrpcTransport.Tests/MessageTopology_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/MessageTopology_Specs.cs deleted file mode 100644 index f4cd8eeeb5b..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/MessageTopology_Specs.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System.Threading.Tasks; - using NUnit.Framework; - - - [TestFixture] - public class Disabling_consume_topology_for_one_message : - GrpcTestFixture - { - [Test] - public async Task Should_only_get_the_consumed_message() - { - await Bus.Publish(new MessageOne {Value = "Invalid"}); - await InputQueueSendEndpoint.Send(new MessageOne {Value = "Valid"}); - - ConsumeContext handled = await _handled; - - Assert.That(handled.Message.Value, Is.EqualTo("Valid")); - } - - Task> _handled; - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - configurator.ConfigureMessageTopology(false); - - _handled = Handled(configurator); - } - - - class MessageOne - { - public string Value { get; set; } - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/Request_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/Request_Specs.cs deleted file mode 100644 index 69cec6b511f..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/Request_Specs.cs +++ /dev/null @@ -1,254 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using TestFramework.Messages; - - - [TestFixture] - public class Sending_a_request_using_the_new_request_client : - GrpcTestFixture - { - [Test] - public async Task Should_receive_the_response() - { - Response message = await _response; - } - - Task> _ping; - Task> _response; - IRequestClient _requestClient; - - [OneTimeSetUp] - public void Setup() - { - _requestClient = Bus.CreateRequestClient(InputQueueAddress, TestTimeout); - - _response = _requestClient.GetResponse(new PingMessage()); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _ping = Handler(configurator, async x => await x.RespondAsync(new PongMessage(x.Message.CorrelationId))); - } - } - - - [TestFixture] - public class Sending_a_request_using_the_new_request_client_via_new_endpoint_name : - GrpcTestFixture - { - [Test] - public async Task Should_receive_the_response() - { - Response message = await _response; - } - - Task> _ping; - Task> _response; - IRequestClient _requestClient; - - [OneTimeSetUp] - public void Setup() - { - _requestClient = Bus.CreateRequestClient(new Uri("exchange:input-queue"), TestTimeout); - - _response = _requestClient.GetResponse(new PingMessage()); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _ping = Handler(configurator, async x => await x.RespondAsync(new PongMessage(x.Message.CorrelationId))); - } - } - - - [TestFixture] - public class Sending_a_request_using_the_new_request_client_in_a_consumer : - GrpcTestFixture - { - [Test] - [Order(0)] - public void Get_response() - { - _response = _requestClient.GetResponse(new PingMessage()); - } - - [Test] - [Order(2)] - public async Task Should_have_the_conversation_id() - { - ConsumeContext ping = await _ping; - ConsumeContext a = await _a; - } - - [Test] - [Order(1)] - public async Task Should_receive_the_response() - { - Response message = await _response; - - Assert.That(message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); - } - - Task> _ping; - Task> _response; - IClientFactory _clientFactory; - IRequestClient _requestClient; - Task> _a; - - [OneTimeSetUp] - public async Task Setup() - { - _clientFactory = Bus.CreateClientFactory(); - - _requestClient = Bus.CreateRequestClient(InputQueueAddress, TestTimeout); - } - - [OneTimeTearDown] - public async Task Teardown() - { - if (_clientFactory is IAsyncDisposable asyncDisposable) - await asyncDisposable.DisposeAsync(); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _ping = Handler(configurator, async x => - { - IRequestClient client = _clientFactory.CreateRequestClient(x, InputQueueAddress); - - await client.GetResponse(new A(), context => context.TimeToLive = TimeSpan.FromSeconds(30), x.CancellationToken); - - await x.RespondAsync(new PongMessage(x.Message.CorrelationId)); - }); - - _a = Handler(configurator, x => x.RespondAsync(new B())); - } - - - class A - { - } - - - class B - { - } - } - - - [TestFixture] - public class Sending_a_request_using_the_new_request_client_in_a_consumer_also : - GrpcTestFixture - { - [Test] - public async Task Should_have_the_conversation_id() - { - ConsumeContext ping = await _ping; - ConsumeContext a = await _a; - - Assert.That(ping.ConversationId, Is.EqualTo(a.ConversationId)); - } - - [Test] - public async Task Should_receive_the_response() - { - Response message = await _response; - } - - Task> _ping; - Task> _response; - IClientFactory _clientFactory; - IRequestClient _requestClient; - Task> _a; - - [OneTimeSetUp] - public async Task Setup() - { - _clientFactory = await Bus.ConnectClientFactory(TestTimeout); - - _requestClient = _clientFactory.CreateRequestClient(InputQueueAddress, TestTimeout); - - _response = _requestClient.GetResponse(new PingMessage()); - } - - [OneTimeTearDown] - public async Task Teardown() - { - if (_clientFactory is IAsyncDisposable asyncDisposable) - await asyncDisposable.DisposeAsync(); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _ping = Handler(configurator, async x => - { - RequestHandle request = _clientFactory.CreateRequest(x, InputQueueAddress, new A(), x.CancellationToken); - - await request.GetResponse(); - - await x.RespondAsync(new PongMessage(x.Message.CorrelationId)); - }); - - _a = Handler(configurator, x => x.RespondAsync(new B())); - } - - - class A - { - } - - - class B - { - } - } - - - [TestFixture] - public class Sending_a_request_to_a_missing_service : - GrpcTestFixture - { - [Test] - public void Should_timeout() - { - Assert.That(async () => await _requestClient.GetResponse(new PingMessage()), Throws.TypeOf()); - } - - IRequestClient _requestClient; - - [OneTimeSetUp] - public void Setup() - { - _requestClient = Bus.CreateRequestClient(InputQueueAddress, TimeSpan.FromSeconds(4)); - } - } - - - [TestFixture] - public class Sending_a_request_to_a_faulty_service : - GrpcTestFixture - { - [Test] - public void Should_receive_the_exception() - { - Assert.That(async () => await _requestClient.GetResponse(new PingMessage()), Throws.TypeOf()); - } - - Task> _ping; - IRequestClient _requestClient; - - [OneTimeSetUp] - public void Setup() - { - _requestClient = Bus.CreateRequestClient(InputQueueAddress, TestTimeout); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _ping = Handler(configurator, async x => throw new InvalidOperationException("This is an expected test failure")); - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/RoutingKeyDirect_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/RoutingKeyDirect_Specs.cs deleted file mode 100644 index fd9bdb8aed2..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/RoutingKeyDirect_Specs.cs +++ /dev/null @@ -1,113 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System; - using System.Threading.Tasks; - using Internals; - using NUnit.Framework; - using RoutingKeyDirect; - using Transports.Fabric; - using Util; - - - namespace RoutingKeyDirect - { - public class Message - { - public Message(string content, string routingKey) - { - Content = content; - RoutingKey = routingKey; - } - - public string RoutingKey { get; set; } - - public string Content { get; set; } - } - } - - - [TestFixture] - public class Using_a_routing_key_and_direct_exchange : - GrpcTestFixture - { - [Test] - public async Task Should_support_routing_by_key_and_exchange_name() - { - var fooHandle = await Subscribe("foo"); - try - { - var barHandle = await Subscribe("bar"); - try - { - await Bus.Publish(new Message("Hello", "foo")); - await Bus.Publish(new Message("World", "bar")); - - await Consumer.Foo.OrTimeout(TimeSpan.FromSeconds(5)); - await Consumer.Bar.OrTimeout(TimeSpan.FromSeconds(5)); - } - finally - { - await barHandle.StopAsync(TestCancellationToken); - } - } - finally - { - await fooHandle.StopAsync(TestCancellationToken); - } - } - - async Task Subscribe(string key) - { - var queueName = $"TestCase-R-{key}"; - var handle = Bus.ConnectReceiveEndpoint(queueName, x => - { - x.ConfigureConsumeTopology = false; - x.Consumer(); - - x.Bind(ExchangeType.Direct, GetRoutingKey(key)); - }); - - await handle.Ready; - - return handle; - } - - protected override void ConfigureGrpcBus(IGrpcBusFactoryConfigurator configurator) - { - configurator.Message(x => x.SetEntityName(ExchangeName)); - configurator.Publish(x => x.ExchangeType = ExchangeType.Direct); - - configurator.Send(x => x.UseRoutingKeyFormatter(context => GetRoutingKey(context.Message.RoutingKey))); - } - - string ExchangeName = "TestCase-Buffer"; - - string GetRoutingKey(string routingKey) - { - return $"prefix-{routingKey}"; - } - - - class Consumer : - IConsumer - { - static readonly TaskCompletionSource _foo = TaskUtil.GetTask(); - static readonly TaskCompletionSource _bar = TaskUtil.GetTask(); - public static Task Foo => _foo.Task; - public static Task Bar => _bar.Task; - - public Task Consume(ConsumeContext context) - { - Console.WriteLine($"Received {context.Message.Content} for {context.RoutingKey()}"); - - if (context.Message.RoutingKey == "foo") - _foo.TrySetResult(context.Message); - - if (context.Message.RoutingKey == "bar") - _bar.TrySetResult(context.Message); - - return Task.CompletedTask; - } - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/RoutingSlipDictionary_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/RoutingSlipDictionary_Specs.cs deleted file mode 100644 index cfaa530d594..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/RoutingSlipDictionary_Specs.cs +++ /dev/null @@ -1,76 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System; - using System.Threading.Tasks; - using Courier.Contracts; - using NUnit.Framework; - using TestFramework; - using Testing; - - - public class DemoArguments - { - public DemoArguments(Guid id) - { - Id = id; - } - - public Guid Id { get; set; } - } - - - public class DemoEvent - { - public DemoEvent(Guid id) - { - Id = id; - } - - public Guid Id { get; set; } - } - - - public class DemoActivityTests : - InMemoryActivityTestFixture - { - protected override void SetupActivities(BusTestHarness testHarness) - { - AddActivityContext(() => new DemoActivity()); - } - - [Test] - public async Task Demo_should_not_fail_to_serialize() - { - var activity = GetActivityContext(); - - Task> completed = InMemoryTestHarness.SubscribeHandler(); - Task> activityCompleted = InMemoryTestHarness.SubscribeHandler(); - - var trackingNumber = NewId.NextGuid(); - var builder = new RoutingSlipBuilder(trackingNumber); - builder.AddSubscription(InMemoryTestHarness.BusAddress, RoutingSlipEvents.All); - builder.AddActivity(activity.Name, activity.ExecuteUri, new DemoArguments(Guid.NewGuid())); - - await InMemoryTestHarness.Bus.Execute(builder.Build()); - - await completed; - - ConsumeContext context = await activityCompleted!; - - Assert.True(await InMemoryTestHarness.Published.Any()); - Assert.AreEqual(trackingNumber, context.Message.TrackingNumber); - } - } - - - public class DemoActivity : IExecuteActivity - { - public async Task Execute(ExecuteContext context) - { - await Task.Delay(500); - - await context.Publish(new DemoEvent(context.Arguments.Id)).ConfigureAwait(false); - return context.Completed(); - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/ScheduleMessage_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/ScheduleMessage_Specs.cs deleted file mode 100644 index a520a83c2ed..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/ScheduleMessage_Specs.cs +++ /dev/null @@ -1,145 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System; - using System.Diagnostics; - using System.Threading.Tasks; - using NUnit.Framework; - - - public class ScheduleMessage_Specs : - GrpcTestFixture - { - Task> _first; - - Task> _second; - - [Test] - public async Task Should_get_both_messages() - { - await InputQueueSendEndpoint.Send(new FirstMessage()); - - await _first; - - await _second; - } - - protected override void ConfigureGrpcBus(IGrpcBusFactoryConfigurator configurator) - { - configurator.UseDelayedMessageScheduler(); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _first = Handler(configurator, async context => - { - await context.ScheduleSend(DateTime.Now, new SecondMessage()); - }); - - _second = Handled(configurator); - } - - - public class FirstMessage - { - } - - - public class SecondMessage - { - } - } - - - public class Should_schedule_in_the_future : - GrpcTestFixture - { - Task> _first; - - Task> _second; - - [Test] - public async Task Should_get_both_messages() - { - await InputQueueSendEndpoint.Send(new FirstMessage()); - - await _first; - - var timer = Stopwatch.StartNew(); - - await _second; - - timer.Stop(); - - Assert.That(timer.Elapsed, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2))); - } - - protected override void ConfigureGrpcBus(IGrpcBusFactoryConfigurator configurator) - { - configurator.UseDelayedMessageScheduler(); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _first = Handler(configurator, async context => - { - await context.ScheduleSend(TimeSpan.FromSeconds(3), new SecondMessage()); - }); - - _second = Handled(configurator); - } - - - public class FirstMessage - { - } - - - public class SecondMessage - { - } - } - - - public class Scheduling_a_published_message : - GrpcTestFixture - { - Task> _first; - - Task> _second; - - [Test] - public async Task Should_get_both_messages() - { - await InputQueueSendEndpoint.Send(new FirstMessage()); - - await _first; - - await _second; - } - - protected override void ConfigureGrpcBus(IGrpcBusFactoryConfigurator configurator) - { - configurator.UseDelayedMessageScheduler(); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - _first = Handler(configurator, async context => - { - await context.SchedulePublish(TimeSpan.FromSeconds(1), new SecondMessage()); - }); - - _second = Handled(configurator); - } - - - public class FirstMessage - { - } - - - public class SecondMessage - { - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/StartStop_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/StartStop_Specs.cs deleted file mode 100644 index 688c6841bf3..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/StartStop_Specs.cs +++ /dev/null @@ -1,105 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System.Threading.Tasks; - using NUnit.Framework; - using TestFramework; - using TestFramework.Messages; - using Testing; - - - [TestFixture] - [Category("Flaky")] - public class StartStop_Specs : - BusTestFixture - { - [Test] - public async Task Should_start_stop_and_start() - { - var bus = MassTransit.Bus.Factory.CreateUsingGrpc(x => - { - ConfigureBusDiagnostics(x); - - x.ReceiveEndpoint("input_queue", e => - { - e.Handler(async context => - { - await context.RespondAsync(new PongMessage()); - }); - }); - }); - - await bus.StartAsync(TestCancellationToken); - try - { - await bus.CreateRequestClient().GetResponse(new PingMessage()); - } - finally - { - await bus.StopAsync(TestCancellationToken); - } - - await bus.StartAsync(TestCancellationToken); - try - { - await bus.CreateRequestClient().GetResponse(new PingMessage()); - } - finally - { - await bus.StopAsync(TestCancellationToken); - } - } - - [Test] - public async Task Should_start_stop_and_start_with_publish_only() - { - var bus = MassTransit.Bus.Factory.CreateUsingGrpc(x => - { - ConfigureBusDiagnostics(x); - - x.ReceiveEndpoint("input_queue", e => - { - e.Handler(async context => - { - if (context.ResponseAddress != null) - await context.RespondAsync(new PongMessage()); - }); - }); - }); - - await bus.StartAsync(TestCancellationToken); - try - { - await bus.Publish(new PingMessage()); - } - finally - { - await bus.StopAsync(TestCancellationToken); - } - - await bus.StartAsync(TestCancellationToken); - try - { - await bus.Publish(new PingMessage()); - } - finally - { - await bus.StopAsync(TestCancellationToken); - } - - await bus.StartAsync(TestCancellationToken); - try - { - await bus.Publish(new PingMessage()); - } - finally - { - await bus.StopAsync(TestCancellationToken); - } - } - - public StartStop_Specs() - : base(new InMemoryTestHarness()) - { - } - } -} diff --git a/tests/MassTransit.GrpcTransport.Tests/TopicExchange_Specs.cs b/tests/MassTransit.GrpcTransport.Tests/TopicExchange_Specs.cs deleted file mode 100644 index 20982435475..00000000000 --- a/tests/MassTransit.GrpcTransport.Tests/TopicExchange_Specs.cs +++ /dev/null @@ -1,179 +0,0 @@ -namespace MassTransit.GrpcTransport.Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using TestFramework; - using Transports.Fabric; - - - public class Using_a_hash_topic_pattern : - GrpcTestFixture - { - Task> _handled; - - public Using_a_hash_topic_pattern() - { - TestTimeout = TimeSpan.FromSeconds(5); - } - - [Test] - [Explicit] - public void Should_wonderful_display() - { - var result = Bus.GetProbeResult(); - - Console.WriteLine(result.ToJsonString()); - } - - [Test] - public async Task Should_match_the_endpoint_binding() - { - var endpoint = await Bus.GetSendEndpoint(new Uri("exchange:test-exchange?type=topic")); - - await endpoint.Send(new A(), x => x.SetRoutingKey("alpha")); - - await _handled; - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _handled = Handled(configurator); - - configurator.Bind("test-exchange", ExchangeType.Topic, "#"); - } - - - public class A - { - public string Value { get; set; } - } - } - - - public class Using_a_wildcard_topic_pattern : - GrpcTestFixture - { - Task> _handled; - - public Using_a_wildcard_topic_pattern() - { - TestTimeout = TimeSpan.FromSeconds(5); - } - - [Test] - public async Task Should_match_the_endpoint_binding() - { - var endpoint = await Bus.GetSendEndpoint(new Uri("exchange:test-exchange?type=topic")); - - await endpoint.Send(new A { Value = "Bad" }, x => x.SetRoutingKey("bus.red")); - await endpoint.Send(new A { Value = "Bad" }, x => x.SetRoutingKey("bus.green")); - await endpoint.Send(new A { Value = "Good" }, x => x.SetRoutingKey("car.blue")); - - ConsumeContext handled = await _handled; - - Assert.That(handled.Message.Value, Is.EqualTo("Good")); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _handled = Handled(configurator); - - configurator.Bind("test-exchange", ExchangeType.Topic, "car.*"); - } - - - public class A - { - public string Value { get; set; } - } - } - - - public class Using_a_wildcard_topic_pattern_too : - GrpcTestFixture - { - Task> _handled; - - public Using_a_wildcard_topic_pattern_too() - { - TestTimeout = TimeSpan.FromSeconds(5); - } - - [Test] - public async Task Should_match_the_endpoint_binding() - { - var endpoint = await Bus.GetSendEndpoint(new Uri("exchange:test-exchange?type=topic")); - - await endpoint.Send(new A { Value = "Bad" }, x => x.SetRoutingKey("bus.red.large")); - await endpoint.Send(new A { Value = "Bad" }, x => x.SetRoutingKey("car.green.small")); - await endpoint.Send(new A { Value = "Bad" }, x => x.SetRoutingKey("bus.green.small")); - await endpoint.Send(new A { Value = "Good" }, x => x.SetRoutingKey("car.blue.large")); - - ConsumeContext handled = await _handled; - - Assert.That(handled.Message.Value, Is.EqualTo("Good")); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _handled = Handled(configurator); - - configurator.Bind("test-exchange", ExchangeType.Topic, "car.*.large"); - } - - - public class A - { - public string Value { get; set; } - } - } - - - public class Using_a_wildcard_topic_pattern_too_via_client : - GrpcClientTestFixture - { - Task> _handled; - - public Using_a_wildcard_topic_pattern_too_via_client() - { - TestTimeout = TimeSpan.FromSeconds(5); - } - - [Test] - public async Task Should_match_the_endpoint_binding() - { - var endpoint = await ClientBus.GetSendEndpoint(new Uri("exchange:test-exchange?type=topic")); - - await endpoint.Send(new A { Value = "Bad" }, x => x.SetRoutingKey("bus.red.large")); - await endpoint.Send(new A { Value = "Bad" }, x => x.SetRoutingKey("car.green.small")); - await endpoint.Send(new A { Value = "Bad" }, x => x.SetRoutingKey("bus.green.small")); - await endpoint.Send(new A { Value = "Good" }, x => x.SetRoutingKey("car.blue.large")); - - ConsumeContext handled = await _handled; - - Assert.That(handled.Message.Value, Is.EqualTo("Good")); - } - - protected override void ConfigureGrpcReceiveEndpoint(IGrpcReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _handled = Handled(configurator); - - configurator.Bind("test-exchange", ExchangeType.Topic, "car.*.large"); - } - - - public class A - { - public string Value { get; set; } - } - } -} diff --git a/tests/MassTransit.HangfireIntegration.Tests/Cleanup_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/Cleanup_Specs.cs index 7f1f4c621e3..56516e048d9 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/Cleanup_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/Cleanup_Specs.cs @@ -33,7 +33,7 @@ public async Task Should_remove_hash_when_job_cancelled() Dictionary items = connection.GetAllEntriesFromHash(hashId); - Assert.Null(items); + Assert.That(items, Is.Null); } [Test] @@ -52,7 +52,7 @@ public async Task Should_remove_hash_when_job_executed() Dictionary items = connection.GetAllEntriesFromHash(hashId); - Assert.Null(items); + Assert.That(items, Is.Null); } Task> _first; diff --git a/tests/MassTransit.HangfireIntegration.Tests/Container_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/Container_Specs.cs index 4418f291cec..0e10f22f3e2 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/Container_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/Container_Specs.cs @@ -4,17 +4,23 @@ namespace MassTransit.HangfireIntegration.Tests using System.Threading.Tasks; using Hangfire; using Hangfire.MemoryStorage; + using MassTransit.Tests; + using MassTransit.Tests.Scenario; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Scheduling; using Testing; - [TestFixture] - public class Using_the_container_setup_for_hangfire + [TestFixture(typeof(Json))] + [TestFixture(typeof(RawJson))] + [TestFixture(typeof(NewtonsoftJson))] + [TestFixture(typeof(NewtonsoftRawJson))] + public class Using_hangfire_with_serializer + where T : new() { [Test] - public async Task Should_have_an_even_cleaner_experience_without_owning_the_container() + public async Task Should_work_properly() { await using var provider = new ServiceCollection() .AddHangfire(h => @@ -24,6 +30,8 @@ public async Task Should_have_an_even_cleaner_experience_without_owning_the_cont }) .AddMassTransitTestHarness(x => { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(30)); + x.AddPublishMessageScheduler(); x.AddHangfireConsumers(); @@ -35,27 +43,91 @@ public async Task Should_have_an_even_cleaner_experience_without_owning_the_cont { cfg.UsePublishMessageScheduler(); + _configuration?.ConfigureBus(context, cfg); + cfg.ConfigureEndpoints(context); }); }) .BuildServiceProvider(true); - var harness = provider.GetTestHarness(); - harness.TestInactivityTimeout = TimeSpan.FromSeconds(30); - - await harness.Start(); + var harness = await provider.StartTestHarness(); await harness.Bus.Publish(new { }); - Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + + Assert.That(await harness.Consumed.Any(), Is.True); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + }); + } + + [Test] + public async Task Should_work_properly_with_message_headers() + { + await using var provider = new ServiceCollection() + .AddHangfire(h => + { + h.UseRecommendedSerializerSettings(); + h.UseMemoryStorage(); + }) + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(30)); + + x.AddPublishMessageScheduler(); + + x.AddHangfireConsumers(); - Assert.That(await harness.Consumed.Any(), Is.True); + x.AddConsumer(); + x.AddConsumer(); - Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + x.UsingInMemory((context, cfg) => + { + cfg.UsePublishMessageScheduler(); + + _configuration?.ConfigureBus(context, cfg); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new { }, x => x.Headers.Set("SimpleHeader", "SimpleValue")); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + + Assert.That(await harness.Consumed.Any(), Is.True); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + }); + + ConsumeContext context = + (await harness.GetConsumerHarness().Consumed.SelectAsync().First()).Context; + + Assert.Multiple(() => + { + Assert.That(context.Headers.TryGetHeader("SimpleHeader", out var header), Is.True); + + Assert.That(header, Is.EqualTo("SimpleValue")); + }); + } + + readonly ITestBusConfiguration _configuration; + + public Using_hangfire_with_serializer() + { + _configuration = new T() as ITestBusConfiguration; } - public class FirstMessageConsumer : + class FirstMessageConsumer : IConsumer { public async Task Consume(ConsumeContext context) @@ -65,7 +137,7 @@ public async Task Consume(ConsumeContext context) } - public class SecondMessageConsumer : + class SecondMessageConsumer : IConsumer { public Task Consume(ConsumeContext context) diff --git a/tests/MassTransit.HangfireIntegration.Tests/DelayRetry_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/DelayRetry_Specs.cs index 3b2bfc8d906..7c085533610 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/DelayRetry_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/DelayRetry_Specs.cs @@ -19,7 +19,7 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _received.Task; - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); } TaskCompletionSource> _received; @@ -74,7 +74,7 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _consumer.Received; - Assert.GreaterOrEqual(_consumer.ReceivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_consumer.ReceivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); } MyConsumer _consumer; @@ -102,7 +102,6 @@ class MyConsumer : { readonly TaskCompletionSource> _received; int _count; - TimeSpan _receivedTimeSpan; Stopwatch _timer; public MyConsumer(TaskCompletionSource> taskCompletionSource) @@ -112,7 +111,7 @@ public MyConsumer(TaskCompletionSource> taskCompleti public Task> Received => _received.Task; - public IComparable ReceivedTimeSpan => _receivedTimeSpan; + public TimeSpan ReceivedTimeSpan { get; private set; } public Task Consume(ConsumeContext context) { @@ -130,7 +129,7 @@ public Task Consume(ConsumeContext context) Console.WriteLine("{0} okay, now is good (retried {1} times)", DateTime.UtcNow, context.Headers.Get("MT-Redelivery-Count", default(int?))); // okay, ready. - _receivedTimeSpan = _timer.Elapsed; + ReceivedTimeSpan = _timer.Elapsed; _received.TrySetResult(context); return Task.CompletedTask; @@ -150,9 +149,12 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _consumer.Received; - Assert.GreaterOrEqual(_consumer.ReceivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.Multiple(() => + { + Assert.That(_consumer.ReceivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); - Assert.That(_consumer.RedeliveryCount, Is.EqualTo(2)); + Assert.That(_consumer.RedeliveryCount, Is.EqualTo(2)); + }); } MyConsumer _consumer; @@ -178,7 +180,6 @@ class MyConsumer : { readonly TaskCompletionSource> _received; int _count; - TimeSpan _receivedTimeSpan; Stopwatch _timer; public MyConsumer(TaskCompletionSource> taskCompletionSource) @@ -188,7 +189,7 @@ public MyConsumer(TaskCompletionSource> taskCompleti public Task> Received => _received.Task; - public IComparable ReceivedTimeSpan => _receivedTimeSpan; + public TimeSpan ReceivedTimeSpan { get; private set; } public int RedeliveryCount { get; set; } @@ -208,7 +209,7 @@ public Task Consume(ConsumeContext context) Console.WriteLine("{0} okay, now is good (retried {1} times)", DateTime.UtcNow, context.Headers.Get("MT-Redelivery-Count", default(int?))); // okay, ready. - _receivedTimeSpan = _timer.Elapsed; + ReceivedTimeSpan = _timer.Elapsed; RedeliveryCount = context.GetRedeliveryCount(); _received.TrySetResult(context); @@ -229,7 +230,7 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _received.Task; - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); } TaskCompletionSource> _received; @@ -285,9 +286,9 @@ public async Task callback_executed_before_defer_the_message_delivery() ConsumeContext context = await _received.Task; - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); var customHeaderValue = context.Headers.Get(customHeader, default(int?)); - Assert.AreEqual(2, customHeaderValue); + Assert.That(customHeaderValue, Is.EqualTo(2)); } TaskCompletionSource> _received; diff --git a/tests/MassTransit.HangfireIntegration.Tests/HangfirePublish_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/HangfirePublish_Specs.cs index f402e2ae8fd..22c08328a05 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/HangfirePublish_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/HangfirePublish_Specs.cs @@ -1,7 +1,9 @@ namespace MassTransit.HangfireIntegration.Tests { using System; + using System.Diagnostics; using System.Threading.Tasks; + using Logging; using NUnit.Framework; @@ -24,6 +26,37 @@ public async Task Should_receive_the_message() await handled; } + [Test] + public async Task Should_preserve_parent_trace_id() + { + using var source = new ActivitySource(nameof(Should_preserve_parent_trace_id)); + using var listener = new ActivityListener + { + ShouldListenTo = x => x.Name == DiagnosticHeaders.DefaultListenerName || x.Name == nameof(Should_preserve_parent_trace_id), + Sample = (ref ActivityCreationOptions options) => ActivitySamplingResult.AllDataAndRecorded + }; + + ActivitySource.AddActivityListener(listener); + + using var parentActivity = source.StartActivity(); + + Task> handled = SubscribeHandler(); + + Bus.ConnectConsumer(() => new SomeMessageConsumer()); + + await Scheduler.ScheduleSend(Bus.Address, DateTime.Now, new SomeMessage + { + SendDate = DateTime.Now, + Source = "Schedule" + }); + + var context = await handled; + + var parentContext = ActivityContext.Parse(context.GetHeader(DiagnosticHeaders.ActivityId), null); + + Assert.That(parentContext.TraceId, Is.EqualTo(parentActivity.TraceId)); + } + class SomeMessageConsumer : IConsumer diff --git a/tests/MassTransit.HangfireIntegration.Tests/MassTransit.HangfireIntegration.Tests.csproj b/tests/MassTransit.HangfireIntegration.Tests/MassTransit.HangfireIntegration.Tests.csproj index 699bb7db2f4..3a480fbb91e 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/MassTransit.HangfireIntegration.Tests.csproj +++ b/tests/MassTransit.HangfireIntegration.Tests/MassTransit.HangfireIntegration.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 @@ -9,11 +9,16 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/tests/MassTransit.HangfireIntegration.Tests/MissingInstanceRedelivery_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/MissingInstanceRedelivery_Specs.cs index 9b977278ee7..43754b4cbfc 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/MissingInstanceRedelivery_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/MissingInstanceRedelivery_Specs.cs @@ -28,7 +28,7 @@ public async Task Should_schedule_the_message_and_redeliver_to_the_instance() await status; - Assert.AreEqual("A", status.Result.Message.ServiceName); + Assert.That(status.Result.Message.ServiceName, Is.EqualTo("A")); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.HangfireIntegration.Tests/PastEvent_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/PastEvent_Specs.cs index a81591f8784..30e9afe47d0 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/PastEvent_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/PastEvent_Specs.cs @@ -69,19 +69,22 @@ public async Task Should_include_message_headers() ConsumeContext context = await handler; - Assert.AreEqual(Bus.Address, context.FaultAddress); - Assert.AreEqual(InputQueueAddress, context.ResponseAddress); - Assert.IsTrue(context.RequestId.HasValue); - Assert.AreEqual(requestId, context.RequestId.Value); - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.AreEqual(correlationId, context.CorrelationId.Value); - Assert.IsTrue(context.ConversationId.HasValue); - Assert.AreEqual(conversationId, context.ConversationId.Value); - Assert.IsTrue(context.InitiatorId.HasValue); - Assert.AreEqual(initiatorId, context.InitiatorId.Value); - - Assert.IsTrue(context.Headers.TryGetHeader("Hello", out var value)); - Assert.AreEqual("World", value); + Assert.Multiple(() => + { + Assert.That(context.FaultAddress, Is.EqualTo(Bus.Address)); + Assert.That(context.ResponseAddress, Is.EqualTo(InputQueueAddress)); + Assert.That(context.RequestId.HasValue, Is.True); + Assert.That(context.RequestId.Value, Is.EqualTo(requestId)); + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(correlationId)); + Assert.That(context.ConversationId.HasValue, Is.True); + Assert.That(context.ConversationId.Value, Is.EqualTo(conversationId)); + Assert.That(context.InitiatorId.HasValue, Is.True); + Assert.That(context.InitiatorId.Value, Is.EqualTo(initiatorId)); + + Assert.That(context.Headers.TryGetHeader("Hello", out var value), Is.True); + Assert.That(value, Is.EqualTo("World")); + }); } [Test] @@ -127,7 +130,7 @@ public async Task Should_reschedule() }); ConsumeContext result = await handler; - Assert.AreEqual(expected, result.Message.Name); + Assert.That(result.Message.Name, Is.EqualTo(expected)); } public Specifying_an_event_reschedule_if_exists() diff --git a/tests/MassTransit.HangfireIntegration.Tests/Recurring_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/Recurring_Specs.cs index 61bb92d675e..d4ffd8f52d5 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/Recurring_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/Recurring_Specs.cs @@ -24,7 +24,7 @@ public async Task Should_cancel_recurring_schedule() await _done; var countBeforeCancel = _count; - Assert.AreEqual(8, _count, "Expected to see 8 interval messages"); + Assert.That(_count, Is.EqualTo(8), "Expected to see 8 interval messages"); await Bus.CancelScheduledRecurringSend(scheduledRecurringMessage); @@ -32,7 +32,7 @@ public async Task Should_cancel_recurring_schedule() await _doneAgain; - Assert.AreEqual(countBeforeCancel, _count, "Expected to see the count matches."); + Assert.That(_count, Is.EqualTo(countBeforeCancel), "Expected to see the count matches."); } [Test] @@ -45,7 +45,7 @@ public async Task Should_handle_now_properly() await _done; - Assert.AreEqual(8, _count, "Expected to see 8 interval messages"); + Assert.That(_count, Is.EqualTo(8), "Expected to see 8 interval messages"); } [Test] @@ -61,7 +61,7 @@ public async Task Should_pause_recurring_schedule() await _done; var countBeforeCancel = _count; - Assert.AreEqual(8, _count, "Expected to see 8 interval messages"); + Assert.That(_count, Is.EqualTo(8), "Expected to see 8 interval messages"); await Bus.PauseScheduledRecurringSend(scheduledRecurringMessage); @@ -69,7 +69,7 @@ public async Task Should_pause_recurring_schedule() await _doneAgain; - Assert.AreEqual(countBeforeCancel, _count, "Expected to see the count matches."); + Assert.That(_count, Is.EqualTo(countBeforeCancel), "Expected to see the count matches."); } Task> _done; diff --git a/tests/MassTransit.HangfireIntegration.Tests/RequestTimeout_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/RequestTimeout_Specs.cs index f533f734de7..e2e26971aeb 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/RequestTimeout_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/RequestTimeout_Specs.cs @@ -25,7 +25,7 @@ await InputQueueSendEndpoint.Send(new Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, x => x.AddressValidationTimeout, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } InMemorySagaRepository _repository; diff --git a/tests/MassTransit.HangfireIntegration.Tests/Request_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/Request_Specs.cs index f411f9a4a30..396ad07bc2f 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/Request_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/Request_Specs.cs @@ -4,6 +4,7 @@ namespace Request_Specs { using System; using System.Threading.Tasks; + using Contracts; using NUnit.Framework; using Testing; @@ -30,10 +31,10 @@ await InputQueueSendEndpoint.Send(new Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, x => x.Registered, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var sagaInstance = _repository[saga.Value].Instance; - Assert.IsFalse(sagaInstance.ValidateAddressRequestId.HasValue); + Assert.That(sagaInstance.ValidateAddressRequestId.HasValue, Is.False); } static Sending_a_request_from_a_state_machine() @@ -107,7 +108,7 @@ protected virtual void ConfigureServiceQueueEndpoint(IReceiveEndpointConfigurato class RequestSettingsImpl : - RequestSettings + RequestSettings { public RequestSettingsImpl(Uri serviceAddress, TimeSpan timeout) { @@ -117,7 +118,11 @@ public RequestSettingsImpl(Uri serviceAddress, TimeSpan timeout) public Uri ServiceAddress { get; } public TimeSpan Timeout { get; } - public TimeSpan? TimeToLive { get; } + public bool ClearRequestIdOnFaulted => false; + public TimeSpan? TimeToLive => null; + public Action> Completed { get; set; } + public Action>> Faulted { get; set; } + public Action>> TimeoutExpired { get; set; } } @@ -187,7 +192,7 @@ public interface NameValidated : class TestStateMachine : MassTransitStateMachine { - public TestStateMachine(RequestSettings settings) + public TestStateMachine(RequestSettings settings) { Event(() => Register, x => { diff --git a/tests/MassTransit.HangfireIntegration.Tests/Reschedule_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/Reschedule_Specs.cs index 57f785c1b4f..a2ca3edffec 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/Reschedule_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/Reschedule_Specs.cs @@ -25,18 +25,22 @@ public async Task Should_reschedule_the_message_with_a_new_token_id() var sagaInstance = _repository[correlationId].Instance; - Assert.NotNull(rescheduledEvent.Message.NewScheduleTokenId); - Assert.AreEqual(sagaInstance.CorrelationId, rescheduledEvent.Message.CorrelationId); - Assert.AreEqual(sagaInstance.ScheduleId, rescheduledEvent.Message.NewScheduleTokenId); + Assert.Multiple(() => + { + Assert.That(rescheduledEvent.Message.NewScheduleTokenId, Is.Not.Null); + Assert.That(rescheduledEvent.Message.CorrelationId, Is.EqualTo(sagaInstance.CorrelationId)); + Assert.That(rescheduledEvent.Message.NewScheduleTokenId, Is.EqualTo(sagaInstance.ScheduleId)); + }); await InputQueueSendEndpoint.Send(new StopCommand(correlationId)); - Guid? saga = await _repository.ShouldNotContainSaga(correlationId, TestTimeout); + Guid? saga = await LoadSagaRepository.ShouldNotContainSaga(correlationId, TestTimeout); - Assert.IsNull(saga); + Assert.That(saga, Is.Null); } InMemorySagaRepository _repository; + ILoadSagaRepository LoadSagaRepository => _repository; TestStateMachine _machine; Task> _rescheduled; diff --git a/tests/MassTransit.HangfireIntegration.Tests/ScheduleMessage_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/ScheduleMessage_Specs.cs index 08ade9d68ea..c59f88186bc 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/ScheduleMessage_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/ScheduleMessage_Specs.cs @@ -20,7 +20,7 @@ public async Task Should_get_both_messages() await _second; if (_secondActivityId != null && _firstActivityId != null) - Assert.That(_secondActivityId.StartsWith(_firstActivityId), Is.True); + Assert.That(_secondActivityId, Does.StartWith(_firstActivityId)); } Task> _second; @@ -54,6 +54,114 @@ public class SecondMessage } + [TestFixture] + public class ScheduleMessageUsingJson_Specs : + HangfireInMemoryTestFixture + { + [Test] + public async Task Should_get_both_messages() + { + await Scheduler.ScheduleSend(InputQueueAddress, DateTime.Now, new FirstMessage()); + + await _first; + + await _second; + + if (_secondActivityId != null && _firstActivityId != null) + Assert.That(_secondActivityId, Does.StartWith(_firstActivityId)); + } + + Task> _second; + Task> _first; + string _firstActivityId; + string _secondActivityId; + + protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) + { + configurator.UseRawJsonSerializer(); + base.ConfigureInMemoryBus(configurator); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + _first = Handler(configurator, async context => + { + _firstActivityId = Activity.Current?.Id; + await context.ScheduleSend(TimeSpan.FromSeconds(5), new SecondMessage()); + }); + + _second = Handler(configurator, async context => + { + _secondActivityId = Activity.Current?.Id; + }); + } + + + public class FirstMessage + { + } + + + public class SecondMessage + { + } + } + + + [TestFixture] + public class ScheduleMessageUsingNewtonsoftJson_Specs : + HangfireInMemoryTestFixture + { + [Test] + public async Task Should_get_both_messages() + { + await Scheduler.ScheduleSend(InputQueueAddress, DateTime.Now, new FirstMessage()); + + await _first; + + await _second; + + if (_secondActivityId != null && _firstActivityId != null) + Assert.That(_secondActivityId, Does.StartWith(_firstActivityId)); + } + + Task> _second; + Task> _first; + string _firstActivityId; + string _secondActivityId; + + protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) + { + configurator.UseNewtonsoftRawJsonSerializer(); + base.ConfigureInMemoryBus(configurator); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + _first = Handler(configurator, async context => + { + _firstActivityId = Activity.Current?.Id; + await context.ScheduleSend(TimeSpan.FromSeconds(5), new SecondMessage()); + }); + + _second = Handler(configurator, async context => + { + _secondActivityId = Activity.Current?.Id; + }); + } + + + public class FirstMessage + { + } + + + public class SecondMessage + { + } + } + + [TestFixture] public class ScheduleMessageBson_Specs : HangfireInMemoryTestFixture @@ -68,7 +176,7 @@ public async Task Should_get_both_messages() await _second; if (_secondActivityId != null && _firstActivityId != null) - Assert.That(_secondActivityId.StartsWith(_firstActivityId), Is.True); + Assert.That(_secondActivityId, Does.StartWith(_firstActivityId)); } Task> _second; @@ -122,8 +230,11 @@ public async Task Should_include_it_with_the_final_message() ConsumeContext second = await _second; - Assert.That(second.ExpirationTime.HasValue, Is.True); - Assert.That(second.ExpirationTime.Value, Is.GreaterThan(DateTime.UtcNow + TimeSpan.FromSeconds(24))); + Assert.Multiple(() => + { + Assert.That(second.ExpirationTime.HasValue, Is.True); + Assert.That(second.ExpirationTime.Value, Is.GreaterThan(DateTime.UtcNow + TimeSpan.FromSeconds(24))); + }); } Task> _second; diff --git a/tests/MassTransit.HangfireIntegration.Tests/ScheduleTimeout_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/ScheduleTimeout_Specs.cs index 68d0ffd497b..1f77d118e4c 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/ScheduleTimeout_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/ScheduleTimeout_Specs.cs @@ -23,7 +23,7 @@ public async Task Should_cancel_when_the_order_is_submitted() Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, _machine.Active, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await InputQueueSendEndpoint.Send(new { MemberNumber = memberNumber }); @@ -55,7 +55,7 @@ public async Task Should_reschedule_the_timeout_when_items_are_added() Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, _machine.Active, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await InputQueueSendEndpoint.Send(new { MemberNumber = memberNumber }); diff --git a/tests/MassTransit.HangfireIntegration.Tests/ScheduledRedelivery_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/ScheduledRedelivery_Specs.cs index e21d0b137b3..18ed4037f75 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/ScheduledRedelivery_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/ScheduledRedelivery_Specs.cs @@ -19,9 +19,12 @@ public async Task Should_use_the_correct_intervals_for_each_redelivery() await Task.WhenAll(_received.Select(x => x.Task)); - Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(0.8))); - Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.8))); - Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2.8))); + Assert.Multiple(() => + { + Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(0.8))); + Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.8))); + Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2.8))); + }); TestContext.Out.WriteLine("Interval: {0}", _timestamps[1] - _timestamps[0]); TestContext.Out.WriteLine("Interval: {0}", _timestamps[2] - _timestamps[1]); diff --git a/tests/MassTransit.HangfireIntegration.Tests/SchedulerLoadInMemory_Specs.cs b/tests/MassTransit.HangfireIntegration.Tests/SchedulerLoadInMemory_Specs.cs index e5f1ebbb7e1..4c50f669828 100644 --- a/tests/MassTransit.HangfireIntegration.Tests/SchedulerLoadInMemory_Specs.cs +++ b/tests/MassTransit.HangfireIntegration.Tests/SchedulerLoadInMemory_Specs.cs @@ -29,7 +29,7 @@ public async Task Should_remove_the_saga_once_completed() await Task.Delay(1000); - Assert.AreEqual(0, _repository.Count); + Assert.That(_repository.Count, Is.EqualTo(0)); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Interop.NServiceBus.Tests/MassTransit.Interop.NServiceBus.Tests.csproj b/tests/MassTransit.Interop.NServiceBus.Tests/MassTransit.Interop.NServiceBus.Tests.csproj index dd8cc9d6f86..51a685c5a00 100644 --- a/tests/MassTransit.Interop.NServiceBus.Tests/MassTransit.Interop.NServiceBus.Tests.csproj +++ b/tests/MassTransit.Interop.NServiceBus.Tests/MassTransit.Interop.NServiceBus.Tests.csproj @@ -1,7 +1,7 @@ - net6.0 + net8.0 enable enable diff --git a/tests/MassTransit.KafkaIntegration.Tests/AvroSerializationFactory.cs b/tests/MassTransit.KafkaIntegration.Tests/AvroSerializationFactory.cs new file mode 100644 index 00000000000..0cb37157a9a --- /dev/null +++ b/tests/MassTransit.KafkaIntegration.Tests/AvroSerializationFactory.cs @@ -0,0 +1,32 @@ +namespace MassTransit.KafkaIntegration.Tests; + +using System.Net.Mime; +using Confluent.Kafka; +using Confluent.Kafka.SyncOverAsync; +using Confluent.SchemaRegistry; +using Confluent.SchemaRegistry.Serdes; +using Serializers; + + +public class AvroKafkaSerializerFactory : + IKafkaSerializerFactory +{ + readonly ISchemaRegistryClient _client; + + public AvroKafkaSerializerFactory(ISchemaRegistryClient client) + { + _client = client; + } + + public ContentType ContentType => new("application/avro"); + + public IDeserializer GetDeserializer() + { + return new AvroDeserializer(_client).AsSyncOverAsync(); + } + + public IAsyncSerializer GetSerializer() + { + return new AvroSerializer(_client); + } +} diff --git a/tests/MassTransit.KafkaIntegration.Tests/Avro_Specs.cs b/tests/MassTransit.KafkaIntegration.Tests/Avro_Specs.cs index eda20d28591..b1e19378925 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/Avro_Specs.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/Avro_Specs.cs @@ -5,11 +5,10 @@ namespace MassTransit.KafkaIntegration.Tests using System.Threading.Tasks; using AvroContracts.AvroContracts; using Confluent.Kafka; - using Confluent.Kafka.SyncOverAsync; using Confluent.SchemaRegistry; - using Confluent.SchemaRegistry.Serdes; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; + using Serializers; using TestFramework; using Testing; @@ -22,21 +21,12 @@ public class Avro_Specs : [Test] public async Task Should_produce() { - static IAsyncSerializer GetSerializer(IServiceProvider provider) - { - return new AvroSerializer(provider.GetService()); - } - - static IDeserializer GetDeserializer(IServiceProvider provider) - { - return new AvroDeserializer(provider.GetService()).AsSyncOverAsync(); - } - await using var provider = new ServiceCollection() .AddSingleton(new CachedSchemaRegistryClient(new Dictionary { { "schema.registry.url", "localhost:8081" }, })) + .AddSingleton() .ConfigureKafkaTestOptions(options => { options.CreateTopicsIfNotExists = true; @@ -50,21 +40,16 @@ static IDeserializer GetDeserializer(IServiceProvider provider) { rider.AddConsumer>(); - rider.AddProducer(Topic, context => context.MessageId.ToString(), (context, cfg) => - { - cfg.SetKeySerializer(GetSerializer(context)); - cfg.SetValueSerializer(GetSerializer(context)); - }); + rider.AddProducer(Topic, context => context.MessageId.ToString()); rider.UsingKafka((context, k) => { + k.SetSerializationFactory(context.GetRequiredService()); + k.TopicEndpoint(Topic, nameof(Avro_Specs), c => { c.AutoOffsetReset = AutoOffsetReset.Earliest; - c.SetKeyDeserializer(GetDeserializer(context)); - c.SetValueDeserializer(GetDeserializer(context)); - c.ConfigureConsumer>(context); }); }); @@ -92,13 +77,16 @@ await producer.Produce(new { Test = "text" }, Pipe.Execute(context var result = await provider.GetTask>(); - Assert.AreEqual("text", result.Message.Test); - Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); - Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); - Assert.That(result.MessageId, Is.EqualTo(messageId)); - Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); - Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); - Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + Assert.Multiple(() => + { + Assert.That(result.Message.Test, Is.EqualTo("text")); + Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); + Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + Assert.That(result.MessageId, Is.EqualTo(messageId)); + Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); + Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); + Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + }); } } @@ -111,21 +99,12 @@ public class Publishing_a_message_to_the_bus_through_the_outbox : [Test] public async Task Should_use_the_default_endpoint_serializer() { - static IAsyncSerializer GetSerializer(IServiceProvider provider) - { - return new AvroSerializer(provider.GetService()); - } - - static IDeserializer GetDeserializer(IServiceProvider provider) - { - return new AvroDeserializer(provider.GetService()).AsSyncOverAsync(); - } - await using var provider = new ServiceCollection() .AddSingleton(new CachedSchemaRegistryClient(new Dictionary { { "schema.registry.url", "localhost:8081" }, })) + .AddSingleton() .ConfigureKafkaTestOptions(options => { options.CreateTopicsIfNotExists = true; @@ -144,23 +123,17 @@ static IDeserializer GetDeserializer(IServiceProvider provider) { rider.AddConsumer(); - rider.AddProducer(Topic, context => context.MessageId.ToString(), (context, cfg) => - { - cfg.SetKeySerializer(GetSerializer(context)); - cfg.SetValueSerializer(GetSerializer(context)); - }); + rider.AddProducer(Topic, context => context.MessageId.ToString()); rider.AddInMemoryInboxOutbox(); rider.UsingKafka((context, k) => { + k.SetSerializationFactory(context.GetRequiredService()); k.TopicEndpoint(Topic, nameof(Avro_Specs), c => { c.AutoOffsetReset = AutoOffsetReset.Earliest; - c.SetKeyDeserializer(GetDeserializer(context)); - c.SetValueDeserializer(GetDeserializer(context)); - c.UseInMemoryInboxOutbox(context); c.ConfigureConsumer(context); @@ -192,8 +165,11 @@ await producer.Produce(new { Test = "text" }, Pipe.Execute(context Assert.That(message, Is.Not.Null); - Assert.That(message.Context.Message.OriginalMessageId, Is.EqualTo(messageId)); - Assert.That(message.Context.Message.OriginalCorrelationId, Is.EqualTo(correlationId)); + Assert.Multiple(() => + { + Assert.That(message.Context.Message.OriginalMessageId, Is.EqualTo(messageId)); + Assert.That(message.Context.Message.OriginalCorrelationId, Is.EqualTo(correlationId)); + }); } diff --git a/tests/MassTransit.KafkaIntegration.Tests/ConfigureTopology_Specs.cs b/tests/MassTransit.KafkaIntegration.Tests/ConfigureTopology_Specs.cs index b592011fb53..d3e11407d85 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/ConfigureTopology_Specs.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/ConfigureTopology_Specs.cs @@ -45,14 +45,14 @@ public async Task Should_create_on_start() var meta = client.GetMetadata(topicName, TimeSpan.FromSeconds(10)); - Assert.AreEqual(1, meta.Topics.Count); + Assert.That(meta.Topics, Has.Count.EqualTo(1)); foreach (var topic in meta.Topics) { - Assert.AreEqual(partitionCount, topic.Partitions.Count); + Assert.That(topic.Partitions, Has.Count.EqualTo(partitionCount)); foreach (var partition in topic.Partitions) - Assert.AreEqual(replicaCount, partition.Replicas.Length); + Assert.That(partition.Replicas, Has.Length.EqualTo(replicaCount)); } } @@ -103,7 +103,7 @@ public async Task Should_bypass_if_created() var result = await provider.GetTask>(); - Assert.NotNull(result); + Assert.That(result, Is.Not.Null); } diff --git a/tests/MassTransit.KafkaIntegration.Tests/DictionaryHeadersDeserializerTests.cs b/tests/MassTransit.KafkaIntegration.Tests/DictionaryHeadersDeserializerTests.cs index 9670fa8c9d0..de843a0ed8c 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/DictionaryHeadersDeserializerTests.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/DictionaryHeadersDeserializerTests.cs @@ -1,5 +1,6 @@ namespace MassTransit.KafkaIntegration.Tests { + using System.Text; using System.Threading.Tasks; using Confluent.Kafka; using NUnit.Framework; @@ -13,8 +14,41 @@ public async Task Should_Deserialize_with_empty_header_value() { var headers = new Headers { new Header("EmptyValue", null) }; var result = DictionaryHeadersSerialize.Deserializer.Deserialize(headers); - Assert.IsTrue(result.TryGetHeader("EmptyValue", out var emptyValue)); - Assert.IsNull(emptyValue); + Assert.Multiple(() => + { + Assert.That(result.TryGetHeader("EmptyValue", out var emptyValue), Is.True); + Assert.That(emptyValue, Is.Null); + }); + } + + [Test] + public async Task Should_Deserialize_with_duplicate_header_value() + { + var headers = new Headers + { + new Header("TestValue", null), + new Header("TestValue", null) + }; + + var result = DictionaryHeadersSerialize.Deserializer.Deserialize(headers); + Assert.Multiple(() => + { + Assert.That(result.TryGetHeader("TestValue", out var emptyValue), Is.True); + Assert.That(emptyValue, Is.Null); + }); + } + + [Test] + public async Task Should_not_throw_when_header_in_different_encoding() + { + var bytes = Encoding.Unicode.GetBytes("test"); + var headers = new Headers { new Header("BadValue", bytes) }; + var result = DictionaryHeadersSerialize.Deserializer.Deserialize(headers); + Assert.Multiple(() => + { + Assert.That(result.TryGetHeader("BadValue", out var value), Is.True); + Assert.That(value, Is.EqualTo(Encoding.UTF8.GetString(bytes))); + }); } } } diff --git a/tests/MassTransit.KafkaIntegration.Tests/Filter_Specs.cs b/tests/MassTransit.KafkaIntegration.Tests/Filter_Specs.cs index 3b2494f0ec2..eb6fd2de3a1 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/Filter_Specs.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/Filter_Specs.cs @@ -61,9 +61,12 @@ public async Task Should_properly_configure_the_filter() await provider.GetTask>(); - Assert.That(_attempts, Is.EqualTo(4)); - Assert.That(_lastCount, Is.EqualTo(2)); - Assert.That(_lastAttempt, Is.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(_attempts, Is.EqualTo(4)); + Assert.That(_lastCount, Is.EqualTo(2)); + Assert.That(_lastAttempt, Is.EqualTo(3)); + }); } @@ -105,8 +108,6 @@ public interface KafkaMessage [TestFixture] public class Using_a_scoped_send_filter { - const string Topic = "scoped-filter-producer"; - [Test] public async Task Should_properly_configure_the_scoped_filter() { @@ -159,6 +160,8 @@ public async Task Should_properly_configure_the_scoped_filter() Assert.That(result.Headers.Get("Scoped-Value"), Is.EqualTo("Hello, World")); } + const string Topic = "scoped-filter-producer"; + public class ScopedContext { @@ -194,6 +197,133 @@ public Task Send(SendContext context, IPipe> next) } + public record KafkaMessage + { + } + } + + + [TestFixture] + public class Using_a_multiple_scoped_send_filters + { + [Test] + public async Task Should_properly_configure_the_scoped_filter() + { + await using var provider = new ServiceCollection() + .AddScoped() + .ConfigureKafkaTestOptions(options => + { + options.CreateTopicsIfNotExists = true; + options.TopicNames = new[] { Topic }; + }) + .AddMassTransitTestHarness(x => + { + x.AddTaskCompletionSource>(); + + x.AddRider(r => + { + r.AddConsumer>(); + + r.AddProducer(Topic); + + r.UsingKafka((context, k) => + { + k.UseSendFilter(typeof(ScopedContextSendFilter<>), context); + + k.UseSendFilter(typeof(SecondScopedContextSendFilter<>), context); + + k.TopicEndpoint(Topic, nameof(Using_a_scoped_send_filter), c => + { + c.AutoOffsetReset = AutoOffsetReset.Earliest; + + c.ConfigureConsumer>(context); + }); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var scopedContext = harness.Scope.ServiceProvider.GetRequiredService(); + + ITopicProducer producer = harness.GetProducer(); + + await producer.Produce(new { }, harness.CancellationToken); + + await provider.GetTask>(); + + await scopedContext.SecondTask.Task; + + await scopedContext.FirstTask.Task; + } + + const string Topic = "scoped-filters-producer"; + + + public class ScopedContext + { + public ScopedContext(ITestHarness harness) + { + FirstTask = harness.GetTask(); + SecondTask = harness.GetTask(); + } + + public TaskCompletionSource FirstTask { get; set; } + public TaskCompletionSource SecondTask { get; set; } + } + + + public class ScopedContextSendFilter : + IFilter> + where T : class + { + readonly ScopedContext _scopedContext; + + public ScopedContextSendFilter(ScopedContext scopedContext) + { + _scopedContext = scopedContext; + } + + public void Probe(ProbeContext context) + { + } + + public Task Send(SendContext context, IPipe> next) + { + _scopedContext.FirstTask.TrySetResult(context); + + return next.Send(context); + } + } + + + public class SecondScopedContextSendFilter : + IFilter> + where T : class + { + readonly ScopedContext _scopedContext; + + public SecondScopedContextSendFilter(ScopedContext scopedContext) + { + _scopedContext = scopedContext; + } + + public void Probe(ProbeContext context) + { + } + + public Task Send(SendContext context, IPipe> next) + { + _scopedContext.SecondTask.TrySetResult(context); + + return next.Send(context); + } + } + + public record KafkaMessage { } diff --git a/tests/MassTransit.KafkaIntegration.Tests/MassTransit.KafkaIntegration.Tests.csproj b/tests/MassTransit.KafkaIntegration.Tests/MassTransit.KafkaIntegration.Tests.csproj index ce195b84b89..793dbd01e76 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/MassTransit.KafkaIntegration.Tests.csproj +++ b/tests/MassTransit.KafkaIntegration.Tests/MassTransit.KafkaIntegration.Tests.csproj @@ -1,14 +1,17 @@ - net6.0 - latest + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/MassTransit.KafkaIntegration.Tests/MultiBus_Specs.cs b/tests/MassTransit.KafkaIntegration.Tests/MultiBus_Specs.cs index 7c7b906902c..f74dda82e55 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/MultiBus_Specs.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/MultiBus_Specs.cs @@ -259,7 +259,7 @@ public async Task Should_receive_message_and_stabilize_group_with_multi_bus() groupInfo = adminClient.ListGroup(nameof(MultiBus_ReBalance_Specs), TimeSpan.FromSeconds(5)); } - Assert.AreEqual(groupInfo.Members.Count, 2); // this fails, second instance consumer assigned to all partitions + Assert.That(groupInfo.Members, Has.Count.EqualTo(2)); // this fails, second instance consumer assigned to all partitions await secondBus.BusControl.StopAsync(TestCancellationToken); } @@ -308,7 +308,7 @@ public interface KafkaMessage public class MultiBus_ConcurrentConsumers_ReBalance_Specs : InMemoryTestFixture { - const string Topic = "concurrent-rebalance-receive-test-multi"; + const string Topic = "concurrent-rebalance-multi"; public MultiBus_ConcurrentConsumers_ReBalance_Specs() { @@ -372,6 +372,10 @@ public async Task Should_receive_message_and_stabilize_group_with_multi_bus_and_ }); }).BuildServiceProvider(); + + IEnumerable kafkaHostedServices = provider.GetServices().OfType(); + await Task.WhenAll(kafkaHostedServices.Select(x => x.StartAsync(TestCancellationToken))); + var busControl = provider.GetRequiredService(); var secondBus = provider.GetRequiredService>(); @@ -398,7 +402,7 @@ public async Task Should_receive_message_and_stabilize_group_with_multi_bus_and_ groupInfo = adminClient.ListGroup(nameof(MultiBus_ConcurrentConsumers_ReBalance_Specs), TimeSpan.FromSeconds(5)); } - Assert.AreEqual(groupInfo.Members.Count, 6); // this fails, second instance consumer assigned to all partitions + Assert.That(groupInfo.Members, Has.Count.EqualTo(6)); // this fails, second instance consumer assigned to all partitions await secondBus.BusControl.StopAsync(TestCancellationToken); } diff --git a/tests/MassTransit.KafkaIntegration.Tests/ProducerPipe_Specs.cs b/tests/MassTransit.KafkaIntegration.Tests/ProducerPipe_Specs.cs index 274f5954d8f..19ffb680c89 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/ProducerPipe_Specs.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/ProducerPipe_Specs.cs @@ -63,10 +63,13 @@ await producer.Produce(new var result = await provider.GetTask(); - Assert.IsTrue(result.TryGetPayload(out _)); - Assert.IsTrue(result.TryGetPayload>(out _)); - Assert.AreEqual(correlationId, result.CorrelationId); - Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + Assert.Multiple(() => + { + Assert.That(result.TryGetPayload(out _), Is.True); + Assert.That(result.TryGetPayload>(out _), Is.True); + Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); + Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + }); await provider.GetTask>(); } @@ -149,7 +152,7 @@ public async Task Should_produce_with_custom_serializer() context.ValueSerializer = new CustomSerializer(); }), harness.CancellationToken); - Assert.IsFalse(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.False); } @@ -217,10 +220,16 @@ public async Task Should_produce() var result = await provider.GetTask(); - Assert.IsTrue(result.TryGetPayload(out KafkaSendContext context)); - Assert.AreEqual(context.Key, key); - Assert.AreEqual(context.CorrelationId, key); - Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + Assert.Multiple(() => + { + Assert.That(result.TryGetPayload(out KafkaSendContext context), Is.True); + Assert.That(context.Key, Is.EqualTo(key)); + Assert.Multiple(() => + { + Assert.That(key, Is.EqualTo(context.CorrelationId)); + Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + }); + }); await provider.GetTask>(); } diff --git a/tests/MassTransit.KafkaIntegration.Tests/Producer_Specs.cs b/tests/MassTransit.KafkaIntegration.Tests/Producer_Specs.cs index cad25684c80..bf9a7833654 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/Producer_Specs.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/Producer_Specs.cs @@ -34,7 +34,6 @@ public async Task Should_receive_messages() r.AddConsumer>(); r.AddProducer(Topic, producerConfig); - r.UsingKafka((context, k) => { k.TopicEndpoint(Topic, consumerConfig, c => @@ -73,18 +72,24 @@ await producer.Produce(new { Text = "text" }, Pipe.Execute(context var result = await provider.GetTask>(); - Assert.AreEqual("text", result.Message.Text); - Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); - Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); - Assert.That(result.MessageId, Is.EqualTo(messageId)); - Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); - Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); - Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + Assert.Multiple(() => + { + Assert.That(result.Message.Text, Is.EqualTo("text")); + Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); + Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + Assert.That(result.MessageId, Is.EqualTo(messageId)); + Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); + Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); + Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + }); var headerType = result.Headers.Get("Special"); Assert.That(headerType, Is.Not.Null); - Assert.That(headerType.Key, Is.EqualTo("Hello")); - Assert.That(headerType.Value, Is.EqualTo("World")); + Assert.Multiple(() => + { + Assert.That(headerType.Key, Is.EqualTo("Hello")); + Assert.That(headerType.Value, Is.EqualTo("World")); + }); } @@ -102,6 +107,60 @@ public interface KafkaMessage } + public class Producer_Provider_Specs + { + const string Topic = "producer-provider"; + + [Test] + public async Task Should_receive_messages() + { + var consumerConfig = new ConsumerConfig { GroupId = nameof(Producer_Provider_Specs) }; + + await using var provider = new ServiceCollection() + .ConfigureKafkaTestOptions(options => + { + options.CreateTopicsIfNotExists = true; + options.TopicNames = new[] { Topic }; + }) + .AddMassTransitTestHarness(x => + { + x.AddTaskCompletionSource>(); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(15)); + x.AddRider(r => + { + r.UsingKafka((_, k) => + { + k.TopicEndpoint(Topic, consumerConfig, c => + { + c.AutoOffsetReset = AutoOffsetReset.Earliest; + c.Handler(_ => Task.CompletedTask); + }); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var producerProvider = harness.Scope.ServiceProvider.GetRequiredService(); + + ITopicProducer producer = producerProvider.GetProducer(new Uri($"topic:{Topic}")); + + await producer.Produce(new { }, harness.CancellationToken); + + await harness.Consumed.Any(); + } + + + public interface KafkaMessage + { + } + } + + public class ProducerWithObserver_Specs { const string Topic = "producer-bus-observer"; @@ -155,9 +214,12 @@ public async Task Should_use_bus_send_observer() var result = await provider.GetTask>(); - Assert.AreEqual("text", result.Message.Text); - Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); - Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + Assert.Multiple(() => + { + Assert.That(result.Message.Text, Is.EqualTo("text")); + Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); + Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + }); await postSendCompletionSource.Task; } @@ -257,8 +319,11 @@ await harness.Bus.Publish(new StartTest var result = await provider.GetTask>(); - Assert.AreEqual("text", result.Message.Text); - Assert.AreEqual(correlationId, result.InitiatorId); + Assert.Multiple(() => + { + Assert.That(result.Message.Text, Is.EqualTo("text")); + Assert.That(result.InitiatorId, Is.EqualTo(correlationId)); + }); } diff --git a/tests/MassTransit.KafkaIntegration.Tests/Publish_Headers.cs b/tests/MassTransit.KafkaIntegration.Tests/Publish_Headers.cs index 8c86fbe710d..4de211cf4c1 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/Publish_Headers.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/Publish_Headers.cs @@ -71,21 +71,25 @@ public async Task Should_receive_correct_headers_in_following_message() var result = await provider.GetTask>(); var ping = await provider.GetTask>(); - Assert.AreEqual(result.ConversationId, ping.ConversationId); + Assert.Multiple(() => + { + Assert.That(ping.ConversationId, Is.EqualTo(result.ConversationId)); + + Assert.That(ping.SourceAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{OriginalTopic}"))); + Assert.That(ping.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{FollowingTopic}"))); - Assert.That(ping.SourceAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{OriginalTopic}"))); - Assert.That(ping.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{FollowingTopic}"))); - Assert.AreEqual(result.DestinationAddress, ping.SourceAddress); + Assert.That(ping.SourceAddress, Is.EqualTo(result.DestinationAddress)); - Assert.AreNotEqual(result.MessageId, ping.MessageId); + Assert.That(ping.MessageId, Is.Not.EqualTo(result.MessageId)); + }); } class OriginalMessageConsumer : IConsumer { - readonly TaskCompletionSource> _orginalMessageTaskCompletionSource; readonly ITopicProducer _followingMessageTopicProducer; + readonly TaskCompletionSource> _orginalMessageTaskCompletionSource; public OriginalMessageConsumer(TaskCompletionSource> orginalMessageTaskCompletionSource, ITopicProducer followingMessageTopicProducer) diff --git a/tests/MassTransit.KafkaIntegration.Tests/Publish_Specs.cs b/tests/MassTransit.KafkaIntegration.Tests/Publish_Specs.cs index a1a50372c96..7d43f9313d2 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/Publish_Specs.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/Publish_Specs.cs @@ -56,9 +56,12 @@ public async Task Should_receive_in_kafka_and_in_bus() var result = await provider.GetTask>(); var ping = await provider.GetTask>(); - Assert.AreEqual(result.CorrelationId, ping.InitiatorId); + Assert.Multiple(() => + { + Assert.That(ping.InitiatorId, Is.EqualTo(result.CorrelationId)); - Assert.That(ping.SourceAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + Assert.That(ping.SourceAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + }); } diff --git a/tests/MassTransit.KafkaIntegration.Tests/Receive_Specs.cs b/tests/MassTransit.KafkaIntegration.Tests/Receive_Specs.cs index b1484dcafdb..b87a5a15548 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/Receive_Specs.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/Receive_Specs.cs @@ -67,11 +67,14 @@ public async Task Should_receive() var result = await provider.GetTask>(); - Assert.AreEqual(message.Value.Text, result.Message.Text); - Assert.AreEqual(sendContext.MessageId, result.MessageId); - Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + await Assert.MultipleAsync(async () => + { + Assert.That(result.Message.Text, Is.EqualTo(message.Value.Text)); + Assert.That(result.MessageId, Is.EqualTo(sendContext.MessageId)); + Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); - Assert.That(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any()); + }); } @@ -111,12 +114,13 @@ public interface KafkaMessage } } + public class ConcurrentKeysReceive_Specs : InMemoryTestFixture { const string Topic = "test-concurrent-keys"; - const int NumMessages = 10; - const int NumKeys = 2; + const int NumMessages = 50; + const int NumKeys = 5; [Test] public async Task Should_receive_concurrently_by_keys() @@ -135,15 +139,8 @@ public async Task Should_receive_concurrently_by_keys() rider.AddConsumer(); rider.AddProducer(Topic); - rider.UsingKafka((context, k) => + rider.UsingKafka((_, _) => { - k.TopicEndpoint(Topic, nameof(ConcurrentKeysReceive_Specs), c => - { - c.AutoOffsetReset = AutoOffsetReset.Earliest; - c.ConcurrentMessageLimit = NumMessages; - - c.ConfigureConsumer(context); - }); }); }); }).BuildServiceProvider(); @@ -155,10 +152,22 @@ public async Task Should_receive_concurrently_by_keys() for (var i = 0; i < NumMessages; i++) await producer.Produce(i % NumKeys, new { Index = i + 1 }, harness.CancellationToken); + var kafka = provider.GetRequiredService(); + + var connected = kafka.ConnectTopicEndpoint(Topic, nameof(ConcurrentKeysReceive_Specs), (context, configurator) => + { + configurator.AutoOffsetReset = AutoOffsetReset.Earliest; + configurator.ConcurrentMessageLimit = NumMessages; + + configurator.ConfigureConsumer(context); + }); + + await connected.Ready.OrCanceled(harness.CancellationToken); + await provider.GetTask>(); IList> receivedMessages = await harness.Consumed.SelectAsync().ToListAsync(); - Assert.That(receivedMessages.Count, Is.EqualTo(NumMessages)); + Assert.That(receivedMessages, Has.Count.EqualTo(NumMessages)); var result = new int[NumKeys]; foreach (IReceivedMessage receivedMessage in receivedMessages) @@ -186,7 +195,7 @@ public Task Consume(ConsumeContext context) { if (Interlocked.Decrement(ref _index) <= 0) _taskCompletionSource.TrySetResult(context); - return Task.CompletedTask; + return Task.Delay(10); } } @@ -327,7 +336,7 @@ public async Task Should_receive_batch() var result = await provider.GetTask>>(); for (var i = 0; i < batchSize; i++) - Assert.AreEqual(i, result.Message[i].Message.Index); + Assert.That(result.Message[i].Message.Index, Is.EqualTo(i)); } @@ -337,6 +346,7 @@ public interface KafkaMessage } } + public class MultiGroupReceive_Specs : InMemoryTestFixture { diff --git a/tests/MassTransit.KafkaIntegration.Tests/TopicConnector_Specs.cs b/tests/MassTransit.KafkaIntegration.Tests/TopicConnector_Specs.cs index 1c222f655db..a92d885d16a 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/TopicConnector_Specs.cs +++ b/tests/MassTransit.KafkaIntegration.Tests/TopicConnector_Specs.cs @@ -1,6 +1,7 @@ namespace MassTransit.KafkaIntegration.Tests { using System; + using System.Collections.Concurrent; using System.Threading.Tasks; using Confluent.Kafka; using Internals; @@ -74,18 +75,24 @@ await producer.Produce(new { Text = "text" }, Pipe.Execute(context var result = await provider.GetTask>(); - Assert.AreEqual("text", result.Message.Text); - Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); - Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); - Assert.That(result.MessageId, Is.EqualTo(messageId)); - Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); - Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); - Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + Assert.Multiple(() => + { + Assert.That(result.Message.Text, Is.EqualTo("text")); + Assert.That(result.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/"))); + Assert.That(result.DestinationAddress, Is.EqualTo(new Uri($"loopback://localhost/{KafkaTopicAddress.PathPrefix}/{Topic}"))); + Assert.That(result.MessageId, Is.EqualTo(messageId)); + Assert.That(result.CorrelationId, Is.EqualTo(correlationId)); + Assert.That(result.InitiatorId, Is.EqualTo(initiatorId)); + Assert.That(result.ConversationId, Is.EqualTo(conversationId)); + }); var headerType = result.Headers.Get("Special"); Assert.That(headerType, Is.Not.Null); - Assert.That(headerType.Key, Is.EqualTo("Hello")); - Assert.That(headerType.Value, Is.EqualTo("World")); + Assert.Multiple(() => + { + Assert.That(headerType.Key, Is.EqualTo("Hello")); + Assert.That(headerType.Value, Is.EqualTo("World")); + }); } @@ -101,4 +108,105 @@ public interface KafkaMessage string Text { get; } } } + + + public class TopicConnector_With_Custom_Offset_Specs : + InMemoryTestFixture + { + const string Topic = "endpoint-connector-with-offset"; + + [Test] + public async Task Should_receive_on_connected_topic() + { + var counters = new ConcurrentDictionary(); + await using var provider = new ServiceCollection() + .AddSingleton(counters) + .ConfigureKafkaTestOptions(options => + { + options.CreateTopicsIfNotExists = true; + options.TopicNames = new[] { Topic }; + }) + .AddMassTransitTestHarness(x => + { + x.AddTaskCompletionSource>(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(30)); + + x.AddRider(rider => + { + rider.AddProducer(Topic); + rider.AddConsumer(); + + rider.UsingKafka((_, _) => + { + }); + }); + }).BuildServiceProvider(); + + var harness = provider.GetTestHarness(); + await harness.Start(); + + var kafka = provider.GetRequiredService(); + + var id = NewId.NextGuid(); + ITopicProducer producer = harness.GetProducer(); + + await producer.Produce(new { Id = id }, harness.CancellationToken); + + var connected = kafka.ConnectTopicEndpoint(Topic, nameof(TopicConnector_With_Custom_Offset_Specs), (context, configurator) => + { + configurator.AutoOffsetReset = AutoOffsetReset.Earliest; + configurator.ConfigureConsumer(context); + }); + + await connected.Ready.OrCanceled(harness.CancellationToken); + + IReceivedMessage received = await harness.Consumed.SelectAsync(harness.CancellationToken).FirstOrDefault(); + + Assert.That(received, Is.Not.Null); + + Assert.That(received.Context.TryGetPayload(out KafkaConsumeContext kafkaConsumeContext), Is.True); + + await connected.StopAsync(harness.CancellationToken); + + connected = kafka.ConnectTopicEndpoint(Topic, nameof(TopicConnector_With_Custom_Offset_Specs), (context, configurator) => + { + configurator.Offset = kafkaConsumeContext.Offset; + configurator.AutoOffsetReset = AutoOffsetReset.Earliest; + + configurator.ConfigureConsumer(context); + }); + + await connected.Ready.OrCanceled(harness.CancellationToken); + + while (counters.TryGetValue(id, out var count) && count < 2) + { + harness.CancellationToken.ThrowIfCancellationRequested(); + await Task.Delay(20); + } + } + + + public interface KafkaMessage + { + Guid Id { get; } + } + + + class CounterConsumer : + IConsumer + { + readonly ConcurrentDictionary _result; + + public CounterConsumer(ConcurrentDictionary result) + { + _result = result; + } + + public Task Consume(ConsumeContext context) + { + _result.AddOrUpdate(context.Message.Id, _ => 1, (_, v) => v + 1); + return Task.CompletedTask; + } + } + } } diff --git a/tests/MassTransit.KafkaIntegration.Tests/docker-compose.yml b/tests/MassTransit.KafkaIntegration.Tests/docker-compose.yml index fa3cf56f064..1ca6a5bbabf 100644 --- a/tests/MassTransit.KafkaIntegration.Tests/docker-compose.yml +++ b/tests/MassTransit.KafkaIntegration.Tests/docker-compose.yml @@ -1,5 +1,4 @@ --- -version: '3' services: zookeeper: image: confluentinc/cp-zookeeper:latest diff --git a/tests/MassTransit.MartenIntegration.Tests/Concurrency_Specs.cs b/tests/MassTransit.MartenIntegration.Tests/Concurrency_Specs.cs new file mode 100644 index 00000000000..d20e3276091 --- /dev/null +++ b/tests/MassTransit.MartenIntegration.Tests/Concurrency_Specs.cs @@ -0,0 +1,162 @@ +namespace MassTransit.MartenIntegration.Tests +{ + using System; + using System.Threading.Tasks; + using ConcurrentSagaTypes; + using Marten; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + [TestFixture] + public class When_consuming_concurrent_messages_for_the_same_saga_instance + { + [Test] + public async Task Should_properly_transition_the_state() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); + + x.AddHandler(async (ConsumeContext context) => + { + await Task.WhenAll( + context.Publish(new RunningA(context.Message.CorrelationId)), + context.Publish(new RunningB(context.Message.CorrelationId))); + }); + + x.AddHandler(async (ConsumeContext context) => + { + await Task.WhenAll( + context.Publish(new CompletingA(context.Message.CorrelationId)), + context.Publish(new CompletingB(context.Message.CorrelationId))); + }); + + x.AddMarten().ApplyAllDatabaseChangesOnStartup(); + + x.AddSagaStateMachine() + .MartenRepository("server=localhost;port=5432;database=MartenTest;user id=postgres;password=Password12!;", r => + { + r.CreateDatabasesForTenants(c => + { + c.MaintenanceDatabase("server=localhost;port=5432;database=postgres;user id=postgres;password=Password12!;"); + c.ForTenant() + .CheckAgainstPgDatabase() + .WithOwner("postgres") + .WithEncoding("UTF-8") + .ConnectionLimit(-1); + }); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseMessageRetry(r => r.Immediate(5)); + cfg.UseMessageScope(context); + cfg.UseInMemoryOutbox(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + Guid correlationId = NewId.NextGuid(); + + await harness.Bus.Publish(new Started(correlationId)); + + Assert.That(await harness.Published.Any(x => x.Context.Message.CorrelationId == correlationId)); + } + } + + + namespace ConcurrentSagaTypes + { + using System; + + + public record Started(Guid CorrelationId); + + + public record Running(Guid CorrelationId); + + + public record RunningA(Guid CorrelationId); + + + public record RunningB(Guid CorrelationId); + + + public record Completing(Guid CorrelationId); + + + public record CompletingA(Guid CorrelationId); + + + public record CompletingB(Guid CorrelationId); + + + public record Completed(Guid CorrelationId); + + + public class TransactionState : + SagaStateMachineInstance + { + public Guid CorrelationId { get; set; } + public int State { get; set; } + + public int RunningStatus { get; set; } + public int CompletingStatus { get; set; } + } + + + public class TransactionStateMachine : + MassTransitStateMachine + { + public TransactionStateMachine() + { + InstanceState(x => x.State); + + Initially( + When(Started) + .TransitionTo(Running) + .Publish(context => new Running(context.Message.CorrelationId)) + ); + + During(Running, + When(RunningFinished) + .TransitionTo(Completing) + .Publish(context => new Completing(context.Saga.CorrelationId)) + ); + + During(Completing, + When(CompletingFinished) + .TransitionTo(Completed) + .Publish(context => new Completed(context.Saga.CorrelationId)) + ); + + CompositeEvent(() => RunningFinished, x => x.RunningStatus, RunningA, RunningB); + CompositeEvent(() => CompletingFinished, x => x.CompletingStatus, CompletingA, CompletingB); + + SetCompletedWhenFinalized(); + } + + // ReSharper disable UnassignedGetOnlyAutoProperty + public State Running { get; } + public State Completing { get; } + public State Completed { get; } + + public Event Started { get; } + public Event RunningA { get; } + public Event RunningB { get; } + public Event RunningFinished { get; } + public Event CompletingA { get; } + public Event CompletingB { get; } + public Event CompletingFinished { get; } + } + } +} diff --git a/tests/MassTransit.MartenIntegration.Tests/Container_Specs.cs b/tests/MassTransit.MartenIntegration.Tests/Container_Specs.cs index 31ceb87a025..758b60bf383 100644 --- a/tests/MassTransit.MartenIntegration.Tests/Container_Specs.cs +++ b/tests/MassTransit.MartenIntegration.Tests/Container_Specs.cs @@ -62,6 +62,7 @@ protected void ConfigureRegistration(IBusRegistrationConfigurator configurator) { r.CreateDatabasesForTenants(c => { + c.MaintenanceDatabase("server=localhost;port=5432;database=postgres;user id=postgres;password=Password12!;"); c.ForTenant() .CheckAgainstPgDatabase() .WithOwner("postgres") diff --git a/tests/MassTransit.MartenIntegration.Tests/MassTransit.MartenIntegration.Tests.csproj b/tests/MassTransit.MartenIntegration.Tests/MassTransit.MartenIntegration.Tests.csproj index b71e35ebbb9..cbeae8e3e35 100644 --- a/tests/MassTransit.MartenIntegration.Tests/MassTransit.MartenIntegration.Tests.csproj +++ b/tests/MassTransit.MartenIntegration.Tests/MassTransit.MartenIntegration.Tests.csproj @@ -1,15 +1,19 @@  - net6.0 + net8.0 + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/tests/MassTransit.MartenIntegration.Tests/SagaPersistenceTests.cs b/tests/MassTransit.MartenIntegration.Tests/SagaPersistenceTests.cs index 7a06648d333..c87b1b3ca86 100644 --- a/tests/MassTransit.MartenIntegration.Tests/SagaPersistenceTests.cs +++ b/tests/MassTransit.MartenIntegration.Tests/SagaPersistenceTests.cs @@ -5,7 +5,6 @@ using Marten; using NUnit.Framework; using Saga; - using Shouldly; using TestFramework; using Testing; @@ -25,14 +24,14 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldBe(sagaId); + Assert.That(found, Is.EqualTo(sagaId)); var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; await InputQueueSendEndpoint.Send(nextMessage); found = await _sagaRepository.Value.ShouldContainSaga(x => x.CorrelationId == sagaId && x.Completed, TestTimeout); - found.ShouldBe(sagaId); + Assert.That(found, Is.EqualTo(sagaId)); } [Test] @@ -44,8 +43,7 @@ public async Task An_initiating_message_should_start_the_saga() await InputQueueSendEndpoint.Send(message); Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - - found.ShouldBe(sagaId); + Assert.That(found, Is.EqualTo(sagaId)); } [Test] @@ -58,14 +56,14 @@ public async Task An_observed_message_should_find_and_update_the_correct_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldBe(sagaId); + Assert.That(found, Is.EqualTo(sagaId)); var nextMessage = new ObservableSagaMessage { Name = "MySimpleSaga" }; await InputQueueSendEndpoint.Send(nextMessage); found = await _sagaRepository.Value.ShouldContainSaga(x => x.CorrelationId == sagaId && x.Observed, TestTimeout); - found.ShouldBe(sagaId); + Assert.That(found, Is.EqualTo(sagaId)); } readonly Lazy> _sagaRepository; @@ -74,7 +72,7 @@ public async Task An_observed_message_should_find_and_update_the_correct_saga() [OneTimeSetUp] public async Task OneTime() { - await _store.Schema.ApplyAllConfiguredChangesToDatabaseAsync(); + await _store.Storage.ApplyAllConfiguredChangesToDatabaseAsync(); } public LocatingAnExistingSaga() @@ -86,6 +84,7 @@ public LocatingAnExistingSaga() x.Connection(connectionString); x.CreateDatabasesForTenants(c => { + c.MaintenanceDatabase("server=localhost;port=5432;database=postgres;user id=postgres;password=Password12!;"); c.ForTenant() .CheckAgainstPgDatabase() .WithOwner("postgres") diff --git a/tests/MassTransit.MartenIntegration.Tests/TwoSaga_Specs.cs b/tests/MassTransit.MartenIntegration.Tests/TwoSaga_Specs.cs new file mode 100644 index 00000000000..9c54b1cd3eb --- /dev/null +++ b/tests/MassTransit.MartenIntegration.Tests/TwoSaga_Specs.cs @@ -0,0 +1,187 @@ +namespace MassTransit.MartenIntegration.Tests +{ + namespace ContainerTests + { + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Marten; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Logging.Abstractions; + using NUnit.Framework; + using TestFramework; + using TestFramework.Sagas; + using Testing; + + public class Using_Marten_with_multiple_sagas : + InMemoryTestFixture + { + readonly IServiceProvider _provider; + + public Using_Marten_with_multiple_sagas() + { + _provider = new ServiceCollection() + .AddMassTransit(ConfigureRegistration) + .AddScoped().BuildServiceProvider(); + } + + [Test] + public async Task Should_work_as_expected() + { + await EnsureMartenHostedServiceIsStarted(); + + Task> started = await ConnectPublishHandler(); + + var correlationId = NewId.NextGuid(); + + await Bus.Publish(new StartTest + { + CorrelationId = correlationId, + TestKey = "Unique" + }); + + await started; + + var repository = _provider.GetRequiredService>(); + + var machine = _provider.GetRequiredService(); + + Guid? sagaId = await repository.ShouldContainSagaInState(correlationId, machine, x => x.Active, TestTimeout); + Assert.That(sagaId.HasValue); + } + + protected void ConfigureRegistration(IBusRegistrationConfigurator configurator) + { + configurator.AddLogging(loggingBuilder => loggingBuilder.AddProvider(NullLoggerProvider.Instance)); + + configurator.AddMarten(options => + { + options.Connection("server=localhost;port=5432;database=MartenTest;user id=postgres;password=Password12!;"); + options.CreateDatabasesForTenants(c => + { + c.MaintenanceDatabase("server=localhost;port=5432;database=postgres;user id=postgres;password=Password12!;"); + c.ForTenant() + .CheckAgainstPgDatabase() + .WithOwner("postgres") + .WithEncoding("UTF-8") + .ConnectionLimit(-1); + }); + }).ApplyAllDatabaseChangesOnStartup(); + + configurator.AddSagaStateMachine() + .MartenRepository(); + configurator.AddSagaStateMachine() + .MartenRepository(); + + configurator.AddBus(provider => BusControl); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + var busRegistrationContext = _provider.GetRequiredService(); + configurator.UseInMemoryOutbox(busRegistrationContext); + configurator.ConfigureSaga(busRegistrationContext); + configurator.ConfigureSaga(busRegistrationContext); + } + + private Task EnsureMartenHostedServiceIsStarted() => + _provider + .GetServices() + .Single(x => x.GetType().Name == "MartenActivator") + .StartAsync(CancellationToken.None); + } + + + public class TestInstance2 : + SagaStateMachineInstance + { + public string CurrentState { get; set; } + public string Key { get; set; } + public Guid CorrelationId { get; set; } + } + + + public class TestStateMachineSaga2 : + MassTransitStateMachine + { + public TestStateMachineSaga2() + { + InstanceState(x => x.CurrentState); + + Initially( + When(Started) + .Then(context => context.Instance.Key = context.Data.TestKey) + .Activity(x => x.OfInstanceType()) + .TransitionTo(Active)); + + SetCompletedWhenFinalized(); + } + + public State Active { get; private set; } + public State Done { get; private set; } + + public Event Started { get; private set; } + } + + + public class PublishTestStartedActivity2 : + IStateMachineActivity + { + readonly ConsumeContext _context; + + public PublishTestStartedActivity2(ConsumeContext context) + { + _context = context; + } + + public void Probe(ProbeContext context) + { + context.CreateScope("publisher"); + } + + public void Accept(StateMachineVisitor visitor) + { + visitor.Visit(this); + } + + public async Task Execute(BehaviorContext context, IBehavior next) + { + await _context.Publish(new TestStarted + { + CorrelationId = context.Instance.CorrelationId, + TestKey = context.Instance.Key + }).ConfigureAwait(false); + + await next.Execute(context).ConfigureAwait(false); + } + + public async Task Execute(BehaviorContext context, IBehavior next) + where T : class + { + await _context.Publish(new TestStarted + { + CorrelationId = context.Instance.CorrelationId, + TestKey = context.Instance.Key + }).ConfigureAwait(false); + + await next.Execute(context).ConfigureAwait(false); + } + + public Task Faulted(BehaviorExceptionContext context, IBehavior next) + where TException : Exception + { + return next.Faulted(context); + } + + public Task Faulted(BehaviorExceptionContext context, IBehavior next) + where TException : Exception + where T : class + { + return next.Faulted(context); + } + } + } +} diff --git a/tests/MassTransit.MongoDbIntegration.Tests/Audit/AuditStore_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/Audit/AuditStore_Specs.cs index baad316c0f9..7bfb803abdb 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/Audit/AuditStore_Specs.cs +++ b/tests/MassTransit.MongoDbIntegration.Tests/Audit/AuditStore_Specs.cs @@ -16,17 +16,20 @@ public class Produces_an_audit_record_for_a_sent_message [Test] public async Task Audit_document_gets_created() { - Assert.AreEqual("Send", _auditDocument.ContextType); - Assert.AreEqual(_sent.Context.MessageId.Value.ToString(), _auditDocument.MessageId); - Assert.AreEqual(_sent.Context.ConversationId.Value.ToString(), _auditDocument.ConversationId); - Assert.AreEqual(_sent.Context.DestinationAddress.ToString(), _auditDocument.DestinationAddress); - Assert.AreEqual(typeof(A).FullName, _auditDocument.MessageType); + Assert.Multiple(() => + { + Assert.That(_auditDocument.ContextType, Is.EqualTo("Send")); + Assert.That(_auditDocument.MessageId, Is.EqualTo(_sent.Context.MessageId.Value.ToString())); + Assert.That(_auditDocument.ConversationId, Is.EqualTo(_sent.Context.ConversationId.Value.ToString())); + Assert.That(_auditDocument.DestinationAddress, Is.EqualTo(_sent.Context.DestinationAddress.ToString())); + Assert.That(_auditDocument.MessageType, Is.EqualTo(typeof(A).FullName)); + }); } [Test] public void Message_payload_matches_sent_message() { - Assert.AreEqual(_sent.Context.Message.Data, JsonConvert.DeserializeObject(_auditDocument.Message).Data); + Assert.That(JsonConvert.DeserializeObject(_auditDocument.Message).Data, Is.EqualTo(_sent.Context.Message.Data)); } [Test] @@ -74,18 +77,21 @@ public class Produces_an_audit_record_for_a_consumed_message [Test] public async Task Audit_document_gets_created() { - Assert.AreEqual("Consume", _auditDocument.ContextType); - Assert.AreEqual(_consumed.Context.MessageId.Value.ToString(), _auditDocument.MessageId); - Assert.AreEqual(_consumed.Context.ConversationId.Value.ToString(), _auditDocument.ConversationId); - Assert.AreEqual(_consumed.Context.ReceiveContext.InputAddress.ToString(), _auditDocument.InputAddress); - Assert.AreEqual(_consumed.Context.DestinationAddress.ToString(), _auditDocument.DestinationAddress); - Assert.AreEqual(typeof(A).FullName, _auditDocument.MessageType); + Assert.Multiple(() => + { + Assert.That(_auditDocument.ContextType, Is.EqualTo("Consume")); + Assert.That(_auditDocument.MessageId, Is.EqualTo(_consumed.Context.MessageId.Value.ToString())); + Assert.That(_auditDocument.ConversationId, Is.EqualTo(_consumed.Context.ConversationId.Value.ToString())); + Assert.That(_auditDocument.InputAddress, Is.EqualTo(_consumed.Context.ReceiveContext.InputAddress.ToString())); + Assert.That(_auditDocument.DestinationAddress, Is.EqualTo(_consumed.Context.DestinationAddress.ToString())); + Assert.That(_auditDocument.MessageType, Is.EqualTo(typeof(A).FullName)); + }); } [Test] public void Message_payload_matches_sent_message() { - Assert.AreEqual(_consumed.Context.Message.Data, JsonConvert.DeserializeObject(_auditDocument.Message).Data); + Assert.That(JsonConvert.DeserializeObject(_auditDocument.Message).Data, Is.EqualTo(_consumed.Context.Message.Data)); } [Test] @@ -134,7 +140,7 @@ public void Should_have_the_consume_record() { var sentRecord = _audit.FirstOrDefault(x => x.ContextType == "Send"); - Assert.NotNull(sentRecord); + Assert.That(sentRecord, Is.Not.Null); } [Test] @@ -142,13 +148,13 @@ public void Should_have_the_sent_record() { var consumedRecord = _audit.FirstOrDefault(x => x.ContextType == "Consume"); - Assert.NotNull(consumedRecord); + Assert.That(consumedRecord, Is.Not.Null); } [Test] public void The_number_of_records_is_two() { - Assert.AreEqual(2, _audit.Count); + Assert.That(_audit, Has.Count.EqualTo(2)); } InMemoryTestHarness _harness; diff --git a/tests/MassTransit.MongoDbIntegration.Tests/BusOutbox_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/BusOutbox_Specs.cs index eaa08e8d62a..bceb4733d70 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/BusOutbox_Specs.cs +++ b/tests/MassTransit.MongoDbIntegration.Tests/BusOutbox_Specs.cs @@ -14,7 +14,6 @@ namespace MassTransit.MongoDbIntegration.Tests using Testing; - [Explicit] [TestFixture] public class Using_the_bus_outbox { @@ -126,8 +125,6 @@ public async Task Should_not_send_when_transaction_aborted() { using var dbContext = harness.Scope.ServiceProvider.GetRequiredService(); - // await dbContext.BeginTransaction(harness.CancellationToken); - var publishEndpoint = harness.Scope.ServiceProvider.GetRequiredService(); await publishEndpoint.Publish(new PingMessage()); @@ -232,6 +229,8 @@ public async Task Should_support_delayed_message_scheduler() await ClearOutbox(provider, harness); + Guid scheduledId = NewId.NextGuid(); + IConsumerTestHarness consumerHarness = harness.GetConsumerHarness(); { @@ -241,7 +240,7 @@ public async Task Should_support_delayed_message_scheduler() var scheduler = harness.Scope.ServiceProvider.GetRequiredService(); - await scheduler.SchedulePublish(TimeSpan.FromSeconds(8), new PingMessage()); + await scheduler.SchedulePublish(TimeSpan.FromSeconds(8), new PingMessage(scheduledId)); await dbContext.CommitTransaction(harness.CancellationToken); } @@ -249,10 +248,10 @@ public async Task Should_support_delayed_message_scheduler() { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - Assert.That(await consumerHarness.Consumed.Any(cts.Token), Is.False); + Assert.That(await consumerHarness.Consumed.Any(x => x.Context.Message.CorrelationId == scheduledId, cts.Token), Is.False); } - Assert.That(await consumerHarness.Consumed.Any(), Is.True); + Assert.That(await consumerHarness.Consumed.Any(x => x.Context.Message.CorrelationId == scheduledId), Is.True); } [Test] @@ -296,19 +295,22 @@ public async Task Should_support_multiple_transactions() var publishEndpoint = scope.ServiceProvider.GetRequiredService(); - await publishEndpoint.Publish(new PingMessage()); + var firstId = NewId.NextGuid(); + await publishEndpoint.Publish(new PingMessage(firstId)); using var cts1 = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - Assert.That(await consumerHarness.Consumed.Any(cts1.Token), Is.False); + Assert.That(await consumerHarness.Consumed.Any(x => x.Context.Message.CorrelationId == firstId, cts1.Token), Is.False); await dbContext.CommitTransaction(harness.CancellationToken); - await publishEndpoint.Publish(new PingMessage()); + var secondId = NewId.NextGuid(); + + await publishEndpoint.Publish(new PingMessage(secondId)); using var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(2)); - Assert.That(await consumerHarness.Consumed.Any(cts2.Token), Is.True); + Assert.That(await consumerHarness.Consumed.Any(x => x.Context.Message.CorrelationId == firstId, cts2.Token), Is.True); await dbContext.CommitTransaction(harness.CancellationToken); } diff --git a/tests/MassTransit.MongoDbIntegration.Tests/CollectionNameFormatter_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/CollectionNameFormatter_Specs.cs index e8008373ba9..16b7efebe60 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/CollectionNameFormatter_Specs.cs +++ b/tests/MassTransit.MongoDbIntegration.Tests/CollectionNameFormatter_Specs.cs @@ -13,7 +13,7 @@ public class DotCollectionNameFormatter_Specs public void Should_return_correct_collection() { var collectionName = _collectionNameFormatter.Saga(); - Assert.AreEqual("simple.sagas", collectionName); + Assert.That(collectionName, Is.EqualTo("simple.sagas")); } readonly ICollectionNameFormatter _collectionNameFormatter; @@ -32,7 +32,7 @@ public class DefaultCollectionNameFormatter_Specs public void Should_return_default_collection_when_null() { var collectionName = _collectionNameFormatter(null).Saga(); - Assert.AreEqual("sagas", collectionName); + Assert.That(collectionName, Is.EqualTo("sagas")); } readonly Func _collectionNameFormatter; @@ -48,7 +48,7 @@ public DefaultCollectionNameFormatter_Specs() public void Should_return_correct_collection(string expected, string result) { var collectionName = _collectionNameFormatter(expected).Saga(); - Assert.AreEqual(result, collectionName); + Assert.That(collectionName, Is.EqualTo(result)); } } } diff --git a/tests/MassTransit.MongoDbIntegration.Tests/Container_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/Container_Specs.cs index 2ec9559e7f5..366f789ca22 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/Container_Specs.cs +++ b/tests/MassTransit.MongoDbIntegration.Tests/Container_Specs.cs @@ -74,8 +74,9 @@ protected void ConfigureRegistration(IBusRegistrationConfigurator configurator) protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { - configurator.UseInMemoryOutbox(); - configurator.ConfigureSaga(_provider.GetRequiredService()); + var busRegistrationContext = _provider.GetRequiredService(); + configurator.UseInMemoryOutbox(busRegistrationContext); + configurator.ConfigureSaga(busRegistrationContext); } } diff --git a/tests/MassTransit.MongoDbIntegration.Tests/Courier/Complete_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/Courier/Complete_Specs.cs index 492829c8eca..4903b593636 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/Courier/Complete_Specs.cs +++ b/tests/MassTransit.MongoDbIntegration.Tests/Courier/Complete_Specs.cs @@ -19,10 +19,13 @@ public async Task Should_complete_the_activity() { ConsumeContext context = await _prepareCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.AreNotEqual(_trackingNumber, context.CorrelationId.Value); + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.Not.EqualTo(_trackingNumber)); + }); } [Test] @@ -30,10 +33,13 @@ public async Task Should_complete_the_routing_slip() { ConsumeContext context = await _completed; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.AreEqual(_trackingNumber, context.CorrelationId.Value); + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(_trackingNumber)); + }); } [Test] @@ -41,10 +47,13 @@ public async Task Should_complete_the_second_activity() { ConsumeContext context = await _sendCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.AreNotEqual(_trackingNumber, context.CorrelationId.Value); + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.Not.EqualTo(_trackingNumber)); + }); } [Test] @@ -61,9 +70,9 @@ public async Task Should_upsert_the_event_into_the_routing_slip() var routingSlip = await (await _collection.FindAsync(query).ConfigureAwait(false)).SingleOrDefaultAsync().ConfigureAwait(false); - Assert.IsNotNull(routingSlip); - Assert.IsNotNull(routingSlip.Events); - Assert.AreEqual(3, routingSlip.Events.Length); + Assert.That(routingSlip, Is.Not.Null); + Assert.That(routingSlip.Events, Is.Not.Null); + Assert.That(routingSlip.Events, Has.Length.EqualTo(3)); } [OneTimeSetUp] @@ -91,7 +100,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin var persister = new RoutingSlipEventPersister(_collection); - configurator.UseRetry(x => + configurator.UseMessageRetry(x => { x.Handle(); x.Interval(10, TimeSpan.FromMilliseconds(20)); diff --git a/tests/MassTransit.MongoDbIntegration.Tests/Courier/RoutingSlipCompleted_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/Courier/RoutingSlipCompleted_Specs.cs index f2411b62e8f..ec93bbcc68b 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/Courier/RoutingSlipCompleted_Specs.cs +++ b/tests/MassTransit.MongoDbIntegration.Tests/Courier/RoutingSlipCompleted_Specs.cs @@ -21,10 +21,13 @@ public async Task Should_process_the_event() { ConsumeContext context = await _completed; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.AreEqual(_trackingNumber, context.CorrelationId.Value); + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(_trackingNumber)); + }); } [Test] @@ -38,14 +41,17 @@ public async Task Should_upsert_the_event_into_the_routing_slip() var routingSlip = await (await _collection.FindAsync(query).ConfigureAwait(false)).SingleOrDefaultAsync().ConfigureAwait(false); - Assert.IsNotNull(routingSlip); - Assert.IsNotNull(routingSlip.Events); - Assert.AreEqual(1, routingSlip.Events.Length); + Assert.That(routingSlip, Is.Not.Null); + Assert.That(routingSlip.Events, Is.Not.Null); + Assert.That(routingSlip.Events, Has.Length.EqualTo(1)); var completed = routingSlip.Events[0] as RoutingSlipCompletedDocument; - Assert.IsNotNull(completed); - Assert.IsTrue(completed.Variables.ContainsKey("Client")); - Assert.AreEqual(27, completed.Variables["Client"]); + Assert.That(completed, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(completed.Variables.ContainsKey("Client"), Is.True); + Assert.That(completed.Variables["Client"], Is.EqualTo(27)); + }); //Assert.AreEqual(received.Timestamp.ToMongoDbDateTime(), read.Timestamp); } diff --git a/tests/MassTransit.MongoDbIntegration.Tests/InboxLock_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/InboxLock_Specs.cs index 34af23eadef..fe7172a81d7 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/InboxLock_Specs.cs +++ b/tests/MassTransit.MongoDbIntegration.Tests/InboxLock_Specs.cs @@ -47,14 +47,13 @@ public async Task Should_block_subsequent_consumers_by_lock() var events = provider.GetRequiredService>(); - Assert.That(events.Count, Is.EqualTo(100)); + Assert.That(events, Has.Count.EqualTo(100)); } } namespace InboxLock { - using System; using System.Linq; @@ -86,19 +85,12 @@ await Task.WhenAll(Enumerable.Range(0, 100).Select(index => public class InboxLockEntityFrameworkConsumerDefinition : ConsumerDefinition { - readonly IServiceProvider _provider; - - public InboxLockEntityFrameworkConsumerDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(10, 50, 100, 100, 100, 100, 100, 100)); - endpointConfigurator.UseMongoDbOutbox(_provider); + endpointConfigurator.UseMongoDbOutbox(context); } } } diff --git a/tests/MassTransit.MongoDbIntegration.Tests/JobConsumer_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/JobConsumer_Specs.cs new file mode 100644 index 00000000000..3735be7abcd --- /dev/null +++ b/tests/MassTransit.MongoDbIntegration.Tests/JobConsumer_Specs.cs @@ -0,0 +1,123 @@ +namespace MassTransit.MongoDbIntegration.Tests +{ + using System; + using System.Threading.Tasks; + using Contracts.JobService; + using JobConsumerTests; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + namespace JobConsumerTests + { + using System; + using System.Threading.Tasks; + using Contracts.JobService; + + + public interface OddJob + { + TimeSpan Duration { get; } + } + + + public class OddJobConsumer : + IJobConsumer + { + public async Task Run(JobContext context) + { + if (context.RetryAttempt == 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + } + } + + + public class OddJobCompletedConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + return Task.CompletedTask; + } + } + } + + + [TestFixture] + public class Using_the_new_job_service_configuration + { + [Test] + public async Task Should_complete_the_job() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); + + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.AddJobSagaStateMachines() + .MongoDbRepository(r => + { + r.Connection = "mongodb://127.0.0.1"; + r.DatabaseName = "jobServiceTest"; + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + try + { + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + var responseJobId = await client.SubmitJob(jobId, new {Duration = TimeSpan.FromSeconds(1)}, p => p.Set("Variable", "Knife")); + + await Assert.MultipleAsync(async () => + { + Assert.That(responseJobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + } + finally + { + await harness.Stop(); + } + } + + [OneTimeSetUp] + public async Task Setup() + { + } + + [OneTimeTearDown] + public async Task Teardown() + { + } + } +} diff --git a/tests/MassTransit.MongoDbIntegration.Tests/JobService/Complete_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/JobService/Complete_Specs.cs deleted file mode 100644 index 7d6e13b3ae6..00000000000 --- a/tests/MassTransit.MongoDbIntegration.Tests/JobService/Complete_Specs.cs +++ /dev/null @@ -1,145 +0,0 @@ -namespace MassTransit.MongoDbIntegration.Tests.JobService -{ - using System; - using System.Threading.Tasks; - using Contracts.JobService; - using Microsoft.Extensions.DependencyInjection; - using NUnit.Framework; - - - public interface CrunchTheNumbers - { - TimeSpan Duration { get; } - } - - - public class CrunchTheNumbersConsumer : - IJobConsumer - { - public async Task Run(JobContext context) - { - await Task.Delay(context.Job.Duration); - } - } - - - [Explicit] - public class Submitting_a_job_to_turnout_via_container : - QuartzInMemoryTestFixture - { - readonly IServiceProvider _provider; - Task> _completed; - - Guid _jobId; - Task> _started; - Task> _submitted; - - public Submitting_a_job_to_turnout_via_container() - { - _provider = new ServiceCollection() - .AddMassTransit(x => - { - x.AddConsumer(); - - x.AddRequestClient>(); - - x.AddSagaRepository() - .MongoDbRepository(r => - { - r.Connection = "mongodb://127.0.0.1"; - r.DatabaseName = "sagaTest"; - }); - x.AddSagaRepository() - .MongoDbRepository(r => - { - r.Connection = "mongodb://127.0.0.1"; - r.DatabaseName = "sagaTest"; - }); - x.AddSagaRepository() - .MongoDbRepository(r => - { - r.Connection = "mongodb://127.0.0.1"; - r.DatabaseName = "sagaTest"; - }); - - x.AddBus(provider => BusControl); - }) - .BuildServiceProvider(); - } - - [Test] - [Order(1)] - public async Task Should_get_the_job_accepted() - { - var requestClient = _provider.GetRequiredService>>(); - - Response response = await requestClient.GetResponse(new - { - JobId = _jobId, - Job = new { Duration = TimeSpan.FromSeconds(1) } - }); - - Assert.That(response.Message.JobId, Is.EqualTo(_jobId)); - - // just to capture all the test output in a single window - ConsumeContext completed = await _completed; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_completed_event() - { - ConsumeContext completed = await _completed; - } - - [Test] - [Order(3)] - public async Task Should_have_published_the_job_started_event() - { - ConsumeContext started = await _started; - } - - [Test] - [Order(2)] - public async Task Should_have_published_the_job_submitted_event() - { - ConsumeContext submitted = await _submitted; - } - - [OneTimeSetUp] - public async Task Arrange() - { - _jobId = NewId.NextGuid(); - } - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - base.ConfigureInMemoryBus(configurator); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - var busRegistrationContext = _provider.GetRequiredService(); - - instance.ConfigureJobServiceEndpoints(x => - { - x.ConfigureSagaRepositories(busRegistrationContext); - }); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.ConfigureConsumer(busRegistrationContext); - }); - }); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - _submitted = Handled(configurator, context => context.Message.JobId == _jobId); - _started = Handled(configurator, context => context.Message.JobId == _jobId); - _completed = Handled(configurator, context => context.Message.JobId == _jobId); - } - } -} diff --git a/tests/MassTransit.MongoDbIntegration.Tests/JobService/QuartzInMemoryTestFixture.cs b/tests/MassTransit.MongoDbIntegration.Tests/JobService/QuartzInMemoryTestFixture.cs deleted file mode 100644 index 1adcc6adafb..00000000000 --- a/tests/MassTransit.MongoDbIntegration.Tests/JobService/QuartzInMemoryTestFixture.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace MassTransit.MongoDbIntegration.Tests.JobService -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using Quartz; - using Scheduling; - using TestFramework; - - - public class QuartzInMemoryTestFixture : - InMemoryTestFixture - { - readonly Lazy _messageScheduler; - ISchedulerFactory _schedulerFactory; - TimeSpan _testOffset; - - public QuartzInMemoryTestFixture() - { - QuartzAddress = new Uri("loopback://localhost/quartz"); - _testOffset = TimeSpan.Zero; - - _messageScheduler = new Lazy(() => - new MessageScheduler(new EndpointScheduleMessageProvider(() => GetSendEndpoint(QuartzAddress)), Bus.Topology)); - } - - protected Uri QuartzAddress { get; } - - protected ISendEndpoint QuartzEndpoint { get; set; } - - protected IMessageScheduler Scheduler => _messageScheduler.Value; - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - configurator.UseInMemoryScheduler(out _schedulerFactory); - - base.ConfigureInMemoryBus(configurator); - } - - protected async Task AdvanceTime(TimeSpan duration) - { - var scheduler = await _schedulerFactory.GetScheduler(TestCancellationToken).ConfigureAwait(false); - - await scheduler.Standby().ConfigureAwait(false); - - _testOffset += duration; - - await scheduler.Start().ConfigureAwait(false); - } - - [OneTimeSetUp] - public async Task Setup_quartz_service() - { - QuartzEndpoint = await GetSendEndpoint(QuartzAddress); - - SystemTime.UtcNow = GetUtcNow; - SystemTime.Now = GetNow; - } - - [OneTimeTearDown] - public void Take_it_down() - { - SystemTime.UtcNow = () => DateTimeOffset.UtcNow; - SystemTime.Now = () => DateTimeOffset.Now; - } - - DateTimeOffset GetUtcNow() - { - return DateTimeOffset.UtcNow + _testOffset; - } - - DateTimeOffset GetNow() - { - return DateTimeOffset.Now + _testOffset; - } - } -} diff --git a/tests/MassTransit.MongoDbIntegration.Tests/MassTransit.MongoDbIntegration.Tests.csproj b/tests/MassTransit.MongoDbIntegration.Tests/MassTransit.MongoDbIntegration.Tests.csproj index 5dc515e88dc..8ca7b550557 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/MassTransit.MongoDbIntegration.Tests.csproj +++ b/tests/MassTransit.MongoDbIntegration.Tests/MassTransit.MongoDbIntegration.Tests.csproj @@ -1,7 +1,7 @@  - net6.0 + net8.0 @@ -12,6 +12,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/MassTransit.MongoDbIntegration.Tests/Outbox_Specs.cs b/tests/MassTransit.MongoDbIntegration.Tests/Outbox_Specs.cs index e6b19d8fd7c..3a19534f845 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/Outbox_Specs.cs +++ b/tests/MassTransit.MongoDbIntegration.Tests/Outbox_Specs.cs @@ -110,19 +110,12 @@ namespace Responsible public class ResponsibleStateDefinition : SagaDefinition { - readonly IServiceProvider _provider; - - public ResponsibleStateDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, - ISagaConfigurator consumerConfigurator) + ISagaConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(100, 1000)); - endpointConfigurator.UseMongoDbOutbox(_provider); + endpointConfigurator.UseMongoDbOutbox(context); } } diff --git a/tests/MassTransit.MongoDbIntegration.Tests/docker-compose.yml b/tests/MassTransit.MongoDbIntegration.Tests/docker-compose.yml index 4aa2da0dfcd..91922be8b62 100644 --- a/tests/MassTransit.MongoDbIntegration.Tests/docker-compose.yml +++ b/tests/MassTransit.MongoDbIntegration.Tests/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - services: mongo1: container_name: mongo1 diff --git a/tests/MassTransit.NHibernateIntegration.Tests/MassTransit.NHibernateIntegration.Tests.csproj b/tests/MassTransit.NHibernateIntegration.Tests/MassTransit.NHibernateIntegration.Tests.csproj index f6673da69cc..f499731804a 100644 --- a/tests/MassTransit.NHibernateIntegration.Tests/MassTransit.NHibernateIntegration.Tests.csproj +++ b/tests/MassTransit.NHibernateIntegration.Tests/MassTransit.NHibernateIntegration.Tests.csproj @@ -1,16 +1,19 @@  - net6.0 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - + diff --git a/tests/MassTransit.NHibernateIntegration.Tests/MissingInstance_Specs.cs b/tests/MassTransit.NHibernateIntegration.Tests/MissingInstance_Specs.cs index 8ff828cd460..63f2de31999 100644 --- a/tests/MassTransit.NHibernateIntegration.Tests/MissingInstance_Specs.cs +++ b/tests/MassTransit.NHibernateIntegration.Tests/MissingInstance_Specs.cs @@ -56,7 +56,7 @@ public async Task Should_publish_the_event_of_the_missing_instance() Response result = await notFound; - Assert.AreEqual("A", result.Message.ServiceName); + Assert.That(result.Message.ServiceName, Is.EqualTo("A")); Assert.That(async () => await status, Throws.TypeOf()); } diff --git a/tests/MassTransit.NHibernateIntegration.Tests/PreInsert_Specs.cs b/tests/MassTransit.NHibernateIntegration.Tests/PreInsert_Specs.cs index c6c413a3514..e76c42464a1 100644 --- a/tests/MassTransit.NHibernateIntegration.Tests/PreInsert_Specs.cs +++ b/tests/MassTransit.NHibernateIntegration.Tests/PreInsert_Specs.cs @@ -103,17 +103,20 @@ public async Task Should_receive_the_published_message() ConsumeContext received = await messageReceived; - Assert.AreEqual(message.CorrelationId, received.Message.TransactionId); + Assert.Multiple(() => + { + Assert.That(received.Message.TransactionId, Is.EqualTo(message.CorrelationId)); - Assert.IsTrue(received.InitiatorId.HasValue, "The initiator should be copied from the CorrelationId"); + Assert.That(received.InitiatorId.HasValue, Is.True, "The initiator should be copied from the CorrelationId"); - Assert.AreEqual(received.InitiatorId.Value, message.CorrelationId, "The initiator should be the saga CorrelationId"); + Assert.That(received.InitiatorId.Value, Is.EqualTo(message.CorrelationId), "The initiator should be the saga CorrelationId"); - Assert.AreEqual(received.SourceAddress, InputQueueAddress, "The published message should have the input queue source address"); + Assert.That(received.SourceAddress, Is.EqualTo(InputQueueAddress), "The published message should have the input queue source address"); + }); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -186,17 +189,20 @@ public async Task Should_receive_the_published_message() ConsumeContext received = await messageReceived; - Assert.AreEqual(sagaId, received.Message.TransactionId); + Assert.Multiple(() => + { + Assert.That(received.Message.TransactionId, Is.EqualTo(sagaId)); - Assert.IsTrue(received.InitiatorId.HasValue, "The initiator should be copied from the CorrelationId"); + Assert.That(received.InitiatorId.HasValue, Is.True, "The initiator should be copied from the CorrelationId"); - Assert.AreEqual(received.InitiatorId.Value, message.CorrelationId, "The initiator should be the saga CorrelationId"); + Assert.That(received.InitiatorId.Value, Is.EqualTo(message.CorrelationId), "The initiator should be the saga CorrelationId"); - Assert.AreEqual(received.SourceAddress, InputQueueAddress, "The published message should have the input queue source address"); + Assert.That(received.SourceAddress, Is.EqualTo(InputQueueAddress), "The published message should have the input queue source address"); + }); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.NHibernateIntegration.Tests/SagaLocator_Specs.cs b/tests/MassTransit.NHibernateIntegration.Tests/SagaLocator_Specs.cs index 4bba651d370..138e05a6e8d 100644 --- a/tests/MassTransit.NHibernateIntegration.Tests/SagaLocator_Specs.cs +++ b/tests/MassTransit.NHibernateIntegration.Tests/SagaLocator_Specs.cs @@ -6,7 +6,6 @@ using MassTransit.Tests.Saga.Messages; using NHibernate; using NUnit.Framework; - using Shouldly; using TestFramework; using Testing; @@ -26,7 +25,7 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; @@ -34,7 +33,7 @@ public async Task A_correlated_message_should_find_the_correct_saga() foundId = await _sagaRepository.Value.ShouldContainSaga(x => x.CorrelationId == sagaId && x.Completed, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); } [Test] @@ -47,7 +46,7 @@ public async Task An_initiating_message_should_start_the_saga() Guid? foundId = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId, Is.Not.Null); } readonly Lazy> _sagaRepository; diff --git a/tests/MassTransit.NHibernateIntegration.Tests/Saving_using_no_custom_types.cs b/tests/MassTransit.NHibernateIntegration.Tests/Saving_using_no_custom_types.cs index 4c7a697f2f3..2e76825049e 100644 --- a/tests/MassTransit.NHibernateIntegration.Tests/Saving_using_no_custom_types.cs +++ b/tests/MassTransit.NHibernateIntegration.Tests/Saving_using_no_custom_types.cs @@ -22,7 +22,7 @@ public async Task Should_have_the_state_machine() var instance = await GetStateMachine(correlationId); - Assert.IsTrue(instance.Screwed); + Assert.That(instance.Screwed, Is.True); } SuperShopper _machine; diff --git a/tests/MassTransit.NHibernateIntegration.Tests/UsingNHibernate_Specs.cs b/tests/MassTransit.NHibernateIntegration.Tests/UsingNHibernate_Specs.cs index 66408bc2f3f..2ba6d9f0a9c 100644 --- a/tests/MassTransit.NHibernateIntegration.Tests/UsingNHibernate_Specs.cs +++ b/tests/MassTransit.NHibernateIntegration.Tests/UsingNHibernate_Specs.cs @@ -20,12 +20,12 @@ public async Task Should_have_removed_the_state_machine() await InputQueueSendEndpoint.Send(new GirlfriendYelling { CorrelationId = correlationId }); Guid? sagaId = await _repository.ShouldContainSaga(correlationId, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); await InputQueueSendEndpoint.Send(new SodOff { CorrelationId = correlationId }); sagaId = await _repository.ShouldNotContainSaga(correlationId, TestTimeout); - Assert.IsFalse(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.False); } [Test] @@ -37,17 +37,17 @@ public async Task Should_have_the_state_machine() Guid? sagaId = await _repository.ShouldContainSaga(correlationId, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); await InputQueueSendEndpoint.Send(new GotHitByACar { CorrelationId = correlationId }); sagaId = await _repository.ShouldContainSagaInState(correlationId, _machine, x => x.Dead, TestTimeout); - Assert.IsTrue(sagaId.HasValue); + Assert.That(sagaId.HasValue, Is.True); var instance = await GetSaga(correlationId); - Assert.IsTrue(instance.Screwed); + Assert.That(instance.Screwed, Is.True); } SuperShopper _machine; diff --git a/tests/MassTransit.NHibernateIntegration.Tests/Vanilla_Specs.cs b/tests/MassTransit.NHibernateIntegration.Tests/Vanilla_Specs.cs index b9296f7a68e..d9d6f43eb70 100644 --- a/tests/MassTransit.NHibernateIntegration.Tests/Vanilla_Specs.cs +++ b/tests/MassTransit.NHibernateIntegration.Tests/Vanilla_Specs.cs @@ -22,7 +22,7 @@ public async Task Should_have_the_state_machine() var instance = await GetStateMachine(correlationId); - Assert.IsTrue(instance.Screwed); + Assert.That(instance.Screwed, Is.True); } SuperShopper _machine; diff --git a/tests/MassTransit.PrometheusIntegration.Tests/ActivityMetric_Specs.cs b/tests/MassTransit.PrometheusIntegration.Tests/ActivityMetric_Specs.cs deleted file mode 100644 index 5c4fae73ab9..00000000000 --- a/tests/MassTransit.PrometheusIntegration.Tests/ActivityMetric_Specs.cs +++ /dev/null @@ -1,96 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Tests -{ - using System; - using System.IO; - using System.Text; - using System.Threading.Tasks; - using Courier.Contracts; - using NUnit.Framework; - using Prometheus; - using TestFramework; - using TestFramework.Courier; - using Testing; - using Testing.Implementations; - - - [TestFixture] - public class ActivityMetric_Specs : - InMemoryActivityTestFixture - { - [Test] - public async Task Should_complete_with_metrics_available() - { - await _completed; - - await _activityMonitor.AwaitBusInactivity(TestCancellationToken); - - using var stream = new MemoryStream(); - await Metrics.DefaultRegistry.CollectAndExportAsTextAsync(stream); - - var text = Encoding.UTF8.GetString(stream.ToArray()); - - Console.WriteLine(text); - - Assert.That(text.Contains("mt_activity_execute_total{service_name=\"unit_test\",activity_name=\"SecondTest\",argument_type=\"Test\"} 1")); - Assert.That(text.Contains("mt_activity_execute_total{service_name=\"unit_test\",activity_name=\"Test\",argument_type=\"Test\"} 1")); - } - - Task> _completed; - RoutingSlip _routingSlip; - IBusActivityMonitor _activityMonitor; - - protected override void SetupActivities(BusTestHarness testHarness) - { - AddActivityContext(() => new TestActivity()); - AddActivityContext(() => new SecondTestActivity()); - } - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - base.ConfigureInMemoryBus(configurator); - - configurator.UsePrometheusMetrics(serviceName: "unit_test"); - - configurator.ReceiveEndpoint("events", x => - { - _completed = Handled(x); - - var testActivity = GetActivityContext(); - var secondActivity = GetActivityContext(); - - Handled(x, context => context.Message.ActivityName.Equals(testActivity.Name)); - Handled(x, context => context.Message.ActivityName.Equals(secondActivity.Name)); - }); - } - - protected override void ConnectObservers(IBus bus) - { - _activityMonitor = bus.CreateBusActivityMonitor(TimeSpan.FromMilliseconds(500)); - } - - [OneTimeSetUp] - public async Task Setup() - { - var testActivity = GetActivityContext(); - var secondActivity = GetActivityContext(); - - var builder = new RoutingSlipBuilder(Guid.NewGuid()); - - builder.AddActivity(testActivity.Name, testActivity.ExecuteUri, new - { - Value = "Hello", - NullValue = (string)null - }); - - builder.AddActivity(secondActivity.Name, secondActivity.ExecuteUri); - - builder.AddVariable("Variable", "Knife"); - builder.AddVariable("Nothing", null); - builder.AddVariable("ToBeRemoved", "Existing"); - - _routingSlip = builder.Build(); - - await Bus.Execute(_routingSlip); - } - } -} diff --git a/tests/MassTransit.PrometheusIntegration.Tests/BatchConsumer_Specs.cs b/tests/MassTransit.PrometheusIntegration.Tests/BatchConsumer_Specs.cs deleted file mode 100644 index cd8e60015d4..00000000000 --- a/tests/MassTransit.PrometheusIntegration.Tests/BatchConsumer_Specs.cs +++ /dev/null @@ -1,143 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Tests -{ - using System; - using System.IO; - using System.Text; - using System.Threading.Tasks; - using NUnit.Framework; - using Prometheus; - using TestFramework; - - - [TestFixture] - public class BatchConsumer_Specs : - InMemoryTestFixture - { - [Test] - public async Task Should_capture_the_bus_instance_metric() - { - await InputQueueSendEndpoint.Send(new BatchMessage()); - await InputQueueSendEndpoint.Send(new BatchMessage()); - await InputQueueSendEndpoint.Send(new BatchMessage()); - await InputQueueSendEndpoint.Send(new BatchMessage()); - await InputQueueSendEndpoint.Send(new BatchMessage()); - - await Bus.Publish(new BatchMessage()); - await Bus.Publish(new BatchMessage()); - await Bus.Publish(new BatchMessage()); - - await InactivityTask; - - using var stream = new MemoryStream(); - await Metrics.DefaultRegistry.CollectAndExportAsTextAsync(stream); - - var text = Encoding.UTF8.GetString(stream.ToArray()); - - Console.WriteLine(text); - - Assert.That(text.Contains("mt_publish_total{service_name=\"unit_test\",message_type=\"BatchMessage\"} 3"), "publish"); - Assert.That(text.Contains("mt_send_total{service_name=\"unit_test\",message_type=\"BatchMessage\"} 5"), "send"); - Assert.That(text.Contains("mt_receive_total{service_name=\"unit_test\",endpoint_address=\"input_queue\"} 8"), "receive"); - Assert.That( - text.Contains("mt_consume_total{service_name=\"unit_test\",message_type=\"BatchMessage\",consumer_type=\"BatchConsumer_BatchMessage\"} 8"), - "batch"); - Assert.That(text.Contains("mt_consume_total{service_name=\"unit_test\",message_type=\"Batch_BatchMessage\",consumer_type=\"TestBatchConsumer\"} 1"), - "consume"); - } - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - configurator.UsePrometheusMetrics(serviceName: "unit_test"); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.ConcurrentMessageLimit = 10; - configurator.Consumer(() => new TestBatchConsumer(), x => - x.Options(options => options.SetMessageLimit(8).SetTimeLimit(s: 5))); - } - - - public class BatchMessage - { - } - - - public class TestBatchConsumer : - IConsumer> - { - public Task Consume(ConsumeContext> context) - { - return Task.CompletedTask; - } - } - } - - - [TestFixture] - public class BatchConsumer_ConnectEndpoint : - InMemoryTestFixture - { - [Test] - public async Task Should_capture_the_bus_instance_metric() - { - var receiveEndpoint = Bus.ConnectReceiveEndpoint("batching", configurator => - { - if (configurator is IInMemoryReceiveEndpointConfigurator cfg) - cfg.ConcurrentMessageLimit = 10; - - configurator.Consumer(() => new TestBatchConsumer(), x => - x.Options(options => options.SetMessageLimit(8).SetTimeLimit(s: 5))); - }); - - await receiveEndpoint.Ready; - - await Bus.Publish(new BatchMessage()); - await Bus.Publish(new BatchMessage()); - await Bus.Publish(new BatchMessage()); - await Bus.Publish(new BatchMessage()); - - await Bus.Publish(new BatchMessage()); - await Bus.Publish(new BatchMessage()); - await Bus.Publish(new BatchMessage()); - await Bus.Publish(new BatchMessage()); - - await InactivityTask; - - using var stream = new MemoryStream(); - await Metrics.DefaultRegistry.CollectAndExportAsTextAsync(stream); - - var text = Encoding.UTF8.GetString(stream.ToArray()); - - Console.WriteLine(text); - - Assert.That(text.Contains("mt_publish_total{service_name=\"unit_test\",message_type=\"BatchMessage\"} 8"), "publish"); - Assert.That(text.Contains("mt_receive_total{service_name=\"unit_test\",endpoint_address=\"batching\"} 8"), "receive"); - Assert.That( - text.Contains("mt_consume_total{service_name=\"unit_test\",message_type=\"BatchMessage\",consumer_type=\"BatchConsumer_BatchMessage\"} 8"), - "batch"); - Assert.That(text.Contains("mt_consume_total{service_name=\"unit_test\",message_type=\"Batch_BatchMessage\",consumer_type=\"TestBatchConsumer\"} 1"), - "consume"); - } - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - configurator.UsePrometheusMetrics(serviceName: "unit_test"); - } - - - public class BatchMessage - { - } - - - public class TestBatchConsumer : - IConsumer> - { - public Task Consume(ConsumeContext> context) - { - return Task.CompletedTask; - } - } - } -} diff --git a/tests/MassTransit.PrometheusIntegration.Tests/BusInstanceCount_Specs.cs b/tests/MassTransit.PrometheusIntegration.Tests/BusInstanceCount_Specs.cs deleted file mode 100644 index d5124b1102a..00000000000 --- a/tests/MassTransit.PrometheusIntegration.Tests/BusInstanceCount_Specs.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Tests -{ - using System.IO; - using System.Text; - using System.Threading.Tasks; - using NUnit.Framework; - using Prometheus; - using TestFramework; - - - [TestFixture] - public class BusInstanceCount_Specs : - InMemoryTestFixture - { - [Test] - public async Task Should_capture_the_bus_instance_metric() - { - using var stream = new MemoryStream(); - await Metrics.DefaultRegistry.CollectAndExportAsTextAsync(stream); - - var text = Encoding.UTF8.GetString(stream.ToArray()); - - Assert.That(text.Contains("mt_bus{service_name=\"unit_test\"} 1")); - } - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - configurator.UsePrometheusMetrics(serviceName: "unit_test"); - } - } -} diff --git a/tests/MassTransit.PrometheusIntegration.Tests/JobConsumer_Specs.cs b/tests/MassTransit.PrometheusIntegration.Tests/JobConsumer_Specs.cs deleted file mode 100644 index 26ad4398e3d..00000000000 --- a/tests/MassTransit.PrometheusIntegration.Tests/JobConsumer_Specs.cs +++ /dev/null @@ -1,97 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Tests -{ - using System; - using System.IO; - using System.Text; - using System.Threading.Tasks; - using Contracts.JobService; - using NUnit.Framework; - using Prometheus; - using TestFramework; - - - [TestFixture] - public class JobConsumer_Specs : - InMemoryTestFixture - { - [Test] - public async Task Should_capture_the_bus_instance_metric() - { - IRequestClient> requestClient = Bus.CreateRequestClient>(); - - var jobId = NewId.NextGuid(); - await requestClient.GetResponse(new - { - JobId = jobId, - Job = new { Duration = TimeSpan.FromSeconds(30) } - }); - - await InactivityTask; - - using var stream = new MemoryStream(); - await Metrics.DefaultRegistry.CollectAndExportAsTextAsync(stream); - - var text = Encoding.UTF8.GetString(stream.ToArray()); - - Console.WriteLine(text); - - Assert.That(text.Contains("mt_send_total{service_name=\"unit_test\",message_type=\"AllocateJobSlot\"} 1"), "allocate"); - Assert.That(text.Contains("mt_send_total{service_name=\"unit_test\",message_type=\"JobSlotAllocated\"} 1"), "allocated"); - Assert.That(text.Contains("mt_send_total{service_name=\"unit_test\",message_type=\"JobSlotReleased\"} 1"), "released"); - Assert.That(text.Contains("mt_send_total{service_name=\"unit_test\",message_type=\"StartJobAttempt\"} 1"), "startJobAttempt"); - Assert.That(text.Contains("mt_send_total{service_name=\"unit_test\",message_type=\"StartJob\"} 1"), "startJob"); - Assert.That(text.Contains("mt_send_total{service_name=\"unit_test\",message_type=\"JobSubmissionAccepted\"} 1"), "accepted"); - Assert.That(text.Contains("mt_publish_total{service_name=\"unit_test\",message_type=\"SubmitJob_TheJob\"} 1"), "submitTheJob"); - Assert.That( - text.Contains("mt_consume_total{service_name=\"unit_test\",message_type=\"SubmitJob_TheJob\",consumer_type=\"SubmitJobConsumer_TheJob\"} 1"), - "submit job"); - Assert.That( - text.Contains("mt_consume_total{service_name=\"unit_test\",message_type=\"TheJob\",consumer_type=\"TestJobConsumer\"} 1"), - "submit job"); - } - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - configurator.UseDelayedMessageScheduler(); - - configurator.UsePrometheusMetrics(serviceName: "unit_test"); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.Consumer(() => new TestJobConsumer(), cfg => - { - cfg.Options>(jobOptions => jobOptions.SetJobTimeout(TimeSpan.FromSeconds(90))); - }); - }); - }); - } - - public JobConsumer_Specs() - { - TestInactivityTimeout = TimeSpan.FromSeconds(1); - } - - - public class TestJobConsumer : - IJobConsumer - { - public Task Run(JobContext context) - { - return Task.CompletedTask; - } - } - - - public interface TheJob - { - TimeSpan Duration { get; } - } - } -} diff --git a/tests/MassTransit.PrometheusIntegration.Tests/MassTransit.PrometheusIntegration.Tests.csproj b/tests/MassTransit.PrometheusIntegration.Tests/MassTransit.PrometheusIntegration.Tests.csproj deleted file mode 100644 index 5569fcb836e..00000000000 --- a/tests/MassTransit.PrometheusIntegration.Tests/MassTransit.PrometheusIntegration.Tests.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - - - diff --git a/tests/MassTransit.PrometheusIntegration.Tests/MessageMetric_Specs.cs b/tests/MassTransit.PrometheusIntegration.Tests/MessageMetric_Specs.cs deleted file mode 100644 index 16d1bb46ebf..00000000000 --- a/tests/MassTransit.PrometheusIntegration.Tests/MessageMetric_Specs.cs +++ /dev/null @@ -1,99 +0,0 @@ -namespace MassTransit.PrometheusIntegration.Tests -{ - using System; - using System.IO; - using System.Text; - using System.Threading.Tasks; - using NUnit.Framework; - using Prometheus; - using TestFramework; - using TestFramework.Messages; - using Testing; - using Testing.Implementations; - - - [TestFixture] - public class MessageMetric_Specs : - InMemoryTestFixture - { - [Test] - public async Task Should_capture_the_bus_instance_metric() - { - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - await InputQueueSendEndpoint.Send(new PingMessage()); - - await Bus.Publish(new PingMessage()); - await Bus.Publish(new PingMessage()); - await Bus.Publish(new PingMessage()); - - await Bus.Publish>(new {Message = new LongMessage()}); - - await _activityMonitor.AwaitBusInactivity(TestCancellationToken); - - using var stream = new MemoryStream(); - await Metrics.DefaultRegistry.CollectAndExportAsTextAsync(stream); - - var text = Encoding.UTF8.GetString(stream.ToArray()); - - Console.WriteLine(text); - - Assert.That(text.Contains("mt_publish_total{service_name=\"unit_test\",message_type=\"PingMessage\"} 3"), "publish"); - Assert.That(text.Contains("mt_send_total{service_name=\"unit_test\",message_type=\"PingMessage\"} 5"), "send"); - Assert.That(text.Contains("mt_receive_total{service_name=\"unit_test\",endpoint_address=\"input_queue\"} 9"), "receive"); - Assert.That(text.Contains("mt_consume_total{service_name=\"unit_test\",message_type=\"PingMessage\",consumer_type=\"TestConsumer\"} 8"), "consume"); - } - - IBusActivityMonitor _activityMonitor; - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - configurator.UsePrometheusMetrics(serviceName: "unit_test"); - } - - protected override void ConnectObservers(IBus bus) - { - _activityMonitor = bus.CreateBusActivityMonitor(TimeSpan.FromMilliseconds(500)); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.Consumer(() => new TestConsumer()); - configurator.Consumer(() => new GenericConsumer>()); - } - } - - - public class LongMessage - { - } - - - public interface GenericMessage - { - T Message { get; } - } - - - public class GenericConsumer : - IConsumer - where T : class - { - public Task Consume(ConsumeContext context) - { - return Task.CompletedTask; - } - } - - - public class TestConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return Task.CompletedTask; - } - } -} diff --git a/tests/MassTransit.QuartzIntegration.Tests/Container_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/Container_Specs.cs index c483737a15a..af28de9d779 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/Container_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/Container_Specs.cs @@ -2,6 +2,8 @@ namespace MassTransit.QuartzIntegration.Tests { using System; using System.Threading.Tasks; + using MassTransit.Tests; + using MassTransit.Tests.Scenario; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Quartz; @@ -9,19 +11,23 @@ namespace MassTransit.QuartzIntegration.Tests using Testing; - [TestFixture] - public class Using_the_container_setup_for_quartz + [TestFixture(typeof(Json))] + [TestFixture(typeof(RawJson))] + [TestFixture(typeof(NewtonsoftJson))] + [TestFixture(typeof(NewtonsoftRawJson))] + public class Using_quartz_with_serializer + where T : new() { [Test] - public async Task Should_have_an_even_cleaner_experience_without_owning_the_container() + public async Task Should_work_properly() { await using var provider = new ServiceCollection() - .AddQuartz(q => - { - q.UseMicrosoftDependencyInjectionJobFactory(); + .AddQuartz(_ => { }) .AddMassTransitTestHarness(x => { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(3)); + x.AddPublishMessageScheduler(); x.AddQuartzConsumers(); @@ -33,6 +39,8 @@ public async Task Should_have_an_even_cleaner_experience_without_owning_the_cont { cfg.UsePublishMessageScheduler(); + _configuration?.ConfigureBus(context, cfg); + cfg.ConfigureEndpoints(context); }); }) @@ -40,20 +48,84 @@ public async Task Should_have_an_even_cleaner_experience_without_owning_the_cont using var adjustment = new QuartzTimeAdjustment(provider); - var harness = provider.GetTestHarness(); - harness.TestInactivityTimeout = TimeSpan.FromSeconds(2); - - await harness.Start(); + var harness = await provider.StartTestHarness(); await harness.Bus.Publish(new { }); - Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + + Assert.That(await harness.Consumed.Any(), Is.True); + }); + + await adjustment.AdvanceTime(TimeSpan.FromSeconds(10)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + } + + [Test] + public async Task Should_work_properly_with_message_headers() + { + await using var provider = new ServiceCollection() + .AddQuartz(_ => + { + }) + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(3)); + + x.AddPublishMessageScheduler(); + + x.AddQuartzConsumers(); + + x.AddConsumer(); + x.AddConsumer(); + + x.UsingInMemory((context, cfg) => + { + cfg.UsePublishMessageScheduler(); + + _configuration?.ConfigureBus(context, cfg); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + using var adjustment = new QuartzTimeAdjustment(provider); - Assert.That(await harness.Consumed.Any(), Is.True); + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new { }, x => x.Headers.Set("SimpleHeader", "SimpleValue")); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + + Assert.That(await harness.Consumed.Any(), Is.True); + }); await adjustment.AdvanceTime(TimeSpan.FromSeconds(10)); Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + + ConsumeContext context = + (await harness.GetConsumerHarness().Consumed.SelectAsync().First()).Context; + + Assert.Multiple(() => + { + Assert.That(context.Headers.TryGetHeader("SimpleHeader", out var header), Is.True); + + Assert.That(header, Is.EqualTo("SimpleValue")); + }); + } + + readonly ITestBusConfiguration _configuration; + + public Using_quartz_with_serializer() + { + _configuration = new T() as ITestBusConfiguration; } diff --git a/tests/MassTransit.QuartzIntegration.Tests/Courier_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/Courier_Specs.cs index 195af6a2f1b..eb3da16afd0 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/Courier_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/Courier_Specs.cs @@ -50,7 +50,7 @@ public async Task Should_retry_and_eventually_succeed() await completed; - Assert.IsFalse(activityFaulted.IsCompleted); + Assert.That(activityFaulted.IsCompleted, Is.False); } protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) @@ -111,7 +111,7 @@ public async Task Should_retry_and_eventually_succeed() await completed; - Assert.IsFalse(activityFaulted.IsCompleted); + Assert.That(activityFaulted.IsCompleted, Is.False); } protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) diff --git a/tests/MassTransit.QuartzIntegration.Tests/DelayRetry_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/DelayRetry_Specs.cs index 8416377edfc..3a7b5f3f11d 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/DelayRetry_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/DelayRetry_Specs.cs @@ -19,7 +19,7 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _received.Task; - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); } TaskCompletionSource> _received; @@ -74,7 +74,7 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _consumer.Received; - Assert.GreaterOrEqual(_consumer.ReceivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_consumer.ReceivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); } MyConsumer _consumer; @@ -95,7 +95,6 @@ class MyConsumer : { readonly TaskCompletionSource> _received; int _count; - TimeSpan _receivedTimeSpan; Stopwatch _timer; public MyConsumer(TaskCompletionSource> taskCompletionSource) @@ -105,7 +104,7 @@ public MyConsumer(TaskCompletionSource> taskCompleti public Task> Received => _received.Task; - public IComparable ReceivedTimeSpan => _receivedTimeSpan; + public TimeSpan ReceivedTimeSpan { get; private set; } public Task Consume(ConsumeContext context) { @@ -123,7 +122,7 @@ public Task Consume(ConsumeContext context) Console.WriteLine("{0} okay, now is good (retried {1} times)", DateTime.UtcNow, context.Headers.Get("MT-Redelivery-Count", default(int?))); // okay, ready. - _receivedTimeSpan = _timer.Elapsed; + ReceivedTimeSpan = _timer.Elapsed; _received.TrySetResult(context); return Task.CompletedTask; @@ -143,9 +142,12 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _consumer.Received; - Assert.GreaterOrEqual(_consumer.ReceivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.Multiple(() => + { + Assert.That(_consumer.ReceivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); - Assert.That(_consumer.RedeliveryCount, Is.EqualTo(2)); + Assert.That(_consumer.RedeliveryCount, Is.EqualTo(2)); + }); } MyConsumer _consumer; @@ -164,7 +166,6 @@ class MyConsumer : { readonly TaskCompletionSource> _received; int _count; - TimeSpan _receivedTimeSpan; Stopwatch _timer; public MyConsumer(TaskCompletionSource> taskCompletionSource) @@ -174,7 +175,7 @@ public MyConsumer(TaskCompletionSource> taskCompleti public Task> Received => _received.Task; - public IComparable ReceivedTimeSpan => _receivedTimeSpan; + public TimeSpan ReceivedTimeSpan { get; private set; } public int RedeliveryCount { get; set; } @@ -194,7 +195,7 @@ public Task Consume(ConsumeContext context) Console.WriteLine("{0} okay, now is good (retried {1} times)", DateTime.UtcNow, context.Headers.Get("MT-Redelivery-Count", default(int?))); // okay, ready. - _receivedTimeSpan = _timer.Elapsed; + ReceivedTimeSpan = _timer.Elapsed; RedeliveryCount = context.GetRedeliveryCount(); _received.TrySetResult(context); @@ -215,10 +216,13 @@ public async Task Should_properly_order_the_middleware() ConsumeContext context = await _consumer.Received; - Assert.GreaterOrEqual(_consumer.ReceivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.Multiple(() => + { + Assert.That(_consumer.ReceivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); - Assert.That(_consumer.RedeliveryCount, Is.EqualTo(2)); - Assert.That(context.GetRedeliveryCount(), Is.EqualTo(2)); + Assert.That(_consumer.RedeliveryCount, Is.EqualTo(2)); + Assert.That(context.GetRedeliveryCount(), Is.EqualTo(2)); + }); } MyConsumer _consumer; @@ -243,7 +247,6 @@ class MyConsumer : { readonly TaskCompletionSource> _received; int _count; - TimeSpan _receivedTimeSpan; Stopwatch _timer; public MyConsumer(TaskCompletionSource> taskCompletionSource) @@ -253,7 +256,7 @@ public MyConsumer(TaskCompletionSource> taskCompleti public Task> Received => _received.Task; - public IComparable ReceivedTimeSpan => _receivedTimeSpan; + public TimeSpan ReceivedTimeSpan { get; private set; } public int RedeliveryCount { get; set; } @@ -273,7 +276,7 @@ public Task Consume(ConsumeContext context) Console.WriteLine("{0} okay, now is good (retried {1} times)", DateTime.UtcNow, context.Headers.Get("MT-Redelivery-Count", default(int?))); // okay, ready. - _receivedTimeSpan = _timer.Elapsed; + ReceivedTimeSpan = _timer.Elapsed; RedeliveryCount = context.GetRedeliveryCount(); _received.TrySetResult(context); @@ -294,7 +297,7 @@ public async Task Should_properly_defer_the_message_delivery() ConsumeContext context = await _received.Task; - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); } TaskCompletionSource> _received; @@ -350,9 +353,9 @@ public async Task callback_executed_before_defer_the_message_delivery() ConsumeContext context = await _received.Task; - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); var customHeaderValue = context.Headers.Get(customHeader, default(int?)); - Assert.AreEqual(2, customHeaderValue); + Assert.That(customHeaderValue, Is.EqualTo(2)); } TaskCompletionSource> _received; diff --git a/tests/MassTransit.QuartzIntegration.Tests/JobDetail_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/JobDetail_Specs.cs index 2dd01743af6..6e1a40c63a6 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/JobDetail_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/JobDetail_Specs.cs @@ -29,9 +29,12 @@ public async Task Should_return_the_properties() await scheduler.ScheduleJob(jobDetail, trigger).ConfigureAwait(false); - Assert.IsTrue(MyJob.Signaled.WaitOne(Utils.Timeout)); + Assert.Multiple(() => + { + Assert.That(MyJob.Signaled.WaitOne(Utils.Timeout), Is.True); - Assert.AreEqual("By Jake", MyJob.SignaledBody); + Assert.That(MyJob.SignaledBody, Is.EqualTo("By Jake")); + }); } [Test] @@ -52,9 +55,12 @@ public async Task Should_return_the_properties_with_custom_factory() await scheduler.ScheduleJob(jobDetail, trigger).ConfigureAwait(false); - Assert.IsTrue(MyJob.Signaled.WaitOne(Utils.Timeout)); + Assert.Multiple(() => + { + Assert.That(MyJob.Signaled.WaitOne(Utils.Timeout), Is.True); - Assert.AreEqual("By Jake", MyJob.SignaledBody); + Assert.That(MyJob.SignaledBody, Is.EqualTo("By Jake")); + }); } diff --git a/tests/MassTransit.QuartzIntegration.Tests/MassTransit.QuartzIntegration.Tests.csproj b/tests/MassTransit.QuartzIntegration.Tests/MassTransit.QuartzIntegration.Tests.csproj index 913dcd1eaa9..1ca3190e61e 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/MassTransit.QuartzIntegration.Tests.csproj +++ b/tests/MassTransit.QuartzIntegration.Tests/MassTransit.QuartzIntegration.Tests.csproj @@ -1,17 +1,22 @@  - net6.0 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + diff --git a/tests/MassTransit.QuartzIntegration.Tests/MissingInstanceRedelivery_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/MissingInstanceRedelivery_Specs.cs index 8353665c3b1..caa0d12d7dc 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/MissingInstanceRedelivery_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/MissingInstanceRedelivery_Specs.cs @@ -24,10 +24,13 @@ public async Task Should_schedule_the_message_and_redeliver_to_the_instance() Response result = await response; - Assert.That(result.Is(out Response _), Is.False); - Assert.That(result.Is(out Response status), Is.True); + Assert.Multiple(() => + { + Assert.That(result.Is(out Response _), Is.False); + Assert.That(result.Is(out Response status), Is.True); - Assert.AreEqual("A", status.Message.ServiceName); + Assert.That(status.Message.ServiceName, Is.EqualTo("A")); + }); (Task> statusTask, Task> notFoundTask) = result; diff --git a/tests/MassTransit.QuartzIntegration.Tests/PastEvent_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/PastEvent_Specs.cs index 9543fcb4176..4f3e7e03c24 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/PastEvent_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/PastEvent_Specs.cs @@ -69,19 +69,22 @@ public async Task Should_include_message_headers() ConsumeContext context = await handler; - Assert.AreEqual(Bus.Address, context.FaultAddress); - Assert.AreEqual(InputQueueAddress, context.ResponseAddress); - Assert.IsTrue(context.RequestId.HasValue); - Assert.AreEqual(requestId, context.RequestId.Value); - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.AreEqual(correlationId, context.CorrelationId.Value); - Assert.IsTrue(context.ConversationId.HasValue); - Assert.AreEqual(conversationId, context.ConversationId.Value); - Assert.IsTrue(context.InitiatorId.HasValue); - Assert.AreEqual(initiatorId, context.InitiatorId.Value); - - Assert.IsTrue(context.Headers.TryGetHeader("Hello", out var value)); - Assert.AreEqual("World", value); + Assert.Multiple(() => + { + Assert.That(context.FaultAddress, Is.EqualTo(Bus.Address)); + Assert.That(context.ResponseAddress, Is.EqualTo(InputQueueAddress)); + Assert.That(context.RequestId.HasValue, Is.True); + Assert.That(context.RequestId.Value, Is.EqualTo(requestId)); + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(correlationId)); + Assert.That(context.ConversationId.HasValue, Is.True); + Assert.That(context.ConversationId.Value, Is.EqualTo(conversationId)); + Assert.That(context.InitiatorId.HasValue, Is.True); + Assert.That(context.InitiatorId.Value, Is.EqualTo(initiatorId)); + + Assert.That(context.Headers.TryGetHeader("Hello", out var value), Is.True); + Assert.That(value, Is.EqualTo("World")); + }); } [Test] @@ -127,7 +130,7 @@ public async Task Should_reschedule() }); ConsumeContext result = await handler; - Assert.AreEqual(expected, result.Message.Name); + Assert.That(result.Message.Name, Is.EqualTo(expected)); } public Specifying_an_event_reschedule_if_exists() diff --git a/tests/MassTransit.QuartzIntegration.Tests/Recurring_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/Recurring_Specs.cs index c655a3f36fc..61f111e4cbc 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/Recurring_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/Recurring_Specs.cs @@ -24,7 +24,7 @@ public async Task Should_cancel_recurring_schedule() await _done; var countBeforeCancel = _count; - Assert.AreEqual(8, _count, "Expected to see 8 interval messages"); + Assert.That(_count, Is.EqualTo(8), "Expected to see 8 interval messages"); await Bus.CancelScheduledRecurringSend(scheduledRecurringMessage); @@ -32,7 +32,7 @@ public async Task Should_cancel_recurring_schedule() await _doneAgain; - Assert.AreEqual(countBeforeCancel, _count, "Expected to see the count matches."); + Assert.That(_count, Is.EqualTo(countBeforeCancel), "Expected to see the count matches."); } [Test] @@ -47,11 +47,14 @@ public async Task Should_contain_additional_headers_that_provide_schedule_key_co await _done; - Assert.Greater(_count, 0, "Expected to see at least one interval"); + Assert.Multiple(() => + { + Assert.That(_count, Is.GreaterThan(0), "Expected to see at least one interval"); - Assert.IsNotNull(_lastInterval.Headers.Get(MessageHeaders.Quartz.ScheduleId)); - Assert.IsNotNull(_lastInterval.Headers.Get(MessageHeaders.Quartz.ScheduleGroup)); + Assert.That(_lastInterval.Headers.Get(MessageHeaders.Quartz.ScheduleId), Is.Not.Null); + Assert.That(_lastInterval.Headers.Get(MessageHeaders.Quartz.ScheduleGroup), Is.Not.Null); + }); } [Test] @@ -66,13 +69,16 @@ public async Task Should_contain_additional_headers_that_provide_time_domain_con await _done; - Assert.Greater(_count, 0, "Expected to see at least one interval"); + Assert.Multiple(() => + { + Assert.That(_count, Is.GreaterThan(0), "Expected to see at least one interval"); - Assert.IsNotNull(_lastInterval.Headers.Get(MessageHeaders.Quartz.Scheduled)); - Assert.IsNotNull(_lastInterval.Headers.Get(MessageHeaders.Quartz.Sent)); - Assert.IsNotNull(_lastInterval.Headers.Get(MessageHeaders.Quartz.NextScheduled)); - Assert.IsNotNull(_lastInterval.Headers.Get(MessageHeaders.Quartz.PreviousSent)); + Assert.That(_lastInterval.Headers.Get(MessageHeaders.Quartz.Scheduled), Is.Not.Null); + Assert.That(_lastInterval.Headers.Get(MessageHeaders.Quartz.Sent), Is.Not.Null); + Assert.That(_lastInterval.Headers.Get(MessageHeaders.Quartz.NextScheduled), Is.Not.Null); + Assert.That(_lastInterval.Headers.Get(MessageHeaders.Quartz.PreviousSent), Is.Not.Null); + }); Console.WriteLine("{0}", _lastInterval.Headers.Get(MessageHeaders.Quartz.NextScheduled)); } @@ -87,7 +93,7 @@ public async Task Should_handle_now_properly() await _done; - Assert.AreEqual(8, _count, "Expected to see 8 interval messages"); + Assert.That(_count, Is.EqualTo(8), "Expected to see 8 interval messages"); } [Test] @@ -103,7 +109,7 @@ public async Task Should_pause_recurring_schedule() await _done; var countBeforeCancel = _count; - Assert.AreEqual(8, _count, "Expected to see 8 interval messages"); + Assert.That(_count, Is.EqualTo(8), "Expected to see 8 interval messages"); await Bus.PauseScheduledRecurringSend(scheduledRecurringMessage); @@ -111,7 +117,7 @@ public async Task Should_pause_recurring_schedule() await _doneAgain; - Assert.AreEqual(countBeforeCancel, _count, "Expected to see the count matches."); + Assert.That(_count, Is.EqualTo(countBeforeCancel), "Expected to see the count matches."); } Task> _done; diff --git a/tests/MassTransit.QuartzIntegration.Tests/RequestTimeout_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/RequestTimeout_Specs.cs index e65b571fe4e..4bcd1adc4b3 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/RequestTimeout_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/RequestTimeout_Specs.cs @@ -25,7 +25,7 @@ await InputQueueSendEndpoint.Send(new Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, x => x.AddressValidationTimeout, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } InMemorySagaRepository _repository; diff --git a/tests/MassTransit.QuartzIntegration.Tests/Request_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/Request_Specs.cs index 0338c186a51..45d39ff3597 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/Request_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/Request_Specs.cs @@ -4,6 +4,7 @@ namespace Request_Specs { using System; using System.Threading.Tasks; + using Contracts; using NUnit.Framework; using Testing; @@ -31,10 +32,10 @@ await InputQueueSendEndpoint.Send(new Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, x => x.Registered, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var sagaInstance = _repository[saga.Value].Instance; - Assert.IsFalse(sagaInstance.ValidateAddressRequestId.HasValue); + Assert.That(sagaInstance.ValidateAddressRequestId.HasValue, Is.False); } static Sending_a_request_from_a_state_machine() @@ -90,12 +91,12 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin protected virtual void ConfigureServiceQueueEndpoint(IReceiveEndpointConfigurator configurator) { - configurator.Handler(async context => + configurator.Handler(context => { Console.WriteLine("Address validated: {0}", context.Message.CorrelationId); if (context.IsResponseAccepted(false)) - await context.RespondAsync(new { }); + return context.RespondAsync(new { }); throw new InvalidOperationException("Response type not accepted"); }); @@ -111,7 +112,7 @@ protected virtual void ConfigureServiceQueueEndpoint(IReceiveEndpointConfigurato class RequestSettingsImpl : - RequestSettings + RequestSettings { public RequestSettingsImpl(Uri serviceAddress, Uri schedulingServiceAddress, TimeSpan timeout) { @@ -125,7 +126,12 @@ public RequestSettingsImpl(Uri serviceAddress, Uri schedulingServiceAddress, Tim public Uri ServiceAddress { get; } public TimeSpan Timeout { get; } - public TimeSpan? TimeToLive { get; } + public bool ClearRequestIdOnFaulted => false; + public TimeSpan? TimeToLive => null; + public Action> Completed { get; set; } + public Action> Completed2 { get; set; } + public Action>> Faulted { get; set; } + public Action>> TimeoutExpired { get; set; } } @@ -176,6 +182,15 @@ public interface AddressValidated : } + public interface AddressInvalidated : + CorrelatedBy + { + string Address { get; } + + string RequestAddress { get; } + } + + public interface ValidateName : CorrelatedBy { @@ -195,7 +210,7 @@ public interface NameValidated : class TestStateMachine : MassTransitStateMachine { - public TestStateMachine(RequestSettings settings) + public TestStateMachine(RequestSettings settings) { Event(() => Register, x => { @@ -257,7 +272,8 @@ public TestStateMachine(RequestSettings settings) .ThenAsync(async context => await Console.Out.WriteLineAsync("Request timed out")) .TransitionTo(NameValidationTimeout)); } // ReSharper disable UnassignedGetOnlyAutoProperty - public Request ValidateAddress { get; } + + public Request ValidateAddress { get; } public Request ValidateName { get; } public Event Register { get; } diff --git a/tests/MassTransit.QuartzIntegration.Tests/Reschedule_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/Reschedule_Specs.cs index 260738fddf2..08249ff2298 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/Reschedule_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/Reschedule_Specs.cs @@ -25,18 +25,22 @@ public async Task Should_reschedule_the_message_with_a_new_token_id() var sagaInstance = _repository[correlationId].Instance; - Assert.NotNull(rescheduledEvent.Message.NewScheduleTokenId); - Assert.AreEqual(sagaInstance.CorrelationId, rescheduledEvent.Message.CorrelationId); - Assert.AreEqual(sagaInstance.ScheduleId, rescheduledEvent.Message.NewScheduleTokenId); + Assert.Multiple(() => + { + Assert.That(rescheduledEvent.Message.NewScheduleTokenId, Is.Not.Null); + Assert.That(rescheduledEvent.Message.CorrelationId, Is.EqualTo(sagaInstance.CorrelationId)); + Assert.That(rescheduledEvent.Message.NewScheduleTokenId, Is.EqualTo(sagaInstance.ScheduleId)); + }); await InputQueueSendEndpoint.Send(new StopCommand(correlationId)); - Guid? saga = await _repository.ShouldNotContainSaga(correlationId, TestTimeout); + Guid? saga = await LoadSagaRepository.ShouldNotContainSaga(correlationId, TestTimeout); - Assert.IsNull(saga); + Assert.That(saga, Is.Null); } InMemorySagaRepository _repository; + ILoadSagaRepository LoadSagaRepository => _repository; TestStateMachine _machine; Task> _rescheduled; diff --git a/tests/MassTransit.QuartzIntegration.Tests/ScheduleMessage_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/ScheduleMessage_Specs.cs index 9d445f4c720..8a93a461801 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/ScheduleMessage_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/ScheduleMessage_Specs.cs @@ -24,7 +24,7 @@ public async Task Should_get_both_messages() await _second; if (_secondActivityId != null && _firstActivityId != null) - Assert.That(_secondActivityId.StartsWith(_firstActivityId), Is.True); + Assert.That(_secondActivityId, Does.StartWith(_firstActivityId)); } Task> _second; @@ -58,6 +58,118 @@ public class SecondMessage } + [TestFixture] + public class ScheduleMessageUsingRawJson_Specs : + QuartzInMemoryTestFixture + { + [Test] + public async Task Should_get_both_messages() + { + await Scheduler.ScheduleSend(InputQueueAddress, DateTime.Now, new FirstMessage()); + + await _first; + + await AdvanceTime(TimeSpan.FromSeconds(10)); + + await _second; + + if (_secondActivityId != null && _firstActivityId != null) + Assert.That(_secondActivityId, Does.StartWith(_firstActivityId)); + } + + Task> _second; + Task> _first; + string _firstActivityId; + string _secondActivityId; + + protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) + { + configurator.UseRawJsonSerializer(); + base.ConfigureInMemoryBus(configurator); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + _first = Handler(configurator, async context => + { + _firstActivityId = Activity.Current?.Id; + await context.ScheduleSend(TimeSpan.FromSeconds(10), new SecondMessage()); + }); + + _second = Handler(configurator, async context => + { + _secondActivityId = Activity.Current?.Id; + }); + } + + + public class FirstMessage + { + } + + + public class SecondMessage + { + } + } + + + [TestFixture] + public class ScheduleMessageUsingNewtonsoftRawJson_Specs : + QuartzInMemoryTestFixture + { + [Test] + public async Task Should_get_both_messages() + { + await Scheduler.ScheduleSend(InputQueueAddress, DateTime.Now, new FirstMessage()); + + await _first; + + await AdvanceTime(TimeSpan.FromSeconds(10)); + + await _second; + + if (_secondActivityId != null && _firstActivityId != null) + Assert.That(_secondActivityId, Does.StartWith(_firstActivityId)); + } + + Task> _second; + Task> _first; + string _firstActivityId; + string _secondActivityId; + + protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) + { + configurator.UseNewtonsoftRawJsonSerializer(RawSerializerOptions.CopyHeaders | RawSerializerOptions.AddTransportHeaders); + base.ConfigureInMemoryBus(configurator); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + _first = Handler(configurator, async context => + { + _firstActivityId = Activity.Current?.Id; + await context.ScheduleSend(TimeSpan.FromSeconds(10), new SecondMessage()); + }); + + _second = Handler(configurator, async context => + { + _secondActivityId = Activity.Current?.Id; + }); + } + + + public class FirstMessage + { + } + + + public class SecondMessage + { + } + } + + [TestFixture] public class ScheduleMessageUsingBson_Specs : QuartzInMemoryTestFixture @@ -74,7 +186,7 @@ public async Task Should_get_both_messages() await _second; if (_secondActivityId != null && _firstActivityId != null) - Assert.That(_secondActivityId.StartsWith(_firstActivityId), Is.True); + Assert.That(_secondActivityId, Does.StartWith(_firstActivityId)); } Task> _second; @@ -131,7 +243,7 @@ public async Task Should_get_both_messages() await _second; if (_secondActivityId != null && _firstActivityId != null) - Assert.That(_secondActivityId.StartsWith(_firstActivityId), Is.True); + Assert.That(_secondActivityId, Does.StartWith(_firstActivityId)); } Task> _second; @@ -192,8 +304,11 @@ public async Task Should_include_it_with_the_final_message() ConsumeContext second = await _second; - Assert.That(second.ExpirationTime.HasValue, Is.True); - Assert.That(second.ExpirationTime.Value, Is.GreaterThan(DateTime.UtcNow + TimeSpan.FromSeconds(20))); + Assert.Multiple(() => + { + Assert.That(second.ExpirationTime.HasValue, Is.True); + Assert.That(second.ExpirationTime.Value, Is.GreaterThan(DateTime.UtcNow + TimeSpan.FromSeconds(20))); + }); } Task> _second; diff --git a/tests/MassTransit.QuartzIntegration.Tests/ScheduleTimeout_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/ScheduleTimeout_Specs.cs index 30eadec3d85..1b90a8bb453 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/ScheduleTimeout_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/ScheduleTimeout_Specs.cs @@ -23,7 +23,7 @@ public async Task Should_cancel_when_the_order_is_submitted() Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, _machine.Active, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await InputQueueSendEndpoint.Send(new { MemberNumber = memberNumber }); @@ -55,7 +55,7 @@ public async Task Should_reschedule_the_timeout_when_items_are_added() Guid? saga = await _repository.ShouldContainSagaInState(x => x.MemberNumber == memberNumber, _machine, _machine.Active, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await InputQueueSendEndpoint.Send(new { MemberNumber = memberNumber }); diff --git a/tests/MassTransit.QuartzIntegration.Tests/ScheduledRedelivery_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/ScheduledRedelivery_Specs.cs index d630e993c33..8ef4bb64f1e 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/ScheduledRedelivery_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/ScheduledRedelivery_Specs.cs @@ -19,9 +19,12 @@ public async Task Should_use_the_correct_intervals_for_each_redelivery() await Task.WhenAll(_received.Select(x => x.Task)); - Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); - Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2))); - Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(3))); + Assert.Multiple(() => + { + Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); + Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2))); + Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(3))); + }); TestContext.Out.WriteLine("Interval: {0}", _timestamps[1] - _timestamps[0]); TestContext.Out.WriteLine("Interval: {0}", _timestamps[2] - _timestamps[1]); diff --git a/tests/MassTransit.QuartzIntegration.Tests/SchedulerLoadInMemory_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/SchedulerLoadInMemory_Specs.cs index 2322d9947ab..40f387d3545 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/SchedulerLoadInMemory_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/SchedulerLoadInMemory_Specs.cs @@ -29,7 +29,7 @@ public async Task Should_remove_the_saga_once_completed() await Task.Delay(1000); - Assert.AreEqual(0, _repository.Count); + Assert.That(_repository.Count, Is.EqualTo(0)); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.QuartzIntegration.Tests/Service_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/Service_Specs.cs index d10e607dd2d..2bd90a3422f 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/Service_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/Service_Specs.cs @@ -52,7 +52,7 @@ public async Task Should_properly_send_the_message() ConsumeContext context = await handlerIA; - Assert.IsTrue(context.GetQuartzSent().HasValue); + Assert.That(context.GetQuartzSent().HasValue, Is.True); } diff --git a/tests/MassTransit.QuartzIntegration.Tests/Turnout/Canceled_Specs.cs b/tests/MassTransit.QuartzIntegration.Tests/Turnout/Canceled_Specs.cs index e473057cc0b..d91e05cd671 100644 --- a/tests/MassTransit.QuartzIntegration.Tests/Turnout/Canceled_Specs.cs +++ b/tests/MassTransit.QuartzIntegration.Tests/Turnout/Canceled_Specs.cs @@ -27,12 +27,7 @@ public async Task Should_get_the_job_accepted() ConsumeContext started = await _started; - await Bus.Publish(new - { - JobId = _jobId, - Reason = "I give up", - InVar.Timestamp - }); + await Bus.CancelJob(_jobId, "I give up"); // just to capture all the test output in a single window ConsumeContext cancelled = await _cancelled; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/AlternateExchange_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/AlternateExchange_Specs.cs index dcdb7b7aeec..564bf8ad645 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/AlternateExchange_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/AlternateExchange_Specs.cs @@ -1,8 +1,102 @@ namespace MassTransit.RabbitMqTransport.Tests { + using System; using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using RabbitMQ.Client; + using TestFramework.Messages; + using Testing; + + + public class Using_alternate_exchange_with_the_outbox + { + [Test] + public async Task Should_deal_with_the_alternate_exchange() + { + const string alternateExchangeName = "unused-message"; + const string alternateQueueName = "unused-message-queue"; + + await using var provider = new ServiceCollection() + .ConfigureRabbitMqTestOptions(options => + { + options.CleanVirtualHost = true; + options.CreateVirtualHostIfNotExists = true; + }) + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => options.VHost = "test"); + + x.AddInMemoryInboxOutbox(); + + x.AddHandler((UnusedMessage _) => Task.CompletedTask) + .Endpoint(e => + { + e.ConfigureConsumeTopology = false; + e.Name = alternateQueueName; + }); + + x.AddConsumer() + .Endpoint(e => e.Name = "outbox-normal"); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.AddConfigureEndpointsCallback((provider, name, cfg) => + { + if (cfg is IRabbitMqReceiveEndpointConfigurator rmq) + { + if (name == alternateQueueName) + { + rmq.Bind(alternateExchangeName); + } + } + + cfg.UseInMemoryInboxOutbox(provider); + }); + + x.UsingRabbitMq((context, cfg) => + { + cfg.PublishTopology.GetMessageTopology() + .BindAlternateExchangeQueue(alternateExchangeName); + + cfg.DeployPublishTopology = true; + + cfg.ConfigureEndpoints(context); + }); + }).BuildServiceProvider(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new PingMessage(), Pipe.Execute(context => + { + }), harness.CancellationToken); + + IReceivedMessage message = await harness.Consumed.SelectAsync().FirstOrDefault(); + + Assert.That(message, Is.Not.Null); + + // Assert.That(message.Context.TryGetPayload(out var rmqContext), Is.True); + // Assert.That(rmqContext.Properties.Headers.TryGetValue(), Is.EqualTo(3)); + } + + + class MessageHandler : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return context.Publish(new UnusedMessage()); + } + } + + + class UnusedMessage + { + } + } [TestFixture] @@ -17,6 +111,19 @@ public async Task Should_create_and_bind_the_exchange_and_properties() await _handled; } + [Test] + public async Task Should_have_the_proper_address() + { + Assert.Multiple(() => + { + Assert.That(Bus.Topology.TryGetPublishAddress(out var address)); + + Assert.That(address, + Is.EqualTo(new Uri( + "rabbitmq://localhost/test/MassTransit.RabbitMqTransport.Tests:AlternateExchange_Specs-TheWorldImploded?alternateexchange=publish-not-delivered"))); + }); + } + Task> _handled; const string AlternateExchangeName = "publish-not-delivered"; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Batching_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Batching_Specs.cs index daac86fd5c9..4022e005d4d 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Batching_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Batching_Specs.cs @@ -20,7 +20,7 @@ public async Task Should_receive_the_message_batch() Batch batch = await _consumer[0]; - Assert.That(batch.Length, Is.EqualTo(5)); + Assert.That(batch, Has.Length.EqualTo(5)); Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Size)); } @@ -54,12 +54,12 @@ public async Task Should_receive_the_message_batch() Batch batch = await _consumer[0]; - Assert.That(batch.Length, Is.EqualTo(5)); + Assert.That(batch, Has.Length.EqualTo(5)); Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Time)); batch = await _consumer[1]; - Assert.That(batch.Length, Is.EqualTo(5)); + Assert.That(batch, Has.Length.EqualTo(5)); Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Time)); } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/BuildTopology_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/BuildTopology_Specs.cs index 556a7aae9ab..64769ff9770 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/BuildTopology_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/BuildTopology_Specs.cs @@ -109,15 +109,23 @@ public void Should_include_a_binding_for_the_second_interface_only() var topology = _builder.BuildBrokerTopology(); - var interfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)).ToString(); - - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == interfaceName), Is.True); - Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == interfaceName && x.Destination.ExchangeName == _inputQueueName), Is.True); - - interfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)).ToString(); - - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == interfaceName), Is.False); - Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == interfaceName && x.Destination.ExchangeName == _inputQueueName), Is.False); + var interfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)); + + Assert.Multiple(() => + { + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == interfaceName), Is.True); + Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == interfaceName && x.Destination.ExchangeName == _inputQueueName), + Is.True); + }); + + interfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)); + + Assert.Multiple(() => + { + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == interfaceName), Is.False); + Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == interfaceName && x.Destination.ExchangeName == _inputQueueName), + Is.False); + }); } [Test] @@ -130,18 +138,21 @@ public void Should_include_a_binding_for_the_single_interface() var topology = _builder.BuildBrokerTopology(); - var singleInterfaceName = _nameFormatter.GetMessageName(typeof(SingleInterface)).ToString(); + var singleInterfaceName = _nameFormatter.GetMessageName(typeof(SingleInterface)); - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); - Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == singleInterfaceName && x.Destination.ExchangeName == _inputQueueName), - Is.True); + Assert.Multiple(() => + { + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); + Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == singleInterfaceName && x.Destination.ExchangeName == _inputQueueName), + Is.True); + }); } [SetUp] public void Setup() { _nameFormatter = new RabbitMqMessageNameFormatter(); - _consumeTopology = new RabbitMqConsumeTopology(RabbitMqBusFactory.MessageTopology, new RabbitMqPublishTopology(RabbitMqBusFactory.MessageTopology)); + _consumeTopology = new RabbitMqConsumeTopology(RabbitMqBusFactory.CreateMessageTopology(), new RabbitMqPublishTopology(RabbitMqBusFactory.CreateMessageTopology())); _builder = new ReceiveEndpointBrokerTopologyBuilder(); @@ -171,16 +182,22 @@ public void Should_include_a_binding_for_the_second_interface_only() var topology = _builder.BuildBrokerTopology(); - var singleInterfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)).ToString(); - var interfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)).ToString(); - - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == interfaceName), Is.True); - Assert.That(topology.Exchanges.Length, Is.EqualTo(2)); - Assert.That(topology.ExchangeBindings.Length, Is.EqualTo(1)); - Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == interfaceName && x.Destination.ExchangeName == singleInterfaceName), - Is.True); - - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); + var singleInterfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)); + var interfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)); + + Assert.Multiple(() => + { + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == interfaceName), Is.True); + Assert.That(topology.Exchanges, Has.Length.EqualTo(2)); + Assert.That(topology.ExchangeBindings, Has.Length.EqualTo(1)); + }); + Assert.Multiple(() => + { + Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == interfaceName && x.Destination.ExchangeName == singleInterfaceName), + Is.True); + + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); + }); } [Test] @@ -191,18 +208,21 @@ public void Should_include_a_binding_for_the_single_interface() var topology = _builder.BuildBrokerTopology(); - var singleInterfaceName = _nameFormatter.GetMessageName(typeof(SingleInterface)).ToString(); + var singleInterfaceName = _nameFormatter.GetMessageName(typeof(SingleInterface)); - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); - Assert.That(topology.Exchanges.Length, Is.EqualTo(1)); - Assert.That(topology.ExchangeBindings.Length, Is.EqualTo(0)); + Assert.Multiple(() => + { + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); + Assert.That(topology.Exchanges, Has.Length.EqualTo(1)); + Assert.That(topology.ExchangeBindings, Is.Empty); + }); } [SetUp] public void Setup() { _nameFormatter = new RabbitMqMessageNameFormatter(); - _publishTopology = new RabbitMqPublishTopology(RabbitMqBusFactory.MessageTopology); + _publishTopology = new RabbitMqPublishTopology(RabbitMqBusFactory.CreateMessageTopology()); _builder = new PublishEndpointBrokerTopologyBuilder(); } @@ -225,16 +245,22 @@ public void Should_include_a_binding_for_the_second_interface_only() var topology = _builder.BuildBrokerTopology(); topology.LogResult(); - var singleInterfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)).ToString(); - var interfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)).ToString(); - - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == interfaceName), Is.True); - Assert.That(topology.Exchanges.Length, Is.EqualTo(2)); - Assert.That(topology.ExchangeBindings.Length, Is.EqualTo(1)); - Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == interfaceName && x.Destination.ExchangeName == singleInterfaceName), - Is.True); - - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); + var singleInterfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)); + var interfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)); + + Assert.Multiple(() => + { + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == interfaceName), Is.True); + Assert.That(topology.Exchanges, Has.Length.EqualTo(2)); + Assert.That(topology.ExchangeBindings, Has.Length.EqualTo(1)); + }); + Assert.Multiple(() => + { + Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == interfaceName && x.Destination.ExchangeName == singleInterfaceName), + Is.True); + + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); + }); } [Test] @@ -246,11 +272,14 @@ public void Should_include_a_binding_for_the_single_interface() var topology = _builder.BuildBrokerTopology(); topology.LogResult(); - var singleInterfaceName = _nameFormatter.GetMessageName(typeof(SingleInterface)).ToString(); + var singleInterfaceName = _nameFormatter.GetMessageName(typeof(SingleInterface)); - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); - Assert.That(topology.Exchanges.Length, Is.EqualTo(1)); - Assert.That(topology.ExchangeBindings.Length, Is.EqualTo(0)); + Assert.Multiple(() => + { + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == singleInterfaceName), Is.True); + Assert.That(topology.Exchanges, Has.Length.EqualTo(1)); + Assert.That(topology.ExchangeBindings, Is.Empty); + }); } [Test] @@ -262,27 +291,35 @@ public void Should_include_a_binding_for_the_third_interface_as_well() var topology = _builder.BuildBrokerTopology(); topology.LogResult(); - var firstInterfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)).ToString(); - var secondInterfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)).ToString(); - var thirdInterfaceName = _nameFormatter.GetMessageName(typeof(ThirdInterface)).ToString(); - - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == secondInterfaceName), Is.True); - Assert.That(topology.Exchanges.Length, Is.EqualTo(3)); - Assert.That(topology.ExchangeBindings.Length, Is.EqualTo(2)); - Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == secondInterfaceName && x.Destination.ExchangeName == firstInterfaceName), - Is.True); - - Assert.That(topology.ExchangeBindings.Any(x => x.Source.ExchangeName == thirdInterfaceName && x.Destination.ExchangeName == secondInterfaceName), - Is.True); - - Assert.That(topology.Exchanges.Any(x => x.ExchangeName == firstInterfaceName), Is.True); + var firstInterfaceName = _nameFormatter.GetMessageName(typeof(FirstInterface)); + var secondInterfaceName = _nameFormatter.GetMessageName(typeof(SecondInterface)); + var thirdInterfaceName = _nameFormatter.GetMessageName(typeof(ThirdInterface)); + + Assert.Multiple(() => + { + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == secondInterfaceName), Is.True); + Assert.That(topology.Exchanges, Has.Length.EqualTo(3)); + Assert.That(topology.ExchangeBindings, Has.Length.EqualTo(2)); + }); + Assert.Multiple(() => + { + Assert.That( + topology.ExchangeBindings.Any(x => x.Source.ExchangeName == secondInterfaceName && x.Destination.ExchangeName == firstInterfaceName), + Is.True); + + Assert.That( + topology.ExchangeBindings.Any(x => x.Source.ExchangeName == thirdInterfaceName && x.Destination.ExchangeName == secondInterfaceName), + Is.True); + + Assert.That(topology.Exchanges.Any(x => x.ExchangeName == firstInterfaceName), Is.True); + }); } [SetUp] public void Setup() { _nameFormatter = new RabbitMqMessageNameFormatter(); - _publishTopology = new RabbitMqPublishTopology(RabbitMqBusFactory.MessageTopology); + _publishTopology = new RabbitMqPublishTopology(RabbitMqBusFactory.CreateMessageTopology()); _builder = new PublishEndpointBrokerTopologyBuilder(PublishBrokerTopologyOptions.MaintainHierarchy); } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Bytes_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Bytes_Specs.cs index 42abc42c57f..23ea81e4af7 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Bytes_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Bytes_Specs.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -24,7 +23,7 @@ public async Task Should_receive_byte_array_of_bigness() ConsumeContext context = await _received; - context.Message.ShouldBe(sent); + Assert.That(context.Message, Is.EqualTo(sent)); } Task> _received; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/ConcurrencyFilter_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/ConcurrencyFilter_Specs.cs index 7fbb3d54662..20bf6ff0b52 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/ConcurrencyFilter_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/ConcurrencyFilter_Specs.cs @@ -28,7 +28,7 @@ public async Task Should_limit_the_consumer() await _complete.Task; - Assert.AreEqual(2, _consumer.MaxDeliveryCount); + Assert.That(_consumer.MaxDeliveryCount, Is.EqualTo(2)); } Consumer _consumer; @@ -92,4 +92,111 @@ class B { } } + + + [TestFixture] + [Category("Flaky")] + public class Using_a_consumer_concurrency_limit_set_to_1 : + RabbitMqTestFixture + { + [Test] + public async Task Should_limit_the_consumer_and_consume_messages_sequentially() + { + _complete = GetTask(); + + int sequenceIndex = 1; + for (var i = 0; i < _messageCount; i++) + { + await Bus.Publish(new A(sequenceIndex++)); + await Bus.Publish(new B(sequenceIndex++)); + } + + await _complete.Task; + + Assert.Multiple(() => + { + Assert.That(_consumer.MaxDeliveryCount, Is.EqualTo(1)); + Assert.That(_consumer.CompletedConsumingSequentially, Is.EqualTo(true)); + }); + } + + Consumer _consumer; + static readonly int _messageCount = 50; + static TaskCompletionSource _complete; + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + base.ConfigureRabbitMqReceiveEndpoint(configurator); + + _consumer = new Consumer(); + configurator.ConcurrentMessageLimit = 1; + configurator.Instance(_consumer, x => x.UseConcurrentMessageLimit(1)); + } + + + class Consumer : + IConsumer, + IConsumer + { + bool _completedConsumingSequentially = true; + int _currentPendingDeliveryCount; + long _deliveryCount; + int _maxPendingDeliveryCount; + int _previousSequenceIndex; + + public int MaxDeliveryCount => _maxPendingDeliveryCount; + public bool CompletedConsumingSequentially => _completedConsumingSequentially; + + public Task Consume(ConsumeContext context) + { + return OnConsume(context.Message.SequenceIndex); + } + + public Task Consume(ConsumeContext context) + { + return OnConsume(context.Message.SequenceIndex); + } + + async Task OnConsume(int sequenceIndex) + { + Interlocked.Increment(ref _deliveryCount); + + var current = Interlocked.Increment(ref _currentPendingDeliveryCount); + while (current > _maxPendingDeliveryCount) + Interlocked.CompareExchange(ref _maxPendingDeliveryCount, current, _maxPendingDeliveryCount); + + await Task.Delay(10); + + Interlocked.Decrement(ref _currentPendingDeliveryCount); + + _completedConsumingSequentially = _completedConsumingSequentially && _previousSequenceIndex == sequenceIndex - 1; + _previousSequenceIndex = sequenceIndex; + + if (_deliveryCount >= _messageCount * 2) + _complete.TrySetResult(true); + } + } + + + class A + { + public A(int sequenceIndex) + { + SequenceIndex = sequenceIndex; + } + + public int SequenceIndex { get; } + } + + + class B + { + public B(int sequenceIndex) + { + SequenceIndex = sequenceIndex; + } + + public int SequenceIndex { get; } + } + } } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Configure_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Configure_Specs.cs index eb7582e3da2..90a66d6847f 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Configure_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Configure_Specs.cs @@ -1,137 +1,120 @@ -namespace MassTransit.RabbitMqTransport.Tests -{ - using System; - using NUnit.Framework; - - - [TestFixture] - public class Configure_Specs - { - [Test] - public void Should_fail_when_late_configuration_happens() - { - var exception = Assert.Throws(() => - { - Bus.Factory.CreateUsingRabbitMq(x => - { - x.Host(new Uri("rabbitmq://[::1]/test/"), h => - { - }); - - x.ReceiveEndpoint("input_queue", e => - { - var inputAddress = e.InputAddress; - - e.Durable = false; - e.AutoDelete = true; - }); - }); - }); - - - Console.WriteLine(string.Join(Environment.NewLine, exception.Results)); - } - - [Test] - public void Should_fail_with_empty_queue_name() - { - var exception = Assert.Throws(() => - { - Bus.Factory.CreateUsingRabbitMq(x => - { - x.Host(new Uri("rabbitmq://[::1]/test/"), h => - { - }); - - x.OverrideDefaultBusEndpointQueueName(""); - }); - }); - - - Console.WriteLine(string.Join(Environment.NewLine, exception.Results)); - } - - [Test] - public void Should_fail_with_invalid_middleware() - { - var exception = Assert.Throws(() => - { - Bus.Factory.CreateUsingRabbitMq(x => - { - x.Host(new Uri("rabbitmq://[::1]/test/"), h => - { - h.RequestedConnectionTimeout(2000); - }); - - x.UseRetry(r => - { - }); - }); - }); - - - Console.WriteLine(string.Join(Environment.NewLine, exception.Results)); - } - - [Test] - public void Should_fail_with_invalid_middleware_on_endpoint() - { - var exception = Assert.Throws(() => - { - Bus.Factory.CreateUsingRabbitMq(x => - { - x.Host(new Uri("rabbitmq://[::1]/test/"), h => - { - }); - - x.ReceiveEndpoint("input_queue", e => - { - e.UseRetry(r => - { - }); - }); - }); - }); - - - Console.WriteLine(string.Join(Environment.NewLine, exception.Results)); - } - - [Test] - public void Should_fail_with_invalid_queue_name() - { - var exception = Assert.Throws(() => - { - Bus.Factory.CreateUsingRabbitMq(x => - { - x.Host(new Uri("rabbitmq://[::1]/test/"), h => - { - }); - - x.ReceiveEndpoint("0(*!)@((*#&!(*&@#/", e => - { - }); - }); - }); - - - Console.WriteLine(string.Join(Environment.NewLine, exception.Results)); - } - - [Test] - public void Should_not_fail_with_warnings() - { - Bus.Factory.CreateUsingRabbitMq(x => - { - x.Host(new Uri("rabbitmq://[::1]/test/"), h => - { - }); - - x.ReceiveEndpoint("input_queue", e => - { - e.PurgeOnStartup = true; - }); - }); - } - } -} +namespace MassTransit.RabbitMqTransport.Tests +{ + using System; + using NUnit.Framework; + using TestFramework.Messages; + + + [TestFixture] + public class Configure_Specs + { + [Test] + public void Should_fail_when_late_configuration_happens() + { + var exception = Assert.Throws(() => + { + Bus.Factory.CreateUsingRabbitMq(x => + { + x.Host(new Uri("rabbitmq://[::1]/test/"), h => + { + }); + + x.ReceiveEndpoint("input_queue", e => + { + var inputAddress = e.InputAddress; + + e.Durable = false; + e.AutoDelete = true; + }); + }); + }); + + + Console.WriteLine(string.Join(Environment.NewLine, exception.Results)); + } + + [Test] + public void Should_fail_with_empty_queue_name() + { + var exception = Assert.Throws(() => + { + Bus.Factory.CreateUsingRabbitMq(x => + { + x.Host(new Uri("rabbitmq://[::1]/test/"), h => + { + }); + + x.OverrideDefaultBusEndpointQueueName(""); + }); + }); + + + Console.WriteLine(string.Join(Environment.NewLine, exception.Results)); + } + + [Test] + public void Should_fail_with_invalid_middleware_on_endpoint() + { + var exception = Assert.Throws(() => + { + Bus.Factory.CreateUsingRabbitMq(x => + { + x.Host(new Uri("rabbitmq://[::1]/test/"), h => + { + }); + + x.ReceiveEndpoint("input_queue", e => + { + e.UseMessageRetry(r => + { + }); + + e.Handler(async _ => + { + }); + }); + }); + }); + + + Console.WriteLine(string.Join(Environment.NewLine, exception.Results)); + } + + [Test] + public void Should_fail_with_invalid_queue_name() + { + var exception = Assert.Throws(() => + { + Bus.Factory.CreateUsingRabbitMq(x => + { + x.Host(new Uri("rabbitmq://[::1]/test/"), h => + { + }); + + x.ReceiveEndpoint("0(*!)@((*#&!(*&@#/", e => + { + }); + }); + }); + + + Console.WriteLine(string.Join(Environment.NewLine, exception.Results)); + } + + [Test] + public void Should_not_fail_with_warnings() + { + Bus.Factory.CreateUsingRabbitMq(x => + { + x.Host(new Uri("rabbitmq://[::1]/test/"), h => + { + }); + + x.ReceiveEndpoint("input_queue", e => + { + e.PurgeOnStartup = true; + }); + }); + } + } +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/ConnectEndpoint_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/ConnectEndpoint_Specs.cs index 3d86e9f75a3..0fb620c7c0c 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/ConnectEndpoint_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/ConnectEndpoint_Specs.cs @@ -21,7 +21,7 @@ async Task ConnectAndRequest() await handle.Ready; - var clientFactory = await handle.CreateClientFactory(); + var clientFactory = handle.CreateClientFactory(); try { using RequestHandle requestHandle = clientFactory.CreateRequest(new PingMessage()); @@ -43,7 +43,7 @@ async Task ConnectAndRequest() var health = BusControl.CheckHealth(); foreach (KeyValuePair healthEndpoint in health.Endpoints) - TestContext.WriteLine("Endpoint: {0}, Status: {1}", healthEndpoint.Key, healthEndpoint.Value.Description); + TestContext.Out.WriteLine("Endpoint: {0}, Status: {1}", healthEndpoint.Key, healthEndpoint.Value.Description); Assert.That(health.Status, Is.EqualTo(BusHealthStatus.Healthy)); } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/ConsumerBind_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/ConsumerBind_Specs.cs index 56c85ba094c..efc73b65e7b 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/ConsumerBind_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/ConsumerBind_Specs.cs @@ -1,285 +1,286 @@ -namespace MassTransit.RabbitMqTransport.Tests -{ - namespace ConsumerBind_Specs - { - using System; - using System.Threading.Tasks; - using MassTransit.Testing; - using NUnit.Framework; - using RabbitMQ.Client; - using Util; - - - public class ConsumerBindingTestFixture : - RabbitMqTestFixture - { - protected override void OnCleanupVirtualHost(IModel model) - { - model.ExchangeDelete(NameFormatter.GetMessageName(typeof(A)).ToString()); - model.ExchangeDelete(NameFormatter.GetMessageName(typeof(B)).ToString()); - } - } - - - [TestFixture] - public class Binding_an_untyped_consumer : - ConsumerBindingTestFixture - { - [Test] - [Order(0)] - public async Task Setup() - { - await InputQueueSendEndpoint.Send(new A()); - await InputQueueSendEndpoint.Send(new B()); - } - - [Test] - public async Task Should_receive_the_message_a() - { - await _testConsumer.A.Task; - } - - [Test] - public async Task Should_receive_the_message_b() - { - await _testConsumer.B.Task; - } - - TestConsumer _testConsumer; - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - _testConsumer = new TestConsumer(GetTask(), GetTask()); - - configurator.Consumer(typeof(TestConsumer), type => - { - return _testConsumer; - }); - } - } - - - [TestFixture] - public class Binding_a_typed_consumer : - ConsumerBindingTestFixture - { - [Test] - public async Task Should_receive_the_message_a() - { - await _testConsumer.A.Task; - } - - [Test] - public async Task Should_receive_the_message_b() - { - await _testConsumer.B.Task; - } - - TestConsumer _testConsumer; - - [OneTimeSetUp] - public async Task Setup() - { - await InputQueueSendEndpoint.Send(new A()); - await InputQueueSendEndpoint.Send(new B()); - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - _testConsumer = new TestConsumer(GetTask(), GetTask()); - - configurator.Consumer(() => _testConsumer); - } - } - - - [TestFixture] - public class Binding_a_handler : - ConsumerBindingTestFixture - { - [Test] - public async Task Should_receive_the_message_a() - { - await _a; - } - - [Test] - public async Task Should_receive_the_message_b() - { - await _b; - } - - Task> _a; - Task> _b; - - [OneTimeSetUp] - public async Task Setup() - { - await InputQueueSendEndpoint.Send(new A()); - await InputQueueSendEndpoint.Send(new B()); - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - _a = Handled(configurator); - _b = Handled(configurator); - } - } - - - [TestFixture] - public class Configuring_a_consumer_without_binding : - ConsumerBindingTestFixture - { - [Test] - public async Task Should_receive_the_message_a() - { - await _a; - } - - [Test] - public async Task Should_receive_the_message_b() - { - await _b; - } - - Task> _a; - Task> _b; - - [OneTimeSetUp] - public async Task Setup() - { - await InputQueueSendEndpoint.Send(new A()); - await InputQueueSendEndpoint.Send(new B()); - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _a = Handled(configurator); - _b = Handled(configurator); - } - } - - - [TestFixture] - public class Binding_a_old_school_saga : - ConsumerBindingTestFixture - { - [Test] - public async Task Should_receive_the_message_a() - { - Guid? sagaId = await _repository.ShouldContainSaga(_sagaId, TestTimeout); - Assert.IsTrue(sagaId.HasValue); - - var saga = _repository[sagaId.Value].Instance; - - await saga.A.Task; - } - - [Test] - public async Task Should_receive_the_message_b() - { - Guid? sagaId = await _repository.ShouldContainSaga(_sagaId, TestTimeout); - Assert.IsTrue(sagaId.HasValue); - - var saga = _repository[sagaId.Value].Instance; - - await InputQueueSendEndpoint.Send(new B { CorrelationId = _sagaId }); - - await saga.B.Task; - } - - InMemorySagaRepository _repository; - Guid _sagaId; - - [OneTimeSetUp] - public async Task Setup() - { - _sagaId = NewId.NextGuid(); - - await InputQueueSendEndpoint.Send(new A { CorrelationId = _sagaId }); - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - _repository = new InMemorySagaRepository(); - - configurator.Saga(_repository); - } - } - - - class TestConsumer : - IConsumer, - IConsumer - { - public TestConsumer(TaskCompletionSource a, TaskCompletionSource b) - { - A = a; - B = b; - } - - public TaskCompletionSource A { get; } - - public TaskCompletionSource B { get; } - - public async Task Consume(ConsumeContext context) - { - A.TrySetResult(context.Message); - } - - public async Task Consume(ConsumeContext context) - { - B.TrySetResult(context.Message); - } - } - - - public class TestSaga : - ISaga, - InitiatedBy, - Orchestrates - { - public TestSaga() - { - } - - public TestSaga(Guid correlationId) - { - CorrelationId = correlationId; - } - - public TaskCompletionSource A { get; } = TaskUtil.GetTask(); - - public TaskCompletionSource B { get; } = TaskUtil.GetTask(); - - public async Task Consume(ConsumeContext context) - { - A.TrySetResult(context.Message); - } - - public Guid CorrelationId { get; set; } - - public async Task Consume(ConsumeContext context) - { - B.TrySetResult(context.Message); - } - } - - - public class A : - CorrelatedBy - { - public Guid CorrelationId { get; set; } - } - - - public class B : - CorrelatedBy - { - public Guid CorrelationId { get; set; } - } - } -} +namespace MassTransit.RabbitMqTransport.Tests +{ + namespace ConsumerBind_Specs + { + using System; + using System.Threading.Tasks; + using NUnit.Framework; + using RabbitMQ.Client; + using Testing; + using Util; + + + public class ConsumerBindingTestFixture : + RabbitMqTestFixture + { + protected override void OnCleanupVirtualHost(IModel model) + { + model.ExchangeDelete(NameFormatter.GetMessageName(typeof(A)).ToString()); + model.ExchangeDelete(NameFormatter.GetMessageName(typeof(B)).ToString()); + } + } + + + [TestFixture] + public class Binding_an_untyped_consumer : + ConsumerBindingTestFixture + { + [Test] + [Order(0)] + public async Task Setup() + { + await InputQueueSendEndpoint.Send(new A()); + await InputQueueSendEndpoint.Send(new B()); + } + + [Test] + public async Task Should_receive_the_message_a() + { + await _testConsumer.A.Task; + } + + [Test] + public async Task Should_receive_the_message_b() + { + await _testConsumer.B.Task; + } + + TestConsumer _testConsumer; + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + _testConsumer = new TestConsumer(GetTask(), GetTask()); + + configurator.Consumer(typeof(TestConsumer), type => + { + return _testConsumer; + }); + } + } + + + [TestFixture] + public class Binding_a_typed_consumer : + ConsumerBindingTestFixture + { + [Test] + public async Task Should_receive_the_message_a() + { + await _testConsumer.A.Task; + } + + [Test] + public async Task Should_receive_the_message_b() + { + await _testConsumer.B.Task; + } + + TestConsumer _testConsumer; + + [OneTimeSetUp] + public async Task Setup() + { + await InputQueueSendEndpoint.Send(new A()); + await InputQueueSendEndpoint.Send(new B()); + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + _testConsumer = new TestConsumer(GetTask(), GetTask()); + + configurator.Consumer(() => _testConsumer); + } + } + + + [TestFixture] + public class Binding_a_handler : + ConsumerBindingTestFixture + { + [Test] + public async Task Should_receive_the_message_a() + { + await _a; + } + + [Test] + public async Task Should_receive_the_message_b() + { + await _b; + } + + Task> _a; + Task> _b; + + [OneTimeSetUp] + public async Task Setup() + { + await InputQueueSendEndpoint.Send(new A()); + await InputQueueSendEndpoint.Send(new B()); + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + _a = Handled(configurator); + _b = Handled(configurator); + } + } + + + [TestFixture] + public class Configuring_a_consumer_without_binding : + ConsumerBindingTestFixture + { + [Test] + public async Task Should_receive_the_message_a() + { + await _a; + } + + [Test] + public async Task Should_receive_the_message_b() + { + await _b; + } + + Task> _a; + Task> _b; + + [OneTimeSetUp] + public async Task Setup() + { + await InputQueueSendEndpoint.Send(new A()); + await InputQueueSendEndpoint.Send(new B()); + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + _a = Handled(configurator); + _b = Handled(configurator); + } + } + + + [TestFixture] + public class Binding_a_old_school_saga : + ConsumerBindingTestFixture + { + [Test] + public async Task Should_receive_the_message_a() + { + Guid? sagaId = await SagaRepository.ShouldContainSaga(_sagaId, TestTimeout); + Assert.That(sagaId.HasValue, Is.True); + + var saga = _repository[sagaId.Value].Instance; + + await saga.A.Task; + } + + [Test] + public async Task Should_receive_the_message_b() + { + Guid? sagaId = await SagaRepository.ShouldContainSaga(_sagaId, TestTimeout); + Assert.That(sagaId.HasValue, Is.True); + + var saga = _repository[sagaId.Value].Instance; + + await InputQueueSendEndpoint.Send(new B { CorrelationId = _sagaId }); + + await saga.B.Task; + } + + InMemorySagaRepository _repository; + ISagaRepository SagaRepository => _repository; + Guid _sagaId; + + [OneTimeSetUp] + public async Task Setup() + { + _sagaId = NewId.NextGuid(); + + await InputQueueSendEndpoint.Send(new A { CorrelationId = _sagaId }); + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + _repository = new InMemorySagaRepository(); + + configurator.Saga(_repository); + } + } + + + class TestConsumer : + IConsumer, + IConsumer + { + public TestConsumer(TaskCompletionSource a, TaskCompletionSource b) + { + A = a; + B = b; + } + + public TaskCompletionSource A { get; } + + public TaskCompletionSource B { get; } + + public async Task Consume(ConsumeContext context) + { + A.TrySetResult(context.Message); + } + + public async Task Consume(ConsumeContext context) + { + B.TrySetResult(context.Message); + } + } + + + public class TestSaga : + ISaga, + InitiatedBy, + Orchestrates + { + public TestSaga() + { + } + + public TestSaga(Guid correlationId) + { + CorrelationId = correlationId; + } + + public TaskCompletionSource A { get; } = TaskUtil.GetTask(); + + public TaskCompletionSource B { get; } = TaskUtil.GetTask(); + + public async Task Consume(ConsumeContext context) + { + A.TrySetResult(context.Message); + } + + public Guid CorrelationId { get; set; } + + public async Task Consume(ConsumeContext context) + { + B.TrySetResult(context.Message); + } + } + + + public class A : + CorrelatedBy + { + public Guid CorrelationId { get; set; } + } + + + public class B : + CorrelatedBy + { + public Guid CorrelationId { get; set; } + } + } +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/ConsumerTimeout_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/ConsumerTimeout_Specs.cs new file mode 100644 index 00000000000..78cc2639352 --- /dev/null +++ b/tests/MassTransit.RabbitMqTransport.Tests/ConsumerTimeout_Specs.cs @@ -0,0 +1,118 @@ +namespace MassTransit.RabbitMqTransport.Tests; + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; + + +[TestFixture] +public class When_the_consumer_timeout_is_reached_waiting_for_a_batch +{ + [Test] + [Explicit] + public async Task Should_properly_handle_message_redelivery() + { + await using var provider = new ServiceCollection() + .ConfigureRabbitMqTestOptions(options => + { + options.CleanVirtualHost = true; + options.CreateVirtualHostIfNotExists = true; + }) + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => options.VHost = "test"); + + x.AddOptions().Configure(options => + { + options.StartTimeout = TimeSpan.FromSeconds(5); + options.StopTimeout = TimeSpan.FromSeconds(5); + options.ConsumerStopTimeout = TimeSpan.FromSeconds(1); + }); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(90), testTimeout: TimeSpan.FromSeconds(120)); + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer(c => c.Options(options => + { + options.TimeLimit = TimeSpan.FromSeconds(80); + options.MessageLimit = 10; + })) + .Endpoint(e => e.AddConfigureEndpointCallback(cfg => + { + if (cfg is IRabbitMqReceiveEndpointConfigurator rmq) + rmq.SetQueueArgument("x-consumer-timeout", 10000); + })); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.PublishBatch(new TextMessage[] + { + new() + { + Text = "High Priority 1", + Priority = "High" + }, + new() + { + Text = "High Priority 2", + Priority = "High" + }, + new() + { + Text = "High Priority 3", + Priority = "High" + } + }); + + await harness.Bus.PublishBatch(new TextMessage[] + { + new() + { + Text = "High Priority 4", + Priority = "High" + }, + new() + { + Text = "High Priority 5", + Priority = "High" + }, + new() + { + Text = "High Priority 6", + Priority = "High" + } + }); + + Assert.That(await harness.Consumed.Any(x => x.Exception == null)); + } + + + public class TextMessage + { + public string Text { get; set; } + public string Priority { get; set; } + } + + + class HighTextMessageConsumer : + IConsumer> + { + public async Task Consume(ConsumeContext> context) + { + LogContext.Debug?.Log("Processing batch of {Count} messages", context.Message.Length); + + foreach (ConsumeContext message in context.Message) + LogContext.Debug?.Log("Got message: {0} {1} {2}", DateTime.UtcNow, message.Message.Text, message.Message.Priority); + } + } +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Container_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Container_Specs.cs index 236e6626d12..946e836e5a7 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Container_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Container_Specs.cs @@ -2,9 +2,9 @@ namespace MassTransit.RabbitMqTransport.Tests { using System.Threading.Tasks; using HarnessContracts; - using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; + using Testing; namespace HarnessContracts @@ -67,9 +67,12 @@ await client.GetResponse(new OrderNumber = "123" }); - Assert.IsTrue(await harness.Sent.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Sent.Any(), Is.True); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); + }); } @@ -126,9 +129,12 @@ await client.GetResponse(new OrderNumber = "123" }); - Assert.IsTrue(await harness.Sent.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Sent.Any(), Is.True); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); + }); } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/DelayDirectExchange_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/DelayDirectExchange_Specs.cs new file mode 100644 index 00000000000..a79c365f164 --- /dev/null +++ b/tests/MassTransit.RabbitMqTransport.Tests/DelayDirectExchange_Specs.cs @@ -0,0 +1,135 @@ +namespace MassTransit.RabbitMqTransport.Tests +{ + using System; + using System.Threading.Tasks; + using DelaySubjects; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using RabbitMQ.Client; + using Testing; + + + [TestFixture] + public class DelayDirectExchange_Specs + { + [Test] + public async Task Should_properly_deliver_the_message() + { + await using var provider = new ServiceCollection() + .ConfigureRabbitMqTestOptions(options => + { + options.CleanVirtualHost = true; + options.CreateVirtualHostIfNotExists = true; + }) + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => options.VHost = "test"); + + x.AddDelayedMessageScheduler(); + + x.AddConsumer(); + x.AddConsumer(); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(3)); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.Send(c => + { + c.UseRoutingKeyFormatter(msg => msg.Message.Priority); + }); + + cfg.Publish(c => + { + c.ExchangeType = ExchangeType.Direct; + }); + + cfg.ReceiveEndpoint($"{nameof(TextMessage)}_Low", e => + { + e.ConfigureConsumeTopology = false; + e.Bind(s => + { + s.RoutingKey = "Low"; + s.ExchangeType = ExchangeType.Direct; + }); + + e.ConfigureConsumer(context); + }); + + cfg.ReceiveEndpoint($"{nameof(TextMessage)}_High", e => + { + e.ConfigureConsumeTopology = false; + e.Bind(s => + { + s.RoutingKey = "High"; + s.ExchangeType = ExchangeType.Direct; + }); + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new TextMessage + { + Text = "High Priority", + Priority = "High" + }); + + await harness.Bus.Publish(new TextMessage + { + Text = "Low Priority 1", + Priority = "Low" + }); + + var scheduler = harness.Scope.ServiceProvider.GetRequiredService(); + + await scheduler.SchedulePublish(DateTime.UtcNow.AddSeconds(1), new TextMessage + { + Text = "Low Priority 2", + Priority = "Low" + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.Any(x => x.Context.Message.Text == "High Priority"), Is.True, "High"); + Assert.That(await harness.Consumed.Any(x => x.Context.Message.Text == "Low Priority 1"), Is.True, "Low 1"); + Assert.That(await harness.Consumed.Any(x => x.Context.Message.Text == "Low Priority 2"), Is.True, "Low 2"); + }); + } + } + + + namespace DelaySubjects + { + class MyHighTextMessageConsumer : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + } + } + + + class MyLowTextMessageConsumer : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + } + } + + + public class TextMessage + { + public string Text { get; set; } + public string Priority { get; set; } + } + } +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/DelayRetry_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/DelayRetry_Specs.cs index f26f62c6372..c24eb119794 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/DelayRetry_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/DelayRetry_Specs.cs @@ -1,409 +1,412 @@ -namespace MassTransit.RabbitMqTransport.Tests -{ - using System; - using System.Diagnostics; - using System.Threading; - using System.Threading.Tasks; - using NUnit.Framework; - using TestFramework; - using TestFramework.Messages; - - - [TestFixture] - [Category("Flaky")] - public class Using_the_delayed_exchange : - RabbitMqTestFixture - { - [Test] - public async Task Should_properly_defer_the_message_delivery() - { - await InputQueueSendEndpoint.Send(new PingMessage()); - - ConsumeContext context = await _received.Task; - - Assert.GreaterOrEqual(_receivedTimeSpan, TimeSpan.FromSeconds(1)); - } - - TaskCompletionSource> _received; - TimeSpan _receivedTimeSpan; - Stopwatch _timer; - int _count; - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _count = 0; - - _received = GetTask>(); - - configurator.Handler(context => - { - if (_timer == null) - _timer = Stopwatch.StartNew(); - - if (_count++ < 2) - { - Console.WriteLine("{0} now is not a good time", DateTime.UtcNow); - throw new IntentionalTestException("I'm so not ready for this jelly."); - } - - _timer.Stop(); - - Console.WriteLine("{0} okay, now is good (retried {1} times)", DateTime.UtcNow, context.Headers.Get("MT-Redelivery-Count", default(int?))); - - // okay, ready. - _receivedTimeSpan = _timer.Elapsed; - _received.TrySetResult(context); - - return Task.CompletedTask; - }, x => x.UseDelayedRedelivery(r => r.Intervals(1000, 2000))); - } - } - - - [TestFixture] - [Category("Flaky")] - public class Delaying_a_message_retry_with_policy : - RabbitMqTestFixture - { - [Test] - public async Task Should_only_defer_up_to_the_retry_count() - { - var pingMessage = new PingMessage(); - - Task>> fault = - SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); - - await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); - - ConsumeContext> faultContext = await fault; - - Assert.That(_count, Is.EqualTo(3)); - } - - int _count; - - protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) - { - configurator.AutoStart = true; - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _count = 0; - - configurator.Handler(context => - { - Interlocked.Increment(ref _count); - - throw new IntentionalTestException(); - }, x => x.UseDelayedRedelivery(r => r.Intervals(100, 200))); - } - } - - - [TestFixture] - [Category("Flaky")] - public class Retrying_a_message_retry_with_policy : - RabbitMqTestFixture - { - [Test] - public async Task Should_only_retry_up_to_the_retry_count() - { - var pingMessage = new PingMessage(); - - Task>> fault = - SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); - - await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); - - ConsumeContext> faultContext = await fault; - - Assert.That(_count, Is.EqualTo(3)); - } - - int _count; - - protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) - { - configurator.AutoStart = true; - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _count = 0; - - configurator.Handler(context => - { - Interlocked.Increment(ref _count); - - throw new IntentionalTestException(); - }, x => x.UseRetry(r => r.Intervals(100, 200))); - } - } - - - [TestFixture] - [Category("Flaky")] - public class Using_delayed_exchange_redelivery_with_a_consumer : - RabbitMqTestFixture - { - [Test] - public async Task Should_retry_each_message_type() - { - var pingMessage = new PingMessage(); - - Task>> pingFault = - SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); - Task>> pongFault = - SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); - - await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); - await InputQueueSendEndpoint.Send(new PongMessage(pingMessage.CorrelationId), x => x.FaultAddress = Bus.Address); - - ConsumeContext> pingFaultContext = await pingFault; - ConsumeContext> pongFaultContext = await pongFault; - - Assert.That(_consumer.PingCount, Is.EqualTo(3)); - Assert.That(_consumer.PongCount, Is.EqualTo(3)); - } - - Consumer _consumer; - - protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) - { - configurator.AutoStart = true; - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - configurator.UseDelayedRedelivery(r => r.Intervals(100, 200)); - - _consumer = new Consumer(); - configurator.Consumer(() => _consumer); - } - - - class Consumer : - IConsumer, - IConsumer - { - public int PingCount; - public int PongCount; - - public Task Consume(ConsumeContext context) - { - Interlocked.Increment(ref PingCount); - - throw new IntentionalTestException(); - } - - public Task Consume(ConsumeContext context) - { - Interlocked.Increment(ref PongCount); - - throw new IntentionalTestException(); - } - } - } - - - [TestFixture] - [Category("Flaky")] - public class Using_delayed_exchange_redelivery_with_a_consumer_and_retry : - RabbitMqTestFixture - { - [Test] - public async Task Should_retry_and_redeliver() - { - var pingMessage = new PingMessage(); - - Task>> pingFault = - SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); - - await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); - await InputQueueSendEndpoint.Send(new PongMessage(pingMessage.CorrelationId), x => x.FaultAddress = Bus.Address); - - ConsumeContext> pingFaultContext = await pingFault; - - Assert.That(Consumer.PingCount, Is.EqualTo(6)); - } - - protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) - { - configurator.AutoStart = true; - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - configurator.UseDelayedRedelivery(r => r.Intervals(100)); - configurator.UseMessageRetry(x => x.Immediate(2)); - - Consumer.PingCount = 0; - - configurator.Consumer(() => new Consumer()); - } - - - class Consumer : - IConsumer - { - public static int PingCount; - - public Consumer() - { - LogContext.Info?.Log("Creating consumer"); - } - - public Task Consume(ConsumeContext context) - { - Interlocked.Increment(ref PingCount); - - throw new IntentionalTestException(); - } - } - } - - - [TestFixture] - [Category("Flaky")] - public class Delaying_a_message_retry_with_policy_but_no_retries : - RabbitMqTestFixture - { - [Test] - public async Task Should_immediately_fault_with_no_delay() - { - var pingMessage = new PingMessage(); - - Task>> fault = - SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); - - await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); - - ConsumeContext> faultContext = await fault; - - Assert.That(_count, Is.EqualTo(1)); - } - - int _count; - - protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) - { - configurator.AutoStart = true; - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _count = 0; - - configurator.Handler(context => - { - Interlocked.Increment(ref _count); - - throw new IntentionalTestException(); - }, x => x.UseDelayedRedelivery(r => r.None())); - } - } - - - [TestFixture] - [Category("Flaky")] - public class Explicitly_deferring_a_message_instead_of_throwing : - RabbitMqTestFixture - { - [Test] - public async Task Should_properly_defer_the_message_delivery() - { - var pingMessage = new PingMessage(); - await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); - - var timer = Stopwatch.StartNew(); - - await _received.Task; - - timer.Stop(); - - Assert.That(timer.Elapsed, Is.GreaterThan(TimeSpan.FromSeconds(0.5))); - } - - TaskCompletionSource> _received; - int _count; - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _count = 0; - - _received = GetTask>(); - - configurator.Handler(async context => - { - if (_count++ == 0) - { - await context.Defer(TimeSpan.FromMilliseconds(1000)); - return; - } - - _received.TrySetResult(context); - }); - } - } - - - [TestFixture] - [Category("Flaky")] - public class Execute_callback_function_during_defer : - RabbitMqTestFixture - { - [Test] - public async Task Should_execute_callback_during_defer_the_message_delivery() - { - var pingMessage = new PingMessage(); - await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); - - await _received.Task; - - Assert.IsTrue(_hit); - } - - TaskCompletionSource> _received; - int _count; - bool _hit; - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _count = 0; - - _received = GetTask>(); - - configurator.Handler(async context => - { - if (_count++ == 0) - { - await context.Defer(TimeSpan.FromMilliseconds(100), (consumeContext, sendContext) => - { - _hit = true; - }); - - return; - } - - _received.TrySetResult(context); - }); - } - } -} +namespace MassTransit.RabbitMqTransport.Tests +{ + using System; + using System.Diagnostics; + using System.Threading; + using System.Threading.Tasks; + using NUnit.Framework; + using TestFramework; + using TestFramework.Messages; + + + [TestFixture] + [Category("Flaky")] + public class Using_the_delayed_exchange : + RabbitMqTestFixture + { + [Test] + public async Task Should_properly_defer_the_message_delivery() + { + await InputQueueSendEndpoint.Send(new PingMessage()); + + ConsumeContext context = await _received.Task; + + Assert.That(_receivedTimeSpan, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); + } + + TaskCompletionSource> _received; + TimeSpan _receivedTimeSpan; + Stopwatch _timer; + int _count; + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + _count = 0; + + _received = GetTask>(); + + configurator.Handler(context => + { + if (_timer == null) + _timer = Stopwatch.StartNew(); + + if (_count++ < 2) + { + Console.WriteLine("{0} now is not a good time", DateTime.UtcNow); + throw new IntentionalTestException("I'm so not ready for this jelly."); + } + + _timer.Stop(); + + Console.WriteLine("{0} okay, now is good (retried {1} times)", DateTime.UtcNow, context.Headers.Get("MT-Redelivery-Count", default(int?))); + + // okay, ready. + _receivedTimeSpan = _timer.Elapsed; + _received.TrySetResult(context); + + return Task.CompletedTask; + }, x => x.UseDelayedRedelivery(r => r.Intervals(1000, 2000))); + } + } + + + [TestFixture] + [Category("Flaky")] + public class Delaying_a_message_retry_with_policy : + RabbitMqTestFixture + { + [Test] + public async Task Should_only_defer_up_to_the_retry_count() + { + var pingMessage = new PingMessage(); + + Task>> fault = + SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); + + await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); + + ConsumeContext> faultContext = await fault; + + Assert.That(_count, Is.EqualTo(3)); + } + + int _count; + + protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) + { + configurator.AutoStart = true; + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + _count = 0; + + configurator.Handler(context => + { + Interlocked.Increment(ref _count); + + throw new IntentionalTestException(); + }, x => x.UseDelayedRedelivery(r => r.Intervals(100, 200))); + } + } + + + [TestFixture] + [Category("Flaky")] + public class Retrying_a_message_retry_with_policy : + RabbitMqTestFixture + { + [Test] + public async Task Should_only_retry_up_to_the_retry_count() + { + var pingMessage = new PingMessage(); + + Task>> fault = + SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); + + await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); + + ConsumeContext> faultContext = await fault; + + Assert.That(_count, Is.EqualTo(3)); + } + + int _count; + + protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) + { + configurator.AutoStart = true; + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + _count = 0; + + configurator.Handler(context => + { + Interlocked.Increment(ref _count); + + throw new IntentionalTestException(); + }, x => x.UseMessageRetry(r => r.Intervals(100, 200))); + } + } + + + [TestFixture] + [Category("Flaky")] + public class Using_delayed_exchange_redelivery_with_a_consumer : + RabbitMqTestFixture + { + [Test] + public async Task Should_retry_each_message_type() + { + var pingMessage = new PingMessage(); + + Task>> pingFault = + SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); + Task>> pongFault = + SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); + + await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); + await InputQueueSendEndpoint.Send(new PongMessage(pingMessage.CorrelationId), x => x.FaultAddress = Bus.Address); + + ConsumeContext> pingFaultContext = await pingFault; + ConsumeContext> pongFaultContext = await pongFault; + + Assert.Multiple(() => + { + Assert.That(_consumer.PingCount, Is.EqualTo(3)); + Assert.That(_consumer.PongCount, Is.EqualTo(3)); + }); + } + + Consumer _consumer; + + protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) + { + configurator.AutoStart = true; + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + configurator.UseDelayedRedelivery(r => r.Intervals(100, 200)); + + _consumer = new Consumer(); + configurator.Consumer(() => _consumer); + } + + + class Consumer : + IConsumer, + IConsumer + { + public int PingCount; + public int PongCount; + + public Task Consume(ConsumeContext context) + { + Interlocked.Increment(ref PingCount); + + throw new IntentionalTestException(); + } + + public Task Consume(ConsumeContext context) + { + Interlocked.Increment(ref PongCount); + + throw new IntentionalTestException(); + } + } + } + + + [TestFixture] + [Category("Flaky")] + public class Using_delayed_exchange_redelivery_with_a_consumer_and_retry : + RabbitMqTestFixture + { + [Test] + public async Task Should_retry_and_redeliver() + { + var pingMessage = new PingMessage(); + + Task>> pingFault = + SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); + + await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); + await InputQueueSendEndpoint.Send(new PongMessage(pingMessage.CorrelationId), x => x.FaultAddress = Bus.Address); + + ConsumeContext> pingFaultContext = await pingFault; + + Assert.That(Consumer.PingCount, Is.EqualTo(6)); + } + + protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) + { + configurator.AutoStart = true; + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + configurator.UseDelayedRedelivery(r => r.Intervals(100)); + configurator.UseMessageRetry(x => x.Immediate(2)); + + Consumer.PingCount = 0; + + configurator.Consumer(() => new Consumer()); + } + + + class Consumer : + IConsumer + { + public static int PingCount; + + public Consumer() + { + LogContext.Info?.Log("Creating consumer"); + } + + public Task Consume(ConsumeContext context) + { + Interlocked.Increment(ref PingCount); + + throw new IntentionalTestException(); + } + } + } + + + [TestFixture] + [Category("Flaky")] + public class Delaying_a_message_retry_with_policy_but_no_retries : + RabbitMqTestFixture + { + [Test] + public async Task Should_immediately_fault_with_no_delay() + { + var pingMessage = new PingMessage(); + + Task>> fault = + SubscribeHandler>(x => x.Message.Message.CorrelationId == pingMessage.CorrelationId); + + await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); + + ConsumeContext> faultContext = await fault; + + Assert.That(_count, Is.EqualTo(1)); + } + + int _count; + + protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) + { + configurator.AutoStart = true; + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + _count = 0; + + configurator.Handler(context => + { + Interlocked.Increment(ref _count); + + throw new IntentionalTestException(); + }, x => x.UseDelayedRedelivery(r => r.None())); + } + } + + + [TestFixture] + [Category("Flaky")] + public class Explicitly_deferring_a_message_instead_of_throwing : + RabbitMqTestFixture + { + [Test] + public async Task Should_properly_defer_the_message_delivery() + { + var pingMessage = new PingMessage(); + await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); + + var timer = Stopwatch.StartNew(); + + await _received.Task; + + timer.Stop(); + + Assert.That(timer.Elapsed, Is.GreaterThan(TimeSpan.FromSeconds(0.5))); + } + + TaskCompletionSource> _received; + int _count; + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + _count = 0; + + _received = GetTask>(); + + configurator.Handler(async context => + { + if (_count++ == 0) + { + await context.Defer(TimeSpan.FromMilliseconds(1000)); + return; + } + + _received.TrySetResult(context); + }); + } + } + + + [TestFixture] + [Category("Flaky")] + public class Execute_callback_function_during_defer : + RabbitMqTestFixture + { + [Test] + public async Task Should_execute_callback_during_defer_the_message_delivery() + { + var pingMessage = new PingMessage(); + await InputQueueSendEndpoint.Send(pingMessage, x => x.FaultAddress = Bus.Address); + + await _received.Task; + + Assert.That(_hit, Is.True); + } + + TaskCompletionSource> _received; + int _count; + bool _hit; + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumeTopology = false; + + _count = 0; + + _received = GetTask>(); + + configurator.Handler(async context => + { + if (_count++ == 0) + { + await context.Defer(TimeSpan.FromMilliseconds(100), (consumeContext, sendContext) => + { + _hit = true; + }); + + return; + } + + _received.TrySetResult(context); + }); + } + } +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/DirectReplyToRequestClient_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/DirectReplyToRequestClient_Specs.cs new file mode 100644 index 00000000000..af4310fe8d6 --- /dev/null +++ b/tests/MassTransit.RabbitMqTransport.Tests/DirectReplyToRequestClient_Specs.cs @@ -0,0 +1,52 @@ +namespace MassTransit.RabbitMqTransport.Tests; + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; + + +[TestFixture] +public class Using_the_direct_reply_to_request_client +{ + [Test] + public async Task Should_return_the_response_directly() + { + await using var provider = new ServiceCollection() + .ConfigureRabbitMqTestOptions(options => + { + options.CleanVirtualHost = true; + options.CreateVirtualHostIfNotExists = true; + }) + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => options.VHost = "test"); + + x.SetKebabCaseEndpointNameFormatter(); + + x.AddHandler(async context => await context.RespondAsync(new DirectResponse())); + + x.SetRabbitMqReplyToRequestClientFactory(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(); + + var harness = await provider.StartTestHarness(); + + Response response = await harness.GetRequestClient().GetResponse(new DirectRequest()); + + + Assert.That(await harness.Consumed.Any(x => x.Exception == null)); + } + + + public record DirectRequest; + + + public record DirectResponse; +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Encrypted_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Encrypted_Specs.cs index bb0032b7aba..93f8552e7af 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Encrypted_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Encrypted_Specs.cs @@ -18,7 +18,7 @@ public async Task Should_succeed() ConsumeContext received = await _handler; - Assert.AreEqual(EncryptedMessageSerializerV2.EncryptedContentType, received.ReceiveContext.ContentType); + Assert.That(received.ReceiveContext.ContentType, Is.EqualTo(EncryptedMessageSerializerV2.EncryptedContentType)); } Task> _handler; @@ -84,7 +84,7 @@ public async Task Should_succeed() ConsumeContext received = await _handler; - Assert.AreEqual(EncryptedMessageSerializer.EncryptedContentType, received.ReceiveContext.ContentType); + Assert.That(received.ReceiveContext.ContentType, Is.EqualTo(EncryptedMessageSerializer.EncryptedContentType)); } Task> _handler; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/EndpointConfiguration_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/EndpointConfiguration_Specs.cs index 1224e327306..43cb15ef469 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/EndpointConfiguration_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/EndpointConfiguration_Specs.cs @@ -3,7 +3,6 @@ namespace MassTransit.RabbitMqTransport.Tests using System; using System.Linq; using System.Threading.Tasks; - using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -11,6 +10,7 @@ namespace MassTransit.RabbitMqTransport.Tests using NUnit.Framework; using TestFramework; using TestFramework.Messages; + using Testing; [TestFixture] @@ -42,9 +42,12 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_overrid var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -73,9 +76,12 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_specifi var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -104,9 +110,12 @@ public async Task Should_include_concurrency_filter_if_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -135,8 +144,11 @@ public async Task Should_include_nothing_if_not_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -157,8 +169,11 @@ public void Should_override_bus_setting_if_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); } [Test] @@ -190,8 +205,11 @@ public void Should_use_bus_setting_if_not_specified() var probe = JObject.Parse(busControl.GetProbeResult().ToJsonString()); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); } @@ -204,7 +222,8 @@ public PingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -223,7 +242,8 @@ public EndpointPingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -233,7 +253,8 @@ class EmptyPingConsumerDefinition : ConsumerDefinition { protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/ErrorQueue_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/ErrorQueue_Specs.cs index 1be337b6ab1..ba438404e29 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/ErrorQueue_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/ErrorQueue_Specs.cs @@ -8,7 +8,6 @@ using Metadata; using NUnit.Framework; using RabbitMQ.Client; - using Shouldly; using TestFramework.Messages; @@ -18,10 +17,17 @@ public class A_serialization_exception : { [Test] public async Task Should_have_the_correlation_id() + { + ConsumeContext context = await _errorHandler; + Assert.That(context.CorrelationId, Is.EqualTo(_correlationId)); + } + + [Test] + public async Task Should_have_the_error_queue_header() { ConsumeContext context = await _errorHandler; - context.CorrelationId.ShouldBe(_correlationId); + Assert.That(context.ReceiveContext.TransportHeaders.Get(MessageHeaders.FaultInputAddress, (Uri)null), Is.EqualTo(InputQueueAddress)); } [Test] @@ -29,7 +35,7 @@ public async Task Should_have_the_exception() { ConsumeContext context = await _errorHandler; - context.ReceiveContext.TransportHeaders.Get("MT-Fault-Message", (string)null).ShouldBe("This is fine, forcing death"); + Assert.That(context.ReceiveContext.TransportHeaders.Get("MT-Fault-Message", (string)null), Is.EqualTo("This is fine, forcing death")); } [Test] @@ -37,7 +43,7 @@ public async Task Should_have_the_host_machine_name() { ConsumeContext context = await _errorHandler; - context.ReceiveContext.TransportHeaders.Get("MT-Host-MachineName", (string)null).ShouldBe(HostMetadataCache.Host.MachineName); + Assert.That(context.ReceiveContext.TransportHeaders.Get("MT-Host-MachineName", (string)null), Is.EqualTo(HostMetadataCache.Host.MachineName)); } [Test] @@ -45,7 +51,7 @@ public async Task Should_have_the_original_destination_address() { ConsumeContext context = await _errorHandler; - context.DestinationAddress.ShouldBe(InputQueueAddress); + Assert.That(context.DestinationAddress, Is.EqualTo(InputQueueAddress)); } [Test] @@ -53,7 +59,7 @@ public async Task Should_have_the_original_fault_address() { ConsumeContext context = await _errorHandler; - context.FaultAddress.ShouldBe(BusAddress); + Assert.That(context.FaultAddress, Is.EqualTo(BusAddress)); } [Test] @@ -61,7 +67,7 @@ public async Task Should_have_the_original_response_address() { ConsumeContext context = await _errorHandler; - context.ResponseAddress.ShouldBe(BusAddress); + Assert.That(context.ResponseAddress, Is.EqualTo(BusAddress)); } [Test] @@ -69,7 +75,7 @@ public async Task Should_have_the_original_source_address() { ConsumeContext context = await _errorHandler; - context.SourceAddress.ShouldBe(BusAddress); + Assert.That(context.SourceAddress, Is.EqualTo(BusAddress)); } [Test] @@ -77,15 +83,7 @@ public async Task Should_have_the_reason() { ConsumeContext context = await _errorHandler; - context.ReceiveContext.TransportHeaders.Get(MessageHeaders.Reason, (string)null).ShouldBe("fault"); - } - - [Test] - public async Task Should_have_the_error_queue_header() - { - ConsumeContext context = await _errorHandler; - - context.ReceiveContext.TransportHeaders.Get(MessageHeaders.FaultInputAddress, (Uri)null).ShouldBe(InputQueueAddress); + Assert.That(context.ReceiveContext.TransportHeaders.Get(MessageHeaders.Reason, (string)null), Is.EqualTo("fault")); } [Test] @@ -134,13 +132,13 @@ public class A_serialization_exception_from_a_bad_message : public async Task Should_have_the_host_machine_name() { var header = Encoding.UTF8.GetString((byte[])_basicGetResult.BasicProperties.Headers["MT-Host-MachineName"]); - header.ShouldBe(HostMetadataCache.Host.MachineName); + Assert.That(header, Is.EqualTo(HostMetadataCache.Host.MachineName)); } [Test] public void Should_have_the_invalid_body() { - _body.ShouldBe("[]"); + Assert.That(_body, Is.EqualTo("[]")); } [Test] @@ -148,7 +146,7 @@ public async Task Should_have_the_reason() { var header = Encoding.UTF8.GetString((byte[])_basicGetResult.BasicProperties.Headers["MT-Reason"]); - header.ShouldBe("fault"); + Assert.That(header, Is.EqualTo("fault")); } string _body; @@ -191,13 +189,13 @@ public class An_empty_message_body : public async Task Should_have_the_host_machine_name() { var header = Encoding.UTF8.GetString((byte[])_basicGetResult.BasicProperties.Headers["MT-Host-MachineName"]); - header.ShouldBe(HostMetadataCache.Host.MachineName); + Assert.That(header, Is.EqualTo(HostMetadataCache.Host.MachineName)); } [Test] public void Should_have_the_invalid_body() { - _body.ShouldBe(""); + Assert.That(_body, Is.EqualTo("")); } [Test] @@ -205,7 +203,7 @@ public async Task Should_have_the_reason() { var header = Encoding.UTF8.GetString((byte[])_basicGetResult.BasicProperties.Headers["MT-Reason"]); - header.ShouldBe("fault"); + Assert.That(header, Is.EqualTo("fault")); } string _body; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/ExcludeTopology_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/ExcludeTopology_Specs.cs index ff7a3d31578..792a74efae6 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/ExcludeTopology_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/ExcludeTopology_Specs.cs @@ -20,8 +20,11 @@ public async Task Should_not_create_the_exchange() ConsumeContext context = await _handled; - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); + }); var settings = GetHostSettings(); @@ -33,7 +36,7 @@ public async Task Should_not_create_the_exchange() using var model = connection.CreateModel(); - var eventExchangeName = RabbitMqBusFactory.MessageTopology.EntityNameFormatter.FormatEntityName(); + var eventExchangeName = RabbitMqBusFactory.CreateMessageTopology().EntityNameFormatter.FormatEntityName(); var exception = Assert.Throws(() => model.ExchangeDeclarePassive(eventExchangeName)); @@ -59,10 +62,10 @@ protected override void OnCleanupVirtualHost(IModel model) { base.OnCleanupVirtualHost(model); - var eventExchangeName = RabbitMqBusFactory.MessageTopology.EntityNameFormatter.FormatEntityName(); + var eventExchangeName = RabbitMqBusFactory.CreateMessageTopology().EntityNameFormatter.FormatEntityName(); model.ExchangeDelete(eventExchangeName); - var routedEventExchangeName = RabbitMqBusFactory.MessageTopology.EntityNameFormatter.FormatEntityName(); + var routedEventExchangeName = RabbitMqBusFactory.CreateMessageTopology().EntityNameFormatter.FormatEntityName(); model.ExchangeDelete(routedEventExchangeName); } } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/HeaderObject_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/HeaderObject_Specs.cs index bc4d78136f6..e7d81f42c36 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/HeaderObject_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/HeaderObject_Specs.cs @@ -47,10 +47,13 @@ await InputQueueSendEndpoint.Send(new PingMessage(), context => var identity = await _header.Task; - Assert.AreEqual(27, identity.IdentityId); - Assert.AreEqual("AAD:Claims", identity.IdentityType); - Assert.AreEqual(3, identity.Claims.Length); - Assert.AreEqual("Azure", identity.Claims[0].Issuer); + Assert.Multiple(() => + { + Assert.That(identity.IdentityId, Is.EqualTo(27)); + Assert.That(identity.IdentityType, Is.EqualTo("AAD:Claims")); + Assert.That(identity.Claims, Has.Length.EqualTo(3)); + }); + Assert.That(identity.Claims[0].Issuer, Is.EqualTo("Azure")); } Task> _handled; @@ -127,11 +130,14 @@ await InputQueueSendEndpoint.Send(new PingMessage(), x => ConsumeContext context = await _handled; - Assert.AreEqual(_now, context.Headers.Get("Now", default(DateTime?))); + Assert.Multiple(() => + { + Assert.That(context.Headers.Get("Now", default(DateTime?)), Is.EqualTo(_now)); - Assert.AreEqual(_later, context.Headers.Get("Later", default(DateTimeOffset?))); + Assert.That(context.Headers.Get("Later", default(DateTimeOffset?)), Is.EqualTo(_later)); - Assert.AreEqual(_sometime, context.Headers.Get("Sometime", default(DateTime?))); + Assert.That(context.Headers.Get("Sometime", default(DateTime?)), Is.EqualTo(_sometime)); + }); } Task> _handled; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/HostConfigurator_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/HostConfigurator_Specs.cs index 7597b3a11c6..9f3503ccacc 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/HostConfigurator_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/HostConfigurator_Specs.cs @@ -1,139 +1,136 @@ -namespace MassTransit.RabbitMqTransport.Tests -{ - using System; - using System.Net.Security; - using System.Security.Authentication; - using NUnit.Framework; - using RabbitMqTransport.Configuration; - using Shouldly; - using Shouldly.ShouldlyExtensionMethods; - - - [TestFixture] - public class HostConfigurator_Specs - { - [Test] - [Description("Default SSL settings should allow remote certificate chain error to maintain backward compatibility.")] - public void Should_allow_remote_certificate_chain_error_by_default() - { - var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); - - configurator.UseSsl(sslConfigurator => - { - }); - - configurator.Settings.AcceptablePolicyErrors.ShouldHaveFlag(SslPolicyErrors.RemoteCertificateChainErrors); - } - - [Test] - [Description("Remote certificate chain errors should be enforced when set.")] - public void Should_enforce_remote_certificate_chain_error_when_set() - { - var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); - - configurator.UseSsl(sslConfigurator => - { - sslConfigurator.EnforcePolicyErrors(SslPolicyErrors.RemoteCertificateChainErrors); - }); - - configurator.Settings.AcceptablePolicyErrors.ShouldNotHaveFlag(SslPolicyErrors.RemoteCertificateChainErrors); - } - - [Test] - [Description("Should parse vhost with escape characteres %2f")] - public void Should_ParseVhost_With_escapes() - { - var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost/%2fv%2fhost")); - - configurator.Settings.VirtualHost.ShouldBe("/v/host"); - } - - [Test] - public void Should_set_assigned_ssl_protocol() - { - var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); - - configurator.UseSsl(sslConfigurator => - { - sslConfigurator.Protocol = SslProtocols.Tls12; - }); - - configurator.Settings.SslProtocol.ShouldBe(SslProtocols.Tls12); - } - - [Test] - [Description("Default SSL settings should not use client certificate as authentication identity")] - public void Should_set_client_certificate_as_authentication_identity_as_false_by_default() - { - var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); - - configurator.UseSsl(sslConfigurator => - { - }); - - configurator.Settings.UseClientCertificateAsAuthenticationIdentity.ShouldBeFalse(); - } - - [Test] - [Description("Default ssl protocol should be tls 1.0 for back compatibility reason")] - public void Should_set_tls10_protocol_by_default() - { - var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); - - configurator.UseSsl(sslConfigurator => - { - }); - - configurator.Settings.SslProtocol.ShouldBe(SslProtocols.Tls); - } - - [Test] - [Description("Custom client certificate selector should be used when set.")] - public void Should_use_custom_client_certificate_selector_when_set() - { - LocalCertificateSelectionCallback customSelector = - (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) - => null; - - var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); - configurator.UseSsl(sslConfigurator => - { - sslConfigurator.CertificateSelectionCallback = customSelector; - }); - - configurator.Settings.CertificateSelectionCallback.ShouldBeSameAs(customSelector); - } - - [Test] - [Description("Custom remote certificate validator should be used when set.")] - public void Should_use_custom_remote_certificate_validator_when_set() - { - RemoteCertificateValidationCallback customValidator = - (sender, certificate, chain, sslPolicyErrors) - => false; - - var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); - configurator.UseSsl(sslConfigurator => - { - sslConfigurator.CertificateValidationCallback = customValidator; - }); - - configurator.Settings.CertificateValidationCallback.ShouldBeSameAs(customValidator); - } - - [TestCase(true)] - [TestCase(false)] - [Description("SSL settings should set use client certificate as authentication identity as specified by configuration")] - public void Should_set_client_certificate_as_authentication_identity_when_configured(bool valueToSet) - { - var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); - - configurator.UseSsl(sslConfigurator => - { - sslConfigurator.UseCertificateAsAuthenticationIdentity = valueToSet; - }); - - configurator.Settings.UseClientCertificateAsAuthenticationIdentity.ShouldBe(valueToSet); - } - } -} +namespace MassTransit.RabbitMqTransport.Tests +{ + using System; + using System.Net.Security; + using System.Security.Authentication; + using NUnit.Framework; + using RabbitMqTransport.Configuration; + + + [TestFixture] + public class HostConfigurator_Specs + { + [Test] + [Description("Default SSL settings should allow remote certificate chain error to maintain backward compatibility.")] + public void Should_allow_remote_certificate_chain_error_by_default() + { + var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); + + configurator.UseSsl(sslConfigurator => + { + }); + + Assert.That(configurator.Settings.AcceptablePolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors), Is.True); + } + + [Test] + [Description("Remote certificate chain errors should be enforced when set.")] + public void Should_enforce_remote_certificate_chain_error_when_set() + { + var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); + + configurator.UseSsl(sslConfigurator => + { + sslConfigurator.EnforcePolicyErrors(SslPolicyErrors.RemoteCertificateChainErrors); + }); + Assert.That(configurator.Settings.AcceptablePolicyErrors.HasFlag(SslPolicyErrors.RemoteCertificateChainErrors), Is.False); + } + + [Test] + [Description("Should parse vhost with escape characteres %2f")] + public void Should_ParseVhost_With_escapes() + { + var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost/%2fv%2fhost")); + + Assert.That(configurator.Settings.VirtualHost, Is.EqualTo("/v/host")); + } + + [Test] + public void Should_set_assigned_ssl_protocol() + { + var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); + + configurator.UseSsl(sslConfigurator => + { + sslConfigurator.Protocol = SslProtocols.Tls12; + }); + + Assert.That(configurator.Settings.SslProtocol, Is.EqualTo(SslProtocols.Tls12)); + } + + [Test] + [Description("Default SSL settings should not use client certificate as authentication identity")] + public void Should_set_client_certificate_as_authentication_identity_as_false_by_default() + { + var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); + + configurator.UseSsl(sslConfigurator => + { + }); + + Assert.That(configurator.Settings.UseClientCertificateAsAuthenticationIdentity, Is.False); + } + + [Test] + [Description("Default ssl protocol should be tls 1.0 for back compatibility reason")] + public void Should_set_tls10_protocol_by_default() + { + var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); + + configurator.UseSsl(sslConfigurator => + { + }); + + Assert.That(configurator.Settings.SslProtocol, Is.EqualTo(SslProtocols.Tls12)); + } + + [Test] + [Description("Custom client certificate selector should be used when set.")] + public void Should_use_custom_client_certificate_selector_when_set() + { + LocalCertificateSelectionCallback customSelector = + (sender, targetHost, localCertificates, remoteCertificate, acceptableIssuers) + => null; + + var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); + configurator.UseSsl(sslConfigurator => + { + sslConfigurator.CertificateSelectionCallback = customSelector; + }); + + Assert.That(configurator.Settings.CertificateSelectionCallback, Is.EqualTo(customSelector)); + } + + [Test] + [Description("Custom remote certificate validator should be used when set.")] + public void Should_use_custom_remote_certificate_validator_when_set() + { + RemoteCertificateValidationCallback customValidator = + (sender, certificate, chain, sslPolicyErrors) + => false; + + var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); + configurator.UseSsl(sslConfigurator => + { + sslConfigurator.CertificateValidationCallback = customValidator; + }); + + Assert.That(configurator.Settings.CertificateValidationCallback, Is.EqualTo(customValidator)); + } + + [TestCase(true)] + [TestCase(false)] + [Description("SSL settings should set use client certificate as authentication identity as specified by configuration")] + public void Should_set_client_certificate_as_authentication_identity_when_configured(bool valueToSet) + { + var configurator = new RabbitMqHostConfigurator(new Uri("rabbitmq://localhost")); + + configurator.UseSsl(sslConfigurator => + { + sslConfigurator.UseCertificateAsAuthenticationIdentity = valueToSet; + }); + + Assert.That(configurator.Settings.UseClientCertificateAsAuthenticationIdentity, Is.EqualTo(valueToSet)); + } + } +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/InMemoryOutboxRedelivery_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/InMemoryOutboxRedelivery_Specs.cs index ddc85c35104..591ea77c3b6 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/InMemoryOutboxRedelivery_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/InMemoryOutboxRedelivery_Specs.cs @@ -194,7 +194,7 @@ protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator con protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) { configurator.UseDelayedRedelivery(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); - configurator.UseRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); + configurator.UseMessageRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); configurator.UseInMemoryOutbox(); configurator.Consumer(); diff --git a/tests/MassTransit.RabbitMqTransport.Tests/JobConsumer_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/JobConsumer_Specs.cs new file mode 100644 index 00000000000..dbe72770371 --- /dev/null +++ b/tests/MassTransit.RabbitMqTransport.Tests/JobConsumer_Specs.cs @@ -0,0 +1,452 @@ +namespace MassTransit.RabbitMqTransport.Tests +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Internals; + using JobConsumerTests; + using Logging; + using MassTransit.Contracts.JobService; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using Microsoft.Extensions.Options; + using NUnit.Framework; + using Testing; + using Util; + + + namespace JobConsumerTests + { + using System; + using System.Threading.Tasks; + using MassTransit.Contracts.JobService; + + + public interface OddJob + { + TimeSpan Duration { get; } + } + + + public class OddJobConsumer : + IJobConsumer + { + public async Task Run(JobContext context) + { + if (context.RetryAttempt == 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + } + } + + + public class OddJobCompletedConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + return Task.CompletedTask; + } + } + } + + + [TestFixture] + public class JobConsumer_Specs + { + [Test] + [Explicit] + public async Task Should_cancel_on_shutdown_and_then_restart_the_job() + { + await using var provider = new ServiceCollection() + .AddMassTransit(x => + { + x.AddOptions(); + x.TryAddSingleton(provider => + new TextWriterLoggerFactory(Console.Out, provider.GetRequiredService>())); + x.TryAddSingleton(typeof(ILogger<>), typeof(Logger<>)); + + x.AddOptions() + .Configure(options => options.VHost = "test"); + + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); + + x.SetInMemorySagaRepositoryProvider(); + + x.AddJobSagaStateMachines(options => + { + options.SlotWaitTime = TimeSpan.FromSeconds(1); + }); + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + IHostedService[] services = provider.GetServices().ToArray(); + + foreach (var service in services) + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + + var jobId = NewId.NextGuid(); + + var connector = provider.GetRequiredService(); + + TaskCompletionSource started = TaskUtil.GetTask(); + TaskCompletionSource completed = TaskUtil.GetTask(); + + var handle = connector.ConnectReceiveEndpoint("observers", (context, cfg) => + { + cfg.Handler(async e => started.TrySetResult(true)); + cfg.Handler(async e => completed.TrySetResult(true)); + }); + + await handle.Ready; + + await using var scope = provider.CreateAsyncScope(); + + _ = await scope.ServiceProvider.GetRequiredService>>() + .SubmitJob(jobId, new { Duration = TimeSpan.FromSeconds(5) }); + + await started.Task.OrTimeout(TimeSpan.FromSeconds(10)); + + await Task.Delay(1000); + + foreach (var service in services.Reverse()) + await service.StopAsync(CancellationToken.None).ConfigureAwait(false); + + await Task.Delay(1000); + + foreach (var service in services) + await service.StartAsync(CancellationToken.None).ConfigureAwait(false); + + await completed.Task.OrTimeout(TimeSpan.FromSeconds(10)); + } + + [Test] + public async Task Should_cancel_the_job() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + Assert.That(await harness.Published.Any(), Is.True); + + await harness.Stop(); + } + + [Test] + public async Task Should_cancel_the_job_and_get_the_status() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + IRequestClient stateClient = harness.GetRequestClient(); + + Response jobState = await stateClient.GetResponse(new { JobId = jobId }); + + Assert.That(jobState.Message.CurrentState, Is.EqualTo("Started")); + + await harness.Bus.CancelJob(jobId); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Sent.Any(), Is.True); + }); + + jobState = await stateClient.GetResponse(new { JobId = jobId }); + + Assert.That(jobState.Message.CurrentState, Is.EqualTo("Canceled")); + + await harness.Stop(); + } + + [Test] + public async Task Should_cancel_the_job_and_retry_it() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Sent.Any(), Is.True); + }); + + await harness.Bus.RetryJob(jobId); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Stop(); + } + + [Test] + public async Task Should_cancel_the_job_while_waiting() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + await harness.Start(); + + var previousJobId = NewId.NextGuid(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + await client.GetResponse(new + { + JobId = previousJobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Sent.Any(x => x.Context.Message.JobId == jobId), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + + await harness.Bus.CancelJob(previousJobId); + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + + await harness.Stop(); + } + + [Test] + public async Task Should_complete_the_job() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + await harness.Start(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(1) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Stop(); + } + + [Test] + public async Task Should_create_a_unique_job_id() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + await harness.Start(); + + await harness.Bus.Publish(new { Duration = TimeSpan.FromSeconds(1) }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + IPublishedMessage publishedMessage = await harness.Published.SelectAsync().First(); + Assert.That(publishedMessage.Context.Message.JobId, Is.Not.EqualTo(Guid.Empty)); + + await harness.Stop(); + } + + [Test] + public async Task Should_return_not_found() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + var jobId = NewId.NextGuid(); + + IRequestClient stateClient = harness.GetRequestClient(); + + var jobState = await stateClient.GetJobState(jobId); + + Assert.That(jobState.CurrentState, Is.EqualTo("NotFound")); + + await harness.Stop(); + } + + static ServiceProvider SetupServiceCollection(bool cleanVirtualHost = true) + { + var provider = new ServiceCollection() + .ConfigureRabbitMqTestOptions(options => + { + options.CleanVirtualHost = cleanVirtualHost; + options.CreateVirtualHostIfNotExists = true; + }) + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => options.VHost = "test"); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); + + x.AddJobSagaStateMachines(); + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + return provider; + } + } +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/KillSwitch_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/KillSwitch_Specs.cs index ade88c2ecdd..d9507f205c0 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/KillSwitch_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/KillSwitch_Specs.cs @@ -3,9 +3,9 @@ namespace MassTransit.RabbitMqTransport.Tests using System; using System.Linq; using System.Threading.Tasks; - using MassTransit.Testing; using NUnit.Framework; using TestFramework; + using Testing; [Category("Flaky")] @@ -20,11 +20,14 @@ public async Task Should_be_degraded_after_too_many_exceptions() await Task.WhenAll(Enumerable.Range(0, 11).Select(x => Bus.Publish(new BadMessage()))); - Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Degraded, TimeSpan.FromSeconds(15)), Is.EqualTo(BusHealthStatus.Degraded)); + await Assert.MultipleAsync(async () => + { + Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Degraded, TimeSpan.FromSeconds(15)), Is.EqualTo(BusHealthStatus.Degraded)); - Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Healthy, TimeSpan.FromSeconds(10)), Is.EqualTo(BusHealthStatus.Healthy)); + Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Healthy, TimeSpan.FromSeconds(10)), Is.EqualTo(BusHealthStatus.Healthy)); - Assert.That(await RabbitMqTestHarness.Consumed.SelectAsync().Take(11).Count(), Is.EqualTo(11)); + Assert.That(await RabbitMqTestHarness.Consumed.SelectAsync().Take(11).Count(), Is.EqualTo(11)); + }); await Task.WhenAll(Enumerable.Range(0, 20).Select(x => Bus.Publish(new GoodMessage()))); diff --git a/tests/MassTransit.RabbitMqTransport.Tests/MassTransit.RabbitMqTransport.Tests.csproj b/tests/MassTransit.RabbitMqTransport.Tests/MassTransit.RabbitMqTransport.Tests.csproj index 8ae0d198781..9837e51d7cf 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/MassTransit.RabbitMqTransport.Tests.csproj +++ b/tests/MassTransit.RabbitMqTransport.Tests/MassTransit.RabbitMqTransport.Tests.csproj @@ -1,19 +1,19 @@  - net6.0 - - - - $(TargetFrameworks);net462 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + diff --git a/tests/MassTransit.RabbitMqTransport.Tests/MessageName_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/MessageName_Specs.cs index 6a2688efbb9..6ffdb0d5dc2 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/MessageName_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/MessageName_Specs.cs @@ -2,7 +2,6 @@ namespace MassTransit.RabbitMqTransport.Tests { using System; using NUnit.Framework; - using Shouldly; using Transports; @@ -19,21 +18,21 @@ public When_converting_a_type_to_a_message_name() public void Should_handle_an_interface_name() { var name = _formatter.GetMessageName(typeof(NameEasyToo)); - name.ToString().ShouldBe("MassTransit.RabbitMqTransport.Tests:NameEasyToo"); + Assert.That(name, Is.EqualTo("MassTransit.RabbitMqTransport.Tests:NameEasyToo")); } [Test] public void Should_handle_nested_classes() { var name = _formatter.GetMessageName(typeof(Nested)); - name.ToString().ShouldBe("MassTransit.RabbitMqTransport.Tests:When_converting_a_type_to_a_message_name-Nested"); + Assert.That(name, Is.EqualTo("MassTransit.RabbitMqTransport.Tests:When_converting_a_type_to_a_message_name-Nested")); } [Test] public void Should_handle_regular_classes() { var name = _formatter.GetMessageName(typeof(NameEasy)); - name.ToString().ShouldBe("MassTransit.RabbitMqTransport.Tests:NameEasy"); + Assert.That(name, Is.EqualTo("MassTransit.RabbitMqTransport.Tests:NameEasy")); } [Test] @@ -46,21 +45,21 @@ public void Should_throw_an_exception_on_an_open_generic_class_name() public void Should_handle_a_closed_single_generic() { var name = _formatter.GetMessageName(typeof(NameGeneric)); - name.ToString().ShouldBe("MassTransit.RabbitMqTransport.Tests:NameGeneric--System:String--"); + Assert.That(name, Is.EqualTo("MassTransit.RabbitMqTransport.Tests:NameGeneric--System:String--")); } [Test] public void Should_handle_a_closed_double_generic() { var name = _formatter.GetMessageName(typeof(NameDoubleGeneric)); - name.ToString().ShouldBe("MassTransit.RabbitMqTransport.Tests:NameDoubleGeneric--System:String::NameEasy--"); + Assert.That(name, Is.EqualTo("MassTransit.RabbitMqTransport.Tests:NameDoubleGeneric--System:String::NameEasy--")); } [Test] public void Should_handle_a_closed_double_generic_with_a_generic() { var name = _formatter.GetMessageName(typeof(NameDoubleGeneric, NameEasy>)); - name.ToString().ShouldBe("MassTransit.RabbitMqTransport.Tests:NameDoubleGeneric--NameGeneric--NameEasyToo--::NameEasy--"); + Assert.That(name, Is.EqualTo("MassTransit.RabbitMqTransport.Tests:NameDoubleGeneric--NameGeneric--NameEasyToo--::NameEasy--")); } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/MessageTopology_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/MessageTopology_Specs.cs index d55d7690ae6..7bff7be114f 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/MessageTopology_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/MessageTopology_Specs.cs @@ -34,4 +34,36 @@ class MessageOne public string Value { get; set; } } } + + + [TestFixture] + public class Disabling_consume_topology_for_one_message_by_type : + RabbitMqTestFixture + { + [Test] + public async Task Should_only_get_the_consumed_message() + { + await Bus.Publish(new MessageTwo { Value = "Invalid" }); + await InputQueueSendEndpoint.Send(new MessageTwo { Value = "Valid" }); + + ConsumeContext handled = await _handled; + + Assert.That(handled.Message.Value, Is.EqualTo("Valid")); + } + + Task> _handled; + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ConfigureMessageTopology(typeof(MessageTwo), false); + + _handled = Handled(configurator); + } + + + class MessageTwo + { + public string Value { get; set; } + } + } } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/OpenTelemetry_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/OpenTelemetry_Specs.cs index eab7965c2b9..922077610f2 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/OpenTelemetry_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/OpenTelemetry_Specs.cs @@ -7,7 +7,6 @@ namespace MassTransit.RabbitMqTransport.Tests using HarnessContracts; using Initializers; using Logging; - using MassTransit.Testing; using Mediator; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; @@ -16,6 +15,7 @@ namespace MassTransit.RabbitMqTransport.Tests using OpenTelemetry.Trace; using TestFramework.Courier; using TestFramework.Messages; + using Testing; [TestFixture] @@ -23,11 +23,10 @@ namespace MassTransit.RabbitMqTransport.Tests public class OpenTelemetry_Specs { [Test] - public async Task Should_report_telemetry_to_jaeger() + public async Task Should_carry_the_baggage_with_newtonsoft() { - using var tracerProvider = CreateTraceProvider("order-api"); - var services = new ServiceCollection(); + AddTraceListener(services, "order-api"); await using var provider = services .AddMassTransitTestHarness(x => @@ -36,6 +35,8 @@ public async Task Should_report_telemetry_to_jaeger() x.UsingRabbitMq((context, cfg) => { + cfg.UseNewtonsoftJsonSerializer(); + cfg.ConfigureEndpoints(context); }); }) @@ -61,19 +62,21 @@ public async Task Should_report_telemetry_to_jaeger() activity?.Stop(); - Assert.IsTrue(await harness.Sent.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Sent.Any(), Is.True); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); - Assert.That(response.Headers.Get("BaggageValue"), Is.EqualTo("IsFull")); + Assert.That(response.Headers.Get("BaggageValue"), Is.EqualTo("IsFull")); + }); } [Test] - public async Task Should_carry_the_baggage_with_newtonsoft() + public async Task Should_report_telemetry_to_jaeger() { - using var tracerProvider = CreateTraceProvider("order-api"); - var services = new ServiceCollection(); + AddTraceListener(services, "order-api"); await using var provider = services .AddMassTransitTestHarness(x => @@ -82,8 +85,6 @@ public async Task Should_carry_the_baggage_with_newtonsoft() x.UsingRabbitMq((context, cfg) => { - cfg.UseNewtonsoftJsonSerializer(); - cfg.ConfigureEndpoints(context); }); }) @@ -109,19 +110,21 @@ public async Task Should_carry_the_baggage_with_newtonsoft() activity?.Stop(); - Assert.IsTrue(await harness.Sent.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Sent.Any(), Is.True); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); - Assert.That(response.Headers.Get("BaggageValue"), Is.EqualTo("IsFull")); + Assert.That(response.Headers.Get("BaggageValue"), Is.EqualTo("IsFull")); + }); } [Test] public async Task Should_report_telemetry_to_jaeger_for_batch_consumer() { - using var tracerProvider = CreateTraceProvider("order-api"); - var services = new ServiceCollection(); + AddTraceListener(services, "order-api"); await using var provider = services .AddMassTransitTestHarness(x => @@ -155,7 +158,7 @@ await harness.Bus.Publish(new activity?.Stop(); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); await harness.Stop(); } @@ -163,9 +166,8 @@ await harness.Bus.Publish(new [Test] public async Task Should_report_telemetry_to_jaeger_for_routing_slip() { - using var tracerProvider = CreateTraceProvider("routing-api"); - var services = new ServiceCollection(); + AddTraceListener(services, "routing-api"); await using var provider = services .AddMassTransitTestHarness(x => @@ -194,16 +196,18 @@ public async Task Should_report_telemetry_to_jaeger_for_routing_slip() Response response = await client.GetResponse(new { Value = "Hello" }); - Assert.That(response.Message.Value, Is.EqualTo("Hello, World!")); - Assert.That(response.Message.Variable, Is.EqualTo("Knife")); + Assert.Multiple(() => + { + Assert.That(response.Message.Value, Is.EqualTo("Hello, World!")); + Assert.That(response.Message.Variable, Is.EqualTo("Knife")); + }); } [Test] public async Task Should_report_telemetry_to_jaeger_from_mediator() { - using var tracerProvider = CreateTraceProvider("mediator"); - var services = new ServiceCollection(); + AddTraceListener(services, "mediator"); await using var provider = services .AddMediator(x => @@ -229,9 +233,10 @@ await client.GetResponse(new [Test] public async Task Should_support_the_saga_harness() { - using var tracerProvider = CreateTraceProvider("saga-api"); + var services = new ServiceCollection(); + AddTraceListener(services, "saga-api"); - await using var provider = new ServiceCollection() + await using var provider = services .AddMassTransitTestHarness(x => { x.SetKebabCaseEndpointNameFormatter(); @@ -256,42 +261,47 @@ public async Task Should_support_the_saga_harness() await harness.Bus.Publish(new Start { CorrelationId = sagaId }); - Assert.IsTrue(await harness.Consumed.Any(), "Message not received"); + Assert.That(await harness.Consumed.Any(), Is.True, "Message not received"); var sagaHarness = provider.GetRequiredService>(); - Assert.That(await sagaHarness.Consumed.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any()); - Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == sagaId)); + Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == sagaId)); + }); var machine = provider.GetRequiredService(); var instance = sagaHarness.Created.ContainsInState(sagaId, machine, machine.Running); - Assert.IsNotNull(instance, "Saga instance not found"); + await Assert.MultipleAsync(async () => + { + Assert.That(instance, Is.Not.Null, "Saga instance not found"); - Assert.IsTrue(await harness.Published.Any(), "Event not published"); + Assert.That(await harness.Published.Any(), Is.True, "Event not published"); + }); } - static TracerProvider CreateTraceProvider(string serviceName) + static void AddTraceListener(IServiceCollection services, string serviceName) { - return Sdk.CreateTracerProviderBuilder() - .SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName)) - .AddSource("MassTransit") - .AddJaegerExporter(o => - { - o.AgentHost = "localhost"; - o.AgentPort = 6831; - o.MaxPayloadSizeInBytes = 4096; - o.ExportProcessorType = ExportProcessorType.Batch; - o.BatchExportProcessorOptions = new BatchExportProcessorOptions + services.AddOpenTelemetry() + .WithTracing(t => t.SetResourceBuilder(ResourceBuilder.CreateDefault().AddService(serviceName)) + .AddSource(DiagnosticHeaders.DefaultListenerName) + .AddJaegerExporter(o => { - MaxQueueSize = 2048, - ScheduledDelayMilliseconds = 5000, - ExporterTimeoutMilliseconds = 30000, - MaxExportBatchSize = 512, - }; - }) - .Build(); + o.AgentHost = "localhost"; + o.AgentPort = 6831; + o.MaxPayloadSizeInBytes = 4096; + o.ExportProcessorType = ExportProcessorType.Batch; + o.BatchExportProcessorOptions = new BatchExportProcessorOptions + { + MaxQueueSize = 2048, + ScheduledDelayMilliseconds = 5000, + ExporterTimeoutMilliseconds = 30000, + MaxExportBatchSize = 512 + }; + })); } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/OutboxFault_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/OutboxFault_Specs.cs new file mode 100644 index 00000000000..c89fa553ef7 --- /dev/null +++ b/tests/MassTransit.RabbitMqTransport.Tests/OutboxFault_Specs.cs @@ -0,0 +1,267 @@ +namespace MassTransit.RabbitMqTransport.Tests +{ + using System; + using System.Net.Mime; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + [TestFixture] + public class When_a_send_faults_in_the_outbox + { + [Test] + public async Task Should_handle_the_redelivery_of_a_scheduled_message() + { + await using var provider = new ServiceCollection() + .ConfigureRabbitMqTestOptions(options => + { + options.CreateVirtualHostIfNotExists = true; + options.CleanVirtualHost = true; + }) + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => options.VHost = "test"); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); + + x.AddConfigureEndpointsCallback((context, name, cfg) => + { + cfg.UseSendFilter(typeof(SomeSendFilter<>), context); + cfg.UsePublishFilter(typeof(SomePublishFilter<>), context); + }); + + x.AddHandler((ConsumeContext context) => context.RespondAsync(new SomeResponse + { + Status = context.Message.Count >= 5 ? "Finished" : "Running" + })); + + x.AddSagaStateMachine(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var id = NewId.NextGuid(); + + await harness.Bus.Publish(new InitialEvent { CorrelationId = id }); + + Assert.That(await harness.Published.Any(x => x.Context.Message.CorrelationId == id)); + + await harness.Stop(); + } + + + class SomeInstance : + SagaStateMachineInstance + { + public State CurrentState { get; set; } + public Guid? TokenId { get; set; } + public int Count { get; set; } + public Guid CorrelationId { get; set; } + } + + + class SomeSendFilter : + IFilter> + where T : class + { + public Task Send(SendContext context, IPipe> next) + { + // if (context.Message is SomeInstanceEvent { Count: 2 }) + // context.Serializer = new DeathComesMeSerializer(); + + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + } + } + + class SomePublishFilter : + IFilter> + where T : class + { + public Task Send(PublishContext context, IPipe> next) + { + if (context.Message is SomeRequest { Count: 2 }) + context.Serializer = new DeathComesMeSerializer(); + + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + } + } + + class DeathComesMeSerializer : + IMessageSerializer + { + public ContentType ContentType => new ContentType("application/death"); + + public MessageBody GetMessageBody(SendContext context) + where T : class + { + throw new MessageNotConfirmedException(context.DestinationAddress, + "exchange:instance => The message was not confirmed: NOT_FOUND - no exchange 'instance' in vhost"); + } + } + + + class SomeStateMachine : + MassTransitStateMachine + { + public SomeStateMachine() + { + InstanceState(x => x.CurrentState); + + Request(() => HandlerRequest, x => + { + x.Timeout = TimeSpan.Zero; + }); + + Schedule(() => ScheduleEvent, x => x.TokenId, x => + { + x.Delay = TimeSpan.FromSeconds(1); + x.Received = r => + { + r.CorrelateById(m => m.Message.CorrelationId); + r.ConfigureConsumeTopology = false; + }; + }); + + Initially( + When(InitialEventReceived) + .Then(context => LogContext.Debug?.Log("Initial event, scheduling instance event")) + .Schedule(ScheduleEvent, context => new SomeInstanceEvent + { + CorrelationId = context.Saga.CorrelationId, + Count = context.Saga.Count++ + }) + .TransitionTo(Running)); + + During(Running, + When(ScheduleEvent.Received) + .Then(context => LogContext.Debug?.Log("Sending request")) + .Request(HandlerRequest, context => new SomeRequest { Count = context.Saga.Count }) + .Schedule(ScheduleEvent, context => new SomeInstanceEvent + { + CorrelationId = context.Saga.CorrelationId, + Count = context.Saga.Count++ + }) + .TransitionTo(Checking) + ); + + During(Checking, + When(ScheduleEvent.Received) + .Then(context => LogContext.Debug?.Log("Scheduled event received while waiting for request")) + .Request(HandlerRequest, context => new SomeRequest { Count = context.Saga.Count }) + .Schedule(ScheduleEvent, context => new SomeInstanceEvent + { + CorrelationId = context.Saga.CorrelationId, + Count = context.Saga.Count++ + }) + .TransitionTo(Suspect) + ); + + During(Suspect, + When(ScheduleEvent.Received) + .Then(context => LogContext.Debug?.Log("Suspect, scheduled event, faulted time")) + .TransitionTo(Failed) + .Publish(context => new InstanceCompleted + { + CorrelationId = context.Saga.CorrelationId, + Result = "Faulted" + }) + ); + + During(Running, Checking, Suspect, + When(HandlerRequest.Completed) + .IfElse(context => context.Message.Status == "Running", running => running + .Then(context => LogContext.Debug?.Log("Response received, back to running")) + .TransitionTo(Running), otherwise => otherwise + .Then(context => LogContext.Debug?.Log("Response received, to completed")) + .Finalize() + ) + ); + + WhenEnter(Final, x => x.Unschedule(ScheduleEvent) + .Publish(context => new InstanceCompleted + { + CorrelationId = context.Saga.CorrelationId, + Result = "Success" + }) + ); + } + + // + // ReSharper disable UnassignedGetOnlyAutoProperty + public Schedule ScheduleEvent { get; } + + public Request HandlerRequest { get; } + + public Event InitialEventReceived { get; } + + public State Running { get; } + public State Checking { get; } + public State Suspect { get; } + public State Failed { get; } + } + + + class SomeSagaDefinition : + SagaDefinition + { + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) + { + endpointConfigurator.UseMessageScope(context); + endpointConfigurator.UseMessageRetry(r => r.Immediate(5)); + endpointConfigurator.UseInMemoryOutbox(context); + } + } + + + public class SomeRequest + { + public int Count { get; set; } + } + + + public class SomeResponse + { + public string Status { get; set; } + } + + + public class SomeInstanceEvent + { + public Guid CorrelationId { get; set; } + public int Count { get; set; } + } + + + public class InstanceCompleted + { + public Guid CorrelationId { get; set; } + public string Result { get; set; } + } + + + public class InitialEvent + { + public Guid CorrelationId { get; set; } + } + } +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/PriorityQueue_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/PriorityQueue_Specs.cs index b4c6db1ac8d..5a1efcaaaf0 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/PriorityQueue_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/PriorityQueue_Specs.cs @@ -2,11 +2,11 @@ { using System; using System.Threading.Tasks; - using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using RabbitMQ.Client; using TestFramework.Messages; + using Testing; [TestFixture] @@ -118,8 +118,11 @@ await harness.Bus.Publish(new PingMessage(), Pipe.Execute(context = Assert.That(message, Is.Not.Null); - Assert.That(message.Context.TryGetPayload(out var rmqContext), Is.True); - Assert.That(rmqContext.Properties.Priority, Is.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(message.Context.TryGetPayload(out var rmqContext), Is.True); + Assert.That(rmqContext.Properties.Priority, Is.EqualTo(3)); + }); } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/PublishFaultChannel_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/PublishFaultChannel_Specs.cs index c2d8cbda47e..28c14a68aa3 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/PublishFaultChannel_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/PublishFaultChannel_Specs.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -29,7 +28,7 @@ public async Task Should_have_the_original_source_address() { ConsumeContext context = await _errorHandler; - context.SourceAddress.ShouldBe(BusAddress); + Assert.That(context.SourceAddress, Is.EqualTo(BusAddress)); } [Test] diff --git a/tests/MassTransit.RabbitMqTransport.Tests/PublishHeader_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/PublishHeader_Specs.cs index e1be06812ca..f1b34e365f4 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/PublishHeader_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/PublishHeader_Specs.cs @@ -4,7 +4,6 @@ namespace MassTransit.RabbitMqTransport.Tests using System.Linq; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework.Messages; @@ -26,8 +25,7 @@ public async Task Should_source_address_from_the_endpoint() ConsumeContext context = await _handled; ConsumeContext responseContext = await responseHandled; - - responseContext.SourceAddress.ShouldBe(InputQueueAddress); + Assert.That(responseContext.SourceAddress, Is.EqualTo(InputQueueAddress)); } Task> _handled; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Publish_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Publish_Specs.cs index 129d3c9fe9a..8769997bd42 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Publish_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Publish_Specs.cs @@ -1,22 +1,15 @@ namespace MassTransit.RabbitMqTransport.Tests { - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using Shouldly; - - namespace Send_Specs { using System; using System.Linq; using System.Threading.Tasks; - using MassTransit.Testing; using NUnit.Framework; using RabbitMQ.Client; using Serialization; - using Shouldly; using TestFramework; + using Testing; [TestFixture] @@ -33,7 +26,7 @@ public async Task Should_be_received() ConsumeContext received = await _receivedA; - Assert.AreEqual(message.Id, received.Message.Id); + Assert.That(received.Message.Id, Is.EqualTo(message.Id)); } Task> _receivedA; @@ -46,6 +39,7 @@ protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpoin } } + [TestFixture] public class WhenAMessageIsSendToTheEndpointWithAGuidHeader : RabbitMqTestFixture @@ -64,7 +58,7 @@ await endpoint.Send(message, context => ConsumeContext received = await _receivedA; - Assert.AreEqual(message.Id, received.Message.Id); + Assert.That(received.Message.Id, Is.EqualTo(message.Id)); } Task> _receivedA; @@ -92,7 +86,7 @@ public async Task Should_be_received() ConsumeContext received = await receivedA; - Assert.AreEqual(message.Id, received.Message.Id); + Assert.That(received.Message.Id, Is.EqualTo(message.Id)); } } @@ -111,9 +105,12 @@ public async Task Should_be_received() ConsumeContext received = await _receivedA; - Assert.AreEqual(message.Id, received.Message.Id); + Assert.Multiple(() => + { + Assert.That(received.Message.Id, Is.EqualTo(message.Id)); - Assert.AreEqual(EncryptedMessageSerializer.EncryptedContentType, received.ReceiveContext.ContentType); + Assert.That(received.ReceiveContext.ContentType, Is.EqualTo(EncryptedMessageSerializer.EncryptedContentType)); + }); } Task> _receivedA; @@ -148,7 +145,7 @@ public async Task Should_be_received() ConsumeContext received = await _receivedA; - Assert.AreEqual(message.Id, received.Message.Id); + Assert.That(received.Message.Id, Is.EqualTo(message.Id)); } Task> _receivedA; @@ -175,7 +172,7 @@ public async Task Should_not_increase_channel_count() ConsumeContext received = await _receivedA; - Assert.AreEqual(message.Id, received.Message.Id); + Assert.That(received.Message.Id, Is.EqualTo(message.Id)); } [Test] @@ -210,7 +207,7 @@ public async Task Should_not_increase_channel_count() ConsumeContext> received = await _faultA; - Assert.AreEqual(message.Id, received.Message.Message.Id); + Assert.That(received.Message.Message.Id, Is.EqualTo(message.Id)); } [Test] @@ -304,14 +301,17 @@ public async Task Should_have_the_receive_endpoint_input_address() ConsumeContext received = await _receivedA; - Assert.AreEqual(message.Id, received.Message.Id); + Assert.That(received.Message.Id, Is.EqualTo(message.Id)); ConsumeContext consumeContext = await _receivedGotA; - consumeContext.SourceAddress.ShouldBe(new Uri("rabbitmq://localhost/test/input_queue")); + Assert.Multiple(() => + { + Assert.That(consumeContext.SourceAddress, Is.EqualTo(new Uri("rabbitmq://localhost/test/input_queue"))); - Assert.That(consumeContext.ReceiveContext.TransportHeaders.Get(MessageHeaders.MessageId, "N/A"), - Is.EqualTo(consumeContext.MessageId.ToString())); + Assert.That(consumeContext.ReceiveContext.TransportHeaders.Get(MessageHeaders.MessageId, "N/A"), + Is.EqualTo(consumeContext.MessageId.ToString())); + }); } Task> _receivedA; @@ -355,11 +355,11 @@ public async Task Should_be_received() await Bus.Publish(message); - _consumer.Received.Select().Any().ShouldBe(true); + Assert.That(_consumer.Received.Select(), Is.Not.Empty); IReceivedMessage receivedMessage = _consumer.Received.Select().First(); - Assert.AreEqual(message.Id, receivedMessage.Context.Message.Id); + Assert.That(receivedMessage.Context.Message.Id, Is.EqualTo(message.Id)); } MultiTestConsumer _consumer; @@ -409,7 +409,7 @@ public async Task Should_not_throw_an_exception() await InputQueueSendEndpoint.Send(new B()); - _consumer.Received.Select().Any().ShouldBe(true); + Assert.That(_consumer.Received.Select(), Is.Not.Empty); } MultiTestConsumer _consumer; @@ -436,6 +436,89 @@ class UnboundMessage } + [TestFixture] + public class When_batch_publish_is_enabled_and_a_message_is_published_to_the_consumer : + RabbitMqTestFixture + { + [Test] + public async Task Should_be_received() + { + var endpoint = await Bus.GetSendEndpoint(InputQueueAddress); + + var message = new A { Id = Guid.NewGuid() }; + await endpoint.Send(message, context => + { + Guid? value = NewId.NextGuid(); + context.Headers.Set(MessageHeaders.SchedulingTokenId, value); + }); + + ConsumeContext received = await _receivedA; + + Assert.That(received.Message.Id, Is.EqualTo(message.Id)); + } + + Task> _receivedA; + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + base.ConfigureRabbitMqReceiveEndpoint(configurator); + _receivedA = Handled(configurator); + } + + protected override void ConfigureRabbitMqHost(IRabbitMqHostConfigurator configurator) + { + base.ConfigureRabbitMqHost(configurator); + + configurator.ConfigureBatchPublish(c => + { + c.Enabled = true; + }); + } + } + + + [TestFixture] + public class When_batch_publish_is_enabled_with_zero_timeout_and_a_message_is_published_to_the_consumer : + RabbitMqTestFixture + { + [Test] + public async Task Should_be_received() + { + var endpoint = await Bus.GetSendEndpoint(InputQueueAddress); + + var message = new A { Id = Guid.NewGuid() }; + await endpoint.Send(message, context => + { + Guid? value = NewId.NextGuid(); + context.Headers.Set(MessageHeaders.SchedulingTokenId, value); + }); + + ConsumeContext received = await _receivedA; + + Assert.That(received.Message.Id, Is.EqualTo(message.Id)); + } + + Task> _receivedA; + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + base.ConfigureRabbitMqReceiveEndpoint(configurator); + _receivedA = Handled(configurator); + } + + protected override void ConfigureRabbitMqHost(IRabbitMqHostConfigurator configurator) + { + base.ConfigureRabbitMqHost(configurator); + + configurator.ConfigureBatchPublish(c => + { + c.Enabled = true; + c.Timeout = TimeSpan.Zero; + }); + } + } + + public class A { public Guid Id { get; set; } @@ -478,47 +561,4 @@ public override int GetHashCode() } } } - - - [TestFixture] - public class When_publishing_an_interface_message : - RabbitMqTestFixture - { - [Test] - public async Task Should_have_correlation_id() - { - await InputQueueSendEndpoint.Send(new - { - IntValue, - StringValue, - CorrelationId = _correlationId - }); - - ConsumeContext message = await _handler; - - message.Message.CorrelationId.ShouldBe(_correlationId); - message.Message.IntValue.ShouldBe(IntValue); - message.Message.StringValue.ShouldBe(StringValue); - } - - const int IntValue = 42; - const string StringValue = "Hello"; - readonly Guid _correlationId = Guid.NewGuid(); - Task> _handler; - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - configurator.ConfigureConsumeTopology = false; - - _handler = Handled(configurator); - } - - - public interface IProxyMe : - CorrelatedBy - { - int IntValue { get; } - string StringValue { get; } - } - } } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/RabbitMqAddress_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/RabbitMqAddress_Specs.cs index 1f5ed6e70cd..2c9e99d70ca 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/RabbitMqAddress_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/RabbitMqAddress_Specs.cs @@ -2,7 +2,6 @@ namespace MassTransit.RabbitMqTransport.Tests { using System; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -11,31 +10,31 @@ public class GivenAVHostAddress [Test] public void Should_have_no_password() { - _hostSettings.Password.ShouldBe(""); + Assert.That(_hostSettings.Password, Is.Empty); } [Test] public void Should_have_no_username() { - _hostSettings.Username.ShouldBe(""); + Assert.That(_hostSettings.Username, Is.Empty); } [Test] public void Should_have_the_queue_name() { - _receiveSettings.QueueName.ShouldBe("queue"); + Assert.That(_receiveSettings.QueueName, Is.EqualTo("queue")); } [Test] public void ShouldNotHaveATtl() { - _hostSettings.Host.ShouldBe("some_server"); + Assert.That(_hostSettings.Host, Is.EqualTo("some_server")); } [Test] public void TheHost() { - _hostSettings.VirtualHost.ShouldBe("thehost"); + Assert.That(_hostSettings.VirtualHost, Is.EqualTo("thehost")); } readonly Uri _uri = new Uri("rabbitmq://some_server/thehost/queue"); @@ -57,7 +56,7 @@ public class GivenAnAddressWithUnderscore [Test] public void Should_have_the_queue_name() { - _receiveSettings.QueueName.ShouldBe("the_queue"); + Assert.That(_receiveSettings.QueueName, Is.EqualTo("the_queue")); } readonly Uri _uri = new Uri("rabbitmq://some_server/thehost/the_queue"); @@ -177,7 +176,7 @@ public class GivenAnAddressWithPeriod [Test] public void Should_have_the_queue_name() { - _receiveSettings.QueueName.ShouldBe("the.queue"); + Assert.That(_receiveSettings.QueueName, Is.EqualTo("the.queue")); } readonly Uri _uri = new Uri("rabbitmq://some_server/thehost/the.queue"); @@ -197,7 +196,7 @@ public class GivenAnAddressWithColon [Test] public void Should_have_the_queue_name() { - _receiveSettings.QueueName.ShouldBe("the:queue"); + Assert.That(_receiveSettings.QueueName, Is.EqualTo("the:queue")); } readonly Uri _uri = new Uri("rabbitmq://some_server/thehost/the:queue"); @@ -214,6 +213,14 @@ public void WhenParsed() [TestFixture] public class Given_a_valid_endpoint_address { + [Test] + public void Should_be_valid_for_international_characters() + { + var hostAddress = new Uri("rabbitmq://localhost/test"); + + var address = new RabbitMqEndpointAddress(hostAddress, new Uri("rabbitmq://localhost/test/ßäöüÄÖÜ1234abc")); + } + [Test] public void Should_return_a_valid_address_for_a_full_address() { @@ -221,8 +228,11 @@ public void Should_return_a_valid_address_for_a_full_address() var address = new RabbitMqEndpointAddress(hostAddress, new Uri("rabbitmq://remote-host/production/client/input-queue")); - Assert.That(address.VirtualHost, Is.EqualTo("production/client")); - Assert.That(address.Name, Is.EqualTo("input-queue")); + Assert.Multiple(() => + { + Assert.That(address.VirtualHost, Is.EqualTo("production/client")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + }); Uri uri = address; @@ -236,9 +246,12 @@ public void Should_return_a_valid_address_for_a_full_address_using_default_virtu var address = new RabbitMqEndpointAddress(hostAddress, new Uri("rabbitmq://remote-host/input-queue")); - Assert.That(address.VirtualHost, Is.EqualTo("/")); - Assert.That(address.Name, Is.EqualTo("input-queue")); - Assert.That(address.Port, Is.EqualTo(5672)); + Assert.Multiple(() => + { + Assert.That(address.VirtualHost, Is.EqualTo("/")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + Assert.That(address.Port, Is.EqualTo(5672)); + }); Uri uri = address; @@ -252,8 +265,11 @@ public void Should_return_a_valid_address_for_a_full_address_with_encoded_slash( var address = new RabbitMqEndpointAddress(hostAddress, new Uri("rabbitmq://remote-host/production%2Fclient/input-queue")); - Assert.That(address.VirtualHost, Is.EqualTo("production/client")); - Assert.That(address.Name, Is.EqualTo("input-queue")); + Assert.Multiple(() => + { + Assert.That(address.VirtualHost, Is.EqualTo("production/client")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + }); Uri uri = address; @@ -336,7 +352,7 @@ public void Should_have_the_queue_name() { _receiveSettings = _uri.GetReceiveSettings(); - _receiveSettings.QueueName.ShouldBe("queue"); + Assert.That(_receiveSettings.QueueName, Is.EqualTo("queue")); } readonly Uri _uri = new Uri("rabbitmq://some_server/thehost/the/queue"); @@ -350,13 +366,13 @@ public class GivenANonVHostAddress [Test] public void TheHost() { - _hostSettings.VirtualHost.ShouldBe("/"); + Assert.That(_hostSettings.VirtualHost, Is.EqualTo("/")); } [Test] public void TheQueue() { - _receiveSettings.QueueName.ShouldBe("the_queue"); + Assert.That(_receiveSettings.QueueName, Is.EqualTo("the_queue")); } readonly Uri _uri = new Uri("rabbitmq://some_server/the_queue"); @@ -379,13 +395,13 @@ public class GivenAPortedAddress [Test] public void TheHost() { - _hostSettings.VirtualHost.ShouldBe("/"); + Assert.That(_hostSettings.VirtualHost, Is.EqualTo("/")); } [Test] public void ThePort() { - _hostSettings.Port.ShouldBe(12); + Assert.That(_hostSettings.Port, Is.EqualTo(12)); } readonly Uri _uri = new Uri("rabbitmq://some_server:12/"); @@ -405,13 +421,13 @@ public class GivenANonPortedAddress [Test] public void TheHost() { - _hostSettings.VirtualHost.ShouldBe("/"); + Assert.That(_hostSettings.VirtualHost, Is.EqualTo("/")); } [Test] public void ThePort() { - _hostSettings.Port.ShouldBe(5672); + Assert.That(_hostSettings.Port, Is.EqualTo(5672)); } readonly Uri _uri = new Uri("rabbitmq://some_server"); @@ -431,25 +447,25 @@ public class GivenATimeToLive [Test] public void HighAvailabilityQueue() { - _receiveSettings.QueueArguments["x-message-ttl"].ShouldBe("30000"); + Assert.That(_receiveSettings.QueueArguments["x-message-ttl"], Is.EqualTo("30000")); } [Test] public void ShouldHaveATtl() { - _receiveSettings.QueueArguments.ContainsKey("x-message-ttl").ShouldBe(true); + Assert.That(_receiveSettings.QueueArguments, Contains.Key("x-message-ttl")); } [Test] public void TheQueueArguments() { - _receiveSettings.QueueArguments.ShouldNotBe(null); + Assert.That(_receiveSettings.QueueArguments, Is.Not.Null); } [Test] public void TheQueueName() { - _receiveSettings.QueueName.ShouldBe("somequeue"); + Assert.That(_receiveSettings.QueueName, Is.EqualTo("somequeue")); } readonly Uri _uri = new Uri("rabbitmq://localhost/mttest/somequeue?ttl=30000"); @@ -469,19 +485,19 @@ public class Given_a_prefetch_count [Test] public void Should_have_the_prefetch_count_on_the_address() { - _receiveSettings.PrefetchCount.ShouldBe((ushort)32); + Assert.That(_receiveSettings.PrefetchCount, Is.EqualTo(32)); } [Test] public void TheQueueArguments() { - _receiveSettings.QueueArguments.ShouldBeEmpty(); + Assert.That(_receiveSettings.QueueArguments, Is.Empty); } [Test] public void TheQueueName() { - _receiveSettings.QueueName.ShouldBe("somequeue"); + Assert.That(_receiveSettings.QueueName, Is.EqualTo("somequeue")); } readonly Uri _uri = new Uri("rabbitmq://localhost/mttest/somequeue?prefetch=32"); @@ -501,32 +517,32 @@ public class Given_a_temporary_queue_was_requested [Test] public void Should_be_auto_delete() { - _receiveSettings.AutoDelete.ShouldBe(true); + Assert.That(_receiveSettings.AutoDelete, Is.True); } [Test] public void Should_be_exclusive_to_the_consumer() { - _receiveSettings.Exclusive.ShouldBe(true); + Assert.That(_receiveSettings.Exclusive, Is.True); } [Test] public void Should_not_be_durable() { - _receiveSettings.Durable.ShouldBe(false); + Assert.That(_receiveSettings.Durable, Is.False); } [Test] public void TheQueueArguments() { - _receiveSettings.QueueArguments.ShouldBeEmpty(); + Assert.That(_receiveSettings.QueueArguments, Is.Empty); } [Test] public void TheQueueName() { var guid = new Guid(_receiveSettings.QueueName); - Assert.AreNotEqual(Guid.Empty, guid); + Assert.That(guid, Is.Not.EqualTo(Guid.Empty)); } readonly Uri _uri = new Uri("rabbitmq://localhost/mttest/*?temporary=true"); @@ -546,13 +562,13 @@ public class Given_encoded_credentials_are_provided_in_the_uri [Test] public void Should_have_decoded_password() { - _hostSettings.Password.ShouldBe(ExpectedPassword); + Assert.That(_hostSettings.Password, Is.EqualTo(ExpectedPassword)); } [Test] public void Should_have_decoded_username() { - _hostSettings.Username.ShouldBe(ExpectedUsername); + Assert.That(_hostSettings.Username, Is.EqualTo(ExpectedUsername)); } const string EncodedUsername = "te%24t"; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/RawJson_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/RawJson_Specs.cs index 1bc56c94120..872bcfa3477 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/RawJson_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/RawJson_Specs.cs @@ -32,9 +32,12 @@ public async Task Should_deserialize() ConsumeContext received = await _receivedA; - Assert.AreEqual(message.Name, received.Message.Name); - Assert.AreEqual(message.Value, received.Message.Value); - Assert.AreEqual(message.Timestamp, received.Message.Timestamp); + Assert.Multiple(() => + { + Assert.That(received.Message.Name, Is.EqualTo(message.Name)); + Assert.That(received.Message.Value, Is.EqualTo(message.Value)); + Assert.That(received.Message.Timestamp, Is.EqualTo(message.Timestamp)); + }); } Task> _receivedA; @@ -42,7 +45,7 @@ public async Task Should_deserialize() protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) { configurator.ClearSerialization(); - configurator.UseRawJsonSerializer(); + configurator.UseRawJsonSerializer(RawSerializerOptions.All); _receivedA = Handled(configurator); } @@ -106,9 +109,12 @@ public async Task Should_deserialize() ConsumeContext received = await _receivedA; - Assert.AreEqual(message.Name, received.Message.Name); - Assert.AreEqual(message.Value, received.Message.Value); - Assert.AreEqual(message.Timestamp, received.Message.Timestamp); + Assert.Multiple(() => + { + Assert.That(received.Message.Name, Is.EqualTo(message.Name)); + Assert.That(received.Message.Value, Is.EqualTo(message.Value)); + Assert.That(received.Message.Timestamp, Is.EqualTo(message.Timestamp)); + }); } Task> _receivedA; @@ -177,28 +183,31 @@ public async Task Should_return_the_header_value_from_the_transport() ConsumeContext context = await _handled; - Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonRawMessageSerializer.JsonContentType), - $"unexpected content-type {context.ReceiveContext.ContentType}"); + Assert.Multiple(() => + { + Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonRawMessageSerializer.JsonContentType), + $"unexpected content-type {context.ReceiveContext.ContentType}"); - Assert.That(context.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(context.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.That(context.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(context.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); - Assert.That(context.Headers.Get(headerName), Is.EqualTo(headerValue)); + Assert.That(context.Headers.Get(headerName), Is.EqualTo(headerValue)); - Assert.IsTrue(context.MessageId.HasValue); - Assert.IsTrue(context.ConversationId.HasValue); - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.IsTrue(context.SentTime.HasValue); - Assert.IsNotNull(context.DestinationAddress); - Assert.IsNotNull(context.Host); - Assert.That(context.SupportedMessageTypes.Count(), Is.EqualTo(1)); + Assert.That(context.MessageId.HasValue, Is.True); + Assert.That(context.ConversationId.HasValue, Is.True); + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.SentTime.HasValue, Is.True); + Assert.That(context.DestinationAddress, Is.Not.Null); + Assert.That(context.Host, Is.Not.Null); + Assert.That(context.SupportedMessageTypes.Count(), Is.EqualTo(1)); + }); } Task> _handled; protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) { - configurator.UseRawJsonSerializer(); + configurator.UseRawJsonSerializer(RawSerializerOptions.All); } protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) @@ -251,12 +260,15 @@ await InputQueueSendEndpoint.Send(message, x => ConsumeContext context = await _handled; - Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(NewtonsoftJsonMessageSerializer.JsonContentType), - $"unexpected content-type {context.ReceiveContext.ContentType}"); + Assert.Multiple(() => + { + Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(NewtonsoftJsonMessageSerializer.JsonContentType), + $"unexpected content-type {context.ReceiveContext.ContentType}"); - Assert.That(context.Message.CorrelationId, Is.EqualTo(message.CommandId)); + Assert.That(context.Message.CorrelationId, Is.EqualTo(message.CommandId)); - Assert.That(context.Headers.Get(headerName), Is.EqualTo(default)); + Assert.That(context.Headers.Get(headerName), Is.EqualTo(default)); + }); } Task> _handled; @@ -272,7 +284,7 @@ protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator con protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) { - configurator.UseNewtonsoftRawJsonDeserializer(); + configurator.UseNewtonsoftRawJsonDeserializer(RawSerializerOptions.AnyMessageType); TaskCompletionSource> handler = GetTask>(); _handler = handler.Task; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Reconnecting_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Reconnecting_Specs.cs index 48e62048f62..74879faf020 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Reconnecting_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Reconnecting_Specs.cs @@ -3,9 +3,9 @@ using System; using System.Linq; using System.Threading.Tasks; - using MassTransit.Testing; using NUnit.Framework; using TestFramework.Messages; + using Testing; using Transports; @@ -20,7 +20,7 @@ public async Task Should_fault_nicely() await Bus.Publish(new ReconnectMessage { Value = "Before" }); var beforeFound = await Task.Run(() => _consumer.Received.Select(x => x.Context.Message.Value == "Before").Any()); - Assert.IsTrue(beforeFound); + Assert.That(beforeFound, Is.True); Console.WriteLine("Okay, restart RabbitMQ"); @@ -51,7 +51,7 @@ public async Task Should_fault_nicely() await Bus.Publish(new ReconnectMessage { Value = "After" }); var afterFound = await Task.Run(() => _consumer.Received.Select(x => x.Context.Message.Value == "After").Any()); - Assert.IsTrue(afterFound); + Assert.That(afterFound, Is.True); } public Reconnecting_Specs() diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Request_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Request_Specs.cs index e7352554077..d4c4cf9addb 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Request_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Request_Specs.cs @@ -3,8 +3,6 @@ using System; using System.Threading.Tasks; using NUnit.Framework; - using Serialization; - using Shouldly; using TestFramework.Messages; @@ -21,7 +19,7 @@ public async Task Should_receive_the_response() Response response = await requestHandle.GetResponse(); - response.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(response.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } public Sending_a_request_using_the_request_client() @@ -63,7 +61,7 @@ public async Task Should_receive_the_response() Response response = await requestHandle.GetResponse(); - response.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(response.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } Task> _ping; @@ -106,7 +104,7 @@ public async Task Should_receive_the_response() Response response = await requestHandle.GetResponse(); - response.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(response.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } public Sending_a_request_with_a_different_host_name() @@ -144,7 +142,8 @@ public async Task Should_receive_the_response() { Response message = await _response; - message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); + Assert.That(message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } public Sending_a_request_using_the_new_request_client() @@ -185,7 +184,7 @@ public async Task Should_receive_the_response() { Response message = await _response; - message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } Task> _ping; @@ -225,7 +224,7 @@ public async Task Should_have_the_conversation_id() ConsumeContext ping = await _ping; ConsumeContext a = await _a; - ping.ConversationId.ShouldBe(a.ConversationId); + Assert.That(ping.ConversationId, Is.EqualTo(a.ConversationId)); } [Test] @@ -234,7 +233,7 @@ public async Task Should_receive_the_response() { Response message = await _response; - message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } Task> _ping; @@ -246,7 +245,7 @@ public async Task Should_receive_the_response() [OneTimeSetUp] public async Task Setup() { - _clientFactory = await Bus.CreateReplyToClientFactory(); + _clientFactory = Bus.CreateReplyToClientFactory(); _requestClient = Bus.CreateRequestClient(InputQueueAddress, TestTimeout); } @@ -296,7 +295,7 @@ public async Task Should_have_the_conversation_id() ConsumeContext ping = await _ping; ConsumeContext a = await _a; - ping.ConversationId.ShouldBe(a.ConversationId); + Assert.That(ping.ConversationId, Is.EqualTo(a.ConversationId)); } [Test] @@ -304,7 +303,7 @@ public async Task Should_receive_the_response() { Response message = await _response; - message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } Task> _ping; @@ -316,7 +315,7 @@ public async Task Should_receive_the_response() [OneTimeSetUp] public async Task Setup() { - _clientFactory = await Bus.ConnectClientFactory(TestTimeout); + _clientFactory = Bus.ConnectClientFactory(TestTimeout); _requestClient = _clientFactory.CreateRequestClient(InputQueueAddress, TestTimeout); @@ -365,7 +364,7 @@ public async Task Should_receive_the_response() { Response response = await _requestClient.GetResponse(new PingMessage()); - response.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(response.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } Task> _ping; @@ -448,7 +447,7 @@ public void Should_receive_the_exception() [OneTimeSetUp] public async Task Setup() { - _clientFactory = await Bus.CreateReplyToClientFactory(); + _clientFactory = Bus.CreateReplyToClientFactory(); _requestClient = _clientFactory.CreateRequestClient(InputQueueAddress, TestTimeout); } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Retry_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Retry_Specs.cs index b902ae1b991..21e0ad3663f 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Retry_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Retry_Specs.cs @@ -30,7 +30,7 @@ public async Task Should_stop_after_limit_exceeded() await InactivityTask; - Assert.LessOrEqual(_attempts[pingId], _limit + 1); + Assert.That(_attempts[pingId], Is.LessThanOrEqualTo(_limit + 1)); } readonly int _limit; @@ -45,7 +45,7 @@ public When_specifying_retry_limit() protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) { - configurator.UseRetry(x => x.Interval(_limit, 200)); + configurator.UseMessageRetry(x => x.Interval(_limit, 200)); configurator.Consumer(() => new RetryLimitConsumer(_attempts)); } @@ -92,7 +92,7 @@ public async Task Should_stop_after_limit_exceeded() await InactivityTask; - Assert.LessOrEqual(_attempts[pingId], _limit + 1); + Assert.That(_attempts[pingId], Is.LessThanOrEqualTo(_limit + 1)); } readonly int _limit; @@ -140,7 +140,7 @@ public async Task Should_stop_after_limit_exceeded() await InactivityTask; - Assert.LessOrEqual(_attempts[pingId], _limit + 1); + Assert.That(_attempts[pingId], Is.LessThanOrEqualTo(_limit + 1)); } readonly int _limit; @@ -188,7 +188,7 @@ public async Task Should_stop_after_limit_exceeded() await InactivityTask; - Assert.LessOrEqual(_attempts[pingId], _limit + 1); + Assert.That(_attempts[pingId], Is.LessThanOrEqualTo(_limit + 1)); } readonly int _limit; @@ -234,7 +234,7 @@ public async Task Should_stop_after_limit_exceeded() await InactivityTask; - Assert.LessOrEqual(_attempts[pingId], _limit + 1); + Assert.That(_attempts[pingId], Is.LessThanOrEqualTo(_limit + 1)); } readonly int _limit; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/ScheduleMessage_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/ScheduleMessage_Specs.cs index f4d66841f41..e711ec33c0e 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/ScheduleMessage_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/ScheduleMessage_Specs.cs @@ -3,7 +3,9 @@ using System; using System.Diagnostics; using System.Threading.Tasks; + using Internals; using NUnit.Framework; + using RabbitMQ.Client; public class ScheduleMessage_Specs : @@ -116,8 +118,11 @@ public async Task Should_get_both_messages() ConsumeContext consumeContext = await _second; - Assert.That(consumeContext.Headers.TryGetHeader("Super-Fun", out var headerValue), Is.True); - Assert.That(headerValue, Is.EqualTo("Super Fun!")); + Assert.Multiple(() => + { + Assert.That(consumeContext.Headers.TryGetHeader("Super-Fun", out var headerValue), Is.True); + Assert.That(headerValue, Is.EqualTo("Super Fun!")); + }); } protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) @@ -185,6 +190,106 @@ public class FirstMessage } + public class SecondMessage + { + } + } + + + public class Should_schedule_in_direct_exchange_type : + Should_schedule_in_any_exchange_type + { + public Should_schedule_in_direct_exchange_type() + : base(ExchangeType.Direct) + { + } + } + + + public class Should_schedule_in_fanout_exchange_type : + Should_schedule_in_any_exchange_type + { + public Should_schedule_in_fanout_exchange_type() + : base(ExchangeType.Fanout) + { + } + } + + + public class Should_schedule_in_headers_exchange_type : + Should_schedule_in_any_exchange_type + { + public Should_schedule_in_headers_exchange_type() + : base(ExchangeType.Headers) + { + } + } + + + public class Should_schedule_in_topic_exchange_type : + Should_schedule_in_any_exchange_type + { + public Should_schedule_in_topic_exchange_type() + : base(ExchangeType.Topic) + { + } + } + + + public abstract class Should_schedule_in_any_exchange_type : + RabbitMqTestFixture + { + private readonly string _exchangeType; + + Task> _first; + + Task> _second; + + protected Should_schedule_in_any_exchange_type(string exchangeType) + : base(inputQueueName: $"input_queue_{exchangeType}") + { + _exchangeType = exchangeType; + } + + [Test] + public async Task Should_get_both_messages() + { + await InputQueueSendEndpoint.Send(new FirstMessage()); + + await _first; + + var timer = Stopwatch.StartNew(); + + await _second.OrTimeout(TimeSpan.FromSeconds(5)); + + timer.Stop(); + + Assert.That(timer.Elapsed, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2))); + } + + protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) + { + configurator.UseDelayedMessageScheduler(); + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.ExchangeType = _exchangeType; + + _first = Handler(configurator, async context => + { + await context.ScheduleSend(TimeSpan.FromSeconds(3), new SecondMessage()); + }); + + _second = Handled(configurator); + } + + + public class FirstMessage + { + } + + public class SecondMessage { } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/SendObserver_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/SendObserver_Specs.cs index 310486e8577..efdcbecc1bb 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/SendObserver_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/SendObserver_Specs.cs @@ -8,7 +8,6 @@ namespace ObserverTests using System.Threading.Tasks; using Internals; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -67,7 +66,7 @@ public async Task Should_not_invoke_post_sent_on_exception() await observer.SendFaulted; - observer.PostSent.Status.ShouldBe(TaskStatus.WaitingForActivation); + Assert.That(observer.PostSent.Status, Is.EqualTo(TaskStatus.WaitingForActivation)); } } @@ -195,8 +194,11 @@ public async Task Should_invoke_the_exception_after_send_failure() await observer.PreSent; await observer.SendFaulted; - Assert.That(observer.PreSentCount, Is.EqualTo(1)); - Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(1)); + Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + }); } } @@ -211,8 +213,11 @@ public async Task Should_invoke_the_observer_after_send() await observer.PreSent; await observer.PostSent; - Assert.That(observer.PreSentCount, Is.EqualTo(1)); - Assert.That(observer.PostSentCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(1)); + Assert.That(observer.PostSentCount, Is.EqualTo(1)); + }); } } @@ -226,8 +231,11 @@ public async Task Should_invoke_the_observer_prior_to_send() await observer.PreSent; - Assert.That(observer.PreSentCount, Is.EqualTo(1)); - Assert.That(observer.PostSentCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(1)); + Assert.That(observer.PostSentCount, Is.EqualTo(1)); + }); } } @@ -242,11 +250,14 @@ public async Task Should_not_invoke_post_sent_on_exception() await observer.SendFaulted; - observer.PostSent.Status.ShouldBe(TaskStatus.WaitingForActivation); + Assert.That(observer.PostSent.Status, Is.EqualTo(TaskStatus.WaitingForActivation)); - Assert.That(observer.PreSentCount, Is.EqualTo(1)); - Assert.That(observer.PostSentCount, Is.EqualTo(0)); - Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(1)); + Assert.That(observer.PostSentCount, Is.EqualTo(0)); + Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + }); } } } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Stream_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Stream_Specs.cs new file mode 100644 index 00000000000..5ba5c622ab3 --- /dev/null +++ b/tests/MassTransit.RabbitMqTransport.Tests/Stream_Specs.cs @@ -0,0 +1,92 @@ +namespace MassTransit.RabbitMqTransport.Tests; + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; + + +[TestFixture] +public class Consuming_messages_from_a_stream +{ + [Test] + public async Task Should_process_messages() + { + await using var provider = new ServiceCollection() + .Configure(options => options.VHost = "test") + .ConfigureRabbitMqTestOptions(options => + { + options.CreateVirtualHostIfNotExists = true; + options.CleanVirtualHost = true; + }) + .AddMassTransitTestHarness(x => + { + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer(); + + x.UsingRabbitMq((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var eventId = NewId.NextGuid(); + + await harness.Scope.ServiceProvider.GetRequiredService().Publish(new BusinessEvent + { + Id = eventId, + Value = "Something" + }); + + Assert.That(await harness.Consumed.Any(x => x.Context.Message.Id == eventId)); + } + + + class EventStreamConsumer : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + await Task.Delay(1); + } + } + + + class EventStreamConsumerDefinition : + ConsumerDefinition + { + public EventStreamConsumerDefinition() + { + EndpointName = "event-stream"; + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) + { + endpointConfigurator.PrefetchCount = 100; + endpointConfigurator.ConcurrentMessageLimit = 1; + + if (endpointConfigurator is IRabbitMqReceiveEndpointConfigurator rmq) + { + rmq.Stream("main-consumer", s => + { + s.MaxAge = TimeSpan.FromDays(14); + + s.FromFirst(); + }); + } + } + } + + + public record BusinessEvent + { + public Guid Id { get; init; } + public string Value { get; init; } + } +} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/TestHarnessOptions_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/TestHarnessOptions_Specs.cs index d5c2e6b794c..0487811b0a5 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/TestHarnessOptions_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/TestHarnessOptions_Specs.cs @@ -2,9 +2,9 @@ namespace MassTransit.RabbitMqTransport.Tests { using System.Threading.Tasks; using HarnessContracts; - using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; + using Testing; [TestFixture] @@ -42,9 +42,12 @@ await client.GetResponse(new OrderNumber = "123" }); - Assert.IsTrue(await harness.Sent.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Sent.Any(), Is.True); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); + }); IReceivedMessage message = await harness.Consumed.SelectAsync().First(); diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Testing/HandlerTest_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Testing/HandlerTest_Specs.cs deleted file mode 100644 index 580e75b7475..00000000000 --- a/tests/MassTransit.RabbitMqTransport.Tests/Testing/HandlerTest_Specs.cs +++ /dev/null @@ -1,97 +0,0 @@ -namespace MassTransit.RabbitMqTransport.Tests.Testing -{ - using System.Linq; - using System.Threading.Tasks; - using MassTransit.Testing; - using NUnit.Framework; - using Shouldly; - - - [TestFixture] - public class Using_the_handler_test_factory - { - [Test] - public void Should_have_published_a_message_of_type_b() - { - _harness.Published.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_have_published_a_message_of_type_d() - { - _harness.Published.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_have_received_a_message_of_type_a() - { - _harness.Consumed.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_have_sent_a_message_of_type_a() - { - _harness.Sent.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_have_sent_a_message_of_type_c() - { - _harness.Sent.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_support_a_simple_handler() - { - _handler.Consumed.Select().Any().ShouldBe(true); - } - - RabbitMqTestHarness _harness; - HandlerTestHarness _handler; - - [OneTimeSetUp] - public async Task Setup() - { - _harness = new RabbitMqTestHarness(); - _handler = _harness.Handler(async context => - { - var endpoint = await context.GetSendEndpoint(context.SourceAddress); - - await endpoint.Send(new C()); - - await context.Publish(new D()); - }); - - await _harness.Start(); - - await _harness.InputQueueSendEndpoint.Send(new A()); - await _harness.Bus.Publish(new B()); - } - - [OneTimeTearDown] - public async Task Teardown() - { - await _harness.Stop(); - } - - - class A - { - } - - - class B - { - } - - - class C - { - } - - - class D - { - } - } -} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/TopologyCorrelationId_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/TopologyCorrelationId_Specs.cs index ded0a800529..0dc67b696a6 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/TopologyCorrelationId_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/TopologyCorrelationId_Specs.cs @@ -6,6 +6,7 @@ namespace MassTransit.RabbitMqTransport.Tests [TestFixture] + [Category("Flaky")] public class When_using_a_name_property_for_correlation : RabbitMqTestFixture { @@ -18,8 +19,11 @@ public async Task Should_handle_named_property() ConsumeContext otherContext = await _otherHandled; - Assert.IsTrue(otherContext.CorrelationId.HasValue); - Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(otherContext.CorrelationId.HasValue, Is.True); + Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + }); } Task> _otherHandled; @@ -42,11 +46,6 @@ public class OtherMessage public class When_using_named_legacy_config : RabbitMqTestFixture { - public When_using_named_legacy_config() - { - MessageCorrelation.UseCorrelationId(x => x.TransactionId); - } - [Test] public async Task Should_handle_named_configured_legacy() { @@ -56,8 +55,16 @@ public async Task Should_handle_named_configured_legacy() ConsumeContext legacyContext = await _legacyHandled; - Assert.IsTrue(legacyContext.CorrelationId.HasValue); - Assert.That(legacyContext.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(legacyContext.CorrelationId.HasValue, Is.True); + Assert.That(legacyContext.CorrelationId.Value, Is.EqualTo(transactionId)); + }); + } + + public When_using_named_legacy_config() + { + MessageCorrelation.UseCorrelationId(x => x.TransactionId); } Task> _legacyHandled; @@ -76,6 +83,7 @@ public class LegacyMessage [TestFixture] + [Category("Flaky")] public class When_using_a_base_event : RabbitMqTestFixture { @@ -88,8 +96,11 @@ public async Task Should_handle_base_event_class() ConsumeContext context = await _handled; - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); + }); } Task> _handled; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/TopologyRoutingKey_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/TopologyRoutingKey_Specs.cs index 2f6fc679928..c1005c8eda8 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/TopologyRoutingKey_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/TopologyRoutingKey_Specs.cs @@ -18,11 +18,14 @@ public async Task Should_handle_base_event_class() ConsumeContext context = await _handled; - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); - Assert.IsTrue(context.TryGetPayload(out var basicConsumeContext)); - Assert.That(basicConsumeContext.RoutingKey, Is.EqualTo(transactionId.ToString())); + Assert.That(context.TryGetPayload(out var basicConsumeContext), Is.True); + Assert.That(basicConsumeContext.RoutingKey, Is.EqualTo(transactionId.ToString())); + }); } Task> _handled; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Turnout/Complete_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/Turnout/Complete_Specs.cs deleted file mode 100644 index 560f4a19573..00000000000 --- a/tests/MassTransit.RabbitMqTransport.Tests/Turnout/Complete_Specs.cs +++ /dev/null @@ -1,210 +0,0 @@ -namespace MassTransit.RabbitMqTransport.Tests.Turnout -{ - using System; - using System.Threading.Tasks; - using MassTransit.Contracts.JobService; - using NUnit.Framework; - - - public interface CrunchTheNumbers - { - TimeSpan Duration { get; } - } - - - public interface NumbersCrunched - { - Guid JobId { get; } - } - - - public class CrunchTheNumbersConsumer : - IJobConsumer - { - public async Task Run(JobContext context) - { - await Task.Delay(context.Job.Duration); - - await context.Publish(new { context.JobId }); - } - } - - - [TestFixture] - [Category("Flaky")] - public class Submitting_a_job_to_turnout : - RabbitMqTestFixture - { - [Test] - [Order(1)] - public async Task Should_get_the_job_accepted() - { - IRequestClient> requestClient = Bus.CreateRequestClient>(); - - Response response = await requestClient.GetResponse(new - { - JobId = _jobId, - Job = new { Duration = TimeSpan.FromSeconds(1) } - }); - - Assert.That(response.Message.JobId, Is.EqualTo(_jobId)); - - // just to capture all the test output in a single window - ConsumeContext completed = await _completed; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_completed_event() - { - ConsumeContext completed = await _completed; - } - - [Test] - [Order(3)] - public async Task Should_have_published_the_job_started_event() - { - ConsumeContext started = await _started; - } - - [Test] - [Order(2)] - public async Task Should_have_published_the_job_submitted_event() - { - ConsumeContext submitted = await _submitted; - } - - [Test] - [Order(5)] - public async Task Should_have_published_the_numbers_crunched_event() - { - ConsumeContext completed = await _crunched; - } - - Guid _jobId; - Task> _completed; - Task> _submitted; - Task> _started; - Task> _crunched; - - [OneTimeSetUp] - public async Task Arrange() - { - _jobId = NewId.NextGuid(); - } - - protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) - { - configurator.UseDelayedMessageScheduler(); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.UseInMemoryOutbox(); - e.Consumer(() => new CrunchTheNumbersConsumer()); - }); - }); - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - _submitted = Handled(configurator, context => context.Message.JobId == _jobId); - _started = Handled(configurator, context => context.Message.JobId == _jobId); - _completed = Handled(configurator, context => context.Message.JobId == _jobId); - _crunched = Handled(configurator, context => context.Message.JobId == _jobId); - } - } - - - [TestFixture] - [Explicit] - public class Submitting_a_job_to_turnout_with_status_checks : - RabbitMqTestFixture - { - [Test] - [Order(1)] - public async Task Should_get_the_job_accepted() - { - IRequestClient> requestClient = Bus.CreateRequestClient>(); - - Response response = await requestClient.GetResponse(new - { - JobId = _jobId, - Job = new { Duration = TimeSpan.FromMinutes(3.5) } - }); - - Assert.That(response.Message.JobId, Is.EqualTo(_jobId)); - - ConsumeContext completed = await _completed; - } - - [Test] - [Order(4)] - public async Task Should_have_published_the_job_completed_event() - { - ConsumeContext completed = await _completed; - } - - [Test] - [Order(3)] - public async Task Should_have_published_the_job_started_event() - { - ConsumeContext started = await _started; - } - - [Test] - [Order(2)] - public async Task Should_have_published_the_job_submitted_event() - { - ConsumeContext submitted = await _submitted; - } - - public Submitting_a_job_to_turnout_with_status_checks() - { - TestTimeout = TimeSpan.FromMinutes(5); - TestInactivityTimeout = TimeSpan.FromMinutes(1); - } - - Guid _jobId; - Task> _completed; - Task> _submitted; - Task> _started; - - [OneTimeSetUp] - public async Task Arrange() - { - _jobId = NewId.NextGuid(); - } - - protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) - { - configurator.UseDelayedMessageScheduler(); - - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(KebabCaseEndpointNameFormatter.Instance); - - configurator.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(); - - instance.ReceiveEndpoint(instance.EndpointNameFormatter.Message(), e => - { - e.Consumer(() => new CrunchTheNumbersConsumer()); - }); - }); - } - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - _submitted = Handled(configurator, context => context.Message.JobId == _jobId); - _started = Handled(configurator, context => context.Message.JobId == _jobId); - _completed = Handled(configurator, context => context.Message.JobId == _jobId); - } - } -} diff --git a/tests/MassTransit.RabbitMqTransport.Tests/TwoActivityCourier_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/TwoActivityCourier_Specs.cs index 111c8144df0..6d193e3062a 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/TwoActivityCourier_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/TwoActivityCourier_Specs.cs @@ -6,9 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Courier.Contracts; - using MassTransit.Testing; using NUnit.Framework; using TestFramework.Courier; + using Testing; [TestFixture] @@ -45,7 +45,7 @@ public async Task Should_include_the_activity_log_data() { var activityCompleted = (await _firstActivityCompleted); - Assert.AreEqual("Hello", activityCompleted.GetResult("OriginalValue")); + Assert.That(activityCompleted.GetResult("OriginalValue"), Is.EqualTo("Hello")); } [Test] @@ -54,7 +54,7 @@ public async Task Should_include_the_variable_set_by_the_activity() { var completed = await _completed; - Assert.AreEqual("Hello, World!", completed.GetVariable("Value")); + Assert.That(completed.GetVariable("Value"), Is.EqualTo("Hello, World!")); } [Test] @@ -63,7 +63,7 @@ public async Task Should_include_the_variables_of_the_completed_routing_slip() { var completed = (await _completed).Message; - Assert.AreEqual("Knife", completed.Variables["Variable"]); + Assert.That(completed.Variables["Variable"], Is.EqualTo("Knife")); } [Test] @@ -72,7 +72,7 @@ public async Task Should_include_the_variables_with_the_activity_log() { var activityCompleted = (await _firstActivityCompleted).Message; - Assert.AreEqual("Knife", activityCompleted.Variables["Variable"]); + Assert.That(activityCompleted.Variables["Variable"], Is.EqualTo("Knife")); } [Test] @@ -81,7 +81,7 @@ public async Task Should_receive_the_first_routing_slip_activity_completed_event { var activityCompleted = (await _firstActivityCompleted).Message; - Assert.AreEqual(_routingSlip.TrackingNumber, activityCompleted.TrackingNumber); + Assert.That(activityCompleted.TrackingNumber, Is.EqualTo(_routingSlip.TrackingNumber)); } [Test] @@ -90,7 +90,7 @@ public async Task Should_receive_the_routing_slip_completed_event() { var completed = (await _completed).Message; - Assert.AreEqual(_routingSlip.TrackingNumber, completed.TrackingNumber); + Assert.That(completed.TrackingNumber, Is.EqualTo(_routingSlip.TrackingNumber)); } [Test] @@ -99,7 +99,7 @@ public async Task Should_receive_the_second_routing_slip_activity_completed_even { var activityCompleted = (await _secondActivityCompleted).Message; - Assert.AreEqual(_routingSlip.TrackingNumber, activityCompleted.TrackingNumber); + Assert.That(activityCompleted.TrackingNumber, Is.EqualTo(_routingSlip.TrackingNumber)); } [Test] @@ -165,7 +165,7 @@ public async Task Setup() var completed = (await _completed).Message; - Assert.AreEqual(_routingSlip.TrackingNumber, completed.TrackingNumber); + Assert.That(completed.TrackingNumber, Is.EqualTo(_routingSlip.TrackingNumber)); } public Executing_with_no_observers() diff --git a/tests/MassTransit.RabbitMqTransport.Tests/UsingCluster_Specs.cs b/tests/MassTransit.RabbitMqTransport.Tests/UsingCluster_Specs.cs index c29694aaa00..37b4bd04bfb 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/UsingCluster_Specs.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/UsingCluster_Specs.cs @@ -19,9 +19,12 @@ public async Task Should_use_the_logical_host_name() ConsumeContext received = await _receivedA; - Assert.AreEqual(message.CorrelationId, received.Message.CorrelationId); + Assert.Multiple(() => + { + Assert.That(received.Message.CorrelationId, Is.EqualTo(message.CorrelationId)); - Assert.AreEqual(_logicalHostAddress.Host, received.DestinationAddress.Host); + Assert.That(received.DestinationAddress.Host, Is.EqualTo(_logicalHostAddress.Host)); + }); } public When_clustering_nodes_into_a_logical_broker() diff --git a/tests/MassTransit.RabbitMqTransport.Tests/Using_the_reply_to_address.cs b/tests/MassTransit.RabbitMqTransport.Tests/Using_the_reply_to_address.cs index 03e4eb6ff5a..f754119efc3 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/Using_the_reply_to_address.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/Using_the_reply_to_address.cs @@ -1,19 +1,19 @@ namespace MassTransit.RabbitMqTransport.Tests { + using System; using System.Threading.Tasks; using NUnit.Framework; using TestFramework.Messages; [TestFixture] - [Category("Flaky")] public class Using_the_reply_to_address : RabbitMqTestFixture { [Test] public async Task Should_deliver_the_response() { - var clientFactory = await Bus.CreateReplyToClientFactory(); + var clientFactory = Bus.CreateReplyToClientFactory(); IRequestClient client = clientFactory.CreateRequestClient(InputQueueAddress, TestTimeout); @@ -25,4 +25,39 @@ protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpoin configurator.Handler(x => x.RespondAsync(x.Message)); } } + + + [TestFixture] + public class Forwarding_a_message_using_reply_to : + RabbitMqTestFixture + { + Uri _serverAddress; + + [Test] + public async Task Should_deliver_the_response() + { + var clientFactory = Bus.CreateReplyToClientFactory(); + + IRequestClient client = clientFactory.CreateRequestClient(InputQueueAddress, TestTimeout); + + Response response = await client.GetResponse(new PingMessage()); + } + + protected override void ConfigureRabbitMqBus(IRabbitMqBusFactoryConfigurator configurator) + { + configurator.ReceiveEndpoint("forward-reply-to", x => + { + x.ConfigureConsumeTopology = false; + + x.Handler(context => context.RespondAsync(context.Message)); + + _serverAddress = x.InputAddress; + }); + } + + protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) + { + configurator.Handler(x => x.Forward(_serverAddress)); + } + } } diff --git a/tests/MassTransit.RabbitMqTransport.Tests/When_a_message_consumer_throws_an_exception.cs b/tests/MassTransit.RabbitMqTransport.Tests/When_a_message_consumer_throws_an_exception.cs index d88882d5b73..3fb6cf1d0f8 100644 --- a/tests/MassTransit.RabbitMqTransport.Tests/When_a_message_consumer_throws_an_exception.cs +++ b/tests/MassTransit.RabbitMqTransport.Tests/When_a_message_consumer_throws_an_exception.cs @@ -2,7 +2,6 @@ { using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -24,7 +23,7 @@ public async Task Should_be_received_by_the_handler() ConsumeContext> fault = await faultHandled; - fault.Message.Message.StringA.ShouldBe("ValueA"); + Assert.That(fault.Message.Message.StringA, Is.EqualTo("ValueA")); } A _message; diff --git a/tests/MassTransit.RabbitMqTransport.Tests/When_a_message_is_published.cs b/tests/MassTransit.RabbitMqTransport.Tests/When_a_message_is_published.cs deleted file mode 100644 index ea9e6b51e95..00000000000 --- a/tests/MassTransit.RabbitMqTransport.Tests/When_a_message_is_published.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace MassTransit.RabbitMqTransport.Tests -{ - using System.Threading.Tasks; - using NUnit.Framework; - using Shouldly; - - - [TestFixture] - public class When_a_message_is_published : - RabbitMqTestFixture - { - [Test] - public async Task Should_be_received_by_the_queue() - { - await Bus.Publish(new A - { - StringA = "ValueA", - StringB = "ValueB" - }).ConfigureAwait(false); - - ConsumeContext context = await _received.ConfigureAwait(false); - - context.Message.StringA.ShouldBe("ValueA"); - - ConsumeContext contextB = await _receivedB.ConfigureAwait(false); - - contextB.Message.StringB.ShouldBe("ValueB"); - } - - Task> _received; - Task> _receivedB; - - protected override void ConfigureRabbitMqReceiveEndpoint(IRabbitMqReceiveEndpointConfigurator configurator) - { - _received = Handled(configurator); - _receivedB = Handled(configurator); - } - - - class A : - B - { - public string StringA { get; set; } - } - - - class B - { - public string StringB { get; set; } - } - } -} diff --git a/tests/MassTransit.RedisIntegration.Tests/JobConsumer_Specs.cs b/tests/MassTransit.RedisIntegration.Tests/JobConsumer_Specs.cs new file mode 100644 index 00000000000..568903f7e60 --- /dev/null +++ b/tests/MassTransit.RedisIntegration.Tests/JobConsumer_Specs.cs @@ -0,0 +1,121 @@ +namespace MassTransit.RedisIntegration.Tests +{ + using System; + using System.Threading.Tasks; + using Contracts.JobService; + using JobConsumerTests; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + namespace JobConsumerTests + { + using System; + using System.Threading.Tasks; + using Contracts.JobService; + + + public interface OddJob + { + TimeSpan Duration { get; } + } + + + public class OddJobConsumer : + IJobConsumer + { + public async Task Run(JobContext context) + { + if (context.RetryAttempt == 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + } + } + + + public class OddJobCompletedConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + return Task.CompletedTask; + } + } + } + + + [TestFixture] + public class Using_the_new_job_service_configuration + { + [Test] + public async Task Should_complete_the_job() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); + + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.AddJobSagaStateMachines() + .RedisRepository(); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + try + { + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(1) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + } + finally + { + await harness.Stop(); + } + } + + [OneTimeSetUp] + public async Task Setup() + { + } + + [OneTimeTearDown] + public async Task Teardown() + { + } + } +} diff --git a/tests/MassTransit.RedisIntegration.Tests/MassTransit.RedisIntegration.Tests.csproj b/tests/MassTransit.RedisIntegration.Tests/MassTransit.RedisIntegration.Tests.csproj index c69ac5da1d8..8ed843d68d8 100644 --- a/tests/MassTransit.RedisIntegration.Tests/MassTransit.RedisIntegration.Tests.csproj +++ b/tests/MassTransit.RedisIntegration.Tests/MassTransit.RedisIntegration.Tests.csproj @@ -1,14 +1,17 @@  - net6.0 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/tests/MassTransit.RedisIntegration.Tests/SagaPersistenceTests.cs b/tests/MassTransit.RedisIntegration.Tests/SagaPersistenceTests.cs index ba7975219d5..a0d0cddb6d0 100644 --- a/tests/MassTransit.RedisIntegration.Tests/SagaPersistenceTests.cs +++ b/tests/MassTransit.RedisIntegration.Tests/SagaPersistenceTests.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using NUnit.Framework; using Saga; - using Shouldly; using StackExchange.Redis; using TestFramework; using Testing; @@ -25,19 +24,20 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; await InputQueueSendEndpoint.Send(nextMessage); found = await _sagaRepository.Value.ShouldContainSaga(sagaId, x => x != null && x.Moved, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); var retrieveRepository = _sagaRepository.Value as ILoadSagaRepository; var retrieved = await retrieveRepository.Load(sagaId); - retrieved.ShouldNotBeNull(); - retrieved.Moved.ShouldBeTrue(); + + Assert.That(retrieved, Is.Not.Null); + Assert.That(retrieved.Moved, Is.True); } [Test] @@ -50,7 +50,7 @@ public async Task An_initiating_message_should_start_the_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); } readonly Lazy> _sagaRepository; @@ -60,7 +60,7 @@ public LocatingAnExistingSaga() var redis = ConnectionMultiplexer.Connect("127.0.0.1"); redis.PreserveAsyncOrder = false; - _sagaRepository = new Lazy>(() => RedisSagaRepository.Create(() => redis.GetDatabase())); + _sagaRepository = new Lazy>(() => RedisSagaRepository.Create(_ => redis, () => redis.GetDatabase())); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -85,19 +85,19 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; await InputQueueSendEndpoint.Send(nextMessage); found = await _sagaRepository.Value.ShouldContainSaga(sagaId, x => x != null && x.Moved, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); var retrieveRepository = _sagaRepository.Value as ILoadSagaRepository; var retrieved = await retrieveRepository.Load(sagaId); - retrieved.ShouldNotBeNull(); - retrieved.Moved.ShouldBeTrue(); + Assert.That(retrieved, Is.Not.Null); + Assert.That(retrieved.Moved, Is.True); } [Test] @@ -110,7 +110,7 @@ public async Task An_initiating_message_should_start_the_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); } readonly Lazy> _sagaRepository; @@ -120,7 +120,7 @@ public LocatingAnExistingSagaWithoutOptimism() var redis = ConnectionMultiplexer.Connect("127.0.0.1"); redis.PreserveAsyncOrder = false; - _sagaRepository = new Lazy>(() => RedisSagaRepository.Create(() => redis.GetDatabase(), false)); + _sagaRepository = new Lazy>(() => RedisSagaRepository.Create(_ => redis, () => redis.GetDatabase(), false)); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -145,19 +145,19 @@ public async Task A_correlated_message_should_find_the_correct_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); var nextMessage = new CompleteSimpleSaga { CorrelationId = sagaId }; await InputQueueSendEndpoint.Send(nextMessage); found = await _sagaRepository.Value.ShouldContainSaga(sagaId, x => x != null && x.Moved, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); var retrieveRepository = _sagaRepository.Value as ILoadSagaRepository; var retrieved = await retrieveRepository.Load(sagaId); - retrieved.ShouldNotBeNull(); - retrieved.Moved.ShouldBeTrue(); + Assert.That(retrieved, Is.Not.Null); + Assert.That(retrieved.Moved, Is.True); } [Test] @@ -170,7 +170,7 @@ public async Task An_initiating_message_should_start_the_saga() Guid? found = await _sagaRepository.Value.ShouldContainSaga(message.CorrelationId, TestTimeout); - found.ShouldNotBeNull(); + Assert.That(found, Is.Not.Null); } readonly Lazy> _sagaRepository; @@ -180,7 +180,8 @@ public LocatingAnExistingSagaWithKeyPrefix() var redis = ConnectionMultiplexer.Connect("127.0.0.1"); redis.PreserveAsyncOrder = false; - _sagaRepository = new Lazy>(() => RedisSagaRepository.Create(() => redis.GetDatabase(), keyPrefix: "test")); + _sagaRepository = new Lazy>(() => + RedisSagaRepository.Create(_ => redis, () => redis.GetDatabase(), keyPrefix: "test")); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.RedisIntegration.Tests/docker-compose.yml b/tests/MassTransit.RedisIntegration.Tests/docker-compose.yml index ff1e99954a0..042d7ead0d2 100644 --- a/tests/MassTransit.RedisIntegration.Tests/docker-compose.yml +++ b/tests/MassTransit.RedisIntegration.Tests/docker-compose.yml @@ -1,9 +1,13 @@ # this compose file will start local services the same as those running on appveyor CI for testing. -version: '2.3' services: redis: - image: "redis:5.0.7" + image: "redis:7.4.1" container_name: redis ports: - '6379:6379' + valkey: + image: "valkey/valkey:8" + container_name: valkey + ports: + - '6379:6379' diff --git a/tests/MassTransit.SignalR.Tests/HubLifeTimeManagerTests.cs b/tests/MassTransit.SignalR.Tests/HubLifeTimeManagerTests.cs index 0bed3980e01..f425a2078cc 100644 --- a/tests/MassTransit.SignalR.Tests/HubLifeTimeManagerTests.cs +++ b/tests/MassTransit.SignalR.Tests/HubLifeTimeManagerTests.cs @@ -14,10 +14,13 @@ public class HubLifeTimeManagerTests : SingleScaleoutBackplaneTestFixture async Task AssertMessageAsync(TestClient client) { var message = await client.ReadAsync().OrTimeout() as InvocationMessage; - Assert.NotNull(message); - Assert.AreEqual("Hello", message.Target); - Assert.AreEqual(1, message.Arguments.Length); - Assert.AreEqual("World", message.Arguments[0].ToString()); + Assert.That(message, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Target, Is.EqualTo("Hello")); + Assert.That(message.Arguments, Has.Length.EqualTo(1)); + }); + Assert.That(message.Arguments[0].ToString(), Is.EqualTo("World")); } static async Task AssertNoMessageAsync(TestClient client, int milliseconds = 1000) @@ -39,21 +42,27 @@ public async Task SendAllAsyncWritesToAllConnectionsOutput() await manager.OnConnectedAsync(connection1).OrTimeout(Harness.TestTimeout); await manager.OnConnectedAsync(connection2).OrTimeout(Harness.TestTimeout); - await manager.SendAllAsync("Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendAllAsync("Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(BackplaneHarness.All.Consumed.Select>().Any()); + Assert.That(BackplaneHarness.All.Consumed.Select>().Any(), Is.True); var message = client1.TryRead() as InvocationMessage; - Assert.NotNull(message); - Assert.AreEqual("Hello", message.Target); - Assert.AreEqual(1, message.Arguments.Length); - Assert.AreEqual("World", message.Arguments[0].ToString()); + Assert.That(message, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Target, Is.EqualTo("Hello")); + Assert.That(message.Arguments, Has.Length.EqualTo(1)); + }); + Assert.That(message.Arguments[0].ToString(), Is.EqualTo("World")); message = client2.TryRead() as InvocationMessage; - Assert.NotNull(message); - Assert.AreEqual("Hello", message.Target); - Assert.AreEqual(1, message.Arguments.Length); - Assert.AreEqual("World", message.Arguments[0].ToString()); + Assert.That(message, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Target, Is.EqualTo("Hello")); + Assert.That(message.Arguments, Has.Length.EqualTo(1)); + }); + Assert.That(message.Arguments[0].ToString(), Is.EqualTo("World")); } } @@ -73,17 +82,23 @@ public async Task SendAllAsyncDoesNotWriteToDisconnectedConnectionsOutput() await manager.OnDisconnectedAsync(connection2).OrTimeout(Harness.TestTimeout); - await manager.SendAllAsync("Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendAllAsync("Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(BackplaneHarness.All.Consumed.Select>().Any()); + Assert.That(BackplaneHarness.All.Consumed.Select>().Any(), Is.True); var message = client1.TryRead() as InvocationMessage; - Assert.NotNull(message); - Assert.AreEqual("Hello", message.Target); - Assert.AreEqual(1, message.Arguments.Length); - Assert.AreEqual("World", message.Arguments[0].ToString()); - - Assert.Null(client2.TryRead()); + Assert.That(message, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Target, Is.EqualTo("Hello")); + Assert.That(message.Arguments, Has.Length.EqualTo(1)); + }); + Assert.Multiple(() => + { + Assert.That(message.Arguments[0].ToString(), Is.EqualTo("World")); + + Assert.That(client2.TryRead(), Is.Null); + }); } } @@ -106,17 +121,23 @@ public async Task SendGroupAsyncWritesToAllConnectionsInGroupOutput() // Because connection is local, should not have any GroupManagement //Assert.IsFalse(backplaneConsumers.GroupManagementConsumer.Consumed.Select>().Any()); - await manager.SendGroupAsync("group", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(BackplaneHarness.Group.Consumed.Select>().Any()); + Assert.That(BackplaneHarness.Group.Consumed.Select>().Any(), Is.True); var message = client1.TryRead() as InvocationMessage; - Assert.NotNull(message); - Assert.AreEqual("Hello", message.Target); - Assert.AreEqual(1, message.Arguments.Length); - Assert.AreEqual("World", message.Arguments[0].ToString()); - - Assert.Null(client2.TryRead()); + Assert.That(message, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Target, Is.EqualTo("Hello")); + Assert.That(message.Arguments, Has.Length.EqualTo(1)); + }); + Assert.Multiple(() => + { + Assert.That(message.Arguments[0].ToString(), Is.EqualTo("World")); + + Assert.That(client2.TryRead(), Is.Null); + }); } } @@ -141,11 +162,11 @@ public async Task DisconnectConnectionRemovesConnectionFromGroup() await Task.Delay(2000); - await manager.SendGroupAsync("name", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendGroupAsync("name", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); await Task.Delay(2000); - Assert.Null(client.TryRead()); + Assert.That(client.TryRead(), Is.Null); } } @@ -162,8 +183,8 @@ public async Task RemoveGroupFromLocalConnectionNotInGroupDoesNothing() await manager.RemoveFromGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); - Assert.IsFalse(BackplaneHarness.GroupManagement.Consumed.Select>() - .Any()); // Should not have published, because connection was local + Assert.That(BackplaneHarness.GroupManagement.Consumed.Select>() + .Any(), Is.False); // Should not have published, because connection was local } } @@ -181,10 +202,10 @@ public async Task AddGroupAsyncForLocalConnectionAlreadyInGroupDoesNothing() await manager.AddToGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); await manager.AddToGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); - await manager.SendGroupAsync("name", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendGroupAsync("name", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); await AssertMessageAsync(client); - Assert.Null(client.TryRead()); + Assert.That(client.TryRead(), Is.Null); } } @@ -207,13 +228,13 @@ public async Task WritingToGroupWithOneConnectionFailingSecondConnectionStillRec await manager.OnConnectedAsync(connection2).OrTimeout(Harness.TestTimeout); await manager.AddToGroupAsync(connection2.ConnectionId, "group").OrTimeout(Harness.TestTimeout); - await manager.SendGroupAsync("group", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); // connection1 will throw when receiving a group message, we are making sure other connections // are not affected by another connection throwing await AssertMessageAsync(client2); // Repeat to check that group can still be sent to - await manager.SendGroupAsync("group", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendGroupAsync("group", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); await AssertMessageAsync(client2); } } @@ -235,7 +256,7 @@ public async Task InvokeUserSendsToAllConnectionsForUser() await manager.OnConnectedAsync(connection2).OrTimeout(Harness.TestTimeout); await manager.OnConnectedAsync(connection3).OrTimeout(Harness.TestTimeout); - await manager.SendUserAsync("userA", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendUserAsync("userA", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); await AssertMessageAsync(client1); await AssertMessageAsync(client2); } @@ -335,7 +356,8 @@ public async Task ExcludedConnectionShouldReceiveSendGroupMessageWhenCasingIsOff await manager.AddToGroupAsync(connection.ConnectionId, groupName).OrTimeout(Harness.TestTimeout); - await manager.SendGroupExceptAsync(groupName, "Hello", new object[] { "World" }, new[] { connection.ConnectionId.ToUpper() }).OrTimeout(Harness.TestTimeout); + await manager.SendGroupExceptAsync(groupName, "Hello", new object[] { "World" }, new[] { connection.ConnectionId.ToUpper() }) + .OrTimeout(Harness.TestTimeout); await AssertMessageAsync(client); } @@ -356,13 +378,13 @@ public async Task StillSubscribedToUserAfterOneOfMultipleConnectionsAssociatedWi await manager.OnConnectedAsync(connection2).OrTimeout(Harness.TestTimeout); await manager.OnConnectedAsync(connection3).OrTimeout(Harness.TestTimeout); - await manager.SendUserAsync("userA", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendUserAsync("userA", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); await AssertMessageAsync(client1); await AssertMessageAsync(client2); // Disconnect one connection for the user await manager.OnDisconnectedAsync(connection1).OrTimeout(Harness.TestTimeout); - await manager.SendUserAsync("userA", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager.SendUserAsync("userA", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); await AssertMessageAsync(client2); } } diff --git a/tests/MassTransit.SignalR.Tests/MassTransit.SignalR.Tests.csproj b/tests/MassTransit.SignalR.Tests/MassTransit.SignalR.Tests.csproj index 7b2f4e68c65..8b673ac69af 100644 --- a/tests/MassTransit.SignalR.Tests/MassTransit.SignalR.Tests.csproj +++ b/tests/MassTransit.SignalR.Tests/MassTransit.SignalR.Tests.csproj @@ -1,15 +1,18 @@  - net6.0 + net8.0 + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - diff --git a/tests/MassTransit.SignalR.Tests/MassTransitHubLifetimeManagerTests.cs b/tests/MassTransit.SignalR.Tests/MassTransitHubLifetimeManagerTests.cs index 0a7fe65c714..ff3850e325e 100644 --- a/tests/MassTransit.SignalR.Tests/MassTransitHubLifetimeManagerTests.cs +++ b/tests/MassTransit.SignalR.Tests/MassTransitHubLifetimeManagerTests.cs @@ -45,20 +45,21 @@ public async Task CamelCasedJsonIsPreservedAcrossMassTransitBoundary() await manager1.OnConnectedAsync(connection1).OrTimeout(Harness.TestTimeout); await manager2.OnConnectedAsync(connection2).OrTimeout(Harness.TestTimeout); - await manager1.SendAllAsync("Hello", new object[] {new TestObject {TestProperty = "Foo"}}).OrTimeout(Harness.TestTimeout); + await manager1.SendAllAsync("Hello", new object[] { new TestObject { TestProperty = "Foo" } }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(backplane2Harness.All.Consumed.Select>().Any()); + Assert.That(backplane2Harness.All.Consumed.Select>().Any(), Is.True); var message = await client2.ReadAsync().OrTimeout() as InvocationMessage; - Assert.NotNull(message); - Assert.AreEqual("Hello", message.Target); - CollectionAssert.AllItemsAreInstancesOfType(message.Arguments, typeof(JsonElement)); + Assert.That(message, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Target, Is.EqualTo("Hello")); + Assert.That(message.Arguments, Is.All.InstanceOf(typeof(JsonElement))); + }); var jsonElement = message.Arguments[0] as JsonElement?; - Assert.NotNull(jsonElement); - Assert.NotNull(jsonElement.Value); + Assert.That(jsonElement, Is.Not.Null); var testProperty = jsonElement.Value.GetProperty("testProperty"); - Assert.NotNull(testProperty);; - Assert.AreEqual("Foo", testProperty.GetString()); + Assert.That(testProperty.GetString(), Is.EqualTo("Foo")); } } finally diff --git a/tests/MassTransit.SignalR.Tests/ScaleoutHubLifetimeManagerTests.cs b/tests/MassTransit.SignalR.Tests/ScaleoutHubLifetimeManagerTests.cs index 98e637815e4..af0c6a6141e 100644 --- a/tests/MassTransit.SignalR.Tests/ScaleoutHubLifetimeManagerTests.cs +++ b/tests/MassTransit.SignalR.Tests/ScaleoutHubLifetimeManagerTests.cs @@ -15,10 +15,13 @@ public class ScaleoutHubLifetimeManagerTests : DoubleScaleoutBackplaneTestFixtur async Task AssertMessageAsync(TestClient client) { var message = await client.ReadAsync().OrTimeout() as InvocationMessage; - Assert.NotNull(message); - Assert.AreEqual("Hello", message.Target); - Assert.AreEqual(1, message.Arguments.Length); - Assert.AreEqual("World", message.Arguments[0].ToString()); + Assert.That(message, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Target, Is.EqualTo("Hello")); + Assert.That(message.Arguments, Has.Length.EqualTo(1)); + }); + Assert.That(message.Arguments[0].ToString(), Is.EqualTo("World")); } [Test] @@ -36,10 +39,13 @@ public async Task InvokeAllAsyncWithMultipleServersWritesToAllConnectionsOutput( await manager1.OnConnectedAsync(connection1).OrTimeout(Harness.TestTimeout); await manager2.OnConnectedAsync(connection2).OrTimeout(Harness.TestTimeout); - await manager1.SendAllAsync("Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager1.SendAllAsync("Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane1Harness.All.Consumed.Select>().Any()); - Assert.IsTrue(Backplane2Harness.All.Consumed.Select>().Any()); + Assert.Multiple(() => + { + Assert.That(Backplane1Harness.All.Consumed.Select>().Any(), Is.True); + Assert.That(Backplane2Harness.All.Consumed.Select>().Any(), Is.True); + }); await AssertMessageAsync(client1); await AssertMessageAsync(client2); @@ -63,11 +69,11 @@ public async Task InvokeAllAsyncWithMultipleServersDoesNotWriteToDisconnectedCon await manager2.OnDisconnectedAsync(connection2).OrTimeout(Harness.TestTimeout); - await manager2.SendAllAsync("Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager2.SendAllAsync("Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); await AssertMessageAsync(client1); - Assert.Null(client2.TryRead()); + Assert.That(client2.TryRead(), Is.Null); } } @@ -83,9 +89,9 @@ public async Task InvokeConnectionAsyncOnServerWithoutConnectionWritesOutputToCo await manager1.OnConnectedAsync(connection).OrTimeout(Harness.TestTimeout); - await manager2.SendConnectionAsync(connection.ConnectionId, "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager2.SendConnectionAsync(connection.ConnectionId, "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane1Harness.Connection.Consumed.Select>().Any()); + Assert.That(Backplane1Harness.Connection.Consumed.Select>().Any(), Is.True); await AssertMessageAsync(client); } @@ -105,9 +111,9 @@ public async Task InvokeGroupAsyncOnServerWithoutConnectionWritesOutputToGroupCo await manager1.AddToGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); - await manager2.SendGroupAsync("name", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager2.SendGroupAsync("name", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane1Harness.Group.Consumed.Select>().Any()); + Assert.That(Backplane1Harness.Group.Consumed.Select>().Any(), Is.True); await AssertMessageAsync(client); } @@ -129,11 +135,11 @@ public async Task RemoveGroupFromConnectionOnDifferentServerNotInGroupDoesNothin await manager2.RemoveFromGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane1Harness.GroupManagement.Consumed.Select>().Any()); + Assert.That(Backplane1Harness.GroupManagement.Consumed.Select>().Any(), Is.True); ConsumeContext> responseContext = await ackHandler; - Assert.AreEqual(manager1.ServerName, responseContext.Message.ServerName); + Assert.That(responseContext.Message.ServerName, Is.EqualTo(manager1.ServerName)); } } @@ -153,15 +159,15 @@ public async Task AddGroupAsyncForConnectionOnDifferentServerWorks() await manager2.AddToGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane1Harness.GroupManagement.Consumed.Select>().Any()); + Assert.That(Backplane1Harness.GroupManagement.Consumed.Select>().Any(), Is.True); ConsumeContext> responseContext = await ackHandler; - Assert.AreEqual(manager1.ServerName, responseContext.Message.ServerName); + Assert.That(responseContext.Message.ServerName, Is.EqualTo(manager1.ServerName)); - await manager2.SendGroupAsync("name", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager2.SendGroupAsync("name", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane1Harness.Group.Consumed.Select>().Any()); + Assert.That(Backplane1Harness.Group.Consumed.Select>().Any(), Is.True); await AssertMessageAsync(client); } @@ -184,18 +190,18 @@ public async Task AddGroupAsyncForConnectionOnDifferentServerAlreadyInGroupDoesN await manager1.AddToGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); await manager2.AddToGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane1Harness.GroupManagement.Consumed.Select>().Any()); + Assert.That(Backplane1Harness.GroupManagement.Consumed.Select>().Any(), Is.True); ConsumeContext> responseContext = await ackHandler; - Assert.AreEqual(manager1.ServerName, responseContext.Message.ServerName); + Assert.That(responseContext.Message.ServerName, Is.EqualTo(manager1.ServerName)); - await manager2.SendGroupAsync("name", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager2.SendGroupAsync("name", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane1Harness.Group.Consumed.Select>().Any()); + Assert.That(Backplane1Harness.Group.Consumed.Select>().Any(), Is.True); await AssertMessageAsync(client); - Assert.Null(client.TryRead()); + Assert.That(client.TryRead(), Is.Null); } } @@ -215,29 +221,32 @@ public async Task RemoveGroupAsyncForConnectionOnDifferentServerWorks() await manager1.AddToGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); - await manager2.SendGroupAsync("name", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager2.SendGroupAsync("name", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); IReceivedMessage> firstMessage = Backplane1Harness.Group.Consumed.Select>().FirstOrDefault(); - Assert.NotNull(firstMessage); + Assert.That(firstMessage, Is.Not.Null); await AssertMessageAsync(client); await manager2.RemoveFromGroupAsync(connection.ConnectionId, "name").OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane1Harness.GroupManagement.Consumed.Select>().Any()); + Assert.That(Backplane1Harness.GroupManagement.Consumed.Select>().Any(), Is.True); ConsumeContext> responseContext = await ackHandler; - Assert.AreEqual(manager1.ServerName, responseContext.Message.ServerName); + Assert.That(responseContext.Message.ServerName, Is.EqualTo(manager1.ServerName)); - await manager2.SendGroupAsync("name", "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager2.SendGroupAsync("name", "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); IReceivedMessage> secondMessage = Backplane1Harness.Group.Consumed.Select>().Skip(1).FirstOrDefault(); - Assert.NotNull(secondMessage); + Assert.Multiple(() => + { + Assert.That(secondMessage, Is.Not.Null); - Assert.Null(client.TryRead()); + Assert.That(client.TryRead(), Is.Null); + }); } } @@ -255,13 +264,16 @@ public async Task InvokeConnectionAsyncForLocalConnectionDoesNotPublishToBackpla await manager1.OnConnectedAsync(connection).OrTimeout(Harness.TestTimeout); await manager2.OnConnectedAsync(connection).OrTimeout(Harness.TestTimeout); - await manager1.SendConnectionAsync(connection.ConnectionId, "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager1.SendConnectionAsync(connection.ConnectionId, "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsFalse(Backplane1Harness.Connection.Consumed.Select>().Any()); - Assert.IsFalse(Backplane2Harness.Connection.Consumed.Select>().Any()); + Assert.Multiple(() => + { + Assert.That(Backplane1Harness.Connection.Consumed.Select>().Any(), Is.False); + Assert.That(Backplane2Harness.Connection.Consumed.Select>().Any(), Is.False); + }); await AssertMessageAsync(client); - Assert.Null(client.TryRead()); + Assert.That(client.TryRead(), Is.Null); } } @@ -280,9 +292,9 @@ public async Task WritingToRemoteConnectionThatFailsDoesNotThrow() // This doesn't throw because there is no connection.ConnectionId on this server so it has to publish to the backplane. // And once that happens there is no way to know if the invocation was successful or not. - await manager1.SendConnectionAsync(connectionMock.ConnectionId, "Hello", new object[] {"World"}).OrTimeout(Harness.TestTimeout); + await manager1.SendConnectionAsync(connectionMock.ConnectionId, "Hello", new object[] { "World" }).OrTimeout(Harness.TestTimeout); - Assert.IsTrue(Backplane2Harness.Connection.Consumed.Select>().Any()); + Assert.That(Backplane2Harness.Connection.Consumed.Select>().Any(), Is.True); } } } diff --git a/tests/MassTransit.SqlTransport.Tests/Address_Specs.cs b/tests/MassTransit.SqlTransport.Tests/Address_Specs.cs new file mode 100644 index 00000000000..6b35753bb53 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/Address_Specs.cs @@ -0,0 +1,305 @@ +namespace MassTransit.DbTransport.Tests +{ + using System; + using NUnit.Framework; + + + [TestFixture] + public class Specifying_a_host_address + { + [Test] + public void Should_support_a_virtual_host() + { + var uri = new Uri("db://localhost/customer_a"); + var address = new SqlHostAddress(uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + + Assert.That((Uri)address, Is.EqualTo(uri)); + }); + } + + [Test] + public void Should_support_a_virtual_host_and_scope() + { + var uri = new Uri("db://localhost/customer_a.billing"); + var address = new SqlHostAddress(uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + Assert.That(address.Area, Is.EqualTo("billing")); + + Assert.That((Uri)address, Is.EqualTo(uri)); + }); + } + + [Test] + public void Should_support_a_virtual_host_and_scope_option_2() + { + var uri = new Uri("db://localhost/customer_a.billing/"); + var address = new SqlHostAddress(uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + Assert.That(address.Area, Is.EqualTo("billing")); + + Assert.That((Uri)address, Is.EqualTo(new Uri("db://localhost/customer_a.billing"))); + }); + } + + [Test] + public void Should_support_a_virtual_host_and_scope_with_queue() + { + var uri = new Uri("db://localhost/customer_a.billing/input-queue"); + + var address = new SqlHostAddress(uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + Assert.That(address.Area, Is.EqualTo("billing")); + + Assert.That((Uri)address, Is.EqualTo(new Uri("db://localhost/customer_a.billing"))); + }); + } + + [Test] + public void Should_support_the_simplest_use_case() + { + var uri = new Uri("db://localhost"); + var address = new SqlHostAddress(uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("/")); + + Assert.That((Uri)address, Is.EqualTo(uri)); + }); + } + + [Test] + public void Should_support_the_simplest_use_case_option_2() + { + var uri = new Uri("db://localhost/"); + var address = new SqlHostAddress(uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("/")); + + Assert.That((Uri)address, Is.EqualTo(uri)); + }); + } + + [Test] + public void Should_throw_on_invalid_scope() + { + Assert.That(() => new SqlHostAddress(new Uri("db://localhost/customer.16/input-queue")), Throws.InstanceOf()); + } + + [Test] + public void Should_throw_on_invalid_virtual_host() + { + Assert.That(() => new SqlHostAddress(new Uri("db://localhost/customer-a.billing/input-queue")), Throws.InstanceOf()); + } + } + + + [TestFixture] + public class Specifying_an_endpoint_address + { + [Test] + public void Should_parse_the_instance_name_from_url() + { + var address = new SqlHostAddress(new Uri("db://localhost/customer_a?instance=instance")); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.InstanceName, Is.EqualTo("instance")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + + Assert.That((Uri)address, Is.EqualTo(new Uri("db://localhost/customer_a?instance=instance"))); + }); + } + + [Test] + public void Should_support_a_virtual_host() + { + var uri = new Uri("db://localhost/customer_a/input-queue"); + var address = new SqlEndpointAddress(new SqlHostAddress(new Uri("db://localhost/customer_a")), uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + + Assert.That((Uri)address, Is.EqualTo(uri)); + }); + } + + [Test] + public void Should_support_a_virtual_host_and_scope() + { + var uri = new Uri("db://localhost/customer_a.billing/input-queue"); + var address = new SqlEndpointAddress(new SqlHostAddress(new Uri("db://localhost/customer_a.billing")), uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + Assert.That(address.Area, Is.EqualTo("billing")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + + Assert.That((Uri)address, Is.EqualTo(uri)); + }); + } + + [Test] + public void Should_support_a_virtual_host_and_scope_option_2() + { + var uri = new Uri("db://localhost/customer_a.billing/input-queue"); + var address = new SqlEndpointAddress(new SqlHostAddress(new Uri("db://localhost/customer_a.billing/")), uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + Assert.That(address.Area, Is.EqualTo("billing")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + + Assert.That((Uri)address, Is.EqualTo(uri)); + }); + } + + [Test] + public void Should_support_a_virtual_host_and_scope_with_queue() + { + var address = new SqlEndpointAddress(new SqlHostAddress(new Uri("db://localhost/customer_a.billing/")), + new Uri("queue:input-queue")); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + Assert.That(address.Area, Is.EqualTo("billing")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + + Assert.That((Uri)address, Is.EqualTo(new Uri("db://localhost/customer_a.billing/input-queue"))); + }); + } + + [Test] + public void Should_support_a_virtual_host_and_scope_with_topic() + { + var address = new SqlEndpointAddress(new SqlHostAddress(new Uri("db://localhost/customer_a.billing/")), + new Uri("topic:namespace:type")); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + Assert.That(address.Area, Is.Null); + Assert.That(address.Name, Is.EqualTo("namespace:type")); + + Assert.That((Uri)address, Is.EqualTo(new Uri("db://localhost/customer_a/namespace:type?type=topic"))); + }); + } + + [Test] + public void Should_support_the_backslash_in_sql_host_names() + { + var hostAddress = new SqlHostAddress("localhost", "instance", default, "customer_a", "billing"); + var address = new SqlEndpointAddress(hostAddress, new Uri("queue:input-queue")); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.InstanceName, Is.EqualTo("instance")); + Assert.That(address.VirtualHost, Is.EqualTo("customer_a")); + Assert.That(address.Area, Is.EqualTo("billing")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + + Assert.That((Uri)address, Is.EqualTo(new Uri("db://localhost/customer_a.billing/input-queue?instance=instance"))); + }); + } + + [Test] + public void Should_support_the_simplest_use_case() + { + Assert.That(() => new SqlEndpointAddress(new SqlHostAddress(new Uri("db://localhost")), + new Uri("db://localhost")), Throws.InstanceOf()); + } + + [Test] + public void Should_support_the_simplest_use_case_option_2() + { + var uri = new Uri("db://localhost/input-queue"); + var address = new SqlEndpointAddress(new SqlHostAddress(new Uri("db://localhost")), uri); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("localhost")); + Assert.That(address.VirtualHost, Is.EqualTo("/")); + Assert.That(address.Name, Is.EqualTo("input-queue")); + + Assert.That((Uri)address, Is.EqualTo(uri)); + }); + } + + [Test] + public void Should_throw_on_invalid_scope() + { + Assert.That(() => new SqlEndpointAddress(new SqlHostAddress(new Uri("db://localhost/customer.16")), + new Uri("db://localhost/customer.16/input-queue")), Throws.InstanceOf()); + } + + [Test] + public void Should_throw_on_invalid_virtual_host() + { + Assert.That(() => new SqlEndpointAddress(new SqlHostAddress(new Uri("db://localhost/customer-a.billing")), + new Uri("db://localhost/customer-a.billing/input-queue")), Throws.InstanceOf()); + } + + [Test] + public void Should_support_ipv6_address() + { + var address = new SqlEndpointAddress(new SqlHostAddress(new Uri("db://[::1]:1433/")), new Uri("queue:input-queue")); + + Assert.Multiple(() => + { + Assert.That(address.Scheme, Is.EqualTo("db")); + Assert.That(address.Host, Is.EqualTo("[::1]")); + Assert.That(address.Port, Is.EqualTo(1433)); + Assert.That(address.Name, Is.EqualTo("input-queue")); + + Assert.That((Uri)address, Is.EqualTo(new Uri("db://[::1]:1433/input-queue"))); + }); + } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/BusOutbox_Specs.cs b/tests/MassTransit.SqlTransport.Tests/BusOutbox_Specs.cs new file mode 100644 index 00000000000..8000ec26d80 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/BusOutbox_Specs.cs @@ -0,0 +1,228 @@ +namespace MassTransit.DbTransport.Tests +{ + using System; + using System.Collections.Generic; + using System.Data; + using System.Linq; + using System.Reflection; + using System.Text.RegularExpressions; + using System.Threading; + using System.Threading.Tasks; + using EntityFrameworkCoreIntegration; + using Logging; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Design; + using Microsoft.EntityFrameworkCore.Metadata.Internal; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using OutboxTypes; + using Testing; + + + public class Using_the_bus_outbox + { + [Test] + public async Task Should_work_with_the_db_transport() + { + await using var provider = new ServiceCollection() + .AddBusOutboxServices() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.AddEntityFrameworkOutbox(o => + { + o.QueryDelay = TimeSpan.FromSeconds(1); + o.UsePostgres(); + + o.UseBusOutbox(bo => + { + bo.MessageDeliveryLimit = 10; + }); + }); + + x.AddConsumer(); + + x.UsingPostgres((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + IConsumerTestHarness consumerHarness = harness.GetConsumerHarness(); + + try + { + { + await using var dbContext = harness.Scope.ServiceProvider.GetRequiredService(); + + var transaction = await dbContext.Database.BeginTransactionAsync(IsolationLevel.Serializable); + + var publishEndpoint = harness.Scope.ServiceProvider.GetRequiredService(); + + await publishEndpoint.Publish(new PingMessage()); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + + Assert.That(await consumerHarness.Consumed.Any(cts.Token), Is.False); + + await dbContext.SaveChangesAsync(harness.CancellationToken); + + await transaction.CommitAsync(); + } + + Assert.That(await consumerHarness.Consumed.Any(), Is.True); + + IReceivedMessage context = harness.Consumed.Select().First(); + + Assert.Multiple(() => + { + Assert.That(context.Context.MessageId, Is.Not.Null); + Assert.That(context.Context.ConversationId, Is.Not.Null); + Assert.That(context.Context.DestinationAddress, Is.Not.Null); + Assert.That(context.Context.SourceAddress, Is.Not.Null); + }); + } + finally + { + await harness.Stop(); + } + } + + + class PingConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + } + + + namespace OutboxTypes + { + public record PingMessage; + } + + + public class ReliableDbContext : + SagaDbContext + { + public ReliableDbContext(DbContextOptions options) + : base(options) + { + } + + protected override IEnumerable Configurations + { + get { yield break; } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasDefaultSchema("reliable"); + + base.OnModelCreating(modelBuilder); + + modelBuilder.AddInboxStateEntity(); + modelBuilder.AddOutboxMessageEntity(); + modelBuilder.AddOutboxStateEntity(); + + foreach (var entity in modelBuilder.Model.GetEntityTypes()) + { + #pragma warning disable EF1001 + if (entity is EntityType { IsImplicitlyCreatedJoinEntityType: true }) + continue; + + entity.SetTableName(entity.DisplayName()); + } + + ChangeEntityNames(modelBuilder); + } + + static void ChangeEntityNames(ModelBuilder builder) + { + foreach (var entity in builder.Model.GetEntityTypes()) + { + var tableName = entity.GetTableName(); + if (!string.IsNullOrWhiteSpace(tableName)) + entity.SetTableName(tableName.ToSnakeCase()); + + foreach (var property in entity.GetProperties()) + property.SetColumnName(property.GetColumnName().ToSnakeCase()); + } + } + } + + + public static partial class StringExtensions + { + public static string ToSnakeCase(this string input) + { + if (string.IsNullOrEmpty(input)) + return input; + + var startUnderscores = MyRegex().Match(input); + return startUnderscores + MyRegex1().Replace(input, "$1_$2").ToLower(); + } + + [GeneratedRegex("^_+")] + private static partial Regex MyRegex(); + + [GeneratedRegex("([a-z0-9])([A-Z])")] + private static partial Regex MyRegex1(); + } + + + public class ReliableDbContextFactory : + IDesignTimeDbContextFactory + { + public ReliableDbContext CreateDbContext(string[] args) + { + var builder = new DbContextOptionsBuilder(); + + Apply(builder); + + return new ReliableDbContext(builder.Options); + } + + public static void Apply(DbContextOptionsBuilder builder) + { + builder.UseNpgsql("host=localhost;user id=postgres;password=Password12!;database=masstransit_transport_tests;", options => + { + options.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); + options.MigrationsHistoryTable("reliable_db_context_ef"); + }); + } + + public ReliableDbContext CreateDbContext(DbContextOptionsBuilder optionsBuilder) + { + return new ReliableDbContext(optionsBuilder.Options); + } + } + + + public static class BusOutboxTestExtensions + { + public static IServiceCollection AddBusOutboxServices(this IServiceCollection services) + { + services.AddDbContext(builder => + { + ReliableDbContextFactory.Apply(builder); + }); + services.AddHostedService>(); + + services.AddOptions().Configure(options => options.Disable("Microsoft")); + + return services; + } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/Configuration_Specs.cs b/tests/MassTransit.SqlTransport.Tests/Configuration_Specs.cs new file mode 100644 index 00000000000..950f16136cc --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/Configuration_Specs.cs @@ -0,0 +1,39 @@ +namespace MassTransit.DbTransport.Tests; + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; + + +[TestFixture] +public class Configuration_Specs +{ + [Test] + public async Task Configuring_an_ipv6_host() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => + { + options.Host = "::1"; + options.Database = "test"; + options.Schema = "transport"; + options.Username = "user"; + options.Password = "password"; + }); + + x.UsingSqlServer((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + Assert.That(harness.Bus.Address.Host, Is.EqualTo("[::1]")); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/DelayedDelivery_Specs.cs b/tests/MassTransit.SqlTransport.Tests/DelayedDelivery_Specs.cs new file mode 100644 index 00000000000..4b93072c2ce --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/DelayedDelivery_Specs.cs @@ -0,0 +1,108 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; +using UnitTests; + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +public class Using_delayed_send + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_be_supported() + { + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); + + _configuration.Configure(x, (context, cfg) => + { + cfg.ReceiveEndpoint("delayed-input-queue", e => + { + e.ConfigureConsumeTopology = false; + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:delayed-input-queue")); + + var testMessage = new TestMessage("Hello, World!"); + + var now = DateTime.UtcNow; + + await endpoint.Send(testMessage, x => x.Delay = TimeSpan.FromSeconds(3)); + + Assert.That(await harness.Consumed.Any(x => x.Context.Message.Id == testMessage.Id), Is.True); + + var then = DateTime.UtcNow; + + Assert.That(then - now, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2))); + } + + readonly T _configuration; + + public Using_delayed_send() + { + _configuration = new T(); + } +} + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +public class Using_delayed_publish + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_be_supported() + { + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); + + _configuration.Configure(x, (context, cfg) => + { + cfg.ReceiveEndpoint("delayed-publish-input-queue", e => + { + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var testMessage = new TestMessage("Hello, World!"); + + var now = DateTime.UtcNow; + + await harness.Bus.Publish(testMessage, x => x.Delay = TimeSpan.FromSeconds(3)); + + Assert.That(await harness.Consumed.Any(x => x.Context.Message.Id == testMessage.Id), Is.True); + + var then = DateTime.UtcNow; + + Assert.That(then - now, Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2))); + } + + readonly T _configuration; + + public Using_delayed_publish() + { + _configuration = new T(); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/ExtensionData_Specs.cs b/tests/MassTransit.SqlTransport.Tests/ExtensionData_Specs.cs new file mode 100644 index 00000000000..018d9cba50d --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/ExtensionData_Specs.cs @@ -0,0 +1,118 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +public class Using_json_extension_data + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_properly_serialize_the_message() + { + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(TextWriter.Null, x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + _configuration.Configure(x, (context, cfg) => + { + cfg.ConfigureJsonSerializerOptions(options => + { + options.SetMessageSerializerOptions(); + options.SetMessageSerializerOptions(); + + return options; + }); + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new WithElement + { + Extra = new Dictionary + { + { "text", JsonSerializer.SerializeToElement("Value") }, + { "number", JsonSerializer.SerializeToElement(2) } + } + }); + + IReceivedMessage message = await harness.Consumed.SelectAsync().FirstOrDefault(); + + Assert.That(message, Is.Not.Null); + Assert.That(message.Exception, Is.Null); + + Assert.That(message.Context.Message.Extra.ContainsKey("text")); + Assert.That(message.Context.Message.Extra.ContainsKey("number")); + + await harness.Bus.Publish(new WithObject + { + Extra = new Dictionary + { + { "text", "Value" }, + { "number", 2 } + } + }); + + IReceivedMessage m2 = await harness.Consumed.SelectAsync().FirstOrDefault(); + + Assert.That(m2, Is.Not.Null); + Assert.That(m2.Exception, Is.Null); + + Assert.That(m2.Context.Message.Extra.ContainsKey("text")); + Assert.That(m2.Context.Message.Extra.ContainsKey("number")); + } + + readonly T _configuration; + + + class ExtensionConsumer : + IConsumer, + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + + + public Using_json_extension_data() + { + _configuration = new T(); + } + + + public record WithObject + { + [JsonExtensionData] + public Dictionary Extra { get; set; } + } + + + public record WithElement + { + [JsonExtensionData] + public Dictionary Extra { get; set; } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/Fault_Specs.cs b/tests/MassTransit.SqlTransport.Tests/Fault_Specs.cs new file mode 100644 index 00000000000..a49c1d60ddb --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/Fault_Specs.cs @@ -0,0 +1,141 @@ +namespace MassTransit.DbTransport.Tests +{ + using System; + using System.Threading.Tasks; + using FaultMessages; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + using UnitTests; + + + [TestFixture] + public class When_a_consumer_throws_an_exception + { + [Test, Explicit] + public async Task Should_dead_letter_skipped_messages() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.UsingPostgres((context, cfg) => + { + cfg.ReceiveEndpoint("input-queue", e => + { + e.PollingInterval = TimeSpan.FromSeconds(.5); + e.ConfigureConsumeTopology = false; + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:input-queue")); + + await endpoint.Send(new TestMessage("Hello, World!")); + + await Task.Delay(2000); + } + + [Test, Explicit] + public async Task Should_publish_fault_and_move_to_the_error_queue() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.AddHandler(async (ConsumeContext> _) => + { + }); + x.AddHandler(async (ConsumeContext _) => throw new ApplicationException("I meant to do that!")); + + x.UsingPostgres((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new + { + MemberName = "Frank", + Address = "123 American Way" + }); + + Assert.That(await harness.Consumed.Any>(), Is.True); + + await harness.Stop(); + } + + [Test, Explicit] + public async Task Should_use_built_in_redelivery_to_redeliver_faulted_messages() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + x.AddHandler(async (ConsumeContext> _) => + { + }); + x.AddHandler(async (ConsumeContext _) => throw new ApplicationException("I meant to do that!")); + + x.AddConfigureEndpointsCallback((_, _, cfg) => + { + cfg.UseDelayedRedelivery(r => r.Interval(3, TimeSpan.FromSeconds(1))); + }); + + x.UsingPostgres((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new + { + MemberName = "Frank", + Address = "123 American Way" + }); + + Assert.That(await harness.Consumed.Any>(), Is.True); + + await harness.Stop(); + } + } + + + namespace FaultMessages + { + [ExcludeFromTopology] + public interface ICommand + { + } + + + public interface MemberUpdateCommand : + ICommand + { + string MemberName { get; } + } + + + public interface UpdateMemberAddress : + MemberUpdateCommand + { + string Address { get; } + } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/IDatabaseTestConfiguration.cs b/tests/MassTransit.SqlTransport.Tests/IDatabaseTestConfiguration.cs new file mode 100644 index 00000000000..110fc01a880 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/IDatabaseTestConfiguration.cs @@ -0,0 +1,19 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using EntityFrameworkCoreIntegration; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + + +public interface IDatabaseTestConfiguration +{ + ILockStatementProvider LockStatementProvider { get; } + + IServiceCollection Create(); + + void Configure(IBusRegistrationConfigurator configurator, Action callback); + + void Apply(DbContextOptionsBuilder builder) + where TDbContext : DbContext; +} diff --git a/tests/MassTransit.SqlTransport.Tests/JobConsumer_Specs.cs b/tests/MassTransit.SqlTransport.Tests/JobConsumer_Specs.cs new file mode 100644 index 00000000000..80f7e2365d9 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/JobConsumer_Specs.cs @@ -0,0 +1,389 @@ +namespace MassTransit.DbTransport.Tests +{ + using System; + using System.Threading.Tasks; + using Contracts.JobService; + using EntityFrameworkCoreIntegration; + using JobConsumerTests; + using Logging; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + namespace JobConsumerTests + { + using System; + using System.Threading.Tasks; + using Contracts.JobService; + + + public interface OddJob + { + TimeSpan Duration { get; } + } + + + public class OddJobConsumer : + IJobConsumer + { + public async Task Run(JobContext context) + { + if (context.RetryAttempt == 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + } + } + + + public class OddJobCompletedConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + return Task.CompletedTask; + } + } + } + + + [TestFixture(typeof(PostgresDatabaseTestConfiguration))] + [TestFixture(typeof(SqlServerDatabaseTestConfiguration))] + public class Using_a_job_consumer + where T : IDatabaseTestConfiguration, new() + { + [Test] + public async Task Should_cancel_the_job() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + Assert.That(await harness.Published.Any(), Is.True); + + await harness.Stop(); + } + + [Test] + public async Task Should_cancel_the_job_and_get_the_status() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + IRequestClient stateClient = harness.GetRequestClient(); + + Response jobState = await stateClient.GetResponse(new { JobId = jobId }); + + Assert.That(jobState.Message.CurrentState, Is.EqualTo("Started")); + + await harness.Bus.CancelJob(jobId); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Sent.Any(), Is.True); + }); + + jobState = await stateClient.GetResponse(new { JobId = jobId }); + + Assert.That(jobState.Message.CurrentState, Is.EqualTo("Canceled")); + + await harness.Stop(); + } + + [Test] + public async Task Should_cancel_the_job_and_retry_it() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Sent.Any(), Is.True); + }); + + await Task.Delay(500); + + await harness.Bus.RetryJob(jobId); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Stop(); + } + + [Test] + public async Task Should_cancel_the_job_while_waiting() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + await harness.Start(); + + var previousJobId = NewId.NextGuid(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + await client.GetResponse(new + { + JobId = previousJobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(10) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Sent.Any(x => x.Context.Message.JobId == jobId), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + + await harness.Bus.CancelJob(previousJobId); + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + + await harness.Stop(); + } + + [Test] + public async Task Should_complete_the_job() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + await harness.Start(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new + { + JobId = jobId, + Job = new { Duration = TimeSpan.FromSeconds(1) } + }); + + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Stop(); + } + + [Test] + public async Task Should_create_a_unique_job_id() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + await harness.Start(); + + await harness.Bus.Publish(new { Duration = TimeSpan.FromSeconds(1) }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + IPublishedMessage publishedMessage = await harness.Published.SelectAsync().First(); + Assert.That(publishedMessage.Context.Message.JobId, Is.Not.EqualTo(Guid.Empty)); + + await harness.Stop(); + } + + [Test] + public async Task Should_return_not_found() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + var jobId = NewId.NextGuid(); + + IRequestClient stateClient = harness.GetRequestClient(); + + var jobState = await stateClient.GetJobState(jobId); + + Assert.That(jobState.CurrentState, Is.EqualTo("NotFound")); + + await harness.Stop(); + } + + ServiceProvider SetupServiceCollection() + { + var provider = _configuration.Create() + .AddDbContext(builder => _configuration.Apply(builder)) + .AddHostedService>() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => options.Disable("Microsoft")); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); + + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); + + x.AddJobSagaStateMachines() + .SetPartitionedReceiveMode() + .EntityFrameworkRepository(r => + { + r.ExistingDbContext(); + r.LockStatementProvider = _configuration.LockStatementProvider; + }); + + _configuration.Configure(x, (context, cfg) => + { + cfg.UseDbMessageScheduler(); + cfg.UseJobSagaPartitionKeyFormatters(); + + // js.OnConfigureEndpoint(endpointConfigurator => + // { + // if (endpointConfigurator is IDbReceiveEndpointConfigurator e) + // { + // e.SetReceiveMode(DbReceiveMode.Partitioned); + // + // e.UseMessageRetry(r => r.Intervals(100, 200, 300, 500, 1000, 2000, 5000)); + // e.UseInMemoryOutbox(context); + // } + // }); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + return provider; + } + + readonly T _configuration; + + public Using_a_job_consumer() + { + _configuration = new T(); + } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/JobServiceSagaDbContextFactory.cs b/tests/MassTransit.SqlTransport.Tests/JobServiceSagaDbContextFactory.cs new file mode 100644 index 00000000000..36a1c8c9f61 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/JobServiceSagaDbContextFactory.cs @@ -0,0 +1,35 @@ +namespace MassTransit.DbTransport.Tests +{ + using System.Reflection; + using EntityFrameworkCoreIntegration; + using Microsoft.EntityFrameworkCore; + using Microsoft.EntityFrameworkCore.Design; + + + public class JobServiceSagaDbContextFactory : + IDesignTimeDbContextFactory + { + public JobServiceSagaDbContext CreateDbContext(params string[] args) + { + var builder = new DbContextOptionsBuilder(); + + Apply(builder); + + return new JobServiceSagaDbContext(builder.Options); + } + + public static void Apply(DbContextOptionsBuilder builder) + { + builder.UseNpgsql("host=localhost;user id=postgres;password=Password12!;database=masstransit_transport_tests;", options => + { + options.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); + options.MigrationsHistoryTable("job_service_db_context_ef"); + }); + } + + public JobServiceSagaDbContext CreateDbContext(DbContextOptionsBuilder optionsBuilder) + { + return new JobServiceSagaDbContext(optionsBuilder.Options); + } + } +} \ No newline at end of file diff --git a/tests/MassTransit.SqlTransport.Tests/LicenseConfiguration.cs b/tests/MassTransit.SqlTransport.Tests/LicenseConfiguration.cs new file mode 100644 index 00000000000..a4bbba21fb3 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/LicenseConfiguration.cs @@ -0,0 +1,16 @@ +#nullable enable +namespace MassTransit.DbTransport.Tests; + +using System; +using NUnit.Framework; + + +static class LicenseConfiguration +{ + const string LicensePathKey = "MT_LICENSE_PATH"; + + public static string? LicensePath => + TestContext.Parameters.Exists(LicensePathKey) + ? TestContext.Parameters.Get(LicensePathKey) + : Environment.GetEnvironmentVariable(LicensePathKey); +} \ No newline at end of file diff --git a/tests/MassTransit.SqlTransport.Tests/MassTransit.SqlTransport.Tests.csproj b/tests/MassTransit.SqlTransport.Tests/MassTransit.SqlTransport.Tests.csproj new file mode 100644 index 00000000000..8aeacd7239f --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/MassTransit.SqlTransport.Tests.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + MassTransit.DbTransport.Tests + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/tests/MassTransit.SqlTransport.Tests/MigrationHostedService.cs b/tests/MassTransit.SqlTransport.Tests/MigrationHostedService.cs new file mode 100644 index 00000000000..cbe521cdd6f --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/MigrationHostedService.cs @@ -0,0 +1,41 @@ +namespace MassTransit.DbTransport.Tests; + +using System.Threading; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; + + +public class MigrationHostedService : + IHostedService + where TDbContext : DbContext +{ + readonly ILogger> _logger; + readonly IServiceScopeFactory _scopeFactory; + TDbContext _context; + IServiceScope _scope; + + public MigrationHostedService(IServiceScopeFactory scopeFactory, ILogger> logger) + { + _scopeFactory = scopeFactory; + _logger = logger; + } + + public async Task StartAsync(CancellationToken cancellationToken) + { + _logger.LogInformation("Applying migrations for {DbContext}", TypeCache.ShortName); + + _scope = _scopeFactory.CreateScope(); + + _context = _scope.ServiceProvider.GetRequiredService(); + + await _context.Database.EnsureDeletedAsync(cancellationToken); + await _context.Database.EnsureCreatedAsync(cancellationToken); + } + + public async Task StopAsync(CancellationToken cancellationToken) + { + } +} \ No newline at end of file diff --git a/tests/MassTransit.SqlTransport.Tests/PartitionKey_Specs.cs b/tests/MassTransit.SqlTransport.Tests/PartitionKey_Specs.cs new file mode 100644 index 00000000000..029a3082dcf --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/PartitionKey_Specs.cs @@ -0,0 +1,100 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using Internals; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +public class Using_partition_keys + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_consume_a_lot_of_published_messages() + { + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(x => + { + x.AddTaskCompletionSource>(); + + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + _configuration.Configure(x, (context, cfg) => + { + cfg.ReceiveEndpoint("partitioned-input-queue", e => + { + e.PrefetchCount = 10; + e.ConcurrentMessageLimit = 10; + e.PurgeOnStartup = true; + + e.SetReceiveMode(SqlReceiveMode.Partitioned); + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:partitioned-input-queue")); + + for (var i = 0; i < MessageLimit; i++) + await endpoint.Send(new PartitionedTestMessage(i + 1), x => x.SetPartitionKey((i % NumKeys).ToString()), harness.CancellationToken); + + await provider.GetTask>(); + + IList> receivedMessages = await harness.Consumed.SelectAsync().ToListAsync(); + Assert.That(receivedMessages, Has.Count.EqualTo(MessageLimit)); + var result = new int[NumKeys]; + + foreach (IReceivedMessage receivedMessage in receivedMessages) + { + ConsumeContext context = receivedMessage.Context; + var key = int.TryParse(context.PartitionKey(), out var value) ? value : 0; + Assert.That(context.Message.Index, Is.GreaterThan(result[key])); + result[key] = context.Message.Index; + } + } + + const int MessageLimit = 30; + const int NumKeys = 2; + + readonly T _configuration; + + public Using_partition_keys() + { + _configuration = new T(); + } + + + public record PartitionedTestMessage(int Index); + + + class PartitionedConsumer : + IConsumer + { + static int _index = MessageLimit; + readonly TaskCompletionSource> _taskCompletionSource; + + public PartitionedConsumer(TaskCompletionSource> taskCompletionSource) + { + _taskCompletionSource = taskCompletionSource; + } + + public async Task Consume(ConsumeContext context) + { + if (Interlocked.Decrement(ref _index) <= 0) + _taskCompletionSource.TrySetResult(context); + + await Task.Delay(4); + } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/PgSql/ChannelName_Specs.cs b/tests/MassTransit.SqlTransport.Tests/PgSql/ChannelName_Specs.cs new file mode 100644 index 00000000000..a5beea3a8cb --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/PgSql/ChannelName_Specs.cs @@ -0,0 +1,37 @@ +namespace MassTransit.DbTransport.Tests.PgSql; + +using System.Threading.Tasks; +using NUnit.Framework; +using SqlTransport.PostgreSql.Helpers; + +[TestFixture] +public class ChannelName_Specs +{ + [TestCase("string that has 40 characters 1234567890")] + public async Task Should_sanitize_schema(string schema) + { + var sanitizedSchema = NotifyChannel.SanitizeSchemaName(schema); + + Assert.That(sanitizedSchema, Has.Length.EqualTo(39)); + Assert.That(sanitizedSchema, Is.EqualTo("string that has 40 characters 123456789")); + } + + [TestCase("string that has 38 characters 12345678")] + [TestCase("string that has 39 characters 123456789")] + public async Task Should_not_sanitize_schema(string schema) + { + var sanitizedSchema = NotifyChannel.SanitizeSchemaName(schema); + + Assert.That(sanitizedSchema, Is.EqualTo(schema)); + } + + [TestCase("")] + [TestCase(" ")] + [TestCase(null)] + public async Task Should_return_default(string schema) + { + var sanitizedSchema = NotifyChannel.SanitizeSchemaName(schema); + + Assert.That(sanitizedSchema, Is.EqualTo("transport")); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/PgSql/Migration_Specs.cs b/tests/MassTransit.SqlTransport.Tests/PgSql/Migration_Specs.cs new file mode 100644 index 00000000000..181c7627e2e --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/PgSql/Migration_Specs.cs @@ -0,0 +1,106 @@ +namespace MassTransit.DbTransport.Tests.PgSql; + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Npgsql; +using NUnit.Framework; +using OutboxTypes; +using SqlTransport.PostgreSql; +using Testing; + + +[TestFixture] +public class Migration_Specs +{ + [Test] + public async Task Should_work_with_data_source() + { + var connectionStringBuilder = new NpgsqlConnectionStringBuilder("host=localhost;user id=postgres;password=Password12!;database=MassTransitUnitTests;"); + + await using var provider = new ServiceCollection() + .AddPostgresMigrationHostedService() + .AddSingleton(_ => NpgsqlDataSource.Create(connectionStringBuilder)) + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = connectionStringBuilder.Host; + options.Port = connectionStringBuilder.Port; + options.Database = connectionStringBuilder.Database; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = connectionStringBuilder.Username; + options.AdminPassword = connectionStringBuilder.Password; + }); + + x.AddConsumer(); + + // ReSharper disable once AccessToDisposedClosure + x.UsingPostgres(context => context.GetRequiredService(), (context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Stop(); + } + + [TestCase("host=localhost;user id=mtAdmin;password=2Legit2Quit;database=sample;")] + [TestCase("host=messaging.postgres.database.azure.com;user id=mtAdmin@messaging;password=2Legit2Quit;database=sample")] + [TestCase("host=messaging.server.com;user id=mtAdmin;password=2Legit2Quit;database=sample")] + public async Task Should_parse_connection_string_into_options(string connectionString) + { + var connectionStringBuilder = new NpgsqlConnectionStringBuilder(connectionString); + + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = connectionStringBuilder.Host; + options.Port = connectionStringBuilder.Port; + options.Database = connectionStringBuilder.Database; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = connectionStringBuilder.Username; + options.AdminPassword = connectionStringBuilder.Password; + }); + + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var opts = provider.GetRequiredService>().Value; + var connection = PostgresSqlTransportConnection.GetDatabaseAdminConnection(opts); + var builder = new NpgsqlConnectionStringBuilder(connection.Connection.ConnectionString); + + Assert.Multiple(() => + { + Assert.That(builder.Database, Is.EqualTo("sample")); + Assert.That(builder.Password, Is.EqualTo("2Legit2Quit")); + Assert.That(builder.Username, Does.StartWith("mtAdmin")); + Assert.That(connectionStringBuilder.Host, Is.EqualTo(builder.Host)); + }); + + var migrationPrincipal = PostgresSqlTransportConnection.GetAdminMigrationPrincipal(opts); + Assert.That(migrationPrincipal, Is.EqualTo("mtAdmin")); + } + + + class SimpleConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/PgSql/MultiHost_Specs.cs b/tests/MassTransit.SqlTransport.Tests/PgSql/MultiHost_Specs.cs new file mode 100644 index 00000000000..c6993a0aa1e --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/PgSql/MultiHost_Specs.cs @@ -0,0 +1,140 @@ +namespace MassTransit.DbTransport.Tests.PgSql; + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using SqlTransport.PostgreSql; + + +[TestFixture] +public class MultiHost_Specs +{ + [Test] + public async Task Should_allow_multiple_host_names() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "local,remote"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var connection = PostgresSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + + Assert.That(connection.Connection.ConnectionString, Does.Contain("local,remote;")); + + var bus = provider.GetRequiredService(); + Assert.That(bus.Address.Host, Is.EqualTo("local")); + } + + [Test] + public async Task Should_allow_multiple_host_names_with_custom_port() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "local,remote"; + options.Port = 1234; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var connection = PostgresSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + + Assert.That(connection.Connection.ConnectionString, Does.Contain("local,remote;")); + Assert.That(connection.Connection.ConnectionString, Does.Contain("Port=1234")); + + var bus = provider.GetRequiredService(); + Assert.That(bus.Address.Host, Is.EqualTo("local")); + Assert.That(bus.Address.Port, Is.EqualTo(1234)); + } + + [Test] + public async Task Should_allow_multiple_host_names_with_custom_ports() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "local:1234,remote:5678"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var transportOptions = provider.GetRequiredService>().Value; + + var connection = PostgresSqlTransportConnection.GetDatabaseConnection(transportOptions); + + Assert.That(connection.Connection.ConnectionString, Does.Contain("local:1234,remote:5678;")); + + var bus = provider.GetRequiredService(); + Assert.That(bus.Address.Host, Is.EqualTo("local")); + } + + [Test] + public async Task Should_allow_single_host_names_with_custom_port() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "local:1234"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var transportOptions = provider.GetRequiredService>().Value; + + var connection = PostgresSqlTransportConnection.GetDatabaseConnection(transportOptions); + + Assert.That(connection.Connection.ConnectionString, Does.Contain("local:1234;")); + + var bus = provider.GetRequiredService(); + Assert.That(bus.Address.Host, Is.EqualTo("local")); + Assert.That(bus.Address.Port, Is.EqualTo(1234)); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/PgSql/PortAddress_Specs.cs b/tests/MassTransit.SqlTransport.Tests/PgSql/PortAddress_Specs.cs new file mode 100644 index 00000000000..a390713ec14 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/PgSql/PortAddress_Specs.cs @@ -0,0 +1,67 @@ +namespace MassTransit.DbTransport.Tests.PgSql; + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using SqlTransport.PostgreSql; + + +[TestFixture] +public class PortAddress_Specs +{ + [Test] + public async Task Should_include_the_port_for_postgres() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "localhost"; + options.Port = 5544; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var connection = PostgresSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + + Assert.That(connection.Connection.ConnectionString, Does.Contain("Port=5544")); + } + + [Test] + public async Task Should_not_include_the_port_for_postgres() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "localhost"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var connection = PostgresSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + + Assert.That(connection.Connection.ConnectionString, Does.Not.Contain("Port=")); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/PgSqlBus_Specs.cs b/tests/MassTransit.SqlTransport.Tests/PgSqlBus_Specs.cs new file mode 100644 index 00000000000..261b7fe92a8 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/PgSqlBus_Specs.cs @@ -0,0 +1,423 @@ +namespace MassTransit.DbTransport.Tests +{ + using System; + using System.Diagnostics; + using System.IO; + using System.Linq; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + using UnitTests; + + + [TestFixture] + public class Configuring_the_postgresql_bus + { + [Test] + [Explicit] + public async Task Should_consume_a_lot_of_messages() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(TextWriter.Null, x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + x.UsingPostgres((context, cfg) => + { + cfg.ReceiveEndpoint("input-queue", e => + { + e.ConfigureConsumeTopology = false; + e.PrefetchCount = 30; + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:input-queue")); + + var options = new ParallelOptions { MaxDegreeOfParallelism = 10 }; + + var timer = Stopwatch.StartNew(); + + const int limit = 1000; + + await Parallel.ForEachAsync(Enumerable.Range(0, limit), options, async (i, token) => + { + await endpoint.Send(new TestMultipleMessage($"Hello, World! {i}"), x => x.SetPartitionKey(i.ToString()), token); + }); + + var sendElapsed = timer.Elapsed; + + await harness.Consumed.SelectAsync().Take(limit).Count(); + + var consumeElapsed = timer.Elapsed; + + timer.Stop(); + + Console.WriteLine("Total send duration: {0:g}", sendElapsed); + Console.WriteLine("Send message rate: {0:F2} (msg/s)", + limit * 1000 / sendElapsed.TotalMilliseconds); + Console.WriteLine("Total consume duration: {0:g}", consumeElapsed); + Console.WriteLine("Consume message rate: {0:F2} (msg/s)", + limit * 1000 / consumeElapsed.TotalMilliseconds); + } + + [Test] + [Explicit] + public async Task Should_consume_a_lot_of_published_messages() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(TextWriter.Null, x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + x.UsingPostgres((context, cfg) => + { + cfg.ReceiveEndpoint("input-queue", e => + { + e.PollingInterval = TimeSpan.FromMilliseconds(10); + e.PrefetchCount = 30; + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var options = new ParallelOptions { MaxDegreeOfParallelism = 10 }; + + var timer = Stopwatch.StartNew(); + + const int limit = 1000; + + await Parallel.ForEachAsync(Enumerable.Range(0, limit), options, async (i, token) => + { + await harness.Bus.Publish(new TestMultipleMessage($"Hello, World! {i}"), x => x.SetPartitionKey(i.ToString()), token); + }); + + var sendElapsed = timer.Elapsed; + + await harness.Consumed.SelectAsync().Take(limit).Count(); + + var consumeElapsed = timer.Elapsed; + + timer.Stop(); + + Console.WriteLine("Total publish duration: {0:g}", sendElapsed); + Console.WriteLine("Publish message rate: {0:F2} (msg/s)", + limit * 1000 / sendElapsed.TotalMilliseconds); + Console.WriteLine("Total consume duration: {0:g}", consumeElapsed); + Console.WriteLine("Consume message rate: {0:F2} (msg/s)", + limit * 1000 / consumeElapsed.TotalMilliseconds); + } + + [Test] + [Explicit] + public async Task Should_give_me_so_much_of_the_datas() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(TextWriter.Null, x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + x.UsingPostgres((context, cfg) => + { + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:heavy-d")); + + var options = new ParallelOptions { MaxDegreeOfParallelism = 10 }; + + var timer = Stopwatch.StartNew(); + + const int limit = 1000; + const int loop = 10; + + await Parallel.ForEachAsync(Enumerable.Range(0, limit), options, async (i, token) => + { + for (int j = 0; j < loop; j++) + { + await endpoint.Send(new TestMessage($"Hello, World! {i}"), context => + { + context.Delay = TimeSpan.FromSeconds(j * 4); + context.SetPartitionKey(i.ToString()); + }, token); + } + }); + + var sendElapsed = timer.Elapsed; + + timer.Stop(); + + Console.WriteLine("Total send duration: {0:g}", sendElapsed); + Console.WriteLine("Send message rate: {0:F2} (msg/s)", + limit * loop * 1000 / sendElapsed.TotalMilliseconds); + + await harness.Stop(); + } + + [Test] + public async Task Should_support_standard_syntax_with_consumers() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + x.UsingPostgres((context, cfg) => + { + cfg.ReceiveEndpoint("input-queue", e => + { + e.ConfigureConsumeTopology = false; + e.PrefetchCount = 30; + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:input-queue")); + + await endpoint.Send(new TestMessage("Hello, World!"), x => + { + x.Headers.Set("Simple-Header", "Some Value"); + }); + + Assert.That(await harness.Consumed.Any(), Is.True); + + IReceivedMessage context = harness.Consumed.Select().Single(); + + Assert.Multiple(() => + { + Assert.That(context.Context.MessageId, Is.Not.Null); + Assert.That(context.Context.ConversationId, Is.Not.Null); + Assert.That(context.Context.DestinationAddress, Is.Not.Null); + Assert.That(context.Context.SourceAddress, Is.Not.Null); + }); + + await harness.Stop(); + } + + [Test] + public async Task Should_support_standard_syntax_with_consumers_and_topology() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + x.UsingPostgres((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new MessageC(NewId.NextGuid())); + + Assert.That(await harness.Consumed.Any(), Is.True); + } + + [Test] + public async Task Should_support_the_standard_syntax() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:input-queue")); + + await endpoint.Send(new TestMessage("Hello, World!"), x => + { + x.Headers.Set("Simple-Header", "Some Value"); + }); + } + + [Test] + public async Task Should_support_the_standard_syntax_with_three_queues() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:input-queue")); + + await endpoint.Send(new TestMessage("Hello, World!"), x => + { + x.Headers.Set("Simple-Header", "Some Value"); + }); + + endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:input-queue-2")); + + await endpoint.Send(new TestMessage("Hello, World!"), x => + { + x.Headers.Set("Simple-Header", "Some Value"); + }); + + endpoint = await harness.Bus.GetSendEndpoint(new Uri("queue:input-queue-3")); + + await endpoint.Send(new TestMessage("Hello, World!"), x => + { + x.Headers.Set("Simple-Header", "Some Value"); + }); + } + } + + + namespace UnitTests + { + using System; + + + public record TestMessage + { + public TestMessage(Guid Id, string Value) + { + this.Id = Id; + this.Value = Value; + } + + public TestMessage(string Value) + { + Id = NewId.NextGuid(); + this.Value = Value; + } + + public TestMessage() + { + } + + public Guid Id { get; init; } + public string Value { get; init; } + } + + + public record TestMultipleMessage + { + public TestMultipleMessage(Guid Id, string Value) + { + this.Id = Id; + this.Value = Value; + } + + public TestMultipleMessage(string Value) + { + Id = NewId.NextGuid(); + this.Value = Value; + } + + public TestMultipleMessage() + { + } + + public Guid Id { get; init; } + public string Value { get; init; } + } + + + public record SlowMessage; + + + public record MessageA(Guid CorrelationId); + + + public record MessageB(Guid CorrelationId) : + MessageA(CorrelationId); + + + public record MessageC(Guid CorrelationId) : + MessageB(CorrelationId); + + + public class TestMessageConsumer : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + } + } + + + public class TestMultipleMessageConsumer : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + } + } + + + public class SlowMessageConsumer : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + await Task.Delay(TimeSpan.FromMinutes(2), context.CancellationToken); + + LogContext.Info?.Log("Consumed the message"); + } + } + + + public class NestedMessageConsumer : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + } + } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/PostgresDatabaseTestConfiguration.cs b/tests/MassTransit.SqlTransport.Tests/PostgresDatabaseTestConfiguration.cs new file mode 100644 index 00000000000..5fed5a2cc38 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/PostgresDatabaseTestConfiguration.cs @@ -0,0 +1,46 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Reflection; +using EntityFrameworkCoreIntegration; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; + + +public class PostgresDatabaseTestConfiguration : + IDatabaseTestConfiguration +{ + public IServiceCollection Create() + { + return new ServiceCollection() + .ConfigurePostgresTransport(); + } + + public ILockStatementProvider LockStatementProvider => new PostgresLockStatementProvider(false); + + public void Apply(DbContextOptionsBuilder builder) + where TDbContext : DbContext + { + builder.UseNpgsql("host=localhost;user id=postgres;password=Password12!;database=MassTransitUnitTests;", m => + { + m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); + m.MigrationsHistoryTable($"__{typeof(TDbContext).Name}"); + }); + } + + public void Configure(IBusRegistrationConfigurator configurator, Action callback) + { + configurator.ConfigurePostgresTransport(); + + configurator.AddConfigureEndpointsCallback((_, cfg) => + { + if (cfg is ISqlReceiveEndpointConfigurator db) + db.PurgeOnStartup = true; + }); + + configurator.UsingPostgres((context, cfg) => + { + callback(context, cfg); + }); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/Provision_Specs.cs b/tests/MassTransit.SqlTransport.Tests/Provision_Specs.cs new file mode 100644 index 00000000000..d0684c0bdfa --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/Provision_Specs.cs @@ -0,0 +1,53 @@ +namespace MassTransit.DbTransport.Tests +{ + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using Testing; + + + [TestFixture] + public class Provisioning_the_transport_database + { + [Test] + [Order(1)] + public async Task Should_create_the_required_schema_tables_and_indices() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness() + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await using var connection = provider.GetTransportConnection(); + + await connection.Open(); + await connection.Close(); + } + + [Test] + [Order(2)] + [Explicit] + public async Task Should_drop_the_database_on_shutdown() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport(delete: true) + .AddMassTransitTestHarness() + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + } + + readonly SqlTransportOptions _options; + + public Provisioning_the_transport_database() + { + _options = new SqlTransportOptions(); + } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/Publish_Specs.cs b/tests/MassTransit.SqlTransport.Tests/Publish_Specs.cs new file mode 100644 index 00000000000..e474067038b --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/Publish_Specs.cs @@ -0,0 +1,118 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; +using UnitTests; + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +public class Using_publish + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_consume_a_lot_of_published_messages() + { + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(TextWriter.Null, x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + _configuration.Configure(x, (context, cfg) => + { + cfg.ReceiveEndpoint("publish-input-queue", e => + { + e.PrefetchCount = 30; + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var options = new ParallelOptions { MaxDegreeOfParallelism = 10 }; + + var timer = Stopwatch.StartNew(); + + const int limit = 1000; + + await Parallel.ForEachAsync(Enumerable.Range(0, limit), options, async (i, token) => + { + await harness.Bus.Publish(new TestMessage($"Hello, World! {i}"), token); + }); + + var sendElapsed = timer.Elapsed; + + await harness.Consumed.SelectAsync().Take(limit).Count(); + + var consumeElapsed = timer.Elapsed; + + timer.Stop(); + + Console.WriteLine("Total publish duration: {0:g}", sendElapsed); + Console.WriteLine("Publish message rate: {0:F2} (msg/s)", + limit * 1000 / sendElapsed.TotalMilliseconds); + Console.WriteLine("Total consume duration: {0:g}", consumeElapsed); + Console.WriteLine("Consume message rate: {0:F2} (msg/s)", + limit * 1000 / consumeElapsed.TotalMilliseconds); + } + + readonly T _configuration; + + public Using_publish() + { + _configuration = new T(); + } +} + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +public class Publishing_a_unsubscribed_message_type + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_not_leave_orphaned_messages() + { + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + _configuration.Configure(x, (context, cfg) => + { + cfg.ReceiveEndpoint("publish-input-queue", e => + { + e.PrefetchCount = 30; + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new TestMessage($"Hello, World!"), harness.CancellationToken); + + await harness.Stop(); + } + + readonly T _configuration; + + public Publishing_a_unsubscribed_message_type() + { + _configuration = new T(); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/Purge_Specs.cs b/tests/MassTransit.SqlTransport.Tests/Purge_Specs.cs new file mode 100644 index 00000000000..37b3cfe8c96 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/Purge_Specs.cs @@ -0,0 +1,51 @@ +namespace MassTransit.DbTransport.Tests; + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; +using UnitTests; + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +[Explicit] +public class Purging_a_queue_on_startup + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_purge_the_queue() + { + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + _configuration.Configure(x, (context, cfg) => + { + cfg.ReceiveEndpoint("empty-queue", e => + { + e.PurgeOnStartup = true; + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new TestMessage("Hello, World!")); + + Assert.That(await harness.Consumed.Any()); + + await harness.Stop(); + } + + readonly T _configuration; + + public Purging_a_queue_on_startup() + { + _configuration = new T(); + } +} \ No newline at end of file diff --git a/tests/MassTransit.SqlTransport.Tests/Redelivery_Specs.cs b/tests/MassTransit.SqlTransport.Tests/Redelivery_Specs.cs new file mode 100644 index 00000000000..ab4172a811b --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/Redelivery_Specs.cs @@ -0,0 +1,78 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Threading.Tasks; +using MassTransit.Tests.Middleware.Caching; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +[TestFixture] +public class When_the_redelivery_header_is_present + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_not_exist_on_outgoing_messages() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddHandler(async (ConsumeContext context) => + { + if (context.GetRedeliveryCount() == 1) + { + await context.Publish(new OutboundMessage()); + return; + } + + throw new TestException("Ouch!"); + }); + + x.AddHandler(async (ConsumeContext _) => + { + }); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + x.AddConfigureEndpointsCallback((_, _, cfg) => + { + cfg.UseDelayedRedelivery(r => r.Interval(10, 1000)); + }); + + _configuration.Configure(x, (context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new InboundMessage()); + + IReceivedMessage message = await harness.Consumed.SelectAsync().Take(1).FirstOrDefault(); + Assert.That(message, Is.Not.Null); + + Assert.That(message.Context.GetHeader(MessageHeaders.RedeliveryCount, default(int?)), Is.Null); + } + + readonly T _configuration; + + public When_the_redelivery_header_is_present() + { + _configuration = new T(); + } + + + class InboundMessage + { + } + + + class OutboundMessage + { + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/RenewLock_Specs.cs b/tests/MassTransit.SqlTransport.Tests/RenewLock_Specs.cs new file mode 100644 index 00000000000..d078cc7d324 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/RenewLock_Specs.cs @@ -0,0 +1,46 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; +using UnitTests; + + +[Explicit] +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +public class Using_a_slow_consumer + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_renew_the_lock() + { + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5), testTimeout: TimeSpan.FromMinutes(5)); + x.AddConsumer(); + + _configuration.Configure(x, (context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new SlowMessage()); + + Assert.That(await harness.Consumed.Any(), Is.True); + } + + readonly T _configuration; + + public Using_a_slow_consumer() + { + _configuration = new T(); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/Request_Specs.cs b/tests/MassTransit.SqlTransport.Tests/Request_Specs.cs new file mode 100644 index 00000000000..28d65a548cc --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/Request_Specs.cs @@ -0,0 +1,60 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using TestFramework.Messages; +using Testing; + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +public class Using_the_request_client + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_properly_return_the_response() + { + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); + x.AddHandler(async context => new PongMessage(context.Message.CorrelationId)); + + _configuration.Configure(x, (context, cfg) => + { + cfg.AutoStart = true; + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + IRequestClient client = harness.GetRequestClient(); + + for (var i = 0; i < 5; i++) + { + var timer = Stopwatch.StartNew(); + + var pingMessage = new PingMessage(); + Response response = await client.GetResponse(pingMessage); + + timer.Stop(); + + Assert.That(response.Message.CorrelationId, Is.EqualTo(pingMessage.CorrelationId)); + + Console.WriteLine("Elapsed: {0}ms", timer.Elapsed.TotalMilliseconds); + } + } + + readonly T _configuration; + + public Using_the_request_client() + { + _configuration = new T(); + } +} \ No newline at end of file diff --git a/tests/MassTransit.SqlTransport.Tests/RoutingSlip_Specs.cs b/tests/MassTransit.SqlTransport.Tests/RoutingSlip_Specs.cs new file mode 100644 index 00000000000..c0d3bd57a0d --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/RoutingSlip_Specs.cs @@ -0,0 +1,55 @@ +namespace MassTransit.DbTransport.Tests; + +using System.Threading.Tasks; +using Courier.Contracts; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using TestFramework.Courier; +using Testing; + + +[TestFixture] +public class Using_a_routing_slip_with_a_custom_subscription +{ + [Test] + public async Task Should_be_sent() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.AddHandler(); + x.AddActivity(); + + x.UsingPostgres((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var trackingNumber = NewId.NextGuid(); + var builder = new RoutingSlipBuilder(trackingNumber); + await builder.AddSubscription(harness.GetHandlerAddress(), RoutingSlipEvents.Completed, RoutingSlipEventContents.All, + x => x.Send(new { Value = "Secret Value" })); + + builder.AddActivity("TestActivity", harness.GetExecuteActivityAddress(), new { Value = "Hello" }); + + await harness.Bus.Execute(builder.Build()); + + IReceivedMessage completed = await harness.Consumed + .SelectAsync(x => x.Context.Message.TrackingNumber == trackingNumber).FirstOrDefault(); + Assert.That(completed, Is.Not.Null); + + Assert.That(completed.Context.Message.Value, Is.EqualTo("Secret Value")); + } + + + public interface RegistrationCompleted : + RoutingSlipCompleted + { + string Value { get; } + } +} \ No newline at end of file diff --git a/tests/MassTransit.SqlTransport.Tests/Scheduler_Specs.cs b/tests/MassTransit.SqlTransport.Tests/Scheduler_Specs.cs new file mode 100644 index 00000000000..3ff0c1c4bfa --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/Scheduler_Specs.cs @@ -0,0 +1,132 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Threading.Tasks; +using Internals; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; + + +[TestFixture(typeof(PostgresDatabaseTestConfiguration))] +[TestFixture(typeof(SqlServerDatabaseTestConfiguration))] +public class Canceling_a_scheduled_message_from_a_consumer + where T : IDatabaseTestConfiguration, new() +{ + [Test] + public async Task Should_be_supported() + { + var testId = NewId.NextGuid(); + + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(x => + { + x.AddSqlMessageScheduler(); + + x.AddHandler(async context => + { + ScheduledMessage scheduledMessage = + await context.ScheduleSend(TimeSpan.FromSeconds(5), new SecondMessage { Id = testId }); + + await Task.Delay(1000); + + await context.CancelScheduledSend(scheduledMessage); + }) + .Endpoint(e => e.Name = "schedule-input"); + + x.AddHandler(async context => + { + await Task.Delay(1, context.CancellationToken); + }) + .Endpoint(e => e.Name = "schedule-input"); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + _configuration.Configure(x, (context, cfg) => + { + cfg.UseSqlMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new FirstMessage()); + + Assert.That(await harness.Consumed.Any()); + Assert.That(await harness.Sent.Any(x => x.Context.Message.Id == testId)); + + Assert.That(async () => await harness.Consumed.Any().OrTimeout(TimeSpan.FromSeconds(5)), Throws.TypeOf()); + } + + [Test] + public async Task Should_be_supported_from_outside_the_consumer() + { + var testId = NewId.NextGuid(); + + Guid? tokenId = default; + Uri destinationAddress = null; + + await using var provider = _configuration.Create() + .AddMassTransitTestHarness(x => + { + x.AddSqlMessageScheduler(); + + x.AddHandler(async context => + { + ScheduledMessage scheduledMessage = await context.ScheduleSend(TimeSpan.FromSeconds(5), new SecondMessage { Id = testId }); + + tokenId = scheduledMessage.TokenId; + destinationAddress = context.ReceiveContext.InputAddress; + }).Endpoint(e => e.Name = "schedule-input"); + + x.AddHandler(async context => + { + await Task.Delay(1, context.CancellationToken); + }) + .Endpoint(e => e.Name = "schedule-input"); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); + + _configuration.Configure(x, (context, cfg) => + { + cfg.UseSqlMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new FirstMessage()); + + Assert.That(await harness.Consumed.Any()); + Assert.That(await harness.Sent.Any(x => x.Context.Message.Id == testId)); + + await Task.Delay(500); + + var scheduler = harness.Scope.ServiceProvider.GetRequiredService(); + await scheduler.CancelScheduledSend(destinationAddress, tokenId.Value, harness.CancellationToken); + + Assert.That(async () => await harness.Consumed.Any().OrTimeout(TimeSpan.FromSeconds(5)), Throws.TypeOf()); + } + + readonly T _configuration; + + public Canceling_a_scheduled_message_from_a_consumer() + { + _configuration = new T(); + } + + + public record FirstMessage; + + + public record SecondMessage + { + public Guid Id { get; init; } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/SqlServer/InstanceName_Specs.cs b/tests/MassTransit.SqlTransport.Tests/SqlServer/InstanceName_Specs.cs new file mode 100644 index 00000000000..28973bb79a7 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/SqlServer/InstanceName_Specs.cs @@ -0,0 +1,109 @@ +namespace MassTransit.DbTransport.Tests.SqlServer; + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using SqlTransport.SqlServer; +using Testing; + + +[TestFixture] +public class InstanceName_Specs +{ + [Test] + public async Task Should_include_the_instance_name() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "localhost\\instance"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingSqlServer(); + }) + .BuildServiceProvider(true); + + var connection = SqlServerSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + + Assert.That(connection.Connection.ConnectionString, Contains.Substring("Data Source=localhost\\instance")); + } + + [Test] + public async Task Should_include_the_instance_name_and_port_and_start() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "localhost\\instance"; + options.Port = 3381; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingSqlServer(); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + Assert.Multiple(() => + { + Assert.That(harness.Bus.Address.Host, Is.EqualTo("localhost")); + Assert.That(harness.Bus.Address.Port, Is.EqualTo(3381)); + Assert.That(harness.Bus.Address.Query, Is.EqualTo("?autodelete=300&instance=instance")); + }); + + var connection = SqlServerSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + + Assert.That(connection.Connection.ConnectionString, Contains.Substring("Data Source=localhost\\instance,3381")); + } + + [Test] + public async Task Should_include_the_instance_name_and_start() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "localhost\\instance"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingSqlServer(); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + Assert.Multiple(() => + { + Assert.That(harness.Bus.Address.GetLeftPart(UriPartial.Authority), Is.EqualTo("db://localhost")); + Assert.That(harness.Bus.Address.Query, Is.EqualTo("?autodelete=300&instance=instance")); + }); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/SqlServer/PortAddress_Specs.cs b/tests/MassTransit.SqlTransport.Tests/SqlServer/PortAddress_Specs.cs new file mode 100644 index 00000000000..1f47f73c781 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/SqlServer/PortAddress_Specs.cs @@ -0,0 +1,98 @@ +namespace MassTransit.DbTransport.Tests.SqlServer; + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using SqlTransport.SqlServer; + + +[TestFixture] +public class PortAddress_Specs +{ + [Test] + public async Task Should_include_the_port() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "localhost"; + options.Database = "masstransit_transport_tests"; + options.Port = 8675; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + x.UsingSqlServer(); + }) + .BuildServiceProvider(true); + + var connection = SqlServerSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + + Assert.That(connection.Connection.ConnectionString, Does.Contain("Data Source=localhost,8675")); + } + + [Test] + public async Task Should_not_blow_up_with_local_db() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "(LocalDb)"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingSqlServer(); + }) + .BuildServiceProvider(true); + + var connection = SqlServerSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + + Assert.Multiple(() => + { + Assert.That(connection.Connection.ConnectionString, Contains.Substring("Data Source=(LocalDb)")); + + Assert.That(provider.GetRequiredService().Address.Host, Is.EqualTo("localdb")); + }); + } + + [Test] + public async Task Should_not_include_the_port() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions().Configure(options => + { + options.Host = "localhost"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + x.UsingPostgres(); + }) + .BuildServiceProvider(true); + + var connection = SqlServerSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + + Assert.That(connection.Connection.ConnectionString, Does.Contain("Data Source=localhost;")); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/SqlServer/Provision_Specs.cs b/tests/MassTransit.SqlTransport.Tests/SqlServer/Provision_Specs.cs new file mode 100644 index 00000000000..5ab9e6de7f3 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/SqlServer/Provision_Specs.cs @@ -0,0 +1,84 @@ +namespace MassTransit.DbTransport.Tests.SqlServer +{ + using System; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Options; + using NUnit.Framework; + using SqlTransport.SqlServer; + using Testing; + + + [TestFixture] + public class Provisioning_the_transport_database + { + [Test] + [Order(1)] + public async Task Should_create_the_required_schema_tables_and_indices() + { + await using var provider = new ServiceCollection() + .ConfigureSqlServerTransport() + .AddMassTransitTestHarness() + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await using var connection = provider.GetTransportConnection(); + + await connection.Open(); + await connection.Close(); + } + + [Test] + [Order(2)] + [Explicit] + public async Task Should_drop_the_database_on_shutdown() + { + await using var provider = new ServiceCollection() + .ConfigureSqlServerTransport(delete: true) + .AddMassTransitTestHarness() + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + } + + readonly SqlTransportOptions _options; + + public Provisioning_the_transport_database() + { + _options = new SqlTransportOptions(); + } + } + + + public static class TestConfigurationExtensions + { + public static IServiceCollection ConfigureSqlServerTransport(this IServiceCollection services, bool create = true, bool delete = false) + { + services.AddOptions().Configure(options => + { + options.Host = "localhost"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "sa"; + options.AdminPassword = "Password12!"; + }); + + services.AddSqlServerMigrationHostedService(create, delete); + + return services; + } + + public static SqlServerSqlTransportConnection GetTransportConnection(this IServiceProvider provider) + { + return SqlServerSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + } + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/SqlServer/Publish_Specs.cs b/tests/MassTransit.SqlTransport.Tests/SqlServer/Publish_Specs.cs new file mode 100644 index 00000000000..5e96fae8571 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/SqlServer/Publish_Specs.cs @@ -0,0 +1,70 @@ +namespace MassTransit.DbTransport.Tests.SqlServer; + +using System; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; +using UnitTests; + +[TestFixture] +public class Using_publish +{ + [Test] + [Explicit] + public async Task Should_consume_a_lot_of_published_messages() + { + await using var provider = new ServiceCollection() + .ConfigureSqlServerTransport() + .AddMassTransitTestHarness(TextWriter.Null,x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + x.UsingSqlServer((context, cfg) => + { + cfg.ReceiveEndpoint("input-queue", e => + { + e.PollingInterval = TimeSpan.FromMilliseconds(10); + e.PrefetchCount = 30; + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var options = new ParallelOptions { MaxDegreeOfParallelism = 10 }; + + var timer = Stopwatch.StartNew(); + + const int limit = 1000; + + await Parallel.ForEachAsync(Enumerable.Range(0, limit), options, async (i, token) => + { + await harness.Bus.Publish(new TestMessage($"Hello, World! {i}"), token); + }); + + var sendElapsed = timer.Elapsed; + + await harness.Consumed.SelectAsync().Take(limit).Count(); + + var consumeElapsed = timer.Elapsed; + + timer.Stop(); + + Console.WriteLine("Total publish duration: {0:g}", sendElapsed); + Console.WriteLine("Publish message rate: {0:F2} (msg/s)", + limit * 1000 / sendElapsed.TotalMilliseconds); + Console.WriteLine("Total consume duration: {0:g}", consumeElapsed); + Console.WriteLine("Consume message rate: {0:F2} (msg/s)", + limit * 1000 / consumeElapsed.TotalMilliseconds); + } +} \ No newline at end of file diff --git a/tests/MassTransit.SqlTransport.Tests/SqlServer/ReceiveEndpoint_Specs.cs b/tests/MassTransit.SqlTransport.Tests/SqlServer/ReceiveEndpoint_Specs.cs new file mode 100644 index 00000000000..d9a2993526b --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/SqlServer/ReceiveEndpoint_Specs.cs @@ -0,0 +1,44 @@ +namespace MassTransit.DbTransport.Tests.SqlServer; + +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; +using UnitTests; + + +[TestFixture] +public class Configuring_a_receive_endpoint_without_topology +{ + [Test] + public async Task Should_create_the_queue() + { + await using var provider = new ServiceCollection() + .ConfigureSqlServerTransport() + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => options.StartTimeout = TimeSpan.FromSeconds(10)); + + x.AddConsumer(); + + x.UsingSqlServer((context, cfg) => + { + cfg.ReceiveEndpoint("input-queue", e => + { + e.ConfigureConsumeTopology = false; + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Stop(); + } +} \ No newline at end of file diff --git a/tests/MassTransit.SqlTransport.Tests/SqlServerDatabaseTestConfiguration.cs b/tests/MassTransit.SqlTransport.Tests/SqlServerDatabaseTestConfiguration.cs new file mode 100644 index 00000000000..2c22606ab1c --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/SqlServerDatabaseTestConfiguration.cs @@ -0,0 +1,48 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using System.Reflection; +using EntityFrameworkCoreIntegration; +using MassTransit.Tests; +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.DependencyInjection; +using SqlServer; + + +public class SqlServerDatabaseTestConfiguration : + IDatabaseTestConfiguration +{ + public ILockStatementProvider LockStatementProvider => new SqlServerLockStatementProvider(false); + + public IServiceCollection Create() + { + return new ServiceCollection() + .ConfigureSqlServerTransport(); + } + + public void Apply(DbContextOptionsBuilder builder) + where TDbContext : DbContext + { + builder.UseSqlServer(LocalDbConnectionStringProvider.GetLocalDbConnectionString(), m => + { + m.MigrationsAssembly(Assembly.GetExecutingAssembly().GetName().Name); + m.MigrationsHistoryTable($"__{typeof(TDbContext).Name}"); + }); + } + + public void Configure(IBusRegistrationConfigurator configurator, Action callback) + { + configurator.ConfigureSqlServerTransport(); + + configurator.AddConfigureEndpointsCallback((_, cfg) => + { + if (cfg is ISqlReceiveEndpointConfigurator db) + db.PurgeOnStartup = true; + }); + + configurator.UsingSqlServer((context, cfg) => + { + callback(context, cfg); + }); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/SubscriptionType_Specs.cs b/tests/MassTransit.SqlTransport.Tests/SubscriptionType_Specs.cs new file mode 100644 index 00000000000..7996e6d612c --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/SubscriptionType_Specs.cs @@ -0,0 +1,162 @@ +namespace MassTransit.SqlTransport.Tests; + +using System; +using System.Threading.Tasks; +using DbTransport.Tests; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using Testing; + + +[TestFixture] +public class When_routing_via_a_routing_key +{ + [Test] + public async Task Should_support_routing_key() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + x.UsingPostgres((context, cfg) => + { + cfg.ReceiveEndpoint("input-queue", e => + { + e.ConfigureConsumeTopology = false; + + e.Subscribe(m => + { + m.SubscriptionType = SqlSubscriptionType.RoutingKey; + m.RoutingKey = "8675309"; + }); + + e.Subscribe(m => + { + m.SubscriptionType = SqlSubscriptionType.RoutingKey; + m.RoutingKey = "655321"; + }); + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new CustomerUpdatedEvent(NewId.NextGuid(), "11223344"), x => x.SetRoutingKey("11223344")); + await harness.Bus.Publish(new CustomerDeletedEvent(NewId.NextGuid(), "655321"), x => x.SetRoutingKey("655321")); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.Any(), Is.False); + Assert.That(await harness.Consumed.Any(), Is.True); + }); + + await harness.Stop(); + } + + + class CustomerEventConsumer : + IConsumer, + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + + + public record CustomerUpdatedEvent(Guid CorrelationId, string CustomerNumber); + + + public record CustomerDeletedEvent(Guid CorrelationId, string CustomerNumber); +} + + +[TestFixture] +public class When_routing_using_a_pattern +{ + [Test] + public async Task Should_support_routing_key() + { + await using var provider = new ServiceCollection() + .ConfigurePostgresTransport() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(2)); + + x.UsingPostgres((context, cfg) => + { + cfg.ReceiveEndpoint("input-queue", e => + { + e.ConfigureConsumeTopology = false; + + e.Subscribe(m => + { + m.SubscriptionType = SqlSubscriptionType.Pattern; + m.RoutingKey = "^[A-Z]+$"; + }); + + e.Subscribe(m => + { + m.SubscriptionType = SqlSubscriptionType.Pattern; + m.RoutingKey = "^[0-9]+$"; + }); + + e.ConfigureConsumer(context); + }); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new ClientUpdatedEvent(NewId.NextGuid(), "11223344"), x => x.SetRoutingKey("11223344")); + await harness.Bus.Publish(new ClientDeletedEvent(NewId.NextGuid(), "655321"), x => x.SetRoutingKey("655321")); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.Any(), Is.False); + Assert.That(await harness.Consumed.Any(), Is.True); + }); + + await harness.Stop(); + } + + + class ClientEventConsumer : + IConsumer, + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + + + public record ClientUpdatedEvent(Guid CorrelationId, string ClientNumber); + + + public record ClientDeletedEvent(Guid CorrelationId, string ClientNumber); +} diff --git a/tests/MassTransit.SqlTransport.Tests/TestConfigurationExtensions.cs b/tests/MassTransit.SqlTransport.Tests/TestConfigurationExtensions.cs new file mode 100644 index 00000000000..480a66a80cf --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/TestConfigurationExtensions.cs @@ -0,0 +1,34 @@ +namespace MassTransit.DbTransport.Tests; + +using System; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using SqlTransport.PostgreSql; + + +public static class TestConfigurationExtensions +{ + public static IServiceCollection ConfigurePostgresTransport(this IServiceCollection services, bool create = true, bool delete = false) + { + services.AddOptions().Configure(options => + { + options.Host = "localhost"; + options.Database = "masstransit_transport_tests"; + options.Schema = "transport"; + options.Role = "transport"; + options.Username = "unit_tests"; + options.Password = "H4rd2Gu3ss!"; + options.AdminUsername = "postgres"; + options.AdminPassword = "Password12!"; + }); + + services.AddPostgresMigrationHostedService(create, delete); + + return services; + } + + public static PostgresSqlTransportConnection GetTransportConnection(this IServiceProvider provider) + { + return PostgresSqlTransportConnection.GetDatabaseConnection(provider.GetRequiredService>().Value); + } +} diff --git a/tests/MassTransit.SqlTransport.Tests/docker-compose.yml b/tests/MassTransit.SqlTransport.Tests/docker-compose.yml new file mode 100644 index 00000000000..fd9f1ef42b6 --- /dev/null +++ b/tests/MassTransit.SqlTransport.Tests/docker-compose.yml @@ -0,0 +1,22 @@ +services: + mssql: + image: "mcr.microsoft.com/azure-sql-edge" + networks: + - dbtransport-network + environment: + - "ACCEPT_EULA=Y" + - "SA_PASSWORD=Password12!" + ports: + - "1433:1433" + postgres: + image: "postgres" + networks: + - dbtransport-network + environment: + - "POSTGRES_PASSWORD=Password12!" + ports: + - "5432:5432" + +networks: + dbtransport-network: + driver: bridge diff --git a/tests/MassTransit.Tests/Audit/AuditFilter_Specs.cs b/tests/MassTransit.Tests/Audit/AuditFilter_Specs.cs index c145701d047..6ef50b3504e 100644 --- a/tests/MassTransit.Tests/Audit/AuditFilter_Specs.cs +++ b/tests/MassTransit.Tests/Audit/AuditFilter_Specs.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -16,7 +15,7 @@ public async Task Should_audit_and_filter_consumed_messages() var expected = _harness.Consumed.Select().Any(); var expectedB = _harness.Consumed.Select().Any(); - _store.Count(x => x.Result.Metadata.ContextType == "Consume").ShouldBe(1); + Assert.That(_store.Count(x => x.Result.Metadata.ContextType == "Consume"), Is.EqualTo(1)); } [Test] @@ -25,7 +24,7 @@ public async Task Should_audit_and_filter_sent_messages() var expected = _harness.Sent.Select().Any(); var expectedB = _harness.Sent.Select().Any(); - _store.Count(x => x.Result.Metadata.ContextType == "Send").ShouldBe(1); + Assert.That(_store.Count(x => x.Result.Metadata.ContextType == "Send"), Is.EqualTo(1)); } InMemoryTestHarness _harness; diff --git a/tests/MassTransit.Tests/Audit/Audit_Specs.cs b/tests/MassTransit.Tests/Audit/Audit_Specs.cs index 1b73730e23c..92cf92cd60a 100644 --- a/tests/MassTransit.Tests/Audit/Audit_Specs.cs +++ b/tests/MassTransit.Tests/Audit/Audit_Specs.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -15,7 +14,7 @@ public async Task Should_audit_consumed_messages() { var expected = _harness.Consumed.Select().Any(); var expectedB = _harness.Consumed.Select().Any(); - _store.Count(x => x.Result.Metadata.ContextType == "Consume").ShouldBe(2); + Assert.That(_store.Count(x => x.Result.Metadata.ContextType == "Consume"), Is.EqualTo(2)); } [Test] @@ -23,7 +22,7 @@ public async Task Should_audit_sent_messages() { var expected = _harness.Sent.Select().Any(); var expectedB = _harness.Sent.Select().Any(); - _store.Count(x => x.Result.Metadata.ContextType == "Send").ShouldBe(2); + Assert.That(_store.Count(x => x.Result.Metadata.ContextType == "Send"), Is.EqualTo(2)); } InMemoryTestHarness _harness; diff --git a/tests/MassTransit.Tests/BadConfiguration_Specs.cs b/tests/MassTransit.Tests/BadConfiguration_Specs.cs index c6935d9635e..4a8c7487da8 100644 --- a/tests/MassTransit.Tests/BadConfiguration_Specs.cs +++ b/tests/MassTransit.Tests/BadConfiguration_Specs.cs @@ -52,7 +52,7 @@ public void Should_throw_for_consumer_retry() { e.Consumer(cc => { - cc.UseRetry(r => + cc.UseMessageRetry(r => { }); }); @@ -83,7 +83,7 @@ public void Should_throw_for_retry() { cfg.ReceiveEndpoint("Hello", e => { - e.UseRetry(r => + e.UseMessageRetry(r => { }); diff --git a/tests/MassTransit.Tests/Batch_Specs.cs b/tests/MassTransit.Tests/Batch_Specs.cs index b326f499668..d915b92e759 100644 --- a/tests/MassTransit.Tests/Batch_Specs.cs +++ b/tests/MassTransit.Tests/Batch_Specs.cs @@ -109,13 +109,14 @@ public async Task Should_receive_one_batch_per_group() await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute(ctx => ctx.CorrelationId = correlation2)); await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute(ctx => ctx.CorrelationId = correlation2)); - await InactivityTask; - var count = await BusTestHarness.Consumed.SelectAsync().Take(6).Count(); - Assert.That(count, Is.EqualTo(6)); + Assert.Multiple(() => + { + Assert.That(count, Is.EqualTo(6)); - Assert.That(_batches.Select(x => x.Length), Is.EquivalentTo(new[] { 1, 2, 3 })); + Assert.That(_batches.Select(x => x.Length), Is.EquivalentTo(new[] { 1, 2, 3 })); + }); } readonly List> _batches = new List>(); @@ -152,12 +153,13 @@ public async Task Should_receive_one_batch_per_group() await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute(ctx => ctx.CorrelationId = correlation2)); await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute(ctx => ctx.CorrelationId = correlation2)); - await InactivityTask; - var count = await BusTestHarness.Consumed.SelectAsync().Take(6).Count(); - Assert.That(count, Is.EqualTo(6)); - Assert.That(_batches.Select(x => x.Length), Is.EquivalentTo(new[] { 1, 2, 3 })); + Assert.Multiple(() => + { + Assert.That(count, Is.EqualTo(6)); + Assert.That(_batches.Select(x => x.Length), Is.EquivalentTo(new[] { 1, 2, 3 })); + }); } readonly List> _batches = new List>(); @@ -167,12 +169,14 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin configurator.ConcurrentMessageLimit = 10; configurator.Consumer(() => - { - TaskCompletionSource> tcs = GetTask>(); - tcs.Task.ContinueWith(t => _batches.Add(t.Result)); - var consumer = new TestBatchConsumer(tcs); - return consumer; - }, cc => cc.Options(x => x.GroupBy(ctx => ctx.CorrelationId?.ToString("D")))); + { + TaskCompletionSource> tcs = GetTask>(); + tcs.Task.ContinueWith(t => _batches.Add(t.Result)); + var consumer = new TestBatchConsumer(tcs); + return consumer; + }, + cc => cc.Options(x => + x.SetTimeLimit(TimeSpan.FromMilliseconds(500)).GroupBy(ctx => ctx.CorrelationId?.ToString("D")))); } } @@ -198,7 +202,7 @@ await Task.WhenAll(mediator.Send(new PingMessage()), Batch batch = await consumer.Completed; - Assert.That(batch.Length, Is.EqualTo(4)); + Assert.That(batch, Has.Length.EqualTo(4)); } } @@ -217,7 +221,7 @@ public async Task Should_not_deliver_duplicate_messages() await _completed.Task; - Assert.That(_duplicateMessages.Count, Is.EqualTo(0)); + Assert.That(_duplicateMessages, Is.Empty); } public Using_a_batch_consumer() @@ -296,6 +300,40 @@ public async Task Consume(ConsumeContext> context) } + [TestFixture] + public class Duplicate_messages_by_id_consumer : + InMemoryTestFixture + { + [Test] + public async Task Should_receive_single_message_within_same_message_id() + { + var correlation1 = NewId.NextGuid(); + + await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute(ctx => ctx.MessageId = correlation1)); + await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute(ctx => ctx.MessageId = correlation1)); + + await InactivityTask; + + var count = await BusTestHarness.Consumed.SelectAsync().Count(); + + Assert.That(count, Is.EqualTo(1)); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.Consumer(() => + { + TaskCompletionSource> tcs = GetTask>(); + return new TestBatchConsumer(tcs); + }, cc => cc.Options(x => + { + x.TimeLimit = TimeSpan.FromSeconds(1); + x.MessageLimit = 2; + })); + } + } + + class TestBatchConsumer : IConsumer> { diff --git a/tests/MassTransit.Tests/BusActivityMonitor_Specs.cs b/tests/MassTransit.Tests/BusActivityMonitor_Specs.cs deleted file mode 100644 index b52b70e725e..00000000000 --- a/tests/MassTransit.Tests/BusActivityMonitor_Specs.cs +++ /dev/null @@ -1,117 +0,0 @@ -namespace MassTransit.Tests -{ - using System; - using System.Collections.Generic; - using System.Threading.Tasks; - using MassTransit.Testing; - using MassTransit.Testing.Implementations; - using NUnit.Framework; - using TestFramework; - using TestFramework.Messages; - - - [TestFixture(TypeArgs = new[] { typeof(SuccessConsumer) })] - [TestFixture(TypeArgs = new[] { typeof(ThrowConsumer) })] - [TestFixture(TypeArgs = new[] { typeof(RandomConsumer) })] - public class BusActivityMonitor_Specs : - InMemoryTestFixture - where TConsumer : class, IConsumer, new() - { - [Test] - [Repeat(RetryPoliciesLength)] - public async Task Should_detect_inactivity() - { - #pragma warning disable 4014 - ActivityTask(); - #pragma warning restore 4014 - - var timeout = await _activityMonitor.AwaitBusInactivity(TimeSpan.FromSeconds(10)).ConfigureAwait(false); - Assert.IsTrue(timeout, "Activity monitor timed out."); - Console.WriteLine($"Bus Inactive : {DateTime.Now}"); - } - - IBusActivityMonitor _activityMonitor; - static IEnumerator RetryEnumerator => GetNextRetryPolicy().GetEnumerator(); - - const int RetryPoliciesLength = 3; - - static IEnumerable GetNextRetryPolicy() - { - while (true) - { - foreach (var retryPolicy in new[] { Retry.None, Retry.Interval(3, TimeSpan.FromMilliseconds(50)), Retry.Immediate(3) }) - yield return retryPolicy; - } - } - - protected override void ConnectObservers(IBus bus) - { - _activityMonitor = bus.CreateBusActivityMonitor(TimeSpan.FromMilliseconds(500)); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - RetryEnumerator.MoveNext(); - var retryPolicy = RetryEnumerator.Current; - #pragma warning disable 618 - configurator.UseRetry(r => r.SetRetryPolicy(x => retryPolicy)); - #pragma warning restore 618 - configurator.Consumer( - x => - { - }); - } - - [OneTimeTearDown] - public void BusActivityMonitor_SpecsTeardown() - { - RetryEnumerator?.Dispose(); - } - - async Task ActivityTask() - { - Console.WriteLine($"Activity Began : {DateTime.Now}"); - for (var i = 0; i < 10; i++) - { - var eventMessage = new PingMessage(Guid.NewGuid()); - await InputQueueSendEndpoint.Send(eventMessage).ConfigureAwait(false); - } - - Console.WriteLine($"Activity Ended : {DateTime.Now}"); - } - } - - - public class SuccessConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return Task.CompletedTask; - } - } - - - public class ThrowConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - throw new ConsumerException("Consumer always throws!"); - } - } - - - public class RandomConsumer : - IConsumer - { - readonly ThreadSafeRandom _random = new ThreadSafeRandom(); - - public Task Consume(ConsumeContext context) - { - if (_random.NextBool()) - throw new ConsumerException("Consumer randomly throws!"); - return Task.CompletedTask; - } - } -} diff --git a/tests/MassTransit.Tests/Caching/Cache_Specs.cs b/tests/MassTransit.Tests/Caching/Cache_Specs.cs index cacf885a21a..8561fc3c82d 100644 --- a/tests/MassTransit.Tests/Caching/Cache_Specs.cs +++ b/tests/MassTransit.Tests/Caching/Cache_Specs.cs @@ -46,12 +46,12 @@ public async Task Should_get_and_add_and_get_same_time() { await Task.Delay(1000); - return new Item(key) {Value = "First"}; + return new Item(key) { Value = "First" }; }); Task second = _newCache.GetOrAdd(key, async key => { - return new Item(key) {Value = "Second"}; + return new Item(key) { Value = "Second" }; }); var item = await second; @@ -62,9 +62,12 @@ public async Task Should_get_and_add_and_get_same_time() await _newCache.Get(key); - Assert.That(timer.Elapsed + TimeSpan.FromSeconds(0.1), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); + Assert.Multiple(() => + { + Assert.That(timer.Elapsed + TimeSpan.FromSeconds(0.1), Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1))); - Assert.That(item.Value, Is.EqualTo("First")); + Assert.That(item.Value, Is.EqualTo("First")); + }); } } @@ -76,7 +79,7 @@ public class Caching_a_bunch_of_values [OneTimeSetUp] public void Setup() { - _newCache = new MassTransitCache>(new UsageCachePolicy(), new CacheOptions {Capacity = 1000}); + _newCache = new MassTransitCache>(new UsageCachePolicy(), new CacheOptions { Capacity = 1000 }); } [Test] @@ -99,7 +102,7 @@ public class Caching_values_and_using_them_too [OneTimeSetUp] public void Setup() { - _newCache = new MassTransitCache>(new UsageCachePolicy(), new CacheOptions {Capacity = 1000}); + _newCache = new MassTransitCache>(new UsageCachePolicy(), new CacheOptions { Capacity = 1000 }); } [Test] @@ -134,7 +137,7 @@ public class Should_handle_a_random_distribution [OneTimeSetUp] public void Setup() { - _newCache = new MassTransitCache>(new UsageCachePolicy(), new CacheOptions {Capacity = 1000}); + _newCache = new MassTransitCache>(new UsageCachePolicy(), new CacheOptions { Capacity = 1000 }); _samples = new int[SampleCount]; @@ -192,7 +195,7 @@ public class Should_handle_a_random_distribution_with_time_to_live public void Setup() { _massTransitCache = new MassTransitCache>(new TimeToLiveCachePolicy(TimeSpan.FromSeconds(30)), - new CacheOptions {Capacity = 1000}); + new CacheOptions { Capacity = 1000 }); _samples = new int[SampleCount]; diff --git a/tests/MassTransit.Tests/Caching/LegacyCacheUpgrade_Specs.cs b/tests/MassTransit.Tests/Caching/LegacyCacheUpgrade_Specs.cs index 8f1ced22ae4..1b18ba9f3e5 100644 --- a/tests/MassTransit.Tests/Caching/LegacyCacheUpgrade_Specs.cs +++ b/tests/MassTransit.Tests/Caching/LegacyCacheUpgrade_Specs.cs @@ -35,8 +35,11 @@ public async Task Should_support_a_simple_addition() var value = await cache.GetOrAdd(helloKey, SimpleValueFactory.Healthy); Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo(helloKey)); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(value.Id, Is.EqualTo(helloKey)); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); + }); } [Test] @@ -51,14 +54,20 @@ public async Task Should_support_a_simple_addition_and_access() Task readValueTask = cache.Get(helloKey); Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo(helloKey)); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(value.Id, Is.EqualTo(helloKey)); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); + }); var readValue = await readValueTask; Assert.That(readValue, Is.Not.Null); - Assert.That(readValue.Id, Is.EqualTo(helloKey)); - Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(readValue.Id, Is.EqualTo(helloKey)); + Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + }); } [Test] @@ -80,14 +89,20 @@ public async Task Should_support_access_to_eventual_success() var value = await goodValueTask; Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo(helloKey)); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(value.Id, Is.EqualTo(helloKey)); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); + }); var readValue = await readValueTask; Assert.That(readValue, Is.Not.Null); - Assert.That(readValue.Id, Is.EqualTo(helloKey)); - Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(readValue.Id, Is.EqualTo(helloKey)); + Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + }); } [Test] @@ -108,16 +123,22 @@ public async Task Should_support_access_to_eventual_success_after_waiting() var value = await goodValueTask; Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo(helloKey)); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(value.Id, Is.EqualTo(helloKey)); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); + }); Task readValueTask = cache.Get(helloKey); var readValue = await readValueTask; Assert.That(readValue, Is.Not.Null); - Assert.That(readValue.Id, Is.EqualTo(helloKey)); - Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(readValue.Id, Is.EqualTo(helloKey)); + Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + }); } } } diff --git a/tests/MassTransit.Tests/Cancellation_Specs.cs b/tests/MassTransit.Tests/Cancellation_Specs.cs new file mode 100644 index 00000000000..1ce68ce77e7 --- /dev/null +++ b/tests/MassTransit.Tests/Cancellation_Specs.cs @@ -0,0 +1,123 @@ +namespace MassTransit.Tests +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using TestFramework.Messages; + + + [TestFixture] + public class When_the_transport_cancels + { + [Test] + public async Task Should_not_produce_a_fault_on_shutdown() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => + { + options.ConsumerStopTimeout = TimeSpan.FromSeconds(1); + options.StopTimeout = TimeSpan.FromSeconds(10); + }); + + x.AddTaskCompletionSource(); + + x.AddHandler(async (ConsumeContext context, TaskCompletionSource source) => + { + source.TrySetResult(context.Message); + + await Task.Delay(5000, context.CancellationToken); + }); + }).BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new PingMessage()); + + await provider.GetRequiredService>().Task; + + await harness.Stop(); + } + + [Test] + public async Task Should_produce_fault_on_timeout() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => + { + options.ConsumerStopTimeout = TimeSpan.FromSeconds(1); + options.StopTimeout = TimeSpan.FromSeconds(10); + }); + + x.AddHandler(async (ConsumeContext context) => + { + await Task.Delay(5000, context.CancellationToken); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseTimeout(t => t.Timeout = TimeSpan.FromSeconds(1)); + + cfg.ConfigureEndpoints(context); + }); + }).BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new PingMessage()); + + Assert.That(await harness.Published.Any>(), Is.True); + + await harness.Stop(); + } + + [Test] + public async Task Should_produce_fault_on_internal_cancellation() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddOptions() + .Configure(options => + { + options.ConsumerStopTimeout = TimeSpan.FromSeconds(1); + options.StopTimeout = TimeSpan.FromSeconds(10); + }); + + x.AddHandler(async (ConsumeContext context) => + { + using var source = new CancellationTokenSource(TimeSpan.FromSeconds(1)); + + await Task.Delay(5000, source.Token); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.ConfigureEndpoints(context); + }); + }).BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new PingMessage()); + + Assert.That(await harness.Published.Any>(), Is.True); + + await harness.Stop(); + } + } +} diff --git a/tests/MassTransit.Tests/ChannelExecutor_Specs.cs b/tests/MassTransit.Tests/ChannelExecutor_Specs.cs deleted file mode 100644 index 11608a8b254..00000000000 --- a/tests/MassTransit.Tests/ChannelExecutor_Specs.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace MassTransit.Tests -{ - using System.Threading; - using System.Threading.Tasks; - using NUnit.Framework; - using Util; - - - [TestFixture] - [Explicit] - public class Creating_and_disposing_a_channel_executor - { - [Test] - public async Task Should_be_low_impact() - { - var executor = new ChannelExecutor(1); - - await executor.DisposeAsync().ConfigureAwait(false); - } - - [Test] - public async Task Should_be_performant() - { - long value = 0; - - var executor = new ChannelExecutor(10); - - const int limit = 10000; - - for (var i = 0; i < limit; i++) - { - await executor.Push(() => - { - Interlocked.Increment(ref value); - - return Task.CompletedTask; - }); - } - - await executor.DisposeAsync().ConfigureAwait(false); - - Assert.That(value, Is.EqualTo(10000)); - } - } -} diff --git a/tests/MassTransit.Tests/CircuitBreaker_Specs.cs b/tests/MassTransit.Tests/CircuitBreaker_Specs.cs index ac9fa99b8e3..67b1c7c3d28 100644 --- a/tests/MassTransit.Tests/CircuitBreaker_Specs.cs +++ b/tests/MassTransit.Tests/CircuitBreaker_Specs.cs @@ -3,8 +3,6 @@ using System; using System.Threading; using System.Threading.Tasks; - using MassTransit.Testing; - using MassTransit.Testing.Implementations; using NUnit.Framework; using TestFramework; @@ -22,14 +20,13 @@ public async Task Should_work() await Task.Delay(50); } - await _activityMonitor.AwaitBusInactivity(); + await InactivityTask; // this is broken, because the faults aren't produced by an open circuit breaker Assert.That(_faultCount, Is.GreaterThanOrEqualTo(3)); } int _faultCount; - IBusActivityMonitor _activityMonitor; protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { @@ -46,11 +43,6 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin configurator.Consumer(() => new BreakingConsumer()); } - protected override void ConnectObservers(IBus bus) - { - _activityMonitor = bus.CreateBusActivityMonitor(TimeSpan.FromMilliseconds(500)); - } - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { configurator.ReceiveEndpoint("faults", cfg => diff --git a/tests/MassTransit.Tests/ConcurrencyLimit_Specs.cs b/tests/MassTransit.Tests/ConcurrencyLimit_Specs.cs index a012b5e9bd2..3588fb5ee22 100644 --- a/tests/MassTransit.Tests/ConcurrencyLimit_Specs.cs +++ b/tests/MassTransit.Tests/ConcurrencyLimit_Specs.cs @@ -33,7 +33,7 @@ public async Task Should_limit_the_consumer() await _complete.Task; - Assert.AreEqual(2, _consumer.MaxDeliveryCount); + Assert.That(_consumer.MaxDeliveryCount, Is.EqualTo(2)); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -119,7 +119,7 @@ public async Task Should_limit_the_saga() await _complete.Task; - Assert.AreEqual(2, ConsumerSaga.MaxDeliveryCount); + Assert.That(ConsumerSaga.MaxDeliveryCount, Is.EqualTo(2)); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/Configuration/ConfigurationObserver_Specs.cs b/tests/MassTransit.Tests/Configuration/ConfigurationObserver_Specs.cs index af24e085c39..f0e5e62b40c 100644 --- a/tests/MassTransit.Tests/Configuration/ConfigurationObserver_Specs.cs +++ b/tests/MassTransit.Tests/Configuration/ConfigurationObserver_Specs.cs @@ -21,7 +21,7 @@ public void Should_invoke_the_observers_for_each_consumer_and_message_type() cfg.ReceiveEndpoint("hello", e => { - e.UseRetry(x => x.Immediate(1)); + e.UseMessageRetry(x => x.Immediate(1)); e.Consumer(x => { @@ -36,9 +36,12 @@ public void Should_invoke_the_observers_for_each_consumer_and_message_type() }); }); - Assert.That(observer.ConsumerTypes.Contains(typeof(MyConsumer))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(MyConsumer), typeof(PingMessage)))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(MyConsumer), typeof(PongMessage)))); + Assert.Multiple(() => + { + Assert.That(observer.ConsumerTypes, Does.Contain(typeof(MyConsumer))); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(MyConsumer), typeof(PingMessage)))); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(MyConsumer), typeof(PongMessage)))); + }); } [Test] @@ -52,15 +55,18 @@ public void Should_invoke_the_observers_for_object_consumer_and_message_type() cfg.ReceiveEndpoint("hello", e => { - e.UseRetry(x => x.Immediate(1)); + e.UseMessageRetry(x => x.Immediate(1)); e.Consumer(typeof(MyConsumer), _ => new MyConsumer()); }); }); - Assert.That(observer.ConsumerTypes.Contains(typeof(MyConsumer))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(MyConsumer), typeof(PingMessage)))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(MyConsumer), typeof(PongMessage)))); + Assert.Multiple(() => + { + Assert.That(observer.ConsumerTypes, Does.Contain(typeof(MyConsumer))); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(MyConsumer), typeof(PingMessage)))); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(MyConsumer), typeof(PongMessage)))); + }); } [Test] @@ -74,7 +80,7 @@ public void Should_invoke_for_the_handler() cfg.ReceiveEndpoint("hello", e => { - e.UseRetry(x => x.Immediate(1)); + e.UseMessageRetry(x => x.Immediate(1)); e.Handler(async context => { @@ -82,7 +88,7 @@ public void Should_invoke_for_the_handler() }); }); - Assert.That(observer.MessageTypes.Contains(typeof(PingMessage))); + Assert.That(observer.MessageTypes, Does.Contain(typeof(PingMessage))); } [Test] @@ -96,15 +102,18 @@ public void Should_invoke_the_observers_for_regular_consumer_and_message_type() cfg.ReceiveEndpoint("hello", e => { - e.UseRetry(x => x.Immediate(1)); + e.UseMessageRetry(x => x.Immediate(1)); e.Consumer(); }); }); - Assert.That(observer.ConsumerTypes.Contains(typeof(MyConsumer))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(MyConsumer), typeof(PingMessage)))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(MyConsumer), typeof(PongMessage)))); + Assert.Multiple(() => + { + Assert.That(observer.ConsumerTypes, Does.Contain(typeof(MyConsumer))); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(MyConsumer), typeof(PingMessage)))); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(MyConsumer), typeof(PongMessage)))); + }); } [Test] @@ -122,7 +131,7 @@ public void Should_invoke_the_observers_for_execute_activity_type() }); }); - Assert.That(observer.ExecuteActivityTypes.Contains((typeof(SetVariableActivity), typeof(SetVariableArguments)))); + Assert.That(observer.ExecuteActivityTypes, Does.Contain((typeof(SetVariableActivity), typeof(SetVariableArguments)))); } [Test] @@ -149,8 +158,11 @@ public void Should_invoke_the_observers_for_activity_type() }); }); - Assert.That(observer.ActivityTypes.Contains((typeof(TestActivity), typeof(TestArguments), compensateAddress))); - Assert.That(observer.CompensateActivityTypes.Contains((typeof(TestActivity), typeof(TestLog)))); + Assert.Multiple(() => + { + Assert.That(observer.ActivityTypes, Does.Contain((typeof(TestActivity), typeof(TestArguments), compensateAddress))); + Assert.That(observer.CompensateActivityTypes, Does.Contain((typeof(TestActivity), typeof(TestLog)))); + }); } diff --git a/tests/MassTransit.Tests/Configuration/DefaultEndpointNameFormatter_Specs.cs b/tests/MassTransit.Tests/Configuration/DefaultEndpointNameFormatter_Specs.cs index 0cd0c55bbdb..1520f41808a 100644 --- a/tests/MassTransit.Tests/Configuration/DefaultEndpointNameFormatter_Specs.cs +++ b/tests/MassTransit.Tests/Configuration/DefaultEndpointNameFormatter_Specs.cs @@ -4,7 +4,6 @@ using System.Linq; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; public class When_consumers_are_generic_classes @@ -27,13 +26,13 @@ public void A_formatter_derives_endpoint_names() [Test] public void Should_all_be_unique() { - _endpointNames.Distinct().Count().ShouldBe(_endpointNames.Count()); + Assert.That(_endpointNames.Distinct().Count(), Is.EqualTo(_endpointNames.Count())); } [Test] public void Should_be_named_after_first_generic_parameter() { - _endpointNames.ShouldBe(new[] {nameof(Msg1), nameof(Msg2), nameof(Msg3)}); + Assert.That(_endpointNames, Is.EqualTo(new[] {nameof(Msg1), nameof(Msg2), nameof(Msg3)})); } diff --git a/tests/MassTransit.Tests/Configuration/SagaConfigurationObserver_Specs.cs b/tests/MassTransit.Tests/Configuration/SagaConfigurationObserver_Specs.cs index 04e59eac5bc..f33c38e95e0 100644 --- a/tests/MassTransit.Tests/Configuration/SagaConfigurationObserver_Specs.cs +++ b/tests/MassTransit.Tests/Configuration/SagaConfigurationObserver_Specs.cs @@ -20,7 +20,7 @@ public void Should_invoke_the_observers_for_each_consumer_and_message_type() cfg.ReceiveEndpoint("hello", e => { - e.UseRetry(x => x.Immediate(1)); + e.UseMessageRetry(x => x.Immediate(1)); e.Saga(new InMemorySagaRepository(), x => { @@ -35,9 +35,12 @@ public void Should_invoke_the_observers_for_each_consumer_and_message_type() }); }); - Assert.That(observer.SagaTypes.Contains(typeof(MySaga))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(MySaga), typeof(PingMessage)))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(MySaga), typeof(PongMessage)))); + Assert.Multiple(() => + { + Assert.That(observer.SagaTypes, Does.Contain(typeof(MySaga))); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(MySaga), typeof(PingMessage)))); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(MySaga), typeof(PongMessage)))); + }); } diff --git a/tests/MassTransit.Tests/Configuration/SagaConnector_Specs.cs b/tests/MassTransit.Tests/Configuration/SagaConnector_Specs.cs index 3ddbfc221ef..223fd707da9 100644 --- a/tests/MassTransit.Tests/Configuration/SagaConnector_Specs.cs +++ b/tests/MassTransit.Tests/Configuration/SagaConnector_Specs.cs @@ -5,7 +5,6 @@ namespace MassTransit.Tests.Configuration using NUnit.Framework; using Saga; using Saga.Messages; - using Shouldly; public class When_a_saga_is_inspected @@ -21,31 +20,31 @@ public void A_consumer_with_consumes_all_interfaces_is_inspected() [Test] public void Should_create_the_builder() { - _factory.ShouldNotBe(null); + Assert.That(_factory, Is.Not.Null); } [Test] public void Should_have_three_subscription_types() { - _factory.Connectors.Count().ShouldBe(3); + Assert.That(_factory.Connectors.Count(), Is.EqualTo(3)); } [Test] public void Should_have_an_a() { - _factory.Connectors.First().MessageType.ShouldBe(typeof(InitiateSimpleSaga)); + Assert.That(_factory.Connectors.First().MessageType, Is.EqualTo(typeof(InitiateSimpleSaga))); } [Test] public void Should_have_a_b() { - _factory.Connectors.Skip(1).First().MessageType.ShouldBe(typeof(CompleteSimpleSaga)); + Assert.That(_factory.Connectors.Skip(1).First().MessageType, Is.EqualTo(typeof(CompleteSimpleSaga))); } [Test] public void Should_have_a_c() { - _factory.Connectors.Skip(2).First().MessageType.ShouldBe(typeof(ObservableSagaMessage)); + Assert.That(_factory.Connectors.Skip(2).First().MessageType, Is.EqualTo(typeof(ObservableSagaMessage))); } } } diff --git a/tests/MassTransit.Tests/Configuration/SendSpecification_Specs.cs b/tests/MassTransit.Tests/Configuration/SendSpecification_Specs.cs index a09ba16f29b..402ce351ac3 100644 --- a/tests/MassTransit.Tests/Configuration/SendSpecification_Specs.cs +++ b/tests/MassTransit.Tests/Configuration/SendSpecification_Specs.cs @@ -176,7 +176,7 @@ static IEnumerable GetMessageTypes() foreach (var baseInterface in GetImplementedInterfaces(typeof(TMessage))) yield return baseInterface; - var baseType = typeof(TMessage).GetTypeInfo().BaseType; + var baseType = typeof(TMessage).BaseType; while (baseType != null && MessageTypeCache.IsValidMessageType(baseType)) { yield return baseType; @@ -184,7 +184,7 @@ static IEnumerable GetMessageTypes() foreach (var baseInterface in GetImplementedInterfaces(baseType)) yield return baseInterface; - baseType = baseType.GetTypeInfo().BaseType; + baseType = baseType.BaseType; } } @@ -195,10 +195,10 @@ static IEnumerable GetImplementedInterfaces(Type baseType) .Where(MessageTypeCache.IsValidMessageType) .ToArray(); - if (baseType.GetTypeInfo().BaseType != null && baseType.GetTypeInfo().BaseType != typeof(object)) + if (baseType.BaseType != null && baseType.BaseType != typeof(object)) { baseInterfaces = baseInterfaces - .Except(baseType.GetTypeInfo().BaseType.GetInterfaces()) + .Except(baseType.BaseType.GetInterfaces()) .Except(baseInterfaces.SelectMany(x => x.GetInterfaces())) .ToArray(); } diff --git a/tests/MassTransit.Tests/ConsumeJsonObject_Specs.cs b/tests/MassTransit.Tests/ConsumeJsonObject_Specs.cs new file mode 100644 index 00000000000..f21ef65505a --- /dev/null +++ b/tests/MassTransit.Tests/ConsumeJsonObject_Specs.cs @@ -0,0 +1,45 @@ +namespace MassTransit.Tests +{ + using System; + using System.Text.Json.Nodes; + using System.Threading.Tasks; + using NUnit.Framework; + using TestFramework; + using TestFramework.Messages; + + + [TestFixture] + public class ConsumeJsonObject_Specs : + InMemoryTestFixture + { + [Test] + public async Task Should_receive_the_json_object() + { + await InputQueueSendEndpoint.Send(new PingMessage()); + + await _completed.Task; + } + + [SetUp] + public void Setup() + { + _completed = GetTask(); + } + + TaskCompletionSource _completed; + + protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) + { + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.Handler(async context => + { + await Console.Out.WriteLineAsync($"Received the json object! {context.Message}"); + + _completed.TrySetResult(context.Message); + }); + } + } +} diff --git a/tests/MassTransit.Tests/ConsumeObserver_Specs.cs b/tests/MassTransit.Tests/ConsumeObserver_Specs.cs index de011925e7b..5dd97198afa 100644 --- a/tests/MassTransit.Tests/ConsumeObserver_Specs.cs +++ b/tests/MassTransit.Tests/ConsumeObserver_Specs.cs @@ -7,7 +7,6 @@ namespace MyNamespace using MassTransit.Testing; using MassTransit.Testing.Implementations; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -28,7 +27,7 @@ public void Should_trigger_the_consume_observer() { IReceivedMessage context = _observer.Messages.Select().First(); - context.ShouldNotBeNull(); + Assert.That(context, Is.Not.Null); } TestConsumeMessageObserver _pingObserver; @@ -90,7 +89,7 @@ public async Task Should_trigger_the_consume_message_observer() IReceivedMessage context = observer.Messages.Select().First(); - context.ShouldNotBeNull(); + Assert.That(context, Is.Not.Null); } [Test] @@ -127,8 +126,11 @@ public async Task Should_trigger_the_consume_message_observer_for_both() await pingObserver.PostConsumed; - Assert.That(observer.Messages.Select().First(), Is.Not.Null); - Assert.That(observer.Messages.Select().First(), Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(observer.Messages.Select().First(), Is.Not.Null); + Assert.That(observer.Messages.Select().First(), Is.Not.Null); + }); } } } diff --git a/tests/MassTransit.Tests/ContainedMessage_Specs.cs b/tests/MassTransit.Tests/ContainedMessage_Specs.cs index 735269c38a1..8bb407e93e6 100644 --- a/tests/MassTransit.Tests/ContainedMessage_Specs.cs +++ b/tests/MassTransit.Tests/ContainedMessage_Specs.cs @@ -2,7 +2,6 @@ { using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -21,9 +20,9 @@ public async Task Should_receive_the_secure_command() { ConsumeContext> context = await _secureCommandHandler; - context.Message.Credentials.ShouldNotBe(null); + Assert.That(context.Message.Credentials, Is.Not.Null); - context.Message.Credentials.Username.ShouldBe("sa"); + Assert.That(context.Message.Credentials.Username, Is.EqualTo("sa")); } Task>> _secureCommandHandler; diff --git a/tests/MassTransit.Containers.Tests/AccessScope_Specs.cs b/tests/MassTransit.Tests/ContainerTests/AccessScope_Specs.cs similarity index 98% rename from tests/MassTransit.Containers.Tests/AccessScope_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/AccessScope_Specs.cs index e113a44de2b..e2837a79e61 100644 --- a/tests/MassTransit.Containers.Tests/AccessScope_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/AccessScope_Specs.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Threading.Tasks; diff --git a/tests/MassTransit.Tests/ContainerTests/Batch_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Batch_Specs.cs new file mode 100644 index 00000000000..deda75a43f4 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Batch_Specs.cs @@ -0,0 +1,577 @@ +namespace MassTransit.Tests.ContainerTests +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using MassTransit.Middleware.InMemoryOutbox; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using TestFramework; + + + [TestFixture] + public class When_batch_limit_is_reached + { + [Test] + public async Task Should_deliver_the_batch_to_the_consumer() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.Any(x => x.Context.Message is { Count: 2, Mode: BatchCompletionMode.Time }), Is.True); + }); + } + } + + + [TestFixture] + public class When_retry_and_in_memory_outbox_are_used_with_batch_consumers + { + [Test] + public async Task Should_deliver_the_batch_to_the_consumer() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + x.AddConfigureEndpointsCallback((context, _, cfg) => + { + cfg.UseMessageRetry(r => r.Immediate(2)); + cfg.UseInMemoryOutbox(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.Any(x => x.Context.Message.Count == 2 && x.Context.Message.Mode == BatchCompletionMode.Time), + Is.True); + }); + } + + [Test] + public async Task Should_deliver_the_batch_to_the_consumer_after_retry() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + x.AddConfigureEndpointsCallback((context, _, cfg) => + { + cfg.UseMessageRetry(r => r.Immediate(2)); + cfg.UseInMemoryOutbox(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.Any(x => x.Context.Message.Count == 2 && x.Context.Message.Mode == BatchCompletionMode.Time), + Is.True); + }); + } + + [Test] + public async Task Should_deliver_the_batch_to_the_consumer_with_message() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => c.Message>(m => m.UseInMemoryOutbox())); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.Any(x => x.Context.Message.Count == 2 && x.Context.Message.Mode == BatchCompletionMode.Time), + Is.True); + }); + } + + + class TestOutboxBatchConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + if (context.TryGetPayload(out _)) + { + context.Respond(new BatchResult + { + Count = context.Message.Length, + Mode = context.Message.Mode + }); + } + else + throw new InvalidOperationException("Outbox context is not available at this point"); + + return Task.CompletedTask; + } + } + + + class TestRetryOutboxBatchConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + if (context.TryGetPayload(out _)) + { + if (context.GetRetryCount() == 0) + throw new IntentionalTestException("First time is not the charm"); + + context.Respond(new BatchResult + { + Count = context.Message.Length, + Mode = context.Message.Mode + }); + } + else + throw new InvalidOperationException("Outbox context is not available at this point"); + + return Task.CompletedTask; + } + } + } + + + [TestFixture] + public class When_a_batch_limit_is_configured + { + [Test] + public async Task Should_deliver_the_batch_to_the_consumer() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => + c.Options(o => o.SetMessageLimit(5).SetTimeLimit(1000))) + .Endpoint(e => e.ConcurrentMessageLimit = 16); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(5).Count(), Is.EqualTo(5)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.Any(x => x.Context.Message.Count == 5 && x.Context.Message.Mode == BatchCompletionMode.Size), + Is.True); + Assert.That(await harness.Published.Any(x => x.Context.Message.Count == 1 && x.Context.Message.Mode == BatchCompletionMode.Time), + Is.True); + }); + } + } + + + [TestFixture] + public class When_a_big_batch_limit_is_configured + { + [Test] + public async Task Should_deliver_the_batch_to_the_consumer() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => + c.Options(o => o.SetMessageLimit(100).SetTimeLimit(10000))) + .Endpoint(e => e.ConcurrentMessageLimit = 101); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(Enumerable.Range(0, 100).Select(_ => new BatchItem())); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(100).Count(), Is.EqualTo(100)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.Any(x => x.Context.Message.Count == 100 && x.Context.Message.Mode == BatchCompletionMode.Size), + Is.True); + }); + } + } + + + [TestFixture] + public class When_a_batch_limit_is_configured_using_a_definition + { + [Test] + public async Task Should_deliver_the_batch_to_the_consumer() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 16); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(5).Count(), Is.EqualTo(5)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.Any(x => x.Context.Message is { Count: 5, Mode: BatchCompletionMode.Size }), Is.True); + Assert.That(await harness.Published.Any(x => x.Context.Message is { Count: 1, Mode: BatchCompletionMode.Time }), Is.True); + }); + } + } + + + [TestFixture] + public class When_a_batch_consumer_faults + { + [Test] + public async Task Should_fault_once_for_each_message_in_the_batch() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); + }); + } + } + + + [TestFixture] + public class When_a_batch_consumer_faults_and_retries + { + [Test] + public async Task Should_fault_once_for_each_message_in_the_batch() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); + + x.AddConfigureEndpointsCallback((_, cfg) => cfg.UseMessageRetry(r => r.Immediate(1))); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); + }); + } + + [Test] + public async Task Should_fault_once_for_each_message_in_the_batch_at_the_consumer_retry() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => + { + c.UseMessageRetry(r => r.Immediate(1)); + c.Options(o => o.SetMessageLimit(2)); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); + }); + } + + [Test] + public async Task Should_fault_once_for_each_message_in_the_batch_with_delayed_redelivery() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); + + x.AddConfigureEndpointsCallback((_, cfg) => + { + cfg.UseDelayedRedelivery(r => r.Intervals(10)); + cfg.UseMessageRetry(r => r.Immediate(1)); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); + }); + } + + [Test] + public async Task Should_fault_once_for_each_message_in_the_batch_with_in_memory_outbox() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); + + x.AddConfigureEndpointsCallback((context, _, cfg) => + { + cfg.UseMessageRetry(r => r.Immediate(2)); + cfg.UseInMemoryOutbox(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); + }); + } + + [Test] + public async Task Should_fault_once_for_each_message_in_the_batch_with_scheduled_redelivery() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); + + x.AddConfigureEndpointsCallback((_, cfg) => + { + cfg.UseScheduledRedelivery(r => r.Intervals(10)); + cfg.UseMessageRetry(r => r.Immediate(1)); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.PublishBatch(new[] { new BatchItem(), new BatchItem() }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Take(2).Count(), Is.EqualTo(2)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); + }); + } + } + + + [TestFixture] + public class Duplicate_messages_by_id_consumer : + InMemoryTestFixture + { + [Test] + public async Task Should_receive_single_message_within_same_message_id() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(c => c.Options(o => o.SetMessageLimit(2))); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var correlation1 = NewId.NextGuid(); + + await harness.Bus.Publish(new BatchItem(), context => context.MessageId = correlation1); + await harness.Bus.Publish(new BatchItem(), context => context.MessageId = correlation1); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.SelectAsync().Count(), Is.EqualTo(1)); + + Assert.That(await harness.GetConsumerHarness().Consumed.Any>(), Is.True); + + Assert.That(await harness.Published.Any(x => x.Context.Message is { Count: 1, Mode: BatchCompletionMode.Time }), Is.True); + }); + } + } + + + public class BatchItem + { + } + + + public class BatchResult + { + public int Count { get; set; } + public BatchCompletionMode Mode { get; set; } + } + + + public class TestBatchConsumerDefinition : + ConsumerDefinition + { + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) + { + endpointConfigurator.UseInMemoryOutbox(context); + consumerConfigurator.Options(o => o.SetMessageLimit(5).SetTimeLimit(1000)); + } + } + + + public class TestBatchConsumer : + IConsumer> + { + public Task Consume(ConsumeContext> context) + { + context.Respond(new BatchResult + { + Count = context.Message.Length, + Mode = context.Message.Mode + }); + + return Task.CompletedTask; + } + } + + + class FailingBatchConsumer : + IConsumer> + { + int _attempts; + + public int Attempts => _attempts; + + public Task Consume(ConsumeContext> context) + { + Interlocked.Increment(ref _attempts); + + throw new IntentionalTestException("Failing Batch Consumer"); + } + } +} diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Conductor.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Conductor.cs similarity index 97% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_Conductor.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Conductor.cs index dea38f9d846..d817d2b4eb7 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Conductor.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Conductor.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; @@ -54,9 +54,6 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con namespace ConductorContracts { - using System; - - public interface SubmitOrder { Guid OrderId { get; } diff --git a/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ConsumeContext.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ConsumeContext.cs new file mode 100644 index 00000000000..d764e4b331a --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ConsumeContext.cs @@ -0,0 +1,840 @@ +namespace MassTransit.Tests.ContainerTests.Common_Tests +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using ConsumeContextTestSubjects; + using Context; + using MassTransit.Middleware.InMemoryOutbox; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using TestFramework; + using TestFramework.Messages; + using UnitOfWorkComponents; + + + public class Common_ConsumeContext : + InMemoryContainerTestFixture + { + Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; + Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; + Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; + + [Test] + public async Task Should_provide_the_consume_context() + { + await InputQueueSendEndpoint.Send(new PingMessage()); + + var consumeContext = await ConsumeContext; + + Assert.That(consumeContext.TryGetPayload(out MessageConsumeContext _), "Is MessageConsumeContext"); + + var publishEndpoint = await PublishEndpoint; + var sendEndpointProvider = await SendEndpointProvider; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); + Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(messageConsumeContext, sendEndpointProvider)"); + }); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + TaskCompletionSource pingTask = GetTask(); + TaskCompletionSource sendEndpointProviderTask = GetTask(); + TaskCompletionSource publishEndpointTask = GetTask(); + + return collection.AddSingleton(pingTask) + .AddSingleton(sendEndpointProviderTask) + .AddSingleton(publishEndpointTask) + .AddScoped() + .AddScoped(); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumers(BusRegistrationContext); + } + } + + + public class Common_ConsumeContext_Outbox : + InMemoryContainerTestFixture + { + Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; + Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; + Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; + + [Test] + public async Task Should_provide_the_outbox() + { + Task>> fault = await ConnectPublishHandler>(); + + await InputQueueSendEndpoint.Send(new PingMessage()); + + var consumeContext = await ConsumeContext; + + Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext _), "Is ConsumerConsumeContext"); + + var publishEndpoint = await PublishEndpoint; + var sendEndpointProvider = await SendEndpointProvider; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); + Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); + }); + + await fault; + + Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); + } + + protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) + { + harness.TestTimeout = TimeSpan.FromSeconds(3); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + TaskCompletionSource pingTask = GetTask(); + TaskCompletionSource sendEndpointProviderTask = GetTask(); + TaskCompletionSource publishEndpointTask = GetTask(); + + return collection + .AddSingleton(pingTask) + .AddSingleton(sendEndpointProviderTask) + .AddSingleton(publishEndpointTask) + .AddScoped() + .AddScoped(); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.UseInMemoryOutbox(BusRegistrationContext); + + configurator.ConfigureConsumers(BusRegistrationContext); + } + } + + + public class Common_ConsumeContext_Outbox_Without_Registration_Context : + InMemoryContainerTestFixture + { + Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; + Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; + Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; + + [Test] + public async Task Should_provide_the_outbox() + { + Task>> fault = await ConnectPublishHandler>(); + + await InputQueueSendEndpoint.Send(new PingMessage()); + + var consumeContext = await ConsumeContext; + + Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext _), "Is ConsumerConsumeContext"); + + var publishEndpoint = await PublishEndpoint; + var sendEndpointProvider = await SendEndpointProvider; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); + Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); + }); + + await fault; + + Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); + } + + protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) + { + harness.TestTimeout = TimeSpan.FromSeconds(3); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + TaskCompletionSource pingTask = GetTask(); + TaskCompletionSource sendEndpointProviderTask = GetTask(); + TaskCompletionSource publishEndpointTask = GetTask(); + + return collection + .AddSingleton(pingTask) + .AddSingleton(sendEndpointProviderTask) + .AddSingleton(publishEndpointTask) + .AddScoped() + .AddScoped(); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.UseInMemoryOutbox(); + + configurator.ConfigureConsumers(BusRegistrationContext); + } + } + + + public class Common_ConsumeContext_Outbox_Batch : + InMemoryContainerTestFixture + { + Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; + Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; + Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; + + [Test] + public async Task Should_provide_the_outbox() + { + Task>> fault = await ConnectPublishHandler>(); + + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + + var consumeContext = await ConsumeContext; + + Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext> _), "Is ConsumerConsumeContext"); + + var publishEndpoint = await PublishEndpoint; + var sendEndpointProvider = await SendEndpointProvider; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); + Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); + }); + + await fault; + + Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); + } + + protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) + { + harness.TestTimeout = TimeSpan.FromSeconds(3); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + TaskCompletionSource pingTask = GetTask(); + TaskCompletionSource sendEndpointProviderTask = GetTask(); + TaskCompletionSource publishEndpointTask = GetTask(); + + return collection + .AddSingleton(pingTask) + .AddSingleton(sendEndpointProviderTask) + .AddSingleton(publishEndpointTask) + .AddScoped() + .AddScoped(); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(x => + x.Options(b => b.SetTimeLimit(200).SetMessageLimit(4))); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.UseDelayedRedelivery(r => r.None()); + configurator.UseMessageRetry(r => r.None()); + configurator.UseInMemoryOutbox(BusRegistrationContext); + configurator.UseUnitOfWork(); + + configurator.ConfigureConsumers(BusRegistrationContext); + } + } + + + public class Common_ConsumeContext_Outbox_Batch_Without_Registration_Context : + InMemoryContainerTestFixture + { + Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; + Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; + Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; + + [Test] + public async Task Should_provide_the_outbox() + { + Task>> fault = await ConnectPublishHandler>(); + + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + + var consumeContext = await ConsumeContext; + + Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext> _), "Is ConsumerConsumeContext"); + + var publishEndpoint = await PublishEndpoint; + var sendEndpointProvider = await SendEndpointProvider; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); + Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); + }); + + await fault; + + Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); + } + + protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) + { + harness.TestTimeout = TimeSpan.FromSeconds(3); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + TaskCompletionSource pingTask = GetTask(); + TaskCompletionSource sendEndpointProviderTask = GetTask(); + TaskCompletionSource publishEndpointTask = GetTask(); + + return collection + .AddSingleton(pingTask) + .AddSingleton(sendEndpointProviderTask) + .AddSingleton(publishEndpointTask) + .AddScoped() + .AddScoped(); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(x => + x.Options(b => b.SetTimeLimit(200).SetMessageLimit(4))); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.UseDelayedRedelivery(r => r.None()); + configurator.UseMessageRetry(r => r.None()); + configurator.UseInMemoryOutbox(); + configurator.UseUnitOfWork(); + + configurator.ConfigureConsumers(BusRegistrationContext); + } + } + + + public class Common_ConsumeContext_Filter_Batch : + InMemoryContainerTestFixture + { + Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; + Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; + Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; + + [Test] + public async Task Should_provide_the_outbox() + { + Task>> fault = await ConnectPublishHandler>(); + + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + + var consumeContext = await ConsumeContext; + + Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext> _), "Is ConsumerConsumeContext"); + + var publishEndpoint = await PublishEndpoint; + var sendEndpointProvider = await SendEndpointProvider; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); + Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); + }); + + await fault; + + Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); + } + + protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) + { + harness.TestTimeout = TimeSpan.FromSeconds(3); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + TaskCompletionSource pingTask = GetTask(); + TaskCompletionSource sendEndpointProviderTask = GetTask(); + TaskCompletionSource publishEndpointTask = GetTask(); + + return collection + .AddSingleton(pingTask) + .AddSingleton(sendEndpointProviderTask) + .AddSingleton(publishEndpointTask) + .AddScoped() + .AddScoped(); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(x => + x.Options(b => b.SetTimeLimit(200).SetMessageLimit(4))); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.UseInMemoryOutbox(BusRegistrationContext); + configurator.UseUnitOfWork(); + + configurator.ConfigureConsumers(BusRegistrationContext); + } + } + + + public class Common_ConsumeContext_Filter_Batch_Without_Registration_Context : + InMemoryContainerTestFixture + { + Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; + Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; + Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; + + [Test] + public async Task Should_provide_the_outbox() + { + Task>> fault = await ConnectPublishHandler>(); + + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + await InputQueueSendEndpoint.Send(new PingMessage()); + + var consumeContext = await ConsumeContext; + + Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext> _), "Is ConsumerConsumeContext"); + + var publishEndpoint = await PublishEndpoint; + var sendEndpointProvider = await SendEndpointProvider; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); + Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); + }); + + await fault; + + Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); + } + + protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) + { + harness.TestTimeout = TimeSpan.FromSeconds(3); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + TaskCompletionSource pingTask = GetTask(); + TaskCompletionSource sendEndpointProviderTask = GetTask(); + TaskCompletionSource publishEndpointTask = GetTask(); + + return collection + .AddSingleton(pingTask) + .AddSingleton(sendEndpointProviderTask) + .AddSingleton(publishEndpointTask) + .AddScoped() + .AddScoped(); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(x => + x.Options(b => b.SetTimeLimit(200).SetMessageLimit(4))); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.UseInMemoryOutbox(); + configurator.UseUnitOfWork(); + + configurator.ConfigureConsumers(BusRegistrationContext); + } + } + + + public class Common_ConsumeContext_Outbox_Solo : + InMemoryContainerTestFixture + { + Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; + Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; + Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; + + [Test] + public async Task Should_provide_the_outbox_to_the_consumer() + { + Task>> fault = await ConnectPublishHandler>(); + + await InputQueueSendEndpoint.Send(new PingMessage()); + + var consumeContext = await ConsumeContext; + + Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext _), "Is ConsumerConsumeContext"); + + var publishEndpoint = await PublishEndpoint; + var sendEndpointProvider = await SendEndpointProvider; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); + Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); + }); + + await fault; + + Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); + } + + protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) + { + harness.TestTimeout = TimeSpan.FromSeconds(3); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + TaskCompletionSource pingTask = GetTask(); + TaskCompletionSource sendEndpointProviderTask = GetTask(); + TaskCompletionSource publishEndpointTask = GetTask(); + + return collection + .AddSingleton(pingTask) + .AddSingleton(sendEndpointProviderTask) + .AddSingleton(publishEndpointTask) + .AddScoped() + .AddScoped(); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.UseInMemoryOutbox(BusRegistrationContext); + + configurator.ConfigureConsumers(BusRegistrationContext); + } + } + + + public class Common_ConsumeContext_Outbox_Solo_Without_Registration_Context : + InMemoryContainerTestFixture + { + Task ConsumeContext => ServiceProvider.GetRequiredService>().Task; + Task PublishEndpoint => ServiceProvider.GetRequiredService>().Task; + Task SendEndpointProvider => ServiceProvider.GetRequiredService>().Task; + + [Test] + public async Task Should_provide_the_outbox_to_the_consumer() + { + Task>> fault = await ConnectPublishHandler>(); + + await InputQueueSendEndpoint.Send(new PingMessage()); + + var consumeContext = await ConsumeContext; + + Assert.That(consumeContext.TryGetPayload(out InMemoryOutboxConsumeContext _), "Is ConsumerConsumeContext"); + + var publishEndpoint = await PublishEndpoint; + var sendEndpointProvider = await SendEndpointProvider; + + Assert.Multiple(() => + { + Assert.That(ReferenceEquals(publishEndpoint, sendEndpointProvider), "ReferenceEquals(publishEndpoint, sendEndpointProvider)"); + Assert.That(ReferenceEquals(consumeContext, sendEndpointProvider), "ReferenceEquals(outboxConsumeContext, sendEndpointProvider)"); + }); + + await fault; + + Assert.That(InMemoryTestHarness.Published.Select().Any(), Is.False, "Outbox Did Not Intercept!"); + } + + protected override void ConfigureInMemoryTestHarness(InMemoryTestHarness harness) + { + harness.TestTimeout = TimeSpan.FromSeconds(3); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + TaskCompletionSource pingTask = GetTask(); + TaskCompletionSource sendEndpointProviderTask = GetTask(); + TaskCompletionSource publishEndpointTask = GetTask(); + + return collection + .AddSingleton(pingTask) + .AddSingleton(sendEndpointProviderTask) + .AddSingleton(publishEndpointTask) + .AddScoped() + .AddScoped(); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.UseInMemoryOutbox(); + + configurator.ConfigureConsumers(BusRegistrationContext); + } + } + + + namespace ConsumeContextTestSubjects + { + using TestFramework.Messages; + + + class DependentConsumer : + IConsumer + { + readonly IAnotherService _anotherService; + readonly IService _service; + + public DependentConsumer(IService service, IAnotherService anotherService) + { + _service = service; + _anotherService = anotherService; + } + + public async Task Consume(ConsumeContext context) + { + await _service.DoIt(); + + _anotherService.Done(); + + throw new IntentionalTestException(); + } + } + + + class DependentBatchConsumer : + IConsumer> + { + readonly IService _service; + readonly UnitOfWork _unitOfWork; + + public DependentBatchConsumer(IService service, UnitOfWork unitOfWork) + { + _service = service; + _unitOfWork = unitOfWork; + } + + public async Task Consume(ConsumeContext> context) + { + await _service.DoIt(); + + _unitOfWork.Add(); + + throw new IntentionalTestException(); + } + } + + + class FlyingSoloConsumer : + IConsumer + { + readonly ConsumeContext _consumeContext; + readonly TaskCompletionSource _consumeContextTask; + readonly IPublishEndpoint _publishEndpoint; + + public FlyingSoloConsumer(IPublishEndpoint publishEndpoint, ISendEndpointProvider sendEndpointProvider, ConsumeContext consumeContext, + TaskCompletionSource consumeContextTask, + TaskCompletionSource publishEndpointTask, + TaskCompletionSource sendEndpointProviderTask) + { + _publishEndpoint = publishEndpoint; + _consumeContext = consumeContext; + _consumeContextTask = consumeContextTask; + publishEndpointTask.TrySetResult(publishEndpoint); + sendEndpointProviderTask.TrySetResult(sendEndpointProvider); + } + + public async Task Consume(ConsumeContext context) + { + await _publishEndpoint.Publish(new { }); + + _consumeContextTask.TrySetResult(_consumeContext); + + throw new IntentionalTestException(); + } + } + + + public interface ServiceDidIt + { + } + + + interface IService + { + Task DoIt(); + } + + + class Service : + IService + { + readonly IPublishEndpoint _publishEndpoint; + + public Service(IPublishEndpoint publishEndpoint, ISendEndpointProvider sendEndpointProvider, + TaskCompletionSource publishEndpointTask, + TaskCompletionSource sendEndpointProviderTask) + { + _publishEndpoint = publishEndpoint; + publishEndpointTask.TrySetResult(publishEndpoint); + sendEndpointProviderTask.TrySetResult(sendEndpointProvider); + } + + public async Task DoIt() + { + await _publishEndpoint.Publish(new { }); + } + } + + + interface IAnotherService + { + void Done(); + } + + + class AnotherService : + IAnotherService + { + readonly TaskCompletionSource _consumeContextTask; + readonly ConsumeContext _context; + + public AnotherService(ConsumeContext context, TaskCompletionSource consumeContextTask) + { + _context = context; + _consumeContextTask = consumeContextTask; + } + + public void Done() + { + _consumeContextTask.TrySetResult(_context); + } + } + + + public class UnitOfWork + { + readonly TaskCompletionSource _consumeContextTask; + readonly ConsumeContext _context; + + public UnitOfWork(ConsumeContext context, TaskCompletionSource consumeContextTask) + { + _context = context; + _consumeContextTask = consumeContextTask; + } + + public void Add() + { + _consumeContextTask.TrySetResult(_context); + } + } + } + + + namespace UnitOfWorkComponents + { + using MassTransit.Configuration; + + + public class UnitOfWorkFilter : + IFilter> + where TMessage : class + { + public void Probe(ProbeContext context) + { + context.CreateFilterScope("uow"); + } + + public async Task Send(ConsumeContext context, IPipe> next) + { + var provider = context.GetPayload(); + var unitOfWork = provider.GetRequiredService(); + + await next.Send(context); + } + } + + + public class UnitOfWorkFilter : + IFilter + where TConsumer : class + where TContext : class, ConsumerConsumeContext + { + public void Probe(ProbeContext context) + { + context.CreateFilterScope("uow"); + } + + public async Task Send(TContext context, IPipe next) + { + var provider = context.GetPayload(); + var unitOfWork = provider.GetRequiredService(); + + await next.Send(context); + } + } + + + public class UnitOfWorkConfigurationObserver : + IConsumerConfigurationObserver + { + public void ConsumerConfigured(IConsumerConfigurator configurator) + where TConsumer : class + { + var filter = new UnitOfWorkFilter, TConsumer>(); + var specification = new FilterPipeSpecification>(filter); + configurator.AddPipeSpecification(specification); + } + + public void ConsumerMessageConfigured(IConsumerMessageConfigurator configurator) + where TConsumer : class + where TMessage : class + { + } + } + + + public static class UnitOfWorkMiddlewareConfiguratorExtensions + { + public static void UseUnitOfWork(this IConsumePipeConfigurator configurator) + { + configurator.ConnectConsumerConfigurationObserver(new UnitOfWorkConfigurationObserver()); + } + } + } +} diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Consumer.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Consumer.cs similarity index 94% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_Consumer.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Consumer.cs index 1a51378e39b..a48e29799ae 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Consumer.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Consumer.cs @@ -1,16 +1,15 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; - using Configuration; using Internals; + using MassTransit.Configuration; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Scenarios; - using Shouldly; using TestFramework; using TestFramework.Messages; - using Testing; public class Common_Consumer : @@ -24,18 +23,18 @@ public async Task Should_receive_using_the_first_consumer() await InputQueueSendEndpoint.Send(new SimpleMessageClass(name)); var lastConsumer = await SimpleConsumer.LastConsumer; - lastConsumer.ShouldNotBe(null); + Assert.That(lastConsumer, Is.Not.Null); var last = await lastConsumer.Last; - last.Name - .ShouldBe(name); + Assert.That(last.Name, Is.EqualTo(name)); var wasDisposed = await lastConsumer.Dependency.WasDisposed; - wasDisposed - .ShouldBe(true); //Dependency was not disposed"); + Assert.Multiple(() => + { + Assert.That(wasDisposed, Is.True, "Dependency was not disposed"); - lastConsumer.Dependency.SomethingDone - .ShouldBe(true); //Dependency was disposed before consumer executed"); + Assert.That(lastConsumer.Dependency.SomethingDone, Is.True, "Dependency was disposed before consumer executed"); + }); } protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) @@ -57,6 +56,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } } + public class Common_Consumer_Service_Scope : InMemoryContainerTestFixture { @@ -68,21 +68,21 @@ public async Task Should_receive_using_the_first_consumer() await InputQueueSendEndpoint.Send(new SimpleMessageClass(name)); var lastConsumer = await SimpleConsumer.LastConsumer.OrCanceled(InMemoryTestHarness.TestCancellationToken); - lastConsumer.ShouldNotBe(null); + Assert.That(lastConsumer, Is.Not.Null); var last = await lastConsumer.Last; - last.Name - .ShouldBe(name); + Assert.That(last.Name, Is.EqualTo(name)); var wasDisposed = await lastConsumer.Dependency.WasDisposed; - wasDisposed - .ShouldBe(true); //Dependency was not disposed"); + Assert.Multiple(() => + { + Assert.That(wasDisposed, Is.True, "Dependency was not disposed"); - lastConsumer.Dependency.SomethingDone - .ShouldBe(true); //Dependency was disposed before consumer executed"); + Assert.That(lastConsumer.Dependency.SomethingDone, Is.True, "Dependency was disposed before consumer executed"); + }); var lasterConsumer = await SimplerConsumer.LastConsumer.OrCanceled(InMemoryTestHarness.TestCancellationToken); - lasterConsumer.ShouldNotBe(null); + Assert.That(lasterConsumer, Is.Not.Null); var laster = await lasterConsumer.Last.OrCanceled(InMemoryTestHarness.TestCancellationToken); } @@ -122,18 +122,18 @@ public async Task Should_receive_using_the_first_consumer() await InputQueueSendEndpoint.Send(new SimpleMessageClass(name)); var lastConsumer = await SimpleConsumer.LastConsumer; - lastConsumer.ShouldNotBe(null); + Assert.That(lastConsumer, Is.Not.Null); var last = await lastConsumer.Last; - last.Name - .ShouldBe(name); + Assert.That(last.Name, Is.EqualTo(name)); var wasDisposed = await lastConsumer.Dependency.WasDisposed; - wasDisposed - .ShouldBe(true); //Dependency was not disposed"); + Assert.Multiple(() => + { + Assert.That(wasDisposed, Is.True, "Dependency was not disposed"); - lastConsumer.Dependency.SomethingDone - .ShouldBe(true); //Dependency was disposed before consumer executed"); + Assert.That(lastConsumer.Dependency.SomethingDone, Is.True, "Dependency was disposed before consumer executed"); + }); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -173,7 +173,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin { configurator.UseMessageRetry(r => r.Immediate(5)); configurator.UseMessageScope(ServiceProvider); - configurator.UseInMemoryOutbox(); + configurator.UseInMemoryOutbox(BusRegistrationContext); configurator.ConfigureConsumer(BusRegistrationContext); } @@ -241,20 +241,20 @@ public Task Consume(ConsumeContext context) public class Common_Consume_Filter : InMemoryContainerTestFixture { + protected readonly TaskCompletionSource TaskCompletionSource; + + public Common_Consume_Filter() + { + TaskCompletionSource = GetTask(); + } + [Test] public async Task Should_use_scope() { await InputQueueSendEndpoint.Send(new { Name = "test" }); var result = await TaskCompletionSource.Task; - Assert.IsNotNull(result); - } - - protected readonly TaskCompletionSource TaskCompletionSource; - - public Common_Consume_Filter() - { - TaskCompletionSource = GetTask(); + Assert.That(result, Is.Not.Null); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -308,6 +308,17 @@ public void Probe(ProbeContext context) public class Common_Consume_FilterScope : InMemoryContainerTestFixture { + protected readonly TaskCompletionSource> EasyASource; + protected readonly TaskCompletionSource> EasyBSource; + protected readonly TaskCompletionSource ScopedContextSource; + + public Common_Consume_FilterScope() + { + ScopedContextSource = GetTask(); + EasyASource = GetTask>(); + EasyBSource = GetTask>(); + } + [Test] public async Task Should_use_the_same_scope_for_consume_and_send() { @@ -327,17 +338,6 @@ public async Task Should_use_the_same_scope_for_consume_and_send() Assert.ThrowsAsync(async () => await context.ConsumeContextEasyB.Task.OrTimeout(s: 2)); } - protected readonly TaskCompletionSource> EasyASource; - protected readonly TaskCompletionSource> EasyBSource; - protected readonly TaskCompletionSource ScopedContextSource; - - public Common_Consume_FilterScope() - { - ScopedContextSource = GetTask(); - EasyASource = GetTask>(); - EasyBSource = GetTask>(); - } - protected override IServiceCollection ConfigureServices(IServiceCollection collection) { return collection.AddSingleton(EasyASource) @@ -487,7 +487,7 @@ public async Task Should_receive_on_the_custom_endpoint() await sendEndpoint.Send(new SimpleMessageClass(name)); var lastConsumer = await SimplerConsumer.LastConsumer.OrCanceled(InMemoryTestHarness.InactivityToken); - lastConsumer.ShouldNotBe(null); + Assert.That(lastConsumer, Is.Not.Null); var last = await lastConsumer.Last.OrCanceled(InMemoryTestHarness.InactivityToken); } @@ -635,6 +635,15 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con public class Common_Consumer_Connect : InMemoryContainerTestFixture { + protected readonly TaskCompletionSource> MessageCompletion; + + public Common_Consumer_Connect() + { + MessageCompletion = GetTask>(); + } + + IReceiveEndpointConnector Connector => ServiceProvider.GetRequiredService(); + [Test] public async Task Should_consume_on_connected_receive_endpoint() { @@ -650,15 +659,6 @@ public async Task Should_consume_on_connected_receive_endpoint() await MessageCompletion.Task; } - protected readonly TaskCompletionSource> MessageCompletion; - - public Common_Consumer_Connect() - { - MessageCompletion = GetTask>(); - } - - IReceiveEndpointConnector Connector => ServiceProvider.GetRequiredService(); - protected override IServiceCollection ConfigureServices(IServiceCollection collection) { return collection.AddSingleton(MessageCompletion); @@ -674,6 +674,17 @@ protected override void ConfigureMassTransit(IBusRegistrationConfigurator config public class Common_Consumer_FilterOrder : InMemoryContainerTestFixture { + readonly TaskCompletionSource> _consumerCompletion; + readonly TaskCompletionSource> _consumerMessageCompletion; + readonly TaskCompletionSource> _messageCompletion; + + public Common_Consumer_FilterOrder() + { + _messageCompletion = GetTask>(); + _consumerCompletion = GetTask>(); + _consumerMessageCompletion = GetTask>(); + } + [Test] public async Task Should_include_container_scope() { @@ -686,17 +697,6 @@ public async Task Should_include_container_scope() await _consumerMessageCompletion.Task; } - readonly TaskCompletionSource> _consumerCompletion; - readonly TaskCompletionSource> _consumerMessageCompletion; - readonly TaskCompletionSource> _messageCompletion; - - public Common_Consumer_FilterOrder() - { - _messageCompletion = GetTask>(); - _consumerCompletion = GetTask>(); - _consumerMessageCompletion = GetTask>(); - } - IFilter> CreateConsumerMessageFilter() { return ServiceProvider.GetRequiredService>>(); @@ -826,6 +826,17 @@ public void Probe(ProbeContext context) public class Common_Consumer_ScopedFilterOrder : InMemoryContainerTestFixture { + readonly TaskCompletionSource> _consumerCompletion; + readonly TaskCompletionSource> _consumerMessageCompletion; + protected readonly TaskCompletionSource> MessageCompletion; + + public Common_Consumer_ScopedFilterOrder() + { + MessageCompletion = GetTask>(); + _consumerCompletion = GetTask>(); + _consumerMessageCompletion = GetTask>(); + } + [Test] public async Task Should_include_container_scope() { @@ -835,21 +846,10 @@ public async Task Should_include_container_scope() var scope = consumerContext.GetPayload(); ConsumerConsumeContext consumerMessageContext = await _consumerMessageCompletion.Task; - Assert.AreEqual(scope, consumerMessageContext.GetPayload()); + Assert.That(consumerMessageContext.GetPayload(), Is.EqualTo(scope)); ConsumeContext messageContext = await MessageCompletion.Task; - Assert.AreEqual(scope, messageContext.GetPayload()); - } - - readonly TaskCompletionSource> _consumerCompletion; - readonly TaskCompletionSource> _consumerMessageCompletion; - protected readonly TaskCompletionSource> MessageCompletion; - - public Common_Consumer_ScopedFilterOrder() - { - MessageCompletion = GetTask>(); - _consumerCompletion = GetTask>(); - _consumerMessageCompletion = GetTask>(); + Assert.That(messageContext.GetPayload(), Is.EqualTo(scope)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Courier.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Courier.cs similarity index 77% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_Courier.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Courier.cs index 1e97ad822b8..372bf2c4ec5 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Courier.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Courier.cs @@ -1,8 +1,8 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; - using Courier.Contracts; + using MassTransit.Courier.Contracts; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using TestFramework; @@ -12,6 +12,11 @@ namespace MassTransit.Containers.Tests.Common_Tests public class Courier_ExecuteActivity : InMemoryContainerTestFixture { + Task> _activityCompleted; + Task> _completed; + Uri _executeAddress; + Guid _trackingNumber; + [Test] public async Task Should_register_and_execute_the_activity() { @@ -34,11 +39,6 @@ public async Task Should_register_and_execute_the_activity() await _activityCompleted; } - Task> _activityCompleted; - Task> _completed; - Uri _executeAddress; - Guid _trackingNumber; - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) { configurator.AddExecuteActivity(); @@ -59,6 +59,10 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con public class Courier_ExecuteActivity_Endpoint : InMemoryContainerTestFixture { + Task> _activityCompleted; + Task> _completed; + Guid _trackingNumber; + [Test] public async Task Should_register_and_execute_the_activity() { @@ -81,10 +85,6 @@ public async Task Should_register_and_execute_the_activity() await _activityCompleted; } - Task> _activityCompleted; - Task> _completed; - Guid _trackingNumber; - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) { configurator.AddExecuteActivity() @@ -96,6 +96,11 @@ protected override void ConfigureMassTransit(IBusRegistrationConfigurator config public class Courier_Activity : InMemoryContainerTestFixture { + Task> _activityCompleted; + Task> _completed; + Uri _executeAddress; + Guid _trackingNumber; + [Test] public async Task Should_register_and_execute_the_activity() { @@ -114,11 +119,54 @@ public async Task Should_register_and_execute_the_activity() await _activityCompleted; } - Task> _activityCompleted; - Task> _completed; + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddActivity(); + } + + protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) + { + configurator.ReceiveEndpoint("execute_testactivity", endpointConfigurator => + { + configurator.ReceiveEndpoint("compensate_testactivity", compensateConfigurator => + { + endpointConfigurator.ConfigureActivity(compensateConfigurator, BusRegistrationContext, typeof(TestActivity)); + }); + + _executeAddress = endpointConfigurator.InputAddress; + }); + } + } + + + public class Courier_Activity_Custom_Subscription : + InMemoryContainerTestFixture + { + Task> _completed; Uri _executeAddress; Guid _trackingNumber; + [Test] + public async Task Should_register_and_execute_the_activity() + { + _completed = SubscribeHandler(); + + _trackingNumber = NewId.NextGuid(); + var builder = new RoutingSlipBuilder(_trackingNumber); + await builder.AddSubscription(Bus.Address, RoutingSlipEvents.Completed, RoutingSlipEventContents.All, async (x) => + { + await x.Send(new { Value = "Secret Value" }); + }); + + builder.AddActivity("TestActivity", _executeAddress, new { Value = "Hello" }); + + await Bus.Execute(builder.Build()); + + ConsumeContext completed = await _completed; + + Assert.That(completed.Message.Value, Is.EqualTo("Secret Value")); + } + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) { configurator.AddActivity(); @@ -139,37 +187,52 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con } + public interface RegistrationCompleted : + RoutingSlipCompleted + { + string Value { get; } + } + + public class Common_Activity_Filter : InMemoryContainerTestFixture { + readonly TaskCompletionSource<(TestActivity, MyId)> _activityTaskCompletionSource; + readonly TaskCompletionSource _executeTaskCompletionSource; + Uri _executeAddress; + + public Common_Activity_Filter() + { + _activityTaskCompletionSource = GetTask<(TestActivity, MyId)>(); + _executeTaskCompletionSource = GetTask(); + } + [Test] public async Task Should_use_scope() { + var completed = SubscribeHandler(); + var trackingNumber = NewId.NextGuid(); var builder = new RoutingSlipBuilder(trackingNumber); builder.AddSubscription(Bus.Address, RoutingSlipEvents.All); builder.AddActivity("TestActivity", _executeAddress, new { Value = "Hello" }); - await Bus.Execute(builder.Build()); + await using var scope = ServiceProvider.CreateAsyncScope(); + var executor = scope.ServiceProvider.GetRequiredService(); + + await executor.Execute(builder.Build(), InMemoryTestHarness.CancellationToken); var result = await _executeTaskCompletionSource.Task; - Assert.IsNotNull(result); + Assert.That(result, Is.Not.Null); var activityResult = await _activityTaskCompletionSource.Task; - Assert.IsNotNull(activityResult); Assert.That(result, Is.EqualTo(activityResult.Item2)); - } - readonly TaskCompletionSource<(TestActivity, MyId)> _activityTaskCompletionSource; - readonly TaskCompletionSource _executeTaskCompletionSource; - Uri _executeAddress; + ConsumeContext completedContext = await completed; - public Common_Activity_Filter() - { - _activityTaskCompletionSource = GetTask<(TestActivity, MyId)>(); - _executeTaskCompletionSource = GetTask(); + Assert.That(completedContext.GetVariable("HeaderValue", ""), Is.EqualTo("Bingo!")); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -185,13 +248,11 @@ protected override void ConfigureMassTransit(IBusRegistrationConfigurator config configurator.AddActivity(); } - protected void ConfigureRegistration(IBusRegistrationConfigurator configurator) - { - configurator.AddActivity(); - } - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { + configurator.UseSendFilter(typeof(TestSendScopedFilter<>), BusRegistrationContext); + configurator.UseExecuteActivityFilter(typeof(TestActivityScopedFilter<>), BusRegistrationContext); + configurator.ReceiveEndpoint("execute_testactivity", endpointConfigurator => { configurator.ReceiveEndpoint("compensate_testactivity", compensateConfigurator => @@ -201,8 +262,6 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con _executeAddress = endpointConfigurator.InputAddress; }); - - configurator.UseExecuteActivityFilter(typeof(TestActivityScopedFilter<>), BusRegistrationContext); } @@ -228,7 +287,8 @@ public async Task Execute(ExecuteContext context return context.CompletedWithVariables(new { OriginalValue = context.Arguments.Value }, new { Value = "Hello, World!", - NullValue = (string)null + NullValue = (string)null, + HeaderValue = context.Headers.Get("ScopedHeader", "") }); } @@ -267,9 +327,30 @@ public void Probe(ProbeContext context) } + public class TestSendScopedFilter : + IFilter> + where T : class + { + public Task Send(SendContext context, IPipe> next) + { + context.Headers.Set("ScopedHeader", "Bingo!"); + + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + } + } + + public class Courier_Activity_Endpoint : InMemoryContainerTestFixture { + Task> _activityCompleted; + Task> _completed; + Guid _trackingNumber; + [Test] public async Task Should_register_and_execute_the_activity() { @@ -288,10 +369,6 @@ public async Task Should_register_and_execute_the_activity() await _activityCompleted; } - Task> _activityCompleted; - Task> _completed; - Guid _trackingNumber; - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) { configurator.AddActivity() diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Discovery.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Discovery.cs similarity index 92% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_Discovery.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Discovery.cs index 8492d227334..8f9dda8987e 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Discovery.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Discovery.cs @@ -1,11 +1,11 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; - using Courier.Contracts; using Discovery; + using MassTransit.Courier.Contracts; using NUnit.Framework; using TestFramework; using TestFramework.Messages; @@ -95,7 +95,8 @@ public Task Consume(ConsumeContext context) public class PingSagaDefinition : SagaDefinition { - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) { var partition = endpointConfigurator.CreatePartitioner(Environment.ProcessorCount); @@ -143,7 +144,8 @@ public PingStateMachineDefinition() EndpointName = "discovery-ping-state"; } - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator) + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) { var partition = endpointConfigurator.CreatePartitioner(Environment.ProcessorCount); @@ -164,7 +166,8 @@ public PingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -243,6 +246,8 @@ public bool WasConfigured(string name) public class Common_Discovery : InMemoryContainerTestFixture { + ReceiveEndpointConfigurationObserver _endpointObserver; + [Test] public async Task Should_complete_the_routing_slip() { @@ -262,11 +267,14 @@ public async Task Should_complete_the_routing_slip() [Test] public void Should_have_properly_configured_every_endpoint() { - Assert.That(_endpointObserver.WasConfigured("ping-queue")); - Assert.That(_endpointObserver.WasConfigured("discovery-ping-state")); - Assert.That(_endpointObserver.WasConfigured("Ping_execute")); - Assert.That(_endpointObserver.WasConfigured("Ping_compensate")); - Assert.That(_endpointObserver.WasConfigured("DiscoveryPing")); + Assert.Multiple(() => + { + Assert.That(_endpointObserver.WasConfigured("ping-queue")); + Assert.That(_endpointObserver.WasConfigured("discovery-ping-state")); + Assert.That(_endpointObserver.WasConfigured("Ping_execute")); + Assert.That(_endpointObserver.WasConfigured("Ping_compensate")); + Assert.That(_endpointObserver.WasConfigured("DiscoveryPing")); + }); // TODO, verify but the harness configures them anyway so // Assert.That(_endpointObserver.WasConfigured("DiscoveryPong"), Is.False); @@ -293,8 +301,6 @@ public async Task Should_receive_the_response_from_the_consumer() Assert.That(pingCompleted.Message.CorrelationId, Is.EqualTo(pingMessage.CorrelationId)); } - ReceiveEndpointConfigurationObserver _endpointObserver; - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) { configurator.SetInMemorySagaRepositoryProvider(); diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_JobConsumer.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_JobConsumer.cs similarity index 96% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_JobConsumer.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_JobConsumer.cs index 1d725ca070d..39a3f0be41b 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_JobConsumer.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_JobConsumer.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; @@ -44,9 +44,6 @@ protected override void ConfigureMassTransit(IBusRegistrationConfigurator config namespace JobConsumerContracts { - using System; - - public interface CrunchTheNumbers { TimeSpan Duration { get; } diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Mediator.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Mediator.cs similarity index 95% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_Mediator.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Mediator.cs index f1028b7eb86..62eed5f9595 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Mediator.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Mediator.cs @@ -1,20 +1,21 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; using Internals; + using MassTransit.Testing; using Mediator; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Scenarios; - using Shouldly; using TestFramework; - using Testing; public class Using_mediator_alongside_the_bus : InMemoryContainerTestFixture { + IMediator Mediator => ServiceProvider.GetRequiredService(); + [Test] public async Task Should_dispatch_to_the_consumer() { @@ -23,13 +24,11 @@ public async Task Should_dispatch_to_the_consumer() await Mediator.Send(new SimpleMessageClass(name)); var lastConsumer = await SimplerConsumer.LastConsumer.OrCanceled(InMemoryTestHarness.TestCancellationToken); - lastConsumer.ShouldNotBe(null); + Assert.That(lastConsumer, Is.Not.Null); await lastConsumer.Last.OrCanceled(InMemoryTestHarness.TestCancellationToken); } - IMediator Mediator => ServiceProvider.GetRequiredService(); - protected override IServiceCollection ConfigureServices(IServiceCollection collection) { return collection.AddMediator(ConfigureRegistration); @@ -45,6 +44,8 @@ void ConfigureRegistration(IMediatorRegistrationConfigurator configurator) public class Publishing_a_message_from_a_mediator_consumer : InMemoryContainerTestFixture { + IMediator Mediator => ServiceProvider.GetRequiredService(); + [Test] public async Task Should_not_transfer_message_headers() { @@ -62,8 +63,6 @@ await Mediator.Send(new Assert.That(submitted.InitiatorId.HasValue, Is.False); } - IMediator Mediator => ServiceProvider.GetRequiredService(); - protected override IServiceCollection ConfigureServices(IServiceCollection collection) { return collection.AddMediator(ConfigureRegistration); @@ -110,6 +109,8 @@ public interface OrderSubmitted public class Common_Mediator_Request : InMemoryContainerTestFixture { + Guid _correlationId; + [Test] public async Task Should_receive_the_response() { @@ -121,14 +122,15 @@ public async Task Should_receive_the_response() Value = "World" }); - Assert.That(response.Message.Value, Is.EqualTo("Hello, World")); - Assert.That(response.ConversationId.Value, Is.EqualTo(response.Message.OriginalConversationId)); - Assert.That(response.InitiatorId.Value, Is.EqualTo(_correlationId)); - Assert.That(response.Message.OriginalInitiatorId, Is.EqualTo(_correlationId)); + Assert.Multiple(() => + { + Assert.That(response.Message.Value, Is.EqualTo("Hello, World")); + Assert.That(response.ConversationId.Value, Is.EqualTo(response.Message.OriginalConversationId)); + Assert.That(response.InitiatorId.Value, Is.EqualTo(_correlationId)); + Assert.That(response.Message.OriginalInitiatorId, Is.EqualTo(_correlationId)); + }); } - Guid _correlationId; - protected override IServiceCollection ConfigureServices(IServiceCollection collection) { return collection.AddMediator(ConfigureRegistration); @@ -281,6 +283,10 @@ public class Pong public class Common_Mediator_Saga : InMemoryContainerTestFixture { + Guid _correlationId; + + IMediator Mediator => ServiceProvider.GetRequiredService(); + [Test] public async Task Should_receive_the_response() { @@ -292,15 +298,11 @@ await Mediator.Send(new OrderNumber = "90210" }); - Guid? foundId = await GetSagaRepository().ShouldContainSaga(_correlationId, TestTimeout); + Guid? foundId = await GetLoadSagaRepository().ShouldContainSaga(_correlationId, TestTimeout); Assert.That(foundId.HasValue, Is.True); } - Guid _correlationId; - - IMediator Mediator => ServiceProvider.GetRequiredService(); - protected override IServiceCollection ConfigureServices(IServiceCollection collection) { return collection.AddMediator(ConfigureRegistration); @@ -359,6 +361,19 @@ public interface OrderSubmitted : public class Common_Mediator_FilterScope : InMemoryContainerTestFixture { + readonly TaskCompletionSource> _easyASource; + readonly TaskCompletionSource> _easyBSource; + readonly TaskCompletionSource _scopedContextSource; + + public Common_Mediator_FilterScope() + { + _scopedContextSource = GetTask(); + _easyASource = GetTask>(); + _easyBSource = GetTask>(); + } + + IMediator Mediator => ServiceProvider.GetRequiredService(); + [Test] public async Task Should_use_the_same_scope_for_consume_and_send() { @@ -378,19 +393,6 @@ public async Task Should_use_the_same_scope_for_consume_and_send() Assert.ThrowsAsync(async () => await context.ConsumeContextEasyB.Task.OrTimeout(100)); } - readonly TaskCompletionSource> _easyASource; - readonly TaskCompletionSource> _easyBSource; - readonly TaskCompletionSource _scopedContextSource; - - public Common_Mediator_FilterScope() - { - _scopedContextSource = GetTask(); - _easyASource = GetTask>(); - _easyBSource = GetTask>(); - } - - IMediator Mediator => ServiceProvider.GetRequiredService(); - protected override IServiceCollection ConfigureServices(IServiceCollection collection) { return collection diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_MissingDependency.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_MissingDependency.cs similarity index 95% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_MissingDependency.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_MissingDependency.cs index d74d12c2ebf..314a5bcd402 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_MissingDependency.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_MissingDependency.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System.Threading.Tasks; using NUnit.Framework; diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Registration.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Registration.cs similarity index 99% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_Registration.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Registration.cs index d26ed969042..00f45718679 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Registration.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Registration.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; diff --git a/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Saga.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Saga.cs new file mode 100644 index 00000000000..f7d5755e8f1 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Saga.cs @@ -0,0 +1,131 @@ +namespace MassTransit.Tests.ContainerTests.Common_Tests +{ + using System; + using System.Threading.Tasks; + using MassTransit.Testing; + using NUnit.Framework; + using Scenarios; + using TestFramework; + + + public class Common_Saga : + InMemoryContainerTestFixture + { + [Test] + public async Task Should_handle_first_message() + { + var sagaId = NewId.NextGuid(); + + var message = new FirstSagaMessage { CorrelationId = sagaId }; + + await InputQueueSendEndpoint.Send(message); + + Guid? foundId = await GetLoadSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); + + Assert.That(foundId.HasValue, Is.True); + } + + [Test] + public async Task Should_handle_second_message() + { + var sagaId = NewId.NextGuid(); + + var message = new FirstSagaMessage { CorrelationId = sagaId }; + + await InputQueueSendEndpoint.Send(message); + + Guid? foundId = await GetLoadSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); + + Assert.That(foundId.HasValue, Is.True); + + var nextMessage = new SecondSagaMessage { CorrelationId = sagaId }; + + await InputQueueSendEndpoint.Send(nextMessage); + + foundId = await GetQuerySagaRepository().ShouldContainSaga(x => x.CorrelationId == sagaId && x.Second.IsCompleted, TestTimeout); + + Assert.That(foundId.HasValue, Is.True); + } + + [Test] + public async Task Should_handle_third_message() + { + var sagaId = NewId.NextGuid(); + + var message = new FirstSagaMessage { CorrelationId = sagaId }; + + await InputQueueSendEndpoint.Send(message); + + Guid? foundId = await GetLoadSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); + + Assert.That(foundId.HasValue, Is.True); + + var nextMessage = new ThirdSagaMessage { CorrelationId = sagaId }; + + await InputQueueSendEndpoint.Send(nextMessage); + + foundId = await GetQuerySagaRepository().ShouldContainSaga(x => x.CorrelationId == sagaId && x.Third.IsCompleted, TestTimeout); + + Assert.That(foundId.HasValue, Is.True); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddSaga() + .InMemoryRepository(); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.ConfigureSaga(BusRegistrationContext); + } + } + + + public class Common_Saga_Endpoint : + InMemoryContainerTestFixture + { + [Test] + public async Task Should_handle_the_message() + { + var sagaId = NewId.NextGuid(); + + var message = new FirstSagaMessage { CorrelationId = sagaId }; + + var sendEndpoint = await Bus.GetSendEndpoint(new Uri("loopback://localhost/custom-endpoint-name")); + + await sendEndpoint.Send(message); + + Guid? foundId = await GetLoadSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); + + Assert.That(foundId.HasValue, Is.True); + } + + [Test] + public async Task Should_use_custom_endpoint_and_definition_together() + { + var sagaId = NewId.NextGuid(); + + var message = new FirstSagaMessage { CorrelationId = sagaId }; + + var sendEndpoint = await Bus.GetSendEndpoint(new Uri("loopback://localhost/custom-second-saga")); + + await sendEndpoint.Send(message); + + Guid? foundId = await GetLoadSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); + + Assert.That(foundId.HasValue, Is.True); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddSaga() + .Endpoint(e => e.Name = "custom-endpoint-name") + .InMemoryRepository(); + + configurator.AddSaga(typeof(SecondSimpleSagaDefinition)) + .Endpoint(e => e.Temporary = true) + .InMemoryRepository(); + } + } +} diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_SagaStateMachine.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_SagaStateMachine.cs similarity index 98% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_SagaStateMachine.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_SagaStateMachine.cs index 59009b179dd..af77b43d109 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_SagaStateMachine.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_SagaStateMachine.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; @@ -52,6 +52,13 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin public class Common_StateMachine_Filter : InMemoryContainerTestFixture { + readonly TaskCompletionSource _taskCompletionSource; + + public Common_StateMachine_Filter() + { + _taskCompletionSource = GetTask(); + } + [Test] public async Task Should_use_scope() { @@ -62,14 +69,7 @@ await InputQueueSendEndpoint.Send(new StartTest }); var result = await _taskCompletionSource.Task; - Assert.NotNull(result); - } - - readonly TaskCompletionSource _taskCompletionSource; - - public Common_StateMachine_Filter() - { - _taskCompletionSource = GetTask(); + Assert.That(result, Is.Not.Null); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -101,6 +101,17 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin public class Common_StateMachine_FilterOrder : InMemoryContainerTestFixture { + readonly TaskCompletionSource> _messageCompletion; + readonly TaskCompletionSource> _sagaCompletion; + readonly TaskCompletionSource> _sagaMessageCompletion; + + public Common_StateMachine_FilterOrder() + { + _messageCompletion = GetTask>(); + _sagaCompletion = GetTask>(); + _sagaMessageCompletion = GetTask>(); + } + [Test] public async Task Should_include_container_scope() { @@ -117,17 +128,6 @@ await InputQueueSendEndpoint.Send(new StartTest await _sagaMessageCompletion.Task; } - readonly TaskCompletionSource> _messageCompletion; - readonly TaskCompletionSource> _sagaCompletion; - readonly TaskCompletionSource> _sagaMessageCompletion; - - public Common_StateMachine_FilterOrder() - { - _messageCompletion = GetTask>(); - _sagaCompletion = GetTask>(); - _sagaMessageCompletion = GetTask>(); - } - IFilter> CreateSagaMessageFilter() { return ServiceProvider.GetRequiredService>>(); diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Scope.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Scope.cs similarity index 95% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_Scope.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Scope.cs index 7396dfdbbb6..a36f4292c6c 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_Scope.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_Scope.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopePublish.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopePublish.cs similarity index 93% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopePublish.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopePublish.cs index 883265f0a92..fdf84d6d1af 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopePublish.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopePublish.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; @@ -30,9 +30,12 @@ public async Task Should_contains_scope_on_publish() var published = await _taskCompletionSource.Task; - Assert.IsTrue(published.TryGetPayload(out var serviceProvider)); + Assert.Multiple(() => + { + Assert.That(published.TryGetPayload(out var serviceProvider), Is.True); - Assert.AreEqual(serviceProvider, ServiceScope.ServiceProvider); + Assert.That(ServiceScope.ServiceProvider, Is.EqualTo(serviceProvider)); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -66,9 +69,12 @@ public async Task Should_contains_scope_on_publish() var published = await _taskCompletionSource.Task; - Assert.IsTrue(published.TryGetPayload(out var serviceProvider)); + Assert.Multiple(() => + { + Assert.That(published.TryGetPayload(out var serviceProvider), Is.True); - Assert.AreEqual(serviceProvider, ServiceScope.ServiceProvider); + Assert.That(ServiceScope.ServiceProvider, Is.EqualTo(serviceProvider)); + }); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -131,7 +137,7 @@ public async Task Should_use_scope() await PublishEndpoint.Publish(new { Name = "test" }); var result = await TaskCompletionSource.Task; - Assert.AreEqual(MyId, result); + Assert.That(result, Is.EqualTo(MyId)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -179,7 +185,7 @@ public async Task Should_use_scope() await PublishEndpoint.Publish(new { Name = "test" }); var result = await TaskCompletionSource.Task; - Assert.AreEqual(MyId, result); + Assert.That(result, Is.EqualTo(MyId)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -249,7 +255,7 @@ public async Task Should_use_scope() var consumer = await ConsumerSource.Task.OrCanceled(InMemoryTestHarness.InactivityToken); - Assert.AreEqual(myId, consumer.MyId); + Assert.That(consumer.MyId, Is.EqualTo(myId)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -274,7 +280,7 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { configurator.UseMessageScope(ServiceProvider); - configurator.UseInMemoryOutbox(); + configurator.UseInMemoryOutbox(BusRegistrationContext); configurator.ConfigureConsumer(BusRegistrationContext); } @@ -350,8 +356,11 @@ public async Task Should_not_use_scoped_filter_to_publish_fault() await InputQueueSendEndpoint.Send(new { Name = "test" }); ConsumeContext> result = await TaskCompletionSource.Task; - Assert.IsNotNull(result); - Assert.IsFalse(Marker.Called); + Assert.Multiple(() => + { + Assert.That(result, Is.Not.Null); + Assert.That(Marker.Called, Is.False); + }); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopeRequestClient.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopeRequestClient.cs similarity index 87% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopeRequestClient.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopeRequestClient.cs index 363dd4ffc2f..95783dfa3a7 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopeRequestClient.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopeRequestClient.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; @@ -27,9 +27,12 @@ public async Task Should_contains_scope_on_publish() var sent = await _taskCompletionSource.Task; - Assert.IsTrue(sent.TryGetPayload(out var serviceProvider)); + Assert.Multiple(() => + { + Assert.That(sent.TryGetPayload(out var serviceProvider), Is.True); - Assert.AreEqual(serviceProvider, ServiceScope.ServiceProvider); + Assert.That(ServiceScope.ServiceProvider, Is.EqualTo(serviceProvider)); + }); } protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) @@ -68,9 +71,12 @@ public async Task Should_contains_scope_on_publish() var sent = await _taskCompletionSource.Task; - Assert.IsTrue(sent.TryGetPayload(out var serviceProvider)); + Assert.Multiple(() => + { + Assert.That(sent.TryGetPayload(out var serviceProvider), Is.True); - Assert.AreEqual(serviceProvider, ServiceScope.ServiceProvider); + Assert.That(ServiceScope.ServiceProvider, Is.EqualTo(serviceProvider)); + }); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -121,9 +127,12 @@ public async Task Should_contains_scope_on_send() var sent = await _taskCompletionSource.Task; - Assert.IsTrue(sent.TryGetPayload(out var serviceProvider)); + Assert.Multiple(() => + { + Assert.That(sent.TryGetPayload(out var serviceProvider), Is.True); - Assert.AreEqual(serviceProvider, ServiceScope.ServiceProvider); + Assert.That(ServiceScope.ServiceProvider, Is.EqualTo(serviceProvider)); + }); } protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopeSend.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopeSend.cs similarity index 91% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopeSend.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopeSend.cs index d300e2f7770..0b97833baa4 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopeSend.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopeSend.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; @@ -29,9 +29,12 @@ public async Task Should_contains_scope_on_send() var sent = await _taskCompletionSource.Task; - Assert.IsTrue(sent.TryGetPayload(out var scope)); + Assert.Multiple(() => + { + Assert.That(sent.TryGetPayload(out var scope), Is.True); - Assert.AreEqual(scope.ServiceProvider, ServiceScope.ServiceProvider); + Assert.That(scope.ServiceProvider, Is.EqualTo(ServiceScope.ServiceProvider)); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -65,9 +68,12 @@ public async Task Should_contains_scope_on_send() var sent = await _taskCompletionSource.Task; - Assert.IsTrue(sent.TryGetPayload(out var scope)); + Assert.Multiple(() => + { + Assert.That(sent.TryGetPayload(out var scope), Is.True); - Assert.AreEqual(scope.ServiceProvider, ServiceScope.ServiceProvider); + Assert.That(scope.ServiceProvider, Is.EqualTo(ServiceScope.ServiceProvider)); + }); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -131,7 +137,7 @@ public async Task Should_use_scope() await endpoint.Send(new { Name = "test" }); var result = await _taskCompletionSource.Task; - Assert.AreEqual(MyId, result); + Assert.That(result, Is.EqualTo(MyId)); } protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) @@ -179,7 +185,7 @@ public async Task Should_use_scope() await SendEndpoint.Send(new { Name = "test" }); var result = await _taskCompletionSource.Task; - Assert.AreEqual(MyId, result); + Assert.That(result, Is.EqualTo(MyId)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopedMediator.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopedMediator.cs similarity index 95% rename from tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopedMediator.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopedMediator.cs index bfcef95d9ac..88da9c5c841 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/Common_ScopedMediator.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/Common_ScopedMediator.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; @@ -29,7 +29,7 @@ public async Task Should_use_scope() await Mediator.Send(new { Name = "test" }); var result = await _taskCompletionSource.Task; - Assert.AreEqual(MyId, result); + Assert.That(result, Is.EqualTo(MyId)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -71,7 +71,7 @@ public async Task Should_use_scope() await Mediator.Publish(new { Name = "test" }); var result = await _taskCompletionSource.Task; - Assert.AreEqual(MyId, result); + Assert.That(result, Is.EqualTo(MyId)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -116,10 +116,10 @@ public async Task Should_use_scope() await Mediator.Send(new { Name = "test" }); var myOtherId = await _otherTaskCompletionSource.Task; - Assert.AreEqual(MyOtherId, myOtherId); + Assert.That(myOtherId, Is.EqualTo(MyOtherId)); var myId = await _taskCompletionSource.Task; - Assert.AreEqual(MyId, myId); + Assert.That(myId, Is.EqualTo(MyId)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -167,10 +167,10 @@ public async Task Should_use_scope() await Mediator.Publish(new { Name = "test" }); var myOtherId = await _otherTaskCompletionSource.Task; - Assert.AreEqual(MyOtherId, myOtherId); + Assert.That(myOtherId, Is.EqualTo(MyOtherId)); var myId = await _taskCompletionSource.Task; - Assert.AreEqual(MyId, myId); + Assert.That(myId, Is.EqualTo(MyId)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) @@ -214,7 +214,7 @@ public async Task Should_use_scope() await client.GetResponse(new { Name = "test" }); var result = await _taskCompletionSource.Task; - Assert.AreEqual(MyId, result); + Assert.That(result, Is.EqualTo(MyId)); } protected override IServiceCollection ConfigureServices(IServiceCollection collection) diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/MultiBusPublishEndpoint_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/MultiBusPublishEndpoint_Specs.cs similarity index 97% rename from tests/MassTransit.Containers.Tests/Common_Tests/MultiBusPublishEndpoint_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/MultiBusPublishEndpoint_Specs.cs index 0993b46ee3f..31e32953b41 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/MultiBusPublishEndpoint_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/MultiBusPublishEndpoint_Specs.cs @@ -1,11 +1,11 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; using DependencyInjection; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; - using Testing; public class MultiBusPublishEndpoint_Specs diff --git a/tests/MassTransit.Tests/ContainerTests/Common_Tests/MultiBus_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/MultiBus_Specs.cs new file mode 100644 index 00000000000..8769aca82bb --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/MultiBus_Specs.cs @@ -0,0 +1,512 @@ +namespace MassTransit.Tests.ContainerTests.Common_Tests +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Contracts; + using MassTransit.Testing; + using MassTransit.Transports; + using Mediator; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Hosting; + using NUnit.Framework; + using Scenarios; + using TestFramework; + using TestFramework.Messages; + + + public class Using_MultiBus : + InMemoryContainerTestFixture + { + public Using_MultiBus() + { + Task1 = GetTask>(); + Task2 = GetTask>(); + } + + TaskCompletionSource> Task1 { get; } + TaskCompletionSource> Task2 { get; } + + IBusOne One => ServiceProvider.GetService(); + IEnumerable HostedServices => ServiceProvider.GetService>(); + + [Test] + public async Task Should_receive() + { + await One.Publish(new SimpleMessageClass("abc")); + + await Task1.Task; + await Task2.Task; + } + + [Test] + public void Should_resolve_bus_declaration() + { + Assert.Multiple(() => + { + Assert.That(ServiceProvider.GetService(), Is.Not.Null); + Assert.That(ServiceProvider.GetService(), Is.Not.Null); + }); + } + + [Test] + public void Should_resolve_bus_instance() + { + Assert.Multiple(() => + { + Assert.That(ServiceProvider.GetService>(), Is.Not.Null); + Assert.That(ServiceProvider.GetService>(), Is.Not.Null); + }); + } + + [Test] + public async Task Should_support_request_client_on_bus_one() + { + IRequestClient client = GetRequestClient(); + + await client.GetResponse(new OneRequest()); + } + + [Test] + public async Task Should_support_request_client_on_bus_two() + { + IRequestClient client = GetRequestClient(); + + await client.GetResponse(new TwoRequest()); + } + + [Test] + public async Task Should_support_request_client_on_default_bus() + { + IRequestClient client = GetRequestClient(); + + await client.GetResponse(new Request()); + } + + protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + configurator.AddRequestClient(); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.ConfigureConsumer(BusRegistrationContext); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + collection = base.ConfigureServices(collection); + + collection.AddSingleton(Task1); + collection.AddSingleton(Task2); + + collection.AddMassTransit(ConfigureOne); + collection.AddMassTransit(ConfigureTwo); + + return collection; + } + + static void ConfigureOne(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + configurator.AddConsumer(); + configurator.UsingInMemory((context, cfg) => + { + cfg.Host(new Uri("loopback://bus-one/")); + cfg.ConfigureEndpoints(context); + }); + configurator.AddRequestClient(); + } + + static void ConfigureTwo(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + configurator.AddConsumer(); + configurator.UsingInMemory((context, cfg) => + { + cfg.Host(new Uri("loopback://bus-two/")); + cfg.ConfigureEndpoints(context); + }); + configurator.AddRequestClient(); + } + + [OneTimeSetUp] + public async Task Setup() + { + await Task.WhenAll(HostedServices.Select(x => x.StartAsync(InMemoryTestHarness.TestCancellationToken))); + } + + [OneTimeTearDown] + public async Task TearDown() + { + await Task.WhenAll(HostedServices.Select(x => x.StopAsync(InMemoryTestHarness.TestCancellationToken))); + } + + + class Consumer1 : + IConsumer + { + readonly IPublishEndpoint _publishEndpoint; + readonly TaskCompletionSource> _taskCompletionSource; + + public Consumer1(IPublishEndpoint publishEndpointDefault, IBusTwo publishEndpoint, + TaskCompletionSource> taskCompletionSource) + { + _publishEndpoint = publishEndpoint; + _taskCompletionSource = taskCompletionSource; + } + + public async Task Consume(ConsumeContext context) + { + _taskCompletionSource.TrySetResult(context); + await _publishEndpoint.Publish(new PingMessage()); + } + } + + + class Consumer2 : + IConsumer + { + readonly TaskCompletionSource> _taskCompletionSource; + + public Consumer2(IPublishEndpoint publishEndpoint, TaskCompletionSource> taskCompletionSource) + { + _taskCompletionSource = taskCompletionSource; + } + + public async Task Consume(ConsumeContext context) + { + _taskCompletionSource.TrySetResult(context); + } + } + + + class RequestConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return context.RespondAsync(new Response()); + } + } + + + class OneRequestConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return context.RespondAsync(new OneResponse()); + } + } + + + class TwoRequestConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return context.RespondAsync(new TwoResponse()); + } + } + } + + + public class Using_MultiBus_With_ConfigureEndpoint : + InMemoryContainerTestFixture + { + int _busOneConfigured; + int _busTwoConfigured; + int _globalConfigured; + + IEnumerable HostedServices => ServiceProvider.GetService>(); + + [Test] + public async Task Should_configure_endpoints_correctly() + { + Assert.Multiple(() => + { + Assert.That(_busOneConfigured, Is.EqualTo(1)); + Assert.That(_busTwoConfigured, Is.EqualTo(1)); + Assert.That(_globalConfigured, Is.EqualTo(2)); + }); + } + + protected override IServiceCollection ConfigureServices(IServiceCollection collection) + { + collection = base.ConfigureServices(collection); + + collection.AddMassTransit(ConfigureOne); + collection.AddMassTransit(ConfigureTwo); + collection.AddSingleton(new GlobalConfigureReceiveEndpoint(() => Interlocked.Increment(ref _globalConfigured))); + + return collection; + } + + void ConfigureOne(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + configurator.AddConfigureEndpointsCallback((_, _, _) => Interlocked.Increment(ref _busOneConfigured)); + configurator.UsingInMemory((context, cfg) => + { + cfg.Host(new Uri("loopback://bus-one/")); + cfg.ConfigureEndpoints(context); + }); + } + + void ConfigureTwo(IBusRegistrationConfigurator configurator) + { + configurator.AddConsumer(); + configurator.AddConfigureEndpointsCallback((_, _, _) => Interlocked.Increment(ref _busTwoConfigured)); + configurator.UsingInMemory((context, cfg) => + { + cfg.Host(new Uri("loopback://bus-two/")); + cfg.ConfigureEndpoints(context); + }); + } + + [OneTimeSetUp] + public async Task Setup() + { + await Task.WhenAll(HostedServices.Select(x => x.StartAsync(InMemoryTestHarness.TestCancellationToken))); + } + + [OneTimeTearDown] + public async Task TearDown() + { + await Task.WhenAll(HostedServices.Select(x => x.StopAsync(InMemoryTestHarness.TestCancellationToken))); + } + + + class GlobalConfigureReceiveEndpoint : + IConfigureReceiveEndpoint + { + readonly Action _action; + + public GlobalConfigureReceiveEndpoint(Action action) + { + _action = action; + } + + public void Configure(string name, IReceiveEndpointConfigurator configurator) + { + _action(); + } + } + + + class Consumer1 : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + + + class Consumer2 : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + } + + + public class Using_consume_context_correctly_with_components : + InMemoryContainerTestFixture + { + [Test] + public async Task Should_receive_in_bus_through_mediator_and_publish() + { + await using var provider = new ServiceCollection() + .AddMediator(cfg => cfg.AddConsumer()) + .AddMassTransitTestHarness(cfg => cfg.AddConsumer()) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + await harness.Start(); + + var mediator = harness.Scope.ServiceProvider.GetRequiredService(); + await mediator.Send(new Request()); + + Assert.That(await harness.Published.Any(), Is.True); + + IReceivedMessage consumed = await harness.Consumed.SelectAsync().FirstOrDefault(); + Assert.That(consumed, Is.Not.Null); + Assert.That(consumed.Context.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/mediator"))); + } + + [Test] + public async Task Should_receive_in_bus_through_mediator_and_send() + { + await using var provider = new ServiceCollection() + .AddMediator(cfg => cfg.AddConsumer()) + .AddMassTransitTestHarness(cfg => cfg.AddConsumer().Endpoint(x => x.Name = "input-queue")) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + await harness.Start(); + + var mediator = harness.Scope.ServiceProvider.GetRequiredService(); + await mediator.Send(new Request()); + + Assert.That(await harness.Sent.Any(), Is.True); + + IReceivedMessage consumed = await harness.Consumed.SelectAsync().FirstOrDefault(); + Assert.That(consumed, Is.Not.Null); + Assert.That(consumed.Context.SourceAddress, Is.EqualTo(new Uri("loopback://localhost/mediator"))); + } + + [Test] + public async Task Should_receive_in_bus_through_mediator_and_publish_with_different_base_address() + { + var baseAddress = new Uri($"loopback://localhost/{Guid.NewGuid()}"); + await using var provider = new ServiceCollection() + .AddMediator(baseAddress, cfg => cfg.AddConsumer()) + .AddMassTransitTestHarness(cfg => cfg.AddConsumer()) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + await harness.Start(); + + var mediator = harness.Scope.ServiceProvider.GetRequiredService(); + await mediator.Send(new Request()); + + Assert.That(await harness.Published.Any(), Is.True); + + IReceivedMessage consumed = await harness.Consumed.SelectAsync().FirstOrDefault(); + Assert.That(consumed, Is.Not.Null); + Assert.That(consumed.Context.SourceAddress, Is.EqualTo(new Uri($"{baseAddress}/mediator"))); + } + + [Test] + public async Task Should_receive_in_bus_through_mediator_and_send_with_different_base_address() + { + var baseAddress = new Uri($"loopback://localhost/{Guid.NewGuid()}"); + await using var provider = new ServiceCollection() + .AddMediator(baseAddress, cfg => cfg.AddConsumer()) + .AddMassTransitTestHarness(cfg => cfg.AddConsumer().Endpoint(x => x.Name = "input-queue")) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + await harness.Start(); + + var mediator = harness.Scope.ServiceProvider.GetRequiredService(); + await mediator.Send(new Request()); + + Assert.That(await harness.Sent.Any(), Is.True); + + IReceivedMessage consumed = await harness.Consumed.SelectAsync().FirstOrDefault(); + Assert.That(consumed, Is.Not.Null); + Assert.That(consumed.Context.SourceAddress, Is.EqualTo(new Uri($"{baseAddress}/mediator"))); + } + + + class MediatorPublishConsumer : + IConsumer + { + readonly IPublishEndpoint _publishEndpoint; + + public MediatorPublishConsumer(IPublishEndpoint publishEndpoint) + { + _publishEndpoint = publishEndpoint; + } + + public Task Consume(ConsumeContext context) + { + return _publishEndpoint.Publish(new OneRequest()); + } + } + + + class MediatorSendConsumer : + IConsumer + { + readonly ISendEndpointProvider _sendEndpointProvider; + + public MediatorSendConsumer(ISendEndpointProvider sendEndpointProvider) + { + _sendEndpointProvider = sendEndpointProvider; + } + + public async Task Consume(ConsumeContext context) + { + var endpoint = await _sendEndpointProvider.GetSendEndpoint(new Uri("queue:input-queue")); + await endpoint.Send(new OneRequest()); + } + } + + + class BusConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + } + + + namespace Contracts + { + public class Request + { + } + + + public class Response + { + } + + + public class OneRequest + { + } + + + public class OneResponse + { + } + + + public class TwoRequest + { + } + + + public class TwoResponse + { + } + + + public interface IBusOne : + IBus + { + } + + + public class BusOne : + BusInstance, + IBusOne + { + public BusOne(IBusControl busControl) + : base(busControl) + { + } + } + + + public interface IBusTwo : + IBus + { + } + } +} diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/MyId.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/MyId.cs similarity index 94% rename from tests/MassTransit.Containers.Tests/Common_Tests/MyId.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/MyId.cs index 1ad09a6bb6b..55d046274b2 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/MyId.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/MyId.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/RequestClientOutbox_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/RequestClientOutbox_Specs.cs similarity index 94% rename from tests/MassTransit.Containers.Tests/Common_Tests/RequestClientOutbox_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/RequestClientOutbox_Specs.cs index 8412ef85779..1c91c8b4582 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/RequestClientOutbox_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/RequestClientOutbox_Specs.cs @@ -1,9 +1,9 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; using Contracts; - using Courier.Contracts; + using MassTransit.Courier.Contracts; using NUnit.Framework; using TestFramework; @@ -30,7 +30,7 @@ protected override void ConfigureMassTransit(IBusRegistrationConfigurator config protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { - configurator.UseInMemoryOutbox(); + configurator.UseInMemoryOutbox(BusRegistrationContext); configurator.ConfigureConsumer(BusRegistrationContext); } @@ -69,6 +69,20 @@ public Task Consume(ConsumeContext context) public class Using_the_outbox_with_a_routing_slip_request : InMemoryContainerTestFixture { + Task> _activityCompleted; + Task> _completed; + Uri _executeAddress; + Guid _trackingNumber; + + public Using_the_outbox_with_a_routing_slip_request() + { + RequestQueueName = "activity-request"; + RequestQueueAddress = new Uri($"queue:{RequestQueueName}"); + } + + Uri RequestQueueAddress { get; } + string RequestQueueName { get; } + [Test] public async Task Should_receive_the_response() { @@ -88,20 +102,6 @@ public async Task Should_receive_the_response() await _activityCompleted; } - Task> _activityCompleted; - Task> _completed; - Uri _executeAddress; - Guid _trackingNumber; - - public Using_the_outbox_with_a_routing_slip_request() - { - RequestQueueName = "activity-request"; - RequestQueueAddress = new Uri($"queue:{RequestQueueName}"); - } - - Uri RequestQueueAddress { get; } - string RequestQueueName { get; } - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) { configurator.SetKebabCaseEndpointNameFormatter(); @@ -110,7 +110,7 @@ protected override void ConfigureMassTransit(IBusRegistrationConfigurator config configurator.AddConfigureEndpointsCallback((name, x) => { - x.UseInMemoryOutbox(); + x.UseInMemoryOutbox(BusRegistrationContext); if (name == KebabCaseEndpointNameFormatter.Instance.ExecuteActivity()) _executeAddress = x.InputAddress; @@ -121,7 +121,7 @@ protected override void ConfigureMassTransit(IBusRegistrationConfigurator config protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { - configurator.UseInMemoryOutbox(); + configurator.UseInMemoryOutbox(BusRegistrationContext); } diff --git a/tests/MassTransit.Containers.Tests/Common_Tests/RequestClient_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Common_Tests/RequestClient_Specs.cs similarity index 94% rename from tests/MassTransit.Containers.Tests/Common_Tests/RequestClient_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/Common_Tests/RequestClient_Specs.cs index b239b6eca70..02dfa3e4b85 100644 --- a/tests/MassTransit.Containers.Tests/Common_Tests/RequestClient_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/Common_Tests/RequestClient_Specs.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Common_Tests +namespace MassTransit.Tests.ContainerTests.Common_Tests { using System; using System.Threading.Tasks; @@ -11,6 +11,17 @@ namespace MassTransit.Containers.Tests.Common_Tests public class Using_the_request_client_across_consumers : InMemoryContainerTestFixture { + Guid _correlationId; + + public Using_the_request_client_across_consumers() + { + SubsequentQueueName = "subsequent_queue"; + SubsequentQueueAddress = new Uri($"queue:{SubsequentQueueName}"); + } + + protected Uri SubsequentQueueAddress { get; } + string SubsequentQueueName { get; } + [Test] public async Task Should_receive_the_response() { @@ -24,23 +35,15 @@ public async Task Should_receive_the_response() Value = "World" }); - Assert.That(response.Message.Value, Is.EqualTo("Hello, World")); - Assert.That(response.ConversationId.Value, Is.EqualTo(response.Message.OriginalConversationId)); - Assert.That(response.InitiatorId.Value, Is.EqualTo(_correlationId)); - Assert.That(response.Message.OriginalInitiatorId, Is.EqualTo(_correlationId)); - } - - Guid _correlationId; - - public Using_the_request_client_across_consumers() - { - SubsequentQueueName = "subsequent_queue"; - SubsequentQueueAddress = new Uri($"queue:{SubsequentQueueName}"); + Assert.Multiple(() => + { + Assert.That(response.Message.Value, Is.EqualTo("Hello, World")); + Assert.That(response.ConversationId.Value, Is.EqualTo(response.Message.OriginalConversationId)); + Assert.That(response.InitiatorId.Value, Is.EqualTo(_correlationId)); + Assert.That(response.Message.OriginalInitiatorId, Is.EqualTo(_correlationId)); + }); } - protected Uri SubsequentQueueAddress { get; } - string SubsequentQueueName { get; } - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) { configurator.AddConsumer(); @@ -169,6 +172,8 @@ public Task Consume(ConsumeContext context) public class Using_the_scoped_client_factory_in_a_consumer : InMemoryContainerTestFixture { + Guid _correlationId; + [Test] public async Task Should_receive_the_response() { @@ -185,8 +190,6 @@ public async Task Should_receive_the_response() Assert.That(response.Message.Value, Is.EqualTo("Hello, World")); } - Guid _correlationId; - protected override void ConfigureMassTransit(IBusRegistrationConfigurator configurator) { configurator.AddConsumer(); diff --git a/tests/MassTransit.Containers.Tests/ContainerTestHarness_Specs.cs b/tests/MassTransit.Tests/ContainerTests/ContainerTestHarness_Specs.cs similarity index 81% rename from tests/MassTransit.Containers.Tests/ContainerTestHarness_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/ContainerTestHarness_Specs.cs index 9bdb2506dd9..5ee24caa52b 100644 --- a/tests/MassTransit.Containers.Tests/ContainerTestHarness_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/ContainerTestHarness_Specs.cs @@ -1,14 +1,14 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Threading; using System.Threading.Tasks; using HarnessContracts; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using TestFramework; using TestFramework.Sagas; - using Testing; namespace HarnessContracts @@ -138,15 +138,21 @@ await harness.Bus.Publish(new OrderNumber = "123" }); - Assert.That(await harness.Published.SelectAsync>().Count(), Is.EqualTo(1)); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.SelectAsync>().Count(), Is.EqualTo(1)); - Assert.That(await harness.Consumed.SelectAsync().Count(), Is.EqualTo(1)); + Assert.That(await harness.Consumed.SelectAsync().Count(), Is.EqualTo(1)); + }); IConsumerTestHarness consumerHarness = harness.GetConsumerHarness(); - Assert.That(await consumerHarness.Consumed.SelectAsync().Count(), Is.EqualTo(1)); + await Assert.MultipleAsync(async () => + { + Assert.That(await consumerHarness.Consumed.SelectAsync().Count(), Is.EqualTo(1)); - Assert.That(SubmitOrderConsumer.Attempts, Is.EqualTo(4)); + Assert.That(SubmitOrderConsumer.Attempts, Is.EqualTo(4)); + }); } @@ -190,9 +196,12 @@ await client.GetResponse(new OrderNumber = "123" }); - Assert.IsTrue(await harness.Sent.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Sent.Any(), Is.True); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); + }); } @@ -233,15 +242,21 @@ public async Task Should_have_a_simple_clean_syntax() TestKey = "Unique" }); - Assert.IsTrue(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any(), Is.True); IReceivedMessage startTest = await harness.Consumed.SelectAsync().First(); - Assert.That(startTest.Context.CorrelationId, Is.EqualTo(correlationId)); + await Assert.MultipleAsync(async () => + { + Assert.That(startTest.Context.CorrelationId, Is.EqualTo(correlationId)); - Assert.IsTrue(await harness.Published.Any()); + Assert.That(await harness.Published.Any(), Is.True); + }); IPublishedMessage testStarted = await harness.Published.SelectAsync().First(); - Assert.That(testStarted.Context.InitiatorId, Is.EqualTo(correlationId)); + Assert.Multiple(() => + { + Assert.That(testStarted.Context.InitiatorId, Is.EqualTo(correlationId)); - Assert.That(startTest.Context.ConversationId, Is.EqualTo(testStarted.Context.ConversationId)); + Assert.That(startTest.Context.ConversationId, Is.EqualTo(testStarted.Context.ConversationId)); + }); } } } diff --git a/tests/MassTransit.Containers.Tests/DateOnlyTimeOnly_Specs.cs b/tests/MassTransit.Tests/ContainerTests/DateOnlyTimeOnly_Specs.cs similarity index 89% rename from tests/MassTransit.Containers.Tests/DateOnlyTimeOnly_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/DateOnlyTimeOnly_Specs.cs index f0e41af4e6c..d472843a738 100644 --- a/tests/MassTransit.Containers.Tests/DateOnlyTimeOnly_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/DateOnlyTimeOnly_Specs.cs @@ -1,12 +1,14 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Text.Json; using System.Text.Json.Serialization; using System.Threading.Tasks; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; - using Testing; + +#if NET8_0_OR_GREATER public class Using_the_date_only_time_only_property_types @@ -53,8 +55,11 @@ await harness.Bus.Publish(new MyMessage IReceivedMessage message = await harness.Consumed.SelectAsync().FirstOrDefault(); - Assert.That(message.Context.Message.Date, Is.EqualTo(dateOnly)); - Assert.That(message.Context.Message.Time, Is.EqualTo(timeOnly)); + Assert.Multiple(() => + { + Assert.That(message.Context.Message.Date, Is.EqualTo(dateOnly)); + Assert.That(message.Context.Message.Time, Is.EqualTo(timeOnly)); + }); } @@ -96,4 +101,5 @@ public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializer writer.WriteStringValue(isoDate); } } +#endif } diff --git a/tests/MassTransit.Containers.Tests/DependencyInjectionTestHarness2_Specs.cs b/tests/MassTransit.Tests/ContainerTests/DependencyInjectionTestHarness2_Specs.cs similarity index 93% rename from tests/MassTransit.Containers.Tests/DependencyInjectionTestHarness2_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/DependencyInjectionTestHarness2_Specs.cs index a5198173efa..bc975e1f214 100644 --- a/tests/MassTransit.Containers.Tests/DependencyInjectionTestHarness2_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/DependencyInjectionTestHarness2_Specs.cs @@ -1,11 +1,11 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Threading.Tasks; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using TestFramework; - using Testing; public interface StartCommand @@ -48,7 +48,7 @@ public async Task Should_not_timeout() // Assert // did the actual saga consume the message - Assert.True(await sagaHarness.Consumed.Any()); + Assert.That(await sagaHarness.Consumed.Any(), Is.True); } finally { @@ -84,7 +84,7 @@ await client.GetResponse(new // Assert // did the actual saga consume the message var sagaHarness = provider.GetRequiredService>(); - Assert.True(await sagaHarness.Consumed.Any()); + Assert.That(await sagaHarness.Consumed.Any(), Is.True); } diff --git a/tests/MassTransit.Containers.Tests/DependencyInjectionTestHarness_Specs.cs b/tests/MassTransit.Tests/ContainerTests/DependencyInjectionTestHarness_Specs.cs similarity index 85% rename from tests/MassTransit.Containers.Tests/DependencyInjectionTestHarness_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/DependencyInjectionTestHarness_Specs.cs index 80594f53e8c..f68d81de5c9 100644 --- a/tests/MassTransit.Containers.Tests/DependencyInjectionTestHarness_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/DependencyInjectionTestHarness_Specs.cs @@ -1,13 +1,13 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Linq.Expressions; using System.Threading.Tasks; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using Scenarios; using TestFramework.Messages; - using Testing; [TestFixture] @@ -96,23 +96,32 @@ await harness.Bus.Publish(new A Value = _testValueA }); - Assert.That(await harness.Published.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any()); - Assert.That(await harness.Consumed.Any()); + Assert.That(await harness.Consumed.Any()); + }); var sagaHarness = provider.GetRequiredService>(); - Assert.That(await sagaHarness.Consumed.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any()); - Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == _sagaId)); + Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == _sagaId)); + }); var saga = sagaHarness.Created.Contains(_sagaId); Assert.That(saga, Is.Not.Null); - Assert.That(saga.ValueA, Is.EqualTo(_testValueA)); + await Assert.MultipleAsync(async () => + { + Assert.That(saga.ValueA, Is.EqualTo(_testValueA)); - Assert.That(await harness.Published.Any()); + Assert.That(await harness.Published.Any()); - Assert.That(await harness.Published.Any(), Is.False); + Assert.That(await harness.Published.Any(), Is.False); + }); } Guid _sagaId; @@ -210,20 +219,26 @@ public async Task Should_support_the_saga_harness() await harness.Bus.Publish(new Start { CorrelationId = sagaId }); - Assert.IsTrue(await harness.Consumed.Any(), "Message not received"); + Assert.That(await harness.Consumed.Any(), Is.True, "Message not received"); ISagaStateMachineTestHarness sagaHarness = harness.GetSagaStateMachineHarness(); - Assert.That(await sagaHarness.Consumed.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any()); - Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == sagaId)); + Assert.That(await sagaHarness.Created.Any(x => x.CorrelationId == sagaId)); + }); var machine = provider.GetRequiredService(); var instance = sagaHarness.Created.ContainsInState(sagaId, sagaHarness.StateMachine, machine.Running); - Assert.IsNotNull(instance, "Saga instance not found"); + await Assert.MultipleAsync(async () => + { + Assert.That(instance, Is.Not.Null, "Saga instance not found"); - Assert.IsTrue(await harness.Published.Any(), "Event not published"); + Assert.That(await harness.Published.Any(), Is.True, "Event not published"); + }); } diff --git a/tests/MassTransit.Tests/ContainerTests/Dispatcher_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Dispatcher_Specs.cs new file mode 100644 index 00000000000..ed178c1bd42 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Dispatcher_Specs.cs @@ -0,0 +1,126 @@ +namespace MassTransit.Tests.ContainerTests +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Context; + using Internals; + using MassTransit.Serialization; + using MassTransit.Testing; + using MassTransit.Transports; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using NUnit.Framework; + using TestFramework; + + + [TestFixture] + public class Dispatching_a_string : + AsyncTestFixture + { + [Test] + public async Task Should_be_handled_by_the_consumer() + { + var services = new ServiceCollection(); + + services.AddSingleton(BusTestFixture.LoggerFactory); + services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); + + services.AddMassTransit(x => + { + x.AddConsumer(); + x.AddConsumer(); + + x.UsingInMemory((context, cfg) => + { + cfg.ConfigureEndpoints(context, filter => filter.Include()); + }); + + x.AddConfigureEndpointsCallback((name, cfg) => cfg.UseRawJsonSerializer()); + }); + + await using var provider = services + .BuildServiceProvider(true); + + await provider.GetRequiredService().StartAsync(TestCancellationToken); + try + { + var receiver = provider.GetRequiredService>(); + + (var bytes, Dictionary headers) = Serialize(new SimpleCommand { Value = "Hello" }); + + await receiver.Dispatch(bytes, headers, TestCancellationToken); + + await SimpleEventConsumer.Completed.OrCanceled(TestCancellationToken); + } + finally + { + await provider.GetRequiredService().StopAsync(TestCancellationToken); + } + } + + static (byte[], Dictionary) Serialize(T obj) + where T : class + { + var serializer = new SystemTextJsonRawMessageSerializer(); + + var sendContext = new MessageSendContext(obj); + + var bytes = serializer.GetMessageBody(sendContext).GetBytes(); + + var headers = new Dictionary + { + { MessageHeaders.ContentType, SystemTextJsonRawMessageSerializer.JsonContentType }, + { MessageHeaders.MessageId, sendContext.MessageId } + }; + + headers.Set(sendContext.Headers); + + return (bytes, headers); + } + + public Dispatching_a_string() + : base(new InMemoryTestHarness()) + { + } + + + class SimpleCommandConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return context.Publish(new SimpleEvent { Value = context.Message.Value }); + } + } + + + class SimpleEventConsumer : + IConsumer + { + static readonly TaskCompletionSource> _source = new TaskCompletionSource>(); + + public static Task> Completed => _source.Task; + + public Task Consume(ConsumeContext context) + { + _source.TrySetResult(context); + + return Task.CompletedTask; + } + } + + + class SimpleCommand + { + public string Value { get; set; } + } + + + class SimpleEvent + { + public string Value { get; set; } + } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/EmptyBody_Specs.cs b/tests/MassTransit.Tests/ContainerTests/EmptyBody_Specs.cs new file mode 100644 index 00000000000..1b0cdfb5739 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/EmptyBody_Specs.cs @@ -0,0 +1,125 @@ +namespace MassTransit.Tests.ContainerTests +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using Context; + using Internals; + using MassTransit.Serialization; + using MassTransit.Testing; + using MassTransit.Transports; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.DependencyInjection.Extensions; + using Microsoft.Extensions.Hosting; + using Microsoft.Extensions.Logging; + using NUnit.Framework; + using TestFramework; + + + [TestFixture] + public class Dispatching_an_empty_message_body : + AsyncTestFixture + { + [Test] + public async Task Should_be_handled_by_the_consumer() + { + var services = new ServiceCollection(); + + services.AddSingleton(BusTestFixture.LoggerFactory); + services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); + + services.AddMassTransit(x => + { + x.AddConsumer(); + x.AddConsumer(); + + x.UsingInMemory((context, cfg) => + { + cfg.ConfigureEndpoints(context, filter => filter.Include()); + }); + + x.AddConfigureEndpointsCallback((name, cfg) => cfg.UseRawJsonSerializer()); + }); + + await using var provider = services + .BuildServiceProvider(true); + + await provider.GetRequiredService().StartAsync(TestCancellationToken); + try + { + var receiver = provider.GetRequiredService>(); + + (var bytes, Dictionary headers) = Serialize(new SimpleCommand { Value = "Hello" }); + + await receiver.Dispatch(bytes, headers, TestCancellationToken); + + await SimpleEventConsumer.Completed.OrCanceled(TestCancellationToken); + } + finally + { + await provider.GetRequiredService().StopAsync(TestCancellationToken); + } + } + + static (byte[], Dictionary) Serialize(T obj) + where T : class + { + var sendContext = new MessageSendContext(obj); + + var bytes = Array.Empty(); + + var headers = new Dictionary + { + { MessageHeaders.ContentType, SystemTextJsonRawMessageSerializer.JsonContentType }, + { MessageHeaders.MessageId, sendContext.MessageId } + }; + + headers.Set(sendContext.Headers); + + return (bytes, headers); + } + + public Dispatching_an_empty_message_body() + : base(new InMemoryTestHarness()) + { + } + + + class SimpleCommandConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return context.Publish(new SimpleEvent { Value = context.Message.Value }); + } + } + + + class SimpleEventConsumer : + IConsumer + { + static readonly TaskCompletionSource> _source = new TaskCompletionSource>(); + + public static Task> Completed => _source.Task; + + public Task Consume(ConsumeContext context) + { + _source.TrySetResult(context); + + return Task.CompletedTask; + } + } + + + class SimpleCommand + { + public string Value { get; set; } + } + + + class SimpleEvent + { + public string Value { get; set; } + } + } +} diff --git a/tests/MassTransit.Containers.Tests/EndpointConfiguration_Specs.cs b/tests/MassTransit.Tests/ContainerTests/EndpointConfiguration_Specs.cs similarity index 84% rename from tests/MassTransit.Containers.Tests/EndpointConfiguration_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/EndpointConfiguration_Specs.cs index 253ed98a12e..abfc21bc362 100644 --- a/tests/MassTransit.Containers.Tests/EndpointConfiguration_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/EndpointConfiguration_Specs.cs @@ -1,8 +1,9 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Linq; using System.Threading.Tasks; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; @@ -10,7 +11,6 @@ namespace MassTransit.Containers.Tests using NUnit.Framework; using TestFramework; using TestFramework.Messages; - using Testing; [TestFixture] @@ -18,67 +18,15 @@ public class EndpointConfiguration_Specs : BusTestFixture { [Test] - public void Should_properly_configure_the_prefetch_count() - { - var busControl = MassTransit.Bus.Factory.CreateUsingInMemory(cfg => - { - cfg.PrefetchCount = 427; - }); - - var probe = JObject.Parse(busControl.GetProbeResult().ToJsonString()); - - var prefetchCount = GetPrefetchCount(probe, 0); - - Assert.That(prefetchCount, Is.EqualTo(427)); - } - - [Test] - public void Should_use_bus_setting_if_not_specified() - { - var busControl = MassTransit.Bus.Factory.CreateUsingInMemory(cfg => - { - cfg.PrefetchCount = 427; - - cfg.ReceiveEndpoint("input-queue", e => - { - }); - }); - - var probe = JObject.Parse(busControl.GetProbeResult().ToJsonString()); - - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); - } - - [Test] - public void Should_override_bus_setting_if_specified() - { - var busControl = MassTransit.Bus.Factory.CreateUsingInMemory(cfg => - { - cfg.PrefetchCount = 427; - - cfg.ReceiveEndpoint("input-queue", e => - { - e.PrefetchCount = 351; - }); - }); - - var jsonString = busControl.GetProbeResult().ToJsonString(); - var probe = JObject.Parse(jsonString); - - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); - } - - [Test] - public async Task Should_include_concurrency_filter_if_specified() + public async Task Should_include_concurrency_filter_if_concurrency_limit_overridden() { var services = new ServiceCollection(); services.AddSingleton(LoggerFactory); services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); services.AddMassTransit(x => { - x.AddConsumer(); + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 100); x.UsingInMemory((context, cfg) => { @@ -94,9 +42,12 @@ public async Task Should_include_concurrency_filter_if_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -125,23 +76,25 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_specifi var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } [Test] - public async Task Should_include_concurrency_filter_if_concurrency_limit_overridden() + public async Task Should_include_concurrency_filter_if_specified() { var services = new ServiceCollection(); services.AddSingleton(LoggerFactory); services.TryAdd(ServiceDescriptor.Singleton(typeof(ILogger<>), typeof(Logger<>))); services.AddMassTransit(x => { - x.AddConsumer() - .Endpoint(e => e.ConcurrentMessageLimit = 100); + x.AddConsumer(); x.UsingInMemory((context, cfg) => { @@ -157,9 +110,12 @@ public async Task Should_include_concurrency_filter_if_concurrency_limit_overrid var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(120)); - Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetConcurrentMessageLimit(probe, 0), Is.EqualTo(100)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } @@ -188,12 +144,74 @@ public async Task Should_include_nothing_if_not_specified() var jsonString = busControl.GetProbeResult().ToJsonString(); var probe = JObject.Parse(jsonString); - Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); - Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); await provider.DisposeAsync(); } + [Test] + public void Should_override_bus_setting_if_specified() + { + var busControl = MassTransit.Bus.Factory.CreateUsingInMemory(cfg => + { + cfg.PrefetchCount = 427; + + cfg.ReceiveEndpoint("input-queue", e => + { + e.PrefetchCount = 351; + }); + }); + + var jsonString = busControl.GetProbeResult().ToJsonString(); + var probe = JObject.Parse(jsonString); + + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(351)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); + } + + [Test] + public void Should_properly_configure_the_prefetch_count() + { + var busControl = MassTransit.Bus.Factory.CreateUsingInMemory(cfg => + { + cfg.PrefetchCount = 427; + }); + + var probe = JObject.Parse(busControl.GetProbeResult().ToJsonString()); + + var prefetchCount = GetPrefetchCount(probe, 0); + + Assert.That(prefetchCount, Is.EqualTo(427)); + } + + [Test] + public void Should_use_bus_setting_if_not_specified() + { + var busControl = MassTransit.Bus.Factory.CreateUsingInMemory(cfg => + { + cfg.PrefetchCount = 427; + + cfg.ReceiveEndpoint("input-queue", e => + { + }); + }); + + var probe = JObject.Parse(busControl.GetProbeResult().ToJsonString()); + + Assert.Multiple(() => + { + Assert.That(GetPrefetchCount(probe, 0), Is.EqualTo(427)); + Assert.That(GetPrefetchCount(probe, 1), Is.EqualTo(427)); + }); + } + class PingConsumerDefinition : ConsumerDefinition @@ -204,7 +222,8 @@ public PingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -223,7 +242,8 @@ public EndpointPingConsumerDefinition() } protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } @@ -232,12 +252,9 @@ protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointC class EmptyPingConsumerDefinition : ConsumerDefinition { - public EmptyPingConsumerDefinition() - { - } - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { } } diff --git a/tests/MassTransit.Containers.Tests/ExcludeFromConfigureEndpoints_Specs.cs b/tests/MassTransit.Tests/ContainerTests/ExcludeFromConfigureEndpoints_Specs.cs similarity index 96% rename from tests/MassTransit.Containers.Tests/ExcludeFromConfigureEndpoints_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/ExcludeFromConfigureEndpoints_Specs.cs index ff07b2577ef..e5f19ba00ec 100644 --- a/tests/MassTransit.Containers.Tests/ExcludeFromConfigureEndpoints_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/ExcludeFromConfigureEndpoints_Specs.cs @@ -1,11 +1,11 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Threading.Tasks; using Common_Tests; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; - using Testing; public class ExcludeConsumerFromConfigureEndpoints_Specs @@ -34,11 +34,7 @@ public async Task Should_exclude_consumer_explicitly() public async Task Should_exclude_consumer_by_attribute() { await using var provider = new ServiceCollection() - .AddMassTransitTestHarness(x => - { - x.AddConsumer() - .ExcludeFromConfigureEndpoints(); - }) + .AddMassTransitTestHarness(x => x.AddConsumer()) .BuildServiceProvider(true); var harness = provider.GetTestHarness(); diff --git a/tests/MassTransit.Containers.Tests/ExcludeTypeFromFilter_Specs.cs b/tests/MassTransit.Tests/ContainerTests/ExcludeTypeFromFilter_Specs.cs similarity index 97% rename from tests/MassTransit.Containers.Tests/ExcludeTypeFromFilter_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/ExcludeTypeFromFilter_Specs.cs index c9cfa651c81..d56e3cd2a0c 100644 --- a/tests/MassTransit.Containers.Tests/ExcludeTypeFromFilter_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/ExcludeTypeFromFilter_Specs.cs @@ -1,11 +1,11 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Threading.Tasks; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using NUnit.Framework; - using Testing; [TestFixture] diff --git a/tests/MassTransit.Containers.Tests/Future_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Future_Specs.cs similarity index 98% rename from tests/MassTransit.Containers.Tests/Future_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/Future_Specs.cs index 7e6b475ca93..b203647bd76 100644 --- a/tests/MassTransit.Containers.Tests/Future_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/Future_Specs.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Threading.Tasks; diff --git a/tests/MassTransit.Containers.Tests/Handler_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Handler_Specs.cs similarity index 90% rename from tests/MassTransit.Containers.Tests/Handler_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/Handler_Specs.cs index 1d31418268f..56c698d3247 100644 --- a/tests/MassTransit.Containers.Tests/Handler_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/Handler_Specs.cs @@ -1,11 +1,11 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System.Threading.Tasks; using HandlerContracts; + using MassTransit.Testing; + using MassTransit.Transports; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; - using Testing; - using Transports; namespace HandlerContracts @@ -75,8 +75,11 @@ public async Task Should_handle_the_message_and_argument() { x.AddHandler(async (MyMessage message, IService service) => { - Assert.That(message, Is.Not.Null); - Assert.That(service, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message, Is.Not.Null); + Assert.That(service, Is.Not.Null); + }); }); }) .BuildServiceProvider(true); @@ -99,8 +102,11 @@ public async Task Should_handle_the_consume_context_and_argument() { x.AddHandler(async (ConsumeContext context, IService service) => { - Assert.That(context, Is.Not.Null); - Assert.That(service, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(context, Is.Not.Null); + Assert.That(service, Is.Not.Null); + }); }); }) .BuildServiceProvider(true); @@ -124,8 +130,11 @@ public async Task Should_handle_the_message_and_arguments() { x.AddHandler(async (MyMessage message, IService service, IService2 service2) => { - Assert.That(message, Is.Not.Null); - Assert.That(service, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message, Is.Not.Null); + Assert.That(service, Is.Not.Null); + }); }); }) .BuildServiceProvider(true); @@ -149,8 +158,11 @@ public async Task Should_handle_the_consume_context_and_arguments() { x.AddHandler(async (ConsumeContext context, IService service, IService2 service2) => { - Assert.That(context, Is.Not.Null); - Assert.That(service, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(context, Is.Not.Null); + Assert.That(service, Is.Not.Null); + }); }); }) .BuildServiceProvider(true); @@ -175,8 +187,11 @@ public async Task Should_handle_the_message_and_arguments3() { x.AddHandler(async (MyMessage message, IService service, IService2 service2, IService3 service3) => { - Assert.That(message, Is.Not.Null); - Assert.That(service, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message, Is.Not.Null); + Assert.That(service, Is.Not.Null); + }); }); }) .BuildServiceProvider(true); @@ -201,8 +216,11 @@ public async Task Should_handle_the_consume_context_and_arguments3() { x.AddHandler(async (ConsumeContext context, IService service, IService2 service2, IService3 service3) => { - Assert.That(context, Is.Not.Null); - Assert.That(service, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(context, Is.Not.Null); + Assert.That(service, Is.Not.Null); + }); }); }) .BuildServiceProvider(true); diff --git a/tests/MassTransit.Containers.Tests/HealthCheck_Specs.cs b/tests/MassTransit.Tests/ContainerTests/HealthCheck_Specs.cs similarity index 97% rename from tests/MassTransit.Containers.Tests/HealthCheck_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/HealthCheck_Specs.cs index b59be369694..23bc4e5732a 100644 --- a/tests/MassTransit.Containers.Tests/HealthCheck_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/HealthCheck_Specs.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Linq; @@ -157,7 +157,7 @@ public async Task Should_be_healthy_with_configured_receive_endpoints() IHostedService[] hostedServices = provider.GetServices().ToArray(); var result = await healthChecks.CheckHealthAsync(TestCancellationToken); - Assert.That(result.Status == HealthStatus.Unhealthy); + Assert.That(result.Status, Is.EqualTo(HealthStatus.Unhealthy)); await Task.WhenAll(hostedServices.Select(x => x.StartAsync(TestCancellationToken))); try @@ -204,7 +204,7 @@ public async Task Should_be_healthy_with_multiple_bus_instances() IHostedService[] hostedServices = provider.GetServices().ToArray(); var result = await healthChecks.CheckHealthAsync(TestCancellationToken); - Assert.That(result.Status == HealthStatus.Unhealthy); + Assert.That(result.Status, Is.EqualTo(HealthStatus.Unhealthy)); await Task.WhenAll(hostedServices.Select(x => x.StartAsync(TestCancellationToken))); try diff --git a/tests/MassTransit.Containers.Tests/KillSwitch_Specs.cs b/tests/MassTransit.Tests/ContainerTests/KillSwitch_Specs.cs similarity index 97% rename from tests/MassTransit.Containers.Tests/KillSwitch_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/KillSwitch_Specs.cs index 60749ee1b77..23df2e3e51a 100644 --- a/tests/MassTransit.Containers.Tests/KillSwitch_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/KillSwitch_Specs.cs @@ -1,9 +1,10 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { using System; using System.Linq; using System.Threading; using System.Threading.Tasks; + using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Diagnostics.HealthChecks; @@ -11,7 +12,6 @@ namespace MassTransit.Containers.Tests using Microsoft.Extensions.Logging; using NUnit.Framework; using TestFramework; - using Testing; [TestFixture] diff --git a/tests/MassTransit.Containers.Tests/MediatorFilter_Specs.cs b/tests/MassTransit.Tests/ContainerTests/MediatorFilter_Specs.cs similarity index 97% rename from tests/MassTransit.Containers.Tests/MediatorFilter_Specs.cs rename to tests/MassTransit.Tests/ContainerTests/MediatorFilter_Specs.cs index 3cb0bc19b59..33552e5bd73 100644 --- a/tests/MassTransit.Containers.Tests/MediatorFilter_Specs.cs +++ b/tests/MassTransit.Tests/ContainerTests/MediatorFilter_Specs.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests +namespace MassTransit.Tests.ContainerTests { namespace MediatorFilter { diff --git a/tests/MassTransit.Tests/ContainerTests/Metrics_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Metrics_Specs.cs new file mode 100644 index 00000000000..8d61b5c2ef5 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Metrics_Specs.cs @@ -0,0 +1,204 @@ +namespace MassTransit.Tests; + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Threading.Tasks; +using MassTransit.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.Extensions.Options; +using Monitoring; +using NUnit.Framework; +using OpenTelemetry.Metrics; +using TestFramework; +using TestFramework.Messages; + + +[TestFixture] +[Explicit] +public class ConsumeMetrics_Specs +{ + [Test] + public async Task Should_be_able_to_add_custom_tag_to_consume_metrics() + { + await using var provider = CreateServiceCollection() + .AddMassTransitTestHarness(configurator => + { + configurator.AddHandler(async (PingMessage _) => + { + }); + + configurator.AddConfigureEndpointsCallback((_, _, e) => e.UseFilter(new MetricsFilter())); + }) + .BuildServiceProvider(); + + var testHarness = provider.GetTestHarness(); + + await testHarness.Start(); + + var instrumentationOptions = provider.GetRequiredService>(); + + using MetricCollector collector = GetMetricCollector(provider, instrumentationOptions.Value.ConsumeTotal); + + await testHarness.Bus.Publish(new PingMessage()); + + Assert.That(await testHarness.Consumed.Any(), Is.True); + + IReadOnlyList> metrics = collector.GetMeasurementSnapshot(); + + Assert.That(metrics, Has.Count.EqualTo(1)); + + foreach (CollectedMeasurement metric in metrics) + Assert.That(metric.Tags, Contains.Key(TagName)); + } + + [Test] + public async Task Should_be_able_to_produce_consume_exception_metrics() + { + await using var provider = CreateServiceCollection() + .AddMassTransitTestHarness(configurator => configurator.AddHandler(async (PingMessage _) => throw new IntentionalTestException())) + .BuildServiceProvider(); + + var testHarness = provider.GetTestHarness(); + + await testHarness.Start(); + + var instrumentationOptions = provider.GetRequiredService>(); + + using MetricCollector totalCollector = GetMetricCollector(provider, instrumentationOptions.Value.ConsumeTotal); + using MetricCollector faultCollector = GetMetricCollector(provider, instrumentationOptions.Value.ConsumeFaultTotal); + + await testHarness.Bus.Publish(new PingMessage()); + + Assert.That(await testHarness.Consumed.Any(), Is.True); + + IReadOnlyList> metrics = totalCollector.GetMeasurementSnapshot(); + IReadOnlyList> faults = faultCollector.GetMeasurementSnapshot(); + + Assert.Multiple(() => + { + Assert.That(metrics, Has.Count.EqualTo(1)); + Assert.That(faults, Has.Count.EqualTo(1)); + }); + + foreach (CollectedMeasurement metric in metrics) + Assert.That(metric.Value, Is.EqualTo(1)); + + foreach (CollectedMeasurement metric in faults) + { + Assert.Multiple(() => + { + Assert.That(metric.Value, Is.EqualTo(1)); + Assert.That(metric.Tags, Does.ContainKey(instrumentationOptions.Value.ExceptionTypeLabel).WithValue(nameof(IntentionalTestException))); + }); + } + } + + [Test] + public async Task Should_be_able_to_produce_consume_metrics() + { + await using var provider = CreateServiceCollection() + .AddSingleton(provider => provider.GetTestHarness().GetTask>()) + .AddSingleton(provider => provider.GetTestHarness().GetTask()) + .AddMassTransitTestHarness(configurator => configurator.AddConsumer()) + .BuildServiceProvider(); + + var testHarness = provider.GetTestHarness(); + await testHarness.Start(); + + var instrumentationOptions = provider.GetRequiredService>(); + + using MetricCollector totalCollector = GetMetricCollector(provider, instrumentationOptions.Value.ConsumeTotal); + using MetricCollector durationCollector = GetMetricCollector(provider, instrumentationOptions.Value.ConsumeDuration); + using MetricCollector inProgressCollector = GetMetricCollector(provider, instrumentationOptions.Value.ConsumerInProgress); + + await testHarness.Bus.Publish(new PingMessage()); + + await provider.GetTask>(); + + IReadOnlyList> inProgress = inProgressCollector.GetMeasurementSnapshot(); + + foreach (CollectedMeasurement metric in inProgress) + Assert.That(metric.Value, Is.EqualTo(1)); + + var completed = provider.GetRequiredService>(); + completed.TrySetResult(true); + + Assert.That(await testHarness.Consumed.Any(), Is.True); + + IReadOnlyList> metrics = totalCollector.GetMeasurementSnapshot(); + inProgress = inProgressCollector.GetMeasurementSnapshot(); + + IReadOnlyList> duration = durationCollector.GetMeasurementSnapshot(); + + Assert.Multiple(() => + { + Assert.That(metrics, Has.Count.EqualTo(1)); + Assert.That(duration, Has.Count.EqualTo(1)); + Assert.That(inProgress, Has.Count.EqualTo(2)); + }); + + foreach (CollectedMeasurement metric in metrics) + Assert.That(metric.Value, Is.EqualTo(1)); + + foreach (CollectedMeasurement metric in duration) + Assert.That(metric.Value, Is.GreaterThan(0)); + } + + const string TagName = "custom-metric-name"; + + + class PingConsumer : + IConsumer + { + readonly TaskCompletionSource _completed; + readonly TaskCompletionSource> _ready; + + public PingConsumer(TaskCompletionSource> ready, TaskCompletionSource completed) + { + _ready = ready; + _completed = completed; + } + + public Task Consume(ConsumeContext context) + { + _ready.TrySetResult(context); + return _completed.Task; + } + } + + + class MetricsFilter : + IFilter + { + public Task Send(ConsumeContext context, IPipe next) + { + context.AddMetricTags(TagName, "test"); + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + } + } + + + protected static ServiceCollection CreateServiceCollection() + { + var collection = new ServiceCollection(); + collection.AddMetrics(); + collection.AddOpenTelemetry() + .WithMetrics(builder => builder + .AddMeter(InstrumentationOptions.MeterName) + .AddConsoleExporter()); + return collection; + } + + protected static MetricCollector GetMetricCollector(IServiceProvider provider, string instrumentation) + where T : struct + { + var meterFactory = provider.GetRequiredService(); + return new MetricCollector(meterFactory, InstrumentationOptions.MeterName, instrumentation); + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/ReceiveEndpointDependency_Specs.cs b/tests/MassTransit.Tests/ContainerTests/ReceiveEndpointDependency_Specs.cs new file mode 100644 index 00000000000..fc550124231 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/ReceiveEndpointDependency_Specs.cs @@ -0,0 +1,135 @@ +namespace MassTransit.Tests.ContainerTests +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using MassTransit.Testing; + using MassTransit.Transports; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Diagnostics.HealthChecks; + using NUnit.Framework; + using TestFramework; + + + public class ReceiveEndpointDependency_Specs + { + [Test] + public async Task Should_not_receive_message_before_dependency_ready() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + var healthChecks = provider.GetService(); + + await harness.Start(); + + await harness.Bus.Publish(new { }); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(cts.Token), Is.True); + Assert.That(await harness.Consumed.Any(cts.Token), Is.False); + }); + + await healthChecks.WaitForHealthStatus(HealthStatus.Unhealthy); + + await harness.Bus.Publish(new { }); + + await healthChecks.WaitForHealthStatus(HealthStatus.Healthy); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.Any(cts.Token), Is.True); + Assert.That(await harness.Consumed.Any(cts.Token), Is.True); + }); + } + + static ServiceProvider SetupServiceCollection() + { + var services = new ServiceCollection() + .AddSingleton() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.AddConsumer(typeof(DependentConsumerDefinition)); + + x.AddTaskCompletionSource(); + }); + + services.AddOptions().Configure(x => x.WaitUntilStarted = false); + + return services.BuildServiceProvider(true); + } + + + class DependentConsumerDefinition : + ConsumerDefinition + { + readonly IReceiveEndpointDependency _dependency; + + public DependentConsumerDefinition(IReceiveEndpointDependency dependency) + { + _dependency = dependency; + } + + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) + { + endpointConfigurator.AddDependency(_dependency); + } + } + + + class TestDependency : + IReceiveEndpointDependency + { + public TestDependency(TaskCompletionSource taskCompletionSource) + { + Ready = taskCompletionSource.Task; + } + + public Task Ready { get; } + } + + + public interface DependencyReadyMessage + { + } + + + public interface DependentMessage + { + } + + + class DependencyConsumer : + IConsumer + { + readonly TaskCompletionSource _taskCompletionSource; + + public DependencyConsumer(TaskCompletionSource taskCompletionSource) + { + _taskCompletionSource = taskCompletionSource; + } + + public Task Consume(ConsumeContext context) + { + _taskCompletionSource.TrySetResult(true); + return Task.CompletedTask; + } + } + + + class DependentConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/RedeliveryHeader_Specs.cs b/tests/MassTransit.Tests/ContainerTests/RedeliveryHeader_Specs.cs new file mode 100644 index 00000000000..b23bd514fb3 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/RedeliveryHeader_Specs.cs @@ -0,0 +1,344 @@ +namespace MassTransit.Tests.ContainerTests +{ + using System; + using System.Threading.Tasks; + using MassTransit.Courier.Contracts; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using RedeliverySubjects; + + + [TestFixture] + public class When_an_activity_is_redelivered + { + [Test] + [Explicit] + public async Task Should_not_copy_redelivery_header_to_next_activity() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetSnakeCaseEndpointNameFormatter(); + x.AddSingleton(); + + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10), testTimeout: TimeSpan.FromMinutes(1)); + + x.AddConsumersFromNamespaceContaining(); + x.AddActivitiesFromNamespaceContaining(); + + x.AddConfigureEndpointsCallback((context, name, cfg) => + { + cfg.UseDelayedRedelivery(r => r.Immediate(3)); + cfg.UseMessageRetry(r => + { + r.Immediate(5); + r.Ignore(typeof(BusinessException)); + }); + cfg.UseInMemoryOutbox(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new RedeliverySubjects.StartCommand() + { + ActivityAExecutionDelay = 1000, + ActivityBExecutionDelay = 1000, + ActivityCExecutionDelay = 1000, + ActivityAThrowOnExecution = false, + ActivityBThrowOnExecution = false, + ActivityCThrowOnExecution = true, + ActivityAThrowOnCompensation = false, + ActivityBThrowOnCompensation = true, + ShouldThrowIgnoredException = false + }); + + Assert.That(await harness.Published.Any()); + } + } + + + namespace RedeliverySubjects + { + using System; + using Microsoft.Extensions.Logging; + + + public record StartCommand + { + public int ActivityAExecutionDelay { get; set; } + public int ActivityBExecutionDelay { get; set; } + public int ActivityCExecutionDelay { get; set; } + public bool ActivityAThrowOnExecution { get; set; } + public bool ActivityBThrowOnExecution { get; set; } + public bool ActivityCThrowOnExecution { get; set; } + public bool ActivityAThrowOnCompensation { get; set; } + public bool ActivityBThrowOnCompensation { get; set; } + public bool ShouldThrowIgnoredException { get; set; } + } + + + public record CommandA + { + public bool ShouldThrowException { get; set; } + public bool ShouldThrowIgnoredException { get; set; } + public int MillisecondsDelay { get; set; } + public bool ShouldThrowInCompensation { get; set; } + } + + + public record CompensateA + { + public bool ShouldThrow { get; set; } + public bool ShouldThrowIgnoredException { get; set; } + } + + + public record CommandB + { + public bool ShouldThrowException { get; set; } + public bool ShouldThrowIgnoredException { get; set; } + public int MillisecondsDelay { get; set; } + public bool ShouldThrowInCompensation { get; set; } + } + + + public record CompensateB + { + public bool ShouldThrow { get; set; } + public bool ShouldThrowIgnoredException { get; set; } + } + + + public record CommandC + { + public bool ShouldThrowException { get; set; } + public bool ShouldThrowIgnoredException { get; set; } + public int MillisecondsDelay { get; set; } + public bool ShouldThrowInCompensation { get; set; } + } + + + public class BusinessException : + Exception + { + public BusinessException() + { + } + + public BusinessException(string message) + : base(message) + { + } + } + + + public class ActivityA : + IActivity + { + readonly ILogger _logger; + + public ActivityA(ILogger logger) + { + _logger = logger; + } + + public async Task Execute(ExecuteContext context) + { + _logger.LogInformation("Executing transaction {StepName} | Retry: {RetryCount} - Redelivery Count: {RedeliveryCount}", nameof(ActivityA), + context.GetRetryCount(), context.GetRedeliveryCount()); + + if (context.Arguments.ShouldThrowException) + { + if (context.Arguments.ShouldThrowIgnoredException) + throw new BusinessException($"An error has occurred executing {nameof(CommandA)}"); + + throw new Exception($"An error has occurred executing {nameof(CommandA)}"); + } + + await Task.Delay(context.Arguments.MillisecondsDelay); + + return context.Completed(new CompensateA + { + ShouldThrow = context.Arguments.ShouldThrowInCompensation, + ShouldThrowIgnoredException = context.Arguments.ShouldThrowIgnoredException + }); + } + + public Task Compensate(CompensateContext context) + { + _logger.LogCritical("Compensating transaction {StepName} | Retry: {RetryCount} - Redelivery Count: {RedeliveryCount}", nameof(ActivityA), + context.GetRetryCount(), context.GetRedeliveryCount()); + + if (context.Log.ShouldThrow) + { + if (context.Log.ShouldThrowIgnoredException) + throw new BusinessException($"An error has occurred executing {nameof(CompensateA)}"); + + throw new Exception($"An error has occurred executing {nameof(CompensateA)}"); + } + + return Task.FromResult(context.Compensated()); + } + } + + + public class ActivityB : + IActivity + { + readonly ILogger _logger; + + public ActivityB(ILogger logger) + { + _logger = logger; + } + + public async Task Execute(ExecuteContext context) + { + _logger.LogInformation("Executing transaction {StepName} | Retry: {RetryCount} - Redelivery Count: {RedeliveryCount}", nameof(ActivityB), + context.GetRetryCount(), context.GetRedeliveryCount()); + + if (context.Arguments.ShouldThrowException) + { + if (context.Arguments.ShouldThrowIgnoredException) + throw new BusinessException($"An error has occurred executing {nameof(CommandB)}"); + + throw new Exception($"An error has occurred executing {nameof(CommandB)}"); + } + + await Task.Delay(context.Arguments.MillisecondsDelay); + + return context.Completed(new CompensateB + { + ShouldThrow = context.Arguments.ShouldThrowInCompensation, + ShouldThrowIgnoredException = context.Arguments.ShouldThrowIgnoredException + }); + } + + public Task Compensate(CompensateContext context) + { + _logger.LogCritical("Compensating transaction {StepName} | Retry: {RetryCount} - Redelivery Count: {RedeliveryCount}", nameof(ActivityB), + context.GetRetryCount(), context.GetRedeliveryCount()); + + if (context.Log.ShouldThrow) + { + if (context.Log.ShouldThrowIgnoredException) + throw new BusinessException($"An error has occurred executing {nameof(CompensateB)}"); + + throw new Exception($"An error has occurred executing {nameof(CompensateB)}"); + } + + return Task.FromResult(context.Compensated()); + } + } + + + public class ActivityC : + IExecuteActivity + { + readonly ILogger _logger; + + public ActivityC(ILogger logger) + { + _logger = logger; + } + + public async Task Execute(ExecuteContext context) + { + _logger.LogInformation("Executing transaction {StepName} | Retry: {RetryCount} - Redelivery Count: {RedeliveryCount}", nameof(ActivityC), + context.GetRetryCount(), context.GetRedeliveryCount()); + + if (context.Arguments.ShouldThrowException) + { + if (context.Arguments.ShouldThrowIgnoredException) + throw new BusinessException($"An error has occurred executing {nameof(CommandC)}"); + + throw new Exception($"An error has occurred executing {nameof(CommandC)}"); + } + + await Task.Delay(context.Arguments.MillisecondsDelay); + + return context.Completed(); + } + } + + + public class StartCommandConsumer : + IConsumer + { + readonly ILogger _logger; + readonly IRoutingSlipExecutor _executor; + readonly IEndpointAddressProvider _addressProvider; + + public StartCommandConsumer(ILogger logger, IRoutingSlipExecutor executor, IEndpointAddressProvider addressProvider) + { + _logger = logger; + _executor = executor; + _addressProvider = addressProvider; + } + + public async Task Consume(ConsumeContext context) + { + _logger.LogDebug("Initiating RoutingSlip..."); + + var builder = new RoutingSlipBuilder(NewId.NextGuid()); + builder.AddActivity(nameof(ActivityA), _addressProvider.GetExecuteEndpoint(), + new CommandA + { + MillisecondsDelay = context.Message.ActivityAExecutionDelay, + ShouldThrowException = context.Message.ActivityAThrowOnExecution, + ShouldThrowInCompensation = context.Message.ActivityAThrowOnCompensation, + ShouldThrowIgnoredException = context.Message.ShouldThrowIgnoredException + }); + builder.AddActivity(nameof(ActivityB), _addressProvider.GetExecuteEndpoint(), + new CommandB + { + MillisecondsDelay = context.Message.ActivityBExecutionDelay, + ShouldThrowException = context.Message.ActivityBThrowOnExecution, + ShouldThrowInCompensation = context.Message.ActivityBThrowOnCompensation, + ShouldThrowIgnoredException = context.Message.ShouldThrowIgnoredException + }); + builder.AddActivity(nameof(ActivityC), _addressProvider.GetExecuteEndpoint(), + new CommandC + { + MillisecondsDelay = context.Message.ActivityCExecutionDelay, + ShouldThrowException = context.Message.ActivityCThrowOnExecution, + ShouldThrowIgnoredException = context.Message.ShouldThrowIgnoredException + }); + + var routingSlip = builder.Build(); + + await _executor.Execute(routingSlip); + } + } + + + public interface IEndpointAddressProvider + { + Uri GetExecuteEndpoint() + where T : class, IExecuteActivity + where TArguments : class; + } + + + public class InMemoryEndpointAddressProvider : + IEndpointAddressProvider + { + readonly IEndpointNameFormatter _formatter; + + public InMemoryEndpointAddressProvider(IEndpointNameFormatter formatter) + { + _formatter = formatter; + } + + public Uri GetExecuteEndpoint() + where T : class, IExecuteActivity + where TArguments : class + { + return new Uri($"exchange:{_formatter.ExecuteActivity()}"); + } + } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/RoutingSlipRequestProxy_Specs.cs b/tests/MassTransit.Tests/ContainerTests/RoutingSlipRequestProxy_Specs.cs new file mode 100644 index 00000000000..2efcbb31373 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/RoutingSlipRequestProxy_Specs.cs @@ -0,0 +1,160 @@ +namespace MassTransit.Tests.ContainerTests +{ + using System.Threading.Tasks; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using RoutingSlipProxySubjects; + using TestFramework.Courier; + + + [TestFixture] + public class Using_the_routing_slip_request_proxy + { + [Test] + public async Task Should_support_retry_options() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetKebabCaseEndpointNameFormatter(); + x.AddSingleton(); + + x.AddConsumer(); + x.AddConsumer(); + + x.AddActivity(); + x.AddActivity(); + x.AddActivity(); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + IRequestClient client = harness.GetRequestClient(); + + Assert.That(async () => await client.GetResponse(new RoutingRequest()), Throws.TypeOf()); + + Assert.That(await harness.Sent.Any>()); + + var fault = await harness.Sent.SelectAsync>().First(); + + Assert.That(fault.Context.Headers.Get(MessageHeaders.FaultRetryCount), Is.EqualTo(1)); + } + } + + + namespace RoutingSlipProxySubjects + { + using System; + using MassTransit.Courier; + using MassTransit.Courier.Contracts; + + + public interface IEndpointAddressProvider + { + Uri GetExecuteEndpoint() + where T : class, IExecuteActivity + where TArguments : class; + + Uri GetResponseConsumerEndpoint() + where TConsumer : RoutingSlipResponseProxy + where TResponse : class + where TRequest : class; + } + + + public class InMemoryEndpointAddressProvider : + IEndpointAddressProvider + { + readonly IEndpointNameFormatter _formatter; + + public InMemoryEndpointAddressProvider(IEndpointNameFormatter formatter) + { + _formatter = formatter; + } + + public Uri GetExecuteEndpoint() + where T : class, IExecuteActivity + where TArguments : class + { + return new Uri($"exchange:{_formatter.ExecuteActivity()}"); + } + + public Uri GetResponseConsumerEndpoint() + where TConsumer : RoutingSlipResponseProxy + where TRequest : class + where TResponse : class + { + return new Uri($"exchange:{_formatter.Consumer()}"); + } + } + + + record RoutingRequest + { + } + + + record RoutingResponse + { + } + + + class RoutingRequestConsumer : + RoutingSlipRequestProxy + { + readonly IEndpointAddressProvider _endpointAddressProvider; + + public RoutingRequestConsumer(IEndpointAddressProvider endpointAddressProvider) + { + _endpointAddressProvider = endpointAddressProvider; + } + + protected override async Task BuildRoutingSlip(RoutingSlipBuilder builder, ConsumeContext request) + { + builder.AddActivity(nameof(TestActivity), _endpointAddressProvider.GetExecuteEndpoint(), new + { + Value = "Hello", + NullValue = (string)null + }); + + builder.AddActivity(nameof(SecondTestActivity), _endpointAddressProvider.GetExecuteEndpoint(), + new + { + Value = "Hello Again", + NullValue = (string)null + }); + + builder.AddActivity(nameof(FaultyActivity), _endpointAddressProvider.GetExecuteEndpoint(), + new + { + Value = "Hello Again", + NullValue = (string)null + }); + } + + protected override Uri GetResponseEndpointAddress(ConsumeContext context) + { + return _endpointAddressProvider.GetResponseConsumerEndpoint(); + } + } + + + class RoutingResponseConsumer : + RoutingSlipResponseProxy + { + public RoutingResponseConsumer() + { + RetryPolicy = Retry.Immediate(1); + } + + protected override IRetryPolicy RetryPolicy { get; } + + protected override Task CreateResponseMessage(ConsumeContext context, RoutingRequest request) + { + return Task.FromResult(new RoutingResponse()); + } + } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/Scenarios/AnotherMessageConsumer.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/AnotherMessageConsumer.cs new file mode 100644 index 00000000000..dd2ce00cc16 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/AnotherMessageConsumer.cs @@ -0,0 +1,8 @@ +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + public interface AnotherMessageConsumer : + IConsumer + { + AnotherMessageInterface Last { get; } + } +} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/AnotherMessageConsumerImpl.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/AnotherMessageConsumerImpl.cs similarity index 91% rename from tests/MassTransit.Containers.Tests/Scenarios/AnotherMessageConsumerImpl.cs rename to tests/MassTransit.Tests/ContainerTests/Scenarios/AnotherMessageConsumerImpl.cs index 958f7ea3cd1..b1c9f4b77d8 100644 --- a/tests/MassTransit.Containers.Tests/Scenarios/AnotherMessageConsumerImpl.cs +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/AnotherMessageConsumerImpl.cs @@ -1,38 +1,38 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - using System; - using System.Threading; - using System.Threading.Tasks; - - - public class AnotherMessageConsumerImpl : - AnotherMessageConsumer - { - readonly ManualResetEvent _received; - AnotherMessageInterface _last; - - public AnotherMessageConsumerImpl() - { - Console.WriteLine("AnotherMessageConsumer()"); - - _received = new ManualResetEvent(false); - } - - public AnotherMessageInterface Last - { - get - { - if (_received.WaitOne(TimeSpan.FromSeconds(8))) - return _last; - - throw new TimeoutException("Timeout waiting for message to be consumed"); - } - } - - public async Task Consume(ConsumeContext context) - { - _last = context.Message; - _received.Set(); - } - } -} +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + using System; + using System.Threading; + using System.Threading.Tasks; + + + public class AnotherMessageConsumerImpl : + AnotherMessageConsumer + { + readonly ManualResetEvent _received; + AnotherMessageInterface _last; + + public AnotherMessageConsumerImpl() + { + Console.WriteLine("AnotherMessageConsumer()"); + + _received = new ManualResetEvent(false); + } + + public AnotherMessageInterface Last + { + get + { + if (_received.WaitOne(TimeSpan.FromSeconds(8))) + return _last; + + throw new TimeoutException("Timeout waiting for message to be consumed"); + } + } + + public async Task Consume(ConsumeContext context) + { + _last = context.Message; + _received.Set(); + } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/Scenarios/AnotherMessageInterface.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/AnotherMessageInterface.cs new file mode 100644 index 00000000000..b31a0fe9b20 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/AnotherMessageInterface.cs @@ -0,0 +1,7 @@ +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + public interface AnotherMessageInterface + { + string Name { get; } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/Scenarios/FirstSagaMessage.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/FirstSagaMessage.cs new file mode 100644 index 00000000000..26a00e2c66d --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/FirstSagaMessage.cs @@ -0,0 +1,11 @@ +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + using System; + + + public class FirstSagaMessage : + CorrelatedBy + { + public Guid CorrelationId { get; set; } + } +} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/ISimpleConsumerDependency.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/ISimpleConsumerDependency.cs similarity index 76% rename from tests/MassTransit.Containers.Tests/Scenarios/ISimpleConsumerDependency.cs rename to tests/MassTransit.Tests/ContainerTests/Scenarios/ISimpleConsumerDependency.cs index 48393122d6e..45cbe1b877d 100644 --- a/tests/MassTransit.Containers.Tests/Scenarios/ISimpleConsumerDependency.cs +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/ISimpleConsumerDependency.cs @@ -1,12 +1,12 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - using System.Threading.Tasks; - - - public interface ISimpleConsumerDependency - { - Task WasDisposed { get; } - bool SomethingDone { get; } - void DoSomething(); - } -} +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + using System.Threading.Tasks; + + + public interface ISimpleConsumerDependency + { + Task WasDisposed { get; } + bool SomethingDone { get; } + void DoSomething(); + } +} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/PingRequestConsumer.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/PingRequestConsumer.cs similarity index 86% rename from tests/MassTransit.Containers.Tests/Scenarios/PingRequestConsumer.cs rename to tests/MassTransit.Tests/ContainerTests/Scenarios/PingRequestConsumer.cs index 5600608cc35..40945171236 100644 --- a/tests/MassTransit.Containers.Tests/Scenarios/PingRequestConsumer.cs +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/PingRequestConsumer.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Scenarios +namespace MassTransit.Tests.ContainerTests.Scenarios { using System.Threading.Tasks; using TestFramework.Messages; diff --git a/tests/MassTransit.Tests/ContainerTests/Scenarios/SecondSagaMessage.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/SecondSagaMessage.cs new file mode 100644 index 00000000000..e846f5024dc --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/SecondSagaMessage.cs @@ -0,0 +1,11 @@ +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + using System; + + + public class SecondSagaMessage : + CorrelatedBy + { + public Guid CorrelationId { get; set; } + } +} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/SecondSimpleSaga.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/SecondSimpleSaga.cs similarity index 95% rename from tests/MassTransit.Containers.Tests/Scenarios/SecondSimpleSaga.cs rename to tests/MassTransit.Tests/ContainerTests/Scenarios/SecondSimpleSaga.cs index 60f40dc6b00..a853a3f9562 100644 --- a/tests/MassTransit.Containers.Tests/Scenarios/SecondSimpleSaga.cs +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/SecondSimpleSaga.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Scenarios +namespace MassTransit.Tests.ContainerTests.Scenarios { using System; using System.Threading.Tasks; diff --git a/tests/MassTransit.Containers.Tests/Scenarios/SimpleConsumer.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleConsumer.cs similarity index 94% rename from tests/MassTransit.Containers.Tests/Scenarios/SimpleConsumer.cs rename to tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleConsumer.cs index 957ca845c0a..7a33dc26e85 100644 --- a/tests/MassTransit.Containers.Tests/Scenarios/SimpleConsumer.cs +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleConsumer.cs @@ -1,63 +1,63 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - using System; - using System.Threading.Tasks; - using Util; - - - public class SimpleConsumer : - IConsumer - { - static readonly TaskCompletionSource _consumerCreated = TaskUtil.GetTask(); - - readonly TaskCompletionSource _received; - - public SimpleConsumer(ISimpleConsumerDependency dependency) - { - Dependency = dependency; - Console.WriteLine("SimpleConsumer()"); - - _received = TaskUtil.GetTask(); - - _consumerCreated.TrySetResult(this); - } - - public Task Last => _received.Task; - - public ISimpleConsumerDependency Dependency { get; } - - public static Task LastConsumer => _consumerCreated.Task; - - public async Task Consume(ConsumeContext message) - { - Dependency.DoSomething(); - - _received.TrySetResult(message.Message); - } - } - - - public class SimplerConsumer : - IConsumer - { - static readonly TaskCompletionSource _consumerCreated = TaskUtil.GetTask(); - - readonly TaskCompletionSource _received; - - public SimplerConsumer() - { - _received = TaskUtil.GetTask(); - - _consumerCreated.TrySetResult(this); - } - - public Task Last => _received.Task; - - public static Task LastConsumer => _consumerCreated.Task; - - public async Task Consume(ConsumeContext message) - { - _received.TrySetResult(message.Message); - } - } -} +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + using System; + using System.Threading.Tasks; + using Util; + + + public class SimpleConsumer : + IConsumer + { + static readonly TaskCompletionSource _consumerCreated = TaskUtil.GetTask(); + + readonly TaskCompletionSource _received; + + public SimpleConsumer(ISimpleConsumerDependency dependency) + { + Dependency = dependency; + Console.WriteLine("SimpleConsumer()"); + + _received = TaskUtil.GetTask(); + + _consumerCreated.TrySetResult(this); + } + + public Task Last => _received.Task; + + public ISimpleConsumerDependency Dependency { get; } + + public static Task LastConsumer => _consumerCreated.Task; + + public async Task Consume(ConsumeContext message) + { + Dependency.DoSomething(); + + _received.TrySetResult(message.Message); + } + } + + + public class SimplerConsumer : + IConsumer + { + static readonly TaskCompletionSource _consumerCreated = TaskUtil.GetTask(); + + readonly TaskCompletionSource _received; + + public SimplerConsumer() + { + _received = TaskUtil.GetTask(); + + _consumerCreated.TrySetResult(this); + } + + public Task Last => _received.Task; + + public static Task LastConsumer => _consumerCreated.Task; + + public async Task Consume(ConsumeContext message) + { + _received.TrySetResult(message.Message); + } + } +} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/SimpleConsumerDependency.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleConsumerDependency.cs similarity index 93% rename from tests/MassTransit.Containers.Tests/Scenarios/SimpleConsumerDependency.cs rename to tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleConsumerDependency.cs index 3d504959ed1..4cd9d8ee91b 100644 --- a/tests/MassTransit.Containers.Tests/Scenarios/SimpleConsumerDependency.cs +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleConsumerDependency.cs @@ -1,45 +1,45 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - using System; - using System.Threading.Tasks; - using Util; - - - public class SimpleConsumerDependency : - ISimpleConsumerDependency, - IDisposable - { - readonly ConsumeContext _consumeContext; - readonly TaskCompletionSource _disposed; - readonly ISendEndpointProvider _sendEndpointProvider; - - public SimpleConsumerDependency(ISendEndpointProvider sendEndpointProvider, ConsumeContext consumeContext) - { - _sendEndpointProvider = sendEndpointProvider; - _consumeContext = consumeContext; - _disposed = TaskUtil.GetTask(); - } - - public void Dispose() - { - Console.WriteLine("Disposing Simple Dependency"); - - _disposed.TrySetResult(true); - } - - public Task WasDisposed => _disposed.Task; - - public void DoSomething() - { - if (_disposed.Task.IsCompleted) - throw new ObjectDisposedException("Should not have disposed of me just yet"); - - if (_sendEndpointProvider != _consumeContext) - throw new InvalidOperationException("The injected types should be the same"); - - SomethingDone = true; - } - - public bool SomethingDone { get; private set; } - } -} +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + using System; + using System.Threading.Tasks; + using Util; + + + public class SimpleConsumerDependency : + ISimpleConsumerDependency, + IDisposable + { + readonly ConsumeContext _consumeContext; + readonly TaskCompletionSource _disposed; + readonly ISendEndpointProvider _sendEndpointProvider; + + public SimpleConsumerDependency(ISendEndpointProvider sendEndpointProvider, ConsumeContext consumeContext) + { + _sendEndpointProvider = sendEndpointProvider; + _consumeContext = consumeContext; + _disposed = TaskUtil.GetTask(); + } + + public void Dispose() + { + Console.WriteLine("Disposing Simple Dependency"); + + _disposed.TrySetResult(true); + } + + public Task WasDisposed => _disposed.Task; + + public void DoSomething() + { + if (_disposed.Task.IsCompleted) + throw new ObjectDisposedException("Should not have disposed of me just yet"); + + if (_sendEndpointProvider != _consumeContext) + throw new InvalidOperationException("The injected types should be the same"); + + SomethingDone = true; + } + + public bool SomethingDone { get; private set; } + } +} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/SimpleMessageClass.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleMessageClass.cs similarity index 77% rename from tests/MassTransit.Containers.Tests/Scenarios/SimpleMessageClass.cs rename to tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleMessageClass.cs index 324ace887a5..17c036ff61f 100644 --- a/tests/MassTransit.Containers.Tests/Scenarios/SimpleMessageClass.cs +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleMessageClass.cs @@ -1,13 +1,13 @@ -namespace MassTransit.Containers.Tests.Scenarios -{ - public class SimpleMessageClass : - SimpleMessageInterface - { - public SimpleMessageClass(string name) - { - Name = name; - } - - public string Name { get; set; } - } -} +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + public class SimpleMessageClass : + SimpleMessageInterface + { + public SimpleMessageClass(string name) + { + Name = name; + } + + public string Name { get; set; } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleMessageInterface.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleMessageInterface.cs new file mode 100644 index 00000000000..79548974839 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleMessageInterface.cs @@ -0,0 +1,7 @@ +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + public interface SimpleMessageInterface + { + string Name { get; } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/Scenarios/SimplePublishedInterface.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimplePublishedInterface.cs new file mode 100644 index 00000000000..63647746c28 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimplePublishedInterface.cs @@ -0,0 +1,7 @@ +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + public interface SimplePublishedInterface + { + string Name { get; } + } +} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/SimpleSaga.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleSaga.cs similarity index 97% rename from tests/MassTransit.Containers.Tests/Scenarios/SimpleSaga.cs rename to tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleSaga.cs index 845419ee452..af0a20049bb 100644 --- a/tests/MassTransit.Containers.Tests/Scenarios/SimpleSaga.cs +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/SimpleSaga.cs @@ -1,4 +1,4 @@ -namespace MassTransit.Containers.Tests.Scenarios +namespace MassTransit.Tests.ContainerTests.Scenarios { using System; using System.Linq.Expressions; diff --git a/tests/MassTransit.Tests/ContainerTests/Scenarios/ThirdSagaMessage.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/ThirdSagaMessage.cs new file mode 100644 index 00000000000..e0b16b29e7f --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/ThirdSagaMessage.cs @@ -0,0 +1,10 @@ +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + using System; + + + public class ThirdSagaMessage + { + public Guid CorrelationId { get; set; } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/Scenarios/WhenAllCompletedOrFaulted.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/WhenAllCompletedOrFaulted.cs new file mode 100644 index 00000000000..c36c56535e8 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/WhenAllCompletedOrFaulted.cs @@ -0,0 +1,84 @@ +namespace MassTransit.Tests.ContainerTests.Scenarios; + +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using TestFramework.Futures; +using TestFramework.Futures.Tests; + + +[TestFixture] +public class WhenAllCompletedOrFaulted : + BatchFuture_Specs +{ + [Test] + public async Task Delayed_success() + { + var batchId = NewId.NextGuid(); + var jobNumbers = new[] { "C12345", "Delay" }; + + var scope = Provider.CreateScope(); + + var client = scope.ServiceProvider.GetRequiredService>(); + + Response response = await client.GetResponse(new + { + CorrelationId = batchId, + JobNumbers = jobNumbers + }, timeout: RequestTimeout.After(s: 5)); + + Assert.That(response.Message.ProcessedJobsNumbers, Is.EqualTo(jobNumbers)); + } + + [Test] + public async Task Error_partially_uploaded() + { + var batchId = NewId.NextGuid(); + var jobNumbers = new[] { "C12345", "Error", "C54321", "Error", "C33454" }; + + var scope = Provider.CreateScope(); + + var client = scope.ServiceProvider.GetRequiredService>(); + + Response response = await client.GetResponse(new + { + CorrelationId = batchId, + JobNumbers = jobNumbers + }); + + switch (response) + { + case (_, BatchFaulted faulted): + //Batch is partially successful, downstream consumers are notified of succeeded uploads + Assert.That(faulted.ProcessedJobsNumbers, Is.EquivalentTo(new[] { "C12345", "C54321", "C33454" })); + break; + default: + Assert.Fail("Unexpected response"); + break; + } + } + + [Test] + public async Task Should_succeed() + { + var batchId = NewId.NextGuid(); + var jobNumbers = new[] { "C12345", "C54321" }; + + var scope = Provider.CreateScope(); + + var client = scope.ServiceProvider.GetRequiredService>(); + + Response response = await client.GetResponse(new + { + CorrelationId = batchId, + JobNumbers = jobNumbers + }, timeout: RequestTimeout.After(s: 5)); + + Assert.That(response.Message.ProcessedJobsNumbers, Is.EquivalentTo(jobNumbers)); + } + + public WhenAllCompletedOrFaulted() + : base(new InMemoryFutureTestFixtureConfigurator()) + { + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/Scenarios/When_registering_a_consumer.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/When_registering_a_consumer.cs new file mode 100644 index 00000000000..f0c14d10994 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/When_registering_a_consumer.cs @@ -0,0 +1,62 @@ +namespace MassTransit.Tests.ContainerTests.Scenarios +{ + using System.Threading.Tasks; + using NUnit.Framework; + using TestFramework; + + + [TestFixture] + public abstract class When_registering_a_consumer : + InMemoryTestFixture + { + [Test] + public async Task Should_receive_using_the_first_consumer() + { + const string name = "Joe"; + + await InputQueueSendEndpoint.Send(new SimpleMessageClass(name)); + + var lastConsumer = await SimpleConsumer.LastConsumer; + Assert.That(lastConsumer, Is.Not.Null); + + var last = await lastConsumer.Last; + Assert.That(last.Name, Is.EqualTo(name)); + + var wasDisposed = await lastConsumer.Dependency.WasDisposed; + Assert.Multiple(() => + { + Assert.That(wasDisposed, Is.True, "Dependency was not disposed"); + + Assert.That(lastConsumer.Dependency.SomethingDone, Is.True, "Dependency was disposed before consumer executed"); + }); + } + } + + + [TestFixture] + public abstract class When_registering_a_consumer_by_interface : + InMemoryTestFixture + { + [Test] + public async Task Should_receive_using_the_first_consumer() + { + const string name = "Joe"; + + await InputQueueSendEndpoint.Send(new SimpleMessageClass(name)); + + var lastConsumer = await SimpleConsumer.LastConsumer; + Assert.That(lastConsumer, Is.Not.Null); + + var last = await lastConsumer.Last; + Assert.That(last.Name, Is.EqualTo(name)); + + var wasDisposed = await lastConsumer.Dependency.WasDisposed; + Assert.Multiple(() => + { + Assert.That(wasDisposed, Is.True, "Dependency was not disposed"); + + Assert.That(lastConsumer.Dependency.SomethingDone, Is.True, "Dependency was disposed before consumer executed"); + }); + } + } +} diff --git a/tests/MassTransit.Containers.Tests/Scenarios/When_registering_a_saga.cs b/tests/MassTransit.Tests/ContainerTests/Scenarios/When_registering_a_saga.cs similarity index 86% rename from tests/MassTransit.Containers.Tests/Scenarios/When_registering_a_saga.cs rename to tests/MassTransit.Tests/ContainerTests/Scenarios/When_registering_a_saga.cs index a430d893aa0..a147456bedb 100644 --- a/tests/MassTransit.Containers.Tests/Scenarios/When_registering_a_saga.cs +++ b/tests/MassTransit.Tests/ContainerTests/Scenarios/When_registering_a_saga.cs @@ -1,11 +1,10 @@ -namespace MassTransit.Containers.Tests.Scenarios +namespace MassTransit.Tests.ContainerTests.Scenarios { using System; using System.Threading.Tasks; + using MassTransit.Testing; using NUnit.Framework; - using Shouldly; using TestFramework; - using Testing; public abstract class When_registering_a_saga : @@ -22,7 +21,7 @@ public async Task Should_have_a_subscription_for_the_first_saga_message() Guid? foundId = await GetSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId.HasValue, Is.True); } [Test] @@ -36,7 +35,7 @@ public async Task Should_have_a_subscription_for_the_second_saga_message() Guid? foundId = await GetSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId.HasValue, Is.True); var nextMessage = new SecondSagaMessage {CorrelationId = sagaId}; @@ -44,7 +43,7 @@ public async Task Should_have_a_subscription_for_the_second_saga_message() foundId = await GetSagaRepository().ShouldContainSaga(x => x.CorrelationId == sagaId && x.Second.IsCompleted, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId.HasValue, Is.True); } [Test] @@ -58,7 +57,7 @@ public async Task Should_have_a_subscription_for_the_third_saga_message() Guid? foundId = await GetSagaRepository().ShouldContainSaga(message.CorrelationId, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId.HasValue, Is.True); var nextMessage = new ThirdSagaMessage {CorrelationId = sagaId}; @@ -66,7 +65,7 @@ public async Task Should_have_a_subscription_for_the_third_saga_message() foundId = await GetSagaRepository().ShouldContainSaga(x => x.CorrelationId == sagaId && x.Third.IsCompleted, TestTimeout); - foundId.HasValue.ShouldBe(true); + Assert.That(foundId.HasValue, Is.True); } protected abstract ISagaRepository GetSagaRepository() diff --git a/tests/MassTransit.Tests/ContainerTests/Scheduler_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Scheduler_Specs.cs new file mode 100644 index 00000000000..3a3576ce896 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Scheduler_Specs.cs @@ -0,0 +1,185 @@ +namespace MassTransit.Tests.ContainerTests +{ + using System; + using System.Threading.Tasks; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using NUnit.Framework; + + + [TestFixture] + public class When_the_scheduler_is_used_with_a_scoped_filter + { + [Test] + public async Task Should_use_same_scope_for_send() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddDelayedMessageScheduler(); + + x.AddSagaStateMachine() + .InMemoryRepository(); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + cfg.UseSendFilter(typeof(SendFilter<>), context); + + cfg.ConfigureEndpoints(context); + }); + }) + .AddScoped() + .AddScoped(typeof(SendFilter<>)) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new { InVar.CorrelationId }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Consumed.Any(), Is.True); + Assert.That(await harness.Sent.Any(), Is.True); + }); + + ISentMessage message = await harness.Sent.SelectAsync().FirstOrDefault(); + Assert.That(message, Is.Not.Null); + + Assert.Multiple(() => + { + Assert.That(message.Context.Headers.TryGetHeader("Scoped-Value", out var value), Is.True); + Assert.That(value, Is.EqualTo("Correct scoped value")); + }); + } + + + public class TestStateMachine : + MassTransitStateMachine + { + public TestStateMachine() + { + InstanceState(x => x.CurrentState, Created); + + Event(() => InitiateEvent, x => x.CorrelateById(context => context.Message.CorrelationId)); + Schedule(() => ExpiredTimeout, x => x.ExpiredTimeoutToken, x => x.Received = r => r.CorrelateById(s => s.Message.CorrelationId)); + + Initially( + When(InitiateEvent) + .Activity(selector => selector.OfType()) + .SendAsync(_ => new Uri("queue:another-queue"), x => x.Init(x.Saga)) + .Schedule(ExpiredTimeout, x => x.Init(x.Saga), _ => TimeSpan.FromSeconds(10)) + .Finalize() + ); + + SetCompletedWhenFinalized(); + } + + public State Created { get; private set; } + + public Event InitiateEvent { get; private set; } + public Schedule ExpiredTimeout { get; set; } = null!; + } + + + public class SendFilter : + IFilter> + where TMessage : class + { + readonly ILogger _logger; + readonly ScopedService _scopedService; + + public SendFilter(ScopedService scopedService, ILogger> logger) + { + _scopedService = scopedService; + _logger = logger; + } + + public Task Send(SendContext context, IPipe> next) + { + _logger.LogInformation("Scoped value for {Message} is : {Value}", typeof(TMessage), _scopedService.ScopedValue); + + context.Headers.Set("Scoped-Value", _scopedService.ScopedValue ?? "NULL"); + + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + } + } + + + public class ScopedService + { + public string ScopedValue { get; set; } + } + + + public class SetScopedValueActivity : + IStateMachineActivity + { + readonly ILogger _logger; + readonly ScopedService _scopedService; + + public SetScopedValueActivity(ScopedService scopedService, ILogger logger) + { + _scopedService = scopedService; + _logger = logger; + } + + public async Task Execute(BehaviorContext context, IBehavior next) + { + _logger.LogInformation("Setting scoped value for {CorrelationId}", context.CorrelationId); + + _scopedService.ScopedValue = "Correct scoped value"; + + await next.Execute(context); + } + + public void Probe(ProbeContext context) + { + } + + public void Accept(StateMachineVisitor visitor) + { + } + + public Task Faulted(BehaviorExceptionContext context, IBehavior next) + where TException : Exception + { + return next.Faulted(context); + } + } + + + public class TestSaga : + SagaStateMachineInstance + { + public int CurrentState { get; set; } + public Guid? ExpiredTimeoutToken { get; set; } + public Guid CorrelationId { get; set; } + } + + + public interface ExpiredEvent : + CorrelatedBy + { + } + + + public interface InitiateEvent : + CorrelatedBy + { + } + + + public interface OutgoingEvent : + CorrelatedBy + { + } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/Stop_Specs.cs b/tests/MassTransit.Tests/ContainerTests/Stop_Specs.cs new file mode 100644 index 00000000000..fc9b7b5796a --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/Stop_Specs.cs @@ -0,0 +1,129 @@ +namespace MassTransit.Tests.ContainerTests +{ + using System; + using System.Threading; + using System.Threading.Tasks; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using TestFramework; + using TestFramework.Messages; + + + [TestFixture] + public class Stop_Specs : + InMemoryTestFixture + { + [Test] + public async Task Should_respect_ConsumerStopTimeout() + { + var collection = new ServiceCollection(); + collection.AddOptions().Configure(options => options.ConsumerStopTimeout = TimeSpan.FromSeconds(1)); + collection.AddMassTransitTestHarness(configurator => + { + configurator.AddTaskCompletionSource>(); + configurator.AddConsumer(); + }); + + await using var provider = collection.BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + await harness.Start(); + + await harness.Bus.Publish(new PingMessage(), harness.CancellationToken); + + await provider.GetTask>(); + + await harness.Stop(harness.CancellationToken); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + Assert.That(await harness.Consumed.Any(x => x.Exception is OperationCanceledException, cts.Token), Is.True); + } + + + class StuckConsumer : + IConsumer + { + readonly TaskCompletionSource> _taskCompletionSource; + + public StuckConsumer(TaskCompletionSource> taskCompletionSource) + { + _taskCompletionSource = taskCompletionSource; + } + + public Task Consume(ConsumeContext context) + { + _taskCompletionSource.TrySetResult(context); + return Task.Delay(TimeSpan.FromMinutes(10), context.CancellationToken); + } + } + } + + + [TestFixture] + public class Stop_Retry_Specs : + InMemoryTestFixture + { + [Test] + public async Task Should_respect_ConsumerStopTimeout() + { + var collection = new ServiceCollection(); + collection.AddOptions().Configure(options => options.ConsumerStopTimeout = TimeSpan.FromSeconds(1)); + collection.AddMassTransitTestHarness(configurator => + { + configurator.AddTaskCompletionSource>(); + configurator.AddConsumer(); + }); + + await using var provider = collection.BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + await harness.Start(); + + await harness.Bus.Publish(new PingMessage(), harness.CancellationToken); + + await provider.GetTask>(); + + await harness.Stop(harness.CancellationToken); + + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(3)); + Assert.That(await harness.Consumed.Any(x => x.Exception is OperationCanceledException, cts.Token), Is.True); + } + + + class StuckConsumer : + IConsumer + { + readonly TaskCompletionSource> _taskCompletionSource; + + public StuckConsumer(TaskCompletionSource> taskCompletionSource) + { + _taskCompletionSource = taskCompletionSource; + } + + public Task Consume(ConsumeContext context) + { + try + { + throw new ArgumentException("Expected error, causing retries"); + } + finally + { + _taskCompletionSource.TrySetResult(context); + } + } + } + + + class StuckConsumerDefinition : + ConsumerDefinition + { + protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) + { + endpointConfigurator.UseMessageRetry(r => r.Interval(10, TimeSpan.FromMinutes(1))); + } + } + } +} diff --git a/tests/MassTransit.Tests/ContainerTests/TenantScope_Specs.cs b/tests/MassTransit.Tests/ContainerTests/TenantScope_Specs.cs new file mode 100644 index 00000000000..341f0a4f7d6 --- /dev/null +++ b/tests/MassTransit.Tests/ContainerTests/TenantScope_Specs.cs @@ -0,0 +1,483 @@ +namespace MassTransit.Tests.ContainerTests +{ + using System; + using System.Threading.Tasks; + using MassTransit.Courier.Contracts; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using NUnit.Framework; + using TestFramework; + + + [TestFixture] + public class When_specifying_a_scoped_filter + { + [Test] + public async Task It_should_be_resolved_after_the_message_retry_filter() + { + await using var provider = new ServiceCollection() + .AddScoped() + .AddScoped() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.AddRequestClient(new Uri($"queue:{DefaultEndpointNameFormatter.Instance.Consumer()}")); + + x.AddConfigureEndpointsCallback((provider, name, cfg) => + { + cfg.UseConsumeFilter(typeof(TenantConsumeContextFilter<>), provider); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseMessageRetry(r => r.Immediate(10)); + cfg.UseSendFilter(typeof(SendTenantHeaderFilter<>), context); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + IRequestClient client = harness.GetRequestClient(); + + await client.GetResponse(new TenantRequest() { FailureCount = 2 }); + } + + [Test] + public async Task It_should_be_resolved_prior_to_resolving_the_activity() + { + await using var provider = new ServiceCollection() + .AddScoped() + .AddScoped() + .AddMassTransitTestHarness(x => + { + x.AddExecuteActivity(); + + x.AddConfigureEndpointsCallback((provider, name, cfg) => + { + cfg.UseExecuteActivityFilter(typeof(TenantExecuteContextFilter<>), provider); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseSendFilter(typeof(SendTenantHeaderFilter<>), context); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var builder = new RoutingSlipBuilder(NewId.NextGuid()); + builder.AddActivity("tenant", new Uri($"queue:{DefaultEndpointNameFormatter.Instance.ExecuteActivity()}")); + + await harness.Bus.Execute(builder.Build()); + + Assert.That(await harness.Published.Any(), Is.True); + } + + [Test] + public async Task It_should_be_resolved_prior_to_resolving_the_consumer() + { + await using var provider = new ServiceCollection() + .AddScoped() + .AddScoped() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + x.AddConfigureEndpointsCallback((provider, name, cfg) => + { + cfg.UseConsumeFilter(typeof(TenantConsumeContextFilter<>), provider); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UsePublishFilter(typeof(PublishTenantHeaderFilter<>), context); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + IRequestClient client = harness.GetRequestClient(); + + await client.GetResponse(new TenantRequest()); + } + + [Test] + public async Task It_should_be_resolved_prior_to_resolving_the_consumer_for_message_type() + { + await using var provider = new ServiceCollection() + .AddScoped() + .AddScoped() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + x.AddConfigureEndpointsCallback((context, name, cfg) => + { + cfg.UseConsumeFilter(context); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UsePublishFilter(typeof(PublishTenantHeaderFilter<>), context); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + IRequestClient client = harness.GetRequestClient(); + + await client.GetResponse(new TenantRequest()); + } + + [Test] + public async Task It_should_be_resolved_prior_to_resolving_the_consumer_for_message_type_publish() + { + await using var provider = new ServiceCollection() + .AddScoped() + .AddScoped() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + x.AddConfigureEndpointsCallback((context, name, cfg) => + { + cfg.UseConsumeFilter(context); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UsePublishFilter(context); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + IRequestClient client = harness.GetRequestClient(); + + await client.GetResponse(new TenantRequest()); + } + + [Test] + public async Task It_should_be_resolved_prior_to_resolving_the_consumer_with_send() + { + await using var provider = new ServiceCollection() + .AddScoped() + .AddScoped() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + x.AddRequestClient(new Uri($"queue:{DefaultEndpointNameFormatter.Instance.Consumer()}")); + + x.AddConfigureEndpointsCallback((provider, name, cfg) => + { + cfg.UseConsumeFilter(typeof(TenantConsumeContextFilter<>), provider); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseSendFilter(typeof(SendTenantHeaderFilter<>), context); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + IRequestClient client = harness.GetRequestClient(); + + await client.GetResponse(new TenantRequest()); + } + + + class TenantConsumer : + IConsumer + { + readonly FakeTenantDbContext _dbContext; + readonly TenantContext _tenantContext; + + public TenantConsumer(FakeTenantDbContext dbContext, TenantContext tenantContext, ILogger logger) + { + _dbContext = dbContext; + _tenantContext = tenantContext; + + logger.LogInformation($"{tenantContext.TenantId} - {dbContext.TenantId} Creating TenantConsumer"); + } + + public Task Consume(ConsumeContext context) + { + if (string.IsNullOrWhiteSpace(_tenantContext.TenantId)) + throw new InvalidOperationException("The tenantId was not present"); + + if (_tenantContext.TenantId != _dbContext.TenantId) + throw new InvalidOperationException("The tenantId was not properly initialized prior to consumer resolution"); + + if (context.Message.FailureCount > 0) + { + if (context.GetRetryCount() < context.Message.FailureCount) + throw new IntentionalTestException("Not yet, not yet."); + } + + return context.RespondAsync(new TenantResponse()); + } + } + + + public interface TenantArguments + { + } + + + class TenantActivity : + IExecuteActivity + { + readonly FakeTenantDbContext _dbContext; + readonly TenantContext _tenantContext; + + public TenantActivity(FakeTenantDbContext dbContext, TenantContext tenantContext, ILogger logger) + { + _dbContext = dbContext; + _tenantContext = tenantContext; + + logger.LogInformation($"{tenantContext.TenantId} - {dbContext.TenantId} Creating TenantActivity"); + } + + public async Task Execute(ExecuteContext context) + { + if (string.IsNullOrWhiteSpace(_tenantContext.TenantId)) + throw new InvalidOperationException("The tenantId was not present"); + + if (_tenantContext.TenantId != _dbContext.TenantId) + throw new InvalidOperationException("The tenantId was not properly initialized prior to consumer resolution"); + + return context.Completed(); + } + } + + + class TenantContext + { + public string TenantId { get; set; } + } + + + class FakeTenantDbContext + { + public FakeTenantDbContext(TenantContext tenantContext, ILogger logger) + { + logger.LogInformation($"{tenantContext.TenantId} Creating FakeTenantDbContext"); + + TenantId = tenantContext.TenantId; + } + + public string TenantId { get; } + } + + + class PublishTenantHeaderFilter : + IFilter> + where T : class + { + readonly ILogger> _logger; + + public PublishTenantHeaderFilter(ILogger> logger) + { + _logger = logger; + } + + public void Probe(ProbeContext context) + { + context.CreateFilterScope("PublishTenantHeaderFilter"); + } + + public Task Send(PublishContext context, IPipe> next) + { + var tenantId = NewId.NextGuid().ToString(); + _logger.LogInformation($"{tenantId} Setting header for tenant"); + context.Headers.Set("X-TenantId", tenantId); + + return next.Send(context); + } + } + + + class PublishTenantHeaderMessageFilter : + IFilter> + { + readonly ILogger _logger; + + public PublishTenantHeaderMessageFilter(ILogger logger) + { + _logger = logger; + } + + public void Probe(ProbeContext context) + { + context.CreateFilterScope("PublishTenantHeaderFilter"); + } + + public Task Send(PublishContext context, IPipe> next) + { + var tenantId = NewId.NextGuid().ToString(); + _logger.LogInformation($"{tenantId} Setting header for tenant"); + context.Headers.Set("X-TenantId", tenantId); + + return next.Send(context); + } + } + + + class SendTenantHeaderFilter : + IFilter> + where T : class + { + readonly ILogger> _logger; + + public SendTenantHeaderFilter(ILogger> logger) + { + _logger = logger; + } + + public void Probe(ProbeContext context) + { + context.CreateFilterScope("SendTenantHeaderFilter"); + } + + public Task Send(SendContext context, IPipe> next) + { + var tenantId = NewId.NextGuid().ToString(); + _logger.LogInformation($"{tenantId} Setting header for tenant"); + context.Headers.Set("X-TenantId", tenantId); + + return next.Send(context); + } + } + + + class TenantConsumeContextFilter : + IFilter> + where T : class + { + readonly ILogger> _logger; + readonly TenantContext _tenantContext; + + public TenantConsumeContextFilter(TenantContext tenantContext, ILogger> logger) + { + _tenantContext = tenantContext; + _logger = logger; + _logger.LogInformation("Creating TenantContextFilter"); + } + + public void Probe(ProbeContext context) + { + context.CreateScope("TenantConsumeContextFilter"); + } + + public Task Send(ConsumeContext context, IPipe> next) + { + var tenantId = context.Headers.Get("X-TenantId", string.Empty); + _tenantContext.TenantId = tenantId; + _logger.LogInformation($"{tenantId} Reading header for tenant"); + return next.Send(context); + } + } + + + class TenantConsumeContextFilter : + IFilter> + { + readonly ILogger _logger; + readonly TenantContext _tenantContext; + + public TenantConsumeContextFilter(TenantContext tenantContext, ILogger logger) + { + _tenantContext = tenantContext; + _logger = logger; + _logger.LogInformation("Creating TenantContextFilter"); + } + + public void Probe(ProbeContext context) + { + context.CreateScope("TenantConsumeContextFilter"); + } + + public Task Send(ConsumeContext context, IPipe> next) + { + var tenantId = context.Headers.Get("X-TenantId", string.Empty); + _tenantContext.TenantId = tenantId; + _logger.LogInformation($"{tenantId} Reading header for tenant"); + return next.Send(context); + } + } + + + class TenantExecuteContextFilter : + IFilter> + where T : class + { + readonly ILogger> _logger; + readonly TenantContext _tenantContext; + + public TenantExecuteContextFilter(TenantContext tenantContext, ILogger> logger) + { + _tenantContext = tenantContext; + _logger = logger; + _logger.LogInformation("Creating TenantContextFilter"); + } + + public void Probe(ProbeContext context) + { + context.CreateScope("TenantExecuteContextFilter"); + } + + public Task Send(ExecuteContext context, IPipe> next) + { + var tenantId = context.Headers.Get("X-TenantId", string.Empty); + _tenantContext.TenantId = tenantId; + _logger.LogInformation($"{tenantId} Reading header for tenant"); + return next.Send(context); + } + } + + + [Serializable] + class TenantRequest + { + public int? FailureCount { get; set; } + } + + + [Serializable] + class TenantResponse + { + } + } +} diff --git a/tests/MassTransit.Tests/Conventional/CustomConsumerMessageConvention.cs b/tests/MassTransit.Tests/Conventional/CustomConsumerMessageConvention.cs index c2cf85bcc76..a8640638431 100644 --- a/tests/MassTransit.Tests/Conventional/CustomConsumerMessageConvention.cs +++ b/tests/MassTransit.Tests/Conventional/CustomConsumerMessageConvention.cs @@ -12,18 +12,18 @@ class CustomConsumerMessageConvention : { public IEnumerable GetMessageTypes() { - var typeInfo = typeof(T).GetTypeInfo(); - if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() == typeof(IHandler<>)) + var consumerType = typeof(T); + if (consumerType.IsGenericType && consumerType.GetGenericTypeDefinition() == typeof(IHandler<>)) { - var interfaceType = new CustomConsumerInterfaceType(typeInfo.GetGenericArguments()[0], typeof(T)); + var interfaceType = new CustomConsumerInterfaceType(consumerType.GetGenericArguments()[0], consumerType); if (MessageTypeCache.IsValidMessageType(interfaceType.MessageType)) yield return interfaceType; } - IEnumerable types = typeof(T).GetInterfaces() - .Where(x => x.GetTypeInfo().IsGenericType) - .Where(x => x.GetTypeInfo().GetGenericTypeDefinition() == typeof(IHandler<>)) - .Select(x => new CustomConsumerInterfaceType(x.GetTypeInfo().GetGenericArguments()[0], typeof(T))) + IEnumerable types = consumerType.GetInterfaces() + .Where(x => x.IsGenericType) + .Where(x => x.GetGenericTypeDefinition() == typeof(IHandler<>)) + .Select(x => new CustomConsumerInterfaceType(x.GetGenericArguments()[0], consumerType)) .Where(x => MessageTypeCache.IsValidMessageType(x.MessageType)); foreach (var type in types) diff --git a/tests/MassTransit.Tests/ConversationId_Specs.cs b/tests/MassTransit.Tests/ConversationId_Specs.cs index 52a5fe2d453..e5c62fb3e80 100644 --- a/tests/MassTransit.Tests/ConversationId_Specs.cs +++ b/tests/MassTransit.Tests/ConversationId_Specs.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -19,7 +18,7 @@ public async Task Should_include_a_conversation_id() ConsumeContext context = await _handled; - context.ConversationId.HasValue.ShouldBe(true); + Assert.That(context.ConversationId.HasValue, Is.True); } Task> _handled; @@ -42,7 +41,7 @@ public async Task Should_include_a_conversation_id() ConsumeContext context = await _handled; - context.ConversationId.HasValue.ShouldBe(true); + Assert.That(context.ConversationId.HasValue, Is.True); } Task> _handled; @@ -67,13 +66,16 @@ public async Task Should_include_a_conversation_id() ConsumeContext context = await _handled; - context.ConversationId.HasValue.ShouldBe(true); + Assert.That(context.ConversationId.HasValue, Is.True); ConsumeContext responseContext = await responseHandled; - responseContext.ConversationId.HasValue.ShouldBe(true); + Assert.Multiple(() => + { + Assert.That(responseContext.ConversationId.HasValue, Is.True); - responseContext.ConversationId.ShouldBe(context.ConversationId); + Assert.That(responseContext.ConversationId, Is.EqualTo(context.ConversationId)); + }); } Task> _handled; @@ -104,11 +106,14 @@ public async Task Should_include_a_new_conversation_id() ConsumeContext responseContext = await responseHandled; - Assert.That(responseContext.ConversationId.HasValue); + Assert.Multiple(() => + { + Assert.That(responseContext.ConversationId.HasValue); - Assert.That(responseContext.ConversationId.Value, Is.Not.EqualTo(conversationId)); + Assert.That(responseContext.ConversationId.Value, Is.Not.EqualTo(conversationId)); - Assert.That(responseContext.Headers.Get(MessageHeaders.InitiatingConversationId), Is.EqualTo(conversationId)); + Assert.That(responseContext.Headers.Get(MessageHeaders.InitiatingConversationId), Is.EqualTo(conversationId)); + }); } Task> _handled; @@ -120,6 +125,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } } + [TestFixture] public class Starting_a_new_conversation_from_a_new_message : InMemoryTestFixture @@ -133,11 +139,14 @@ public async Task Should_not_capture_an_initiating_conversation_id() ConsumeContext context = await _handled; - Assert.That(context.ConversationId.HasValue); + Assert.Multiple(() => + { + Assert.That(context.ConversationId.HasValue); - Assert.That(context.ConversationId.Value, Is.EqualTo(conversationId)); + Assert.That(context.ConversationId.Value, Is.EqualTo(conversationId)); - Assert.That(context.Headers.Get(MessageHeaders.InitiatingConversationId), Is.Null); + Assert.That(context.Headers.Get(MessageHeaders.InitiatingConversationId), Is.Null); + }); } Task> _handled; diff --git a/tests/MassTransit.Tests/CorrelationId_Specs.cs b/tests/MassTransit.Tests/CorrelationId_Specs.cs index 645343a543b..9223df2546e 100644 --- a/tests/MassTransit.Tests/CorrelationId_Specs.cs +++ b/tests/MassTransit.Tests/CorrelationId_Specs.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -20,8 +19,11 @@ public async Task Should_include_a_correlation_id() ConsumeContext context = await _handled; - context.CorrelationId.HasValue.ShouldBe(true); - context.CorrelationId.Value.ShouldBe(pingMessage.CorrelationId); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(pingMessage.CorrelationId)); + }); } Task> _handled; @@ -46,8 +48,11 @@ public async Task Should_include_a_correlation_id() ConsumeContext context = await _handled; - context.CorrelationId.HasValue.ShouldBe(true); - context.CorrelationId.Value.ShouldBe(pingMessage.CorrelationId); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(pingMessage.CorrelationId)); + }); } Task> _handled; @@ -66,14 +71,17 @@ public class Sending_a_correlation_id_message : [Test] public async Task Should_include_a_correlation_id() { - var message = new A {CorrelationId = NewId.NextGuid()}; + var message = new A { CorrelationId = NewId.NextGuid() }; await InputQueueSendEndpoint.Send(message); ConsumeContext context = await _handled; - context.CorrelationId.HasValue.ShouldBe(true); - context.CorrelationId.Value.ShouldBe(message.CorrelationId); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(message.CorrelationId)); + }); } Task> _handled; @@ -98,14 +106,17 @@ public class Sending_a_command_id_message : [Test] public async Task Should_include_a_correlation_id() { - var message = new A {CommandId = NewId.NextGuid()}; + var message = new A { CommandId = NewId.NextGuid() }; await InputQueueSendEndpoint.Send(message); ConsumeContext context = await _handled; - context.CorrelationId.HasValue.ShouldBe(true); - context.CorrelationId.Value.ShouldBe(message.CommandId); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(message.CommandId)); + }); } Task> _handled; @@ -130,14 +141,17 @@ public class Sending_an_event_id_message : [Test] public async Task Should_include_a_correlation_id() { - var message = new A {EventId = NewId.NextGuid()}; + var message = new A { EventId = NewId.NextGuid() }; await InputQueueSendEndpoint.Send(message); ConsumeContext context = await _handled; - context.CorrelationId.HasValue.ShouldBe(true); - context.CorrelationId.Value.ShouldBe(message.EventId); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(message.EventId)); + }); } Task> _handled; @@ -163,14 +177,17 @@ public class Sending_a_nullable_correlation_id_message : [Test] public async Task Should_include_a_correlation_id() { - var message = new A {CorrelationId = NewId.NextGuid()}; + var message = new A { CorrelationId = NewId.NextGuid() }; await InputQueueSendEndpoint.Send(message); ConsumeContext context = await _handled; - context.CorrelationId.HasValue.ShouldBe(true); - context.CorrelationId.Value.ShouldBe(message.CorrelationId.Value); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(message.CorrelationId.Value)); + }); } Task> _handled; @@ -186,4 +203,46 @@ class A public Guid? CorrelationId { get; set; } } } + + + [TestFixture] + public class Sending_a_nullable_correlation_id_base_class_message : + InMemoryTestFixture + { + [Test] + public async Task Should_include_a_correlation_id() + { + var message = new A { CorrelationId = NewId.NextGuid() }; + + await InputQueueSendEndpoint.Send(message); + + ConsumeContext context = await _handled; + + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(message.CorrelationId.Value)); + }); + } + + Task> _handled; + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + _handled = Handled(configurator); + } + + + public interface IA + { + Guid? CorrelationId { get; } + } + + + class A : + IA + { + public Guid? CorrelationId { get; set; } + } + } } diff --git a/tests/MassTransit.Tests/Courier/ArgumentOverload_Specs.cs b/tests/MassTransit.Tests/Courier/ArgumentOverload_Specs.cs index b60ac015d74..2fdd380b635 100644 --- a/tests/MassTransit.Tests/Courier/ArgumentOverload_Specs.cs +++ b/tests/MassTransit.Tests/Courier/ArgumentOverload_Specs.cs @@ -18,9 +18,12 @@ public async Task Should_override_variables_in_the_routing_slip() { ConsumeContext context = await _completed; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.AreEqual("Used", context.GetVariable("Test")); + Assert.That(context.GetVariable("Test"), Is.EqualTo("Used")); + }); } [Test] @@ -28,7 +31,7 @@ public async Task Should_receive_the_routing_slip_activity_completed_event() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } Task> _completed; @@ -75,7 +78,7 @@ public async Task Should_receive_the_routing_slip_activity_completed_event() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -83,9 +86,12 @@ public async Task Should_use_variables_in_the_routing_slip() { ConsumeContext context = await _completed; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.AreEqual("Used", context.GetVariable("Test")); + Assert.That(context.GetVariable("Test"), Is.EqualTo("Used")); + }); } Task> _completed; @@ -128,7 +134,7 @@ public async Task Should_receive_the_routing_slip_activity_completed_event() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -136,9 +142,12 @@ public async Task Should_use_variables_in_the_routing_slip() { ConsumeContext context = await _completed; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.AreEqual("Used", context.GetVariable("Test")); + Assert.That(context.GetVariable("Test"), Is.EqualTo("Used")); + }); } Task> _completed; @@ -185,9 +194,12 @@ public async Task Should_receive_the_routing_slip_activity_completed_event() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.That(context.GetArgument("GuidValue"), Is.EqualTo(_trackingNumber)); + Assert.That(context.GetArgument("GuidValue"), Is.EqualTo(_trackingNumber)); + }); } [Test] @@ -195,9 +207,12 @@ public async Task Should_use_variables_in_the_routing_slip() { ConsumeContext context = await _completed; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.AreEqual("Used", context.GetVariable("Test")); + Assert.That(context.GetVariable("Test"), Is.EqualTo("Used")); + }); } Task> _completed; diff --git a/tests/MassTransit.Tests/Courier/DoubleActivity_Specs.cs b/tests/MassTransit.Tests/Courier/DoubleActivity_Specs.cs new file mode 100644 index 00000000000..4b301b0d850 --- /dev/null +++ b/tests/MassTransit.Tests/Courier/DoubleActivity_Specs.cs @@ -0,0 +1,187 @@ +#nullable enable +namespace MassTransit.Tests.Courier +{ + using System; + using System.Text.Json; + using System.Threading.Tasks; + using DoubleActivity; + using MassTransit.Courier.Contracts; + using MassTransit.Serialization; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + + + namespace DoubleActivity + { + using System; + using System.Text.Json; + using System.Text.Json.Serialization; + using System.Threading.Tasks; + + + public class PointActivityArguments + { + public Point Point { get; set; } = new Point(); + } + + + public class PointActivity : IExecuteActivity + { + public Task Execute(ExecuteContext context) + { + Console.WriteLine("Received point: " + context.Arguments.Point.X + "/" + context.Arguments.Point.Y); + return Task.FromResult(context.Completed()); + } + } + + + public class Point + { + public double X { get; set; } + public double Y { get; set; } + } + + + public class PointConverter : JsonConverter + { + public override Point? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException("Unexpected token, expected an object with x,y properties"); + + var originalDepth = reader.CurrentDepth; + + if (reader.TokenType == JsonTokenType.Null) + { + reader.Read(); + return null; + } + + reader.Read(); + + var x = double.NaN; + var y = double.NaN; + + while (reader.TokenType == JsonTokenType.PropertyName) + { + var name = reader.GetString()?.ToLower(); + reader.Read(); + + try + { + switch (name) + { + case "x": + if (reader.TokenType == JsonTokenType.Number) + x = reader.GetDouble(); + else + throw new JsonException($"{name.ToUpper()}-component was no number!"); + break; + case "y": + if (reader.TokenType == JsonTokenType.Number) + y = reader.GetDouble(); + else + throw new JsonException($"{name.ToUpper()}-component was no number!"); + break; + } + + reader.Read(); + } + catch (Exception e) when (e is InvalidOperationException || e is FormatException) + { + throw new JsonException($"{name?.ToUpper()}-component of the coordinate was missing or malformatted"); + } + } + + while (reader.TokenType != JsonTokenType.EndObject || reader.CurrentDepth != originalDepth) + reader.Read(); + + if (double.IsNaN(x)) + throw new JsonException("X-component of the coordiante was missing or malformatted"); + + if (double.IsNaN(y)) + throw new JsonException("Y-component of the coordiante was missing or malformatted"); + + return new Point + { + X = x, + Y = y + }; + } + + public override void Write(Utf8JsonWriter writer, Point? value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + writer.WriteNumber("x", value.X); + writer.WriteNumber("y", value.Y); + writer.WriteEndObject(); + } + } + } + + + [TestFixture] + public class DoubleActivity_Specs + { + [Test] + public async Task Should_properly_deserialize_the_juicy_double() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetKebabCaseEndpointNameFormatter(); + x.AddExecuteActivity(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var builder = new RoutingSlipBuilder(NewId.NextGuid()); + + builder.AddActivity( + nameof(PointActivity), + new Uri("queue:point_execute"), + new PointActivityArguments + { + Point = new Point + { + X = 1.2, + Y = 2.3 + } + }); + + var routingSlip = builder.Build(); + + await harness.Bus.Execute(routingSlip); + + Assert.That(await harness.Published.Any()); + } + + [SetUp] + public void Setup() + { + _options = SystemTextJsonMessageSerializer.Options; + + SystemTextJsonMessageSerializer.Options = new JsonSerializerOptions(SystemTextJsonMessageSerializer.Options); + SystemTextJsonMessageSerializer.Options.Converters.Add(new PointConverter()); + } + + [TearDown] + public void Teardown() + { + if (_options != null) + SystemTextJsonMessageSerializer.Options = _options; + } + + JsonSerializerOptions? _options; + } +} diff --git a/tests/MassTransit.Tests/Courier/FaultActivityEvent_Specs.cs b/tests/MassTransit.Tests/Courier/FaultActivityEvent_Specs.cs index 1c7b8bec918..6abdb23333a 100644 --- a/tests/MassTransit.Tests/Courier/FaultActivityEvent_Specs.cs +++ b/tests/MassTransit.Tests/Courier/FaultActivityEvent_Specs.cs @@ -19,7 +19,7 @@ public async Task Should_compensate_completed_activity() ConsumeContext compensated = await _activityCompensated; ConsumeContext completed = await _activityCompleted; - Assert.AreEqual(completed.Message.TrackingNumber, compensated.Message.TrackingNumber); + Assert.That(compensated.Message.TrackingNumber, Is.EqualTo(completed.Message.TrackingNumber)); } [Test] @@ -27,7 +27,7 @@ public async Task Should_compensate_first_activity() { ConsumeContext context = await _activityCompensated; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -35,7 +35,7 @@ public async Task Should_complete_activity_with_log() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual("Hello", context.GetResult("OriginalValue")); + Assert.That(context.GetResult("OriginalValue"), Is.EqualTo("Hello")); } [Test] @@ -43,7 +43,7 @@ public async Task Should_complete_first_activity() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -51,7 +51,7 @@ public async Task Should_complete_second_activity() { ConsumeContext context = await _secondActivityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -59,7 +59,7 @@ public async Task Should_complete_with_variable() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual("Knife", context.GetVariable("Variable")); + Assert.That(context.GetVariable("Variable"), Is.EqualTo("Knife")); } [Test] @@ -67,7 +67,7 @@ public async Task Should_fault_activity_with_variable() { ConsumeContext context = await _activityFaulted; - Assert.AreEqual("Knife", context.GetVariable("Variable")); + Assert.That(context.GetVariable("Variable"), Is.EqualTo("Knife")); } [Test] @@ -75,7 +75,7 @@ public async Task Should_fault_third_activity() { ConsumeContext context = await _activityFaulted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -83,7 +83,7 @@ public async Task Should_fault_with_variable() { ConsumeContext context = await _faulted; - Assert.AreEqual("Knife", context.GetVariable("Variable")); + Assert.That(context.GetVariable("Variable"), Is.EqualTo("Knife")); } Task> _faulted; diff --git a/tests/MassTransit.Tests/Courier/Fault_Specs.cs b/tests/MassTransit.Tests/Courier/Fault_Specs.cs index ae899f24177..d4165f9c4fb 100644 --- a/tests/MassTransit.Tests/Courier/Fault_Specs.cs +++ b/tests/MassTransit.Tests/Courier/Fault_Specs.cs @@ -153,7 +153,7 @@ public async Task Should_publish_the_faulted_routing_slip_event_and_set_variable ConsumeContext context = await handled; - Assert.AreEqual("Data", context.GetVariable("Test")); + Assert.That(context.GetVariable("Test"), Is.EqualTo("Data")); } protected override void SetupActivities(BusTestHarness testHarness) diff --git a/tests/MassTransit.Tests/Courier/FaultyRedeliveredActivityWithVariable_Specs.cs b/tests/MassTransit.Tests/Courier/FaultyRedeliveredActivityWithVariable_Specs.cs index 60c00083d06..37df4869d73 100644 --- a/tests/MassTransit.Tests/Courier/FaultyRedeliveredActivityWithVariable_Specs.cs +++ b/tests/MassTransit.Tests/Courier/FaultyRedeliveredActivityWithVariable_Specs.cs @@ -12,20 +12,6 @@ namespace MassTransit.Tests.Courier public class FaultyRedeliveredActivityVariables_Specs : InMemoryActivityTestFixture { - class MessageVariables - { - public string Test { get; set; } - } - - - class MessageWithVariables - { - public MessageVariables Variables { get; set; } - } - - - TaskCompletionSource> _received; - [Test] public async Task Should_publish_the_completed_event_and_redeliver() { @@ -43,9 +29,24 @@ public async Task Should_publish_the_completed_event_and_redeliver() await completed; var message = (await _received.Task).Message; - Assert.AreEqual("Data", message.Variables.Test); + Assert.That(message.Variables.Test, Is.EqualTo("Data")); } + + class MessageVariables + { + public string Test { get; set; } + } + + + class MessageWithVariables + { + public MessageVariables Variables { get; set; } + } + + + TaskCompletionSource> _received; + protected override void SetupActivities(BusTestHarness testHarness) { AddActivityContext(() => new SetVariablesFaultyActivity()); diff --git a/tests/MassTransit.Tests/Courier/FaultyRetriedActivityWithVariable_Specs.cs b/tests/MassTransit.Tests/Courier/FaultyRetriedActivityWithVariable_Specs.cs index cb6f02797c5..dc604ca9f52 100644 --- a/tests/MassTransit.Tests/Courier/FaultyRetriedActivityWithVariable_Specs.cs +++ b/tests/MassTransit.Tests/Courier/FaultyRetriedActivityWithVariable_Specs.cs @@ -12,20 +12,6 @@ namespace MassTransit.Tests.Courier public class FaultyRetriedActivityVariables_Specs : InMemoryActivityTestFixture { - class MessageVariables - { - public string Test { get; set; } - } - - - class MessageWithVariables - { - public MessageVariables Variables { get; set; } - } - - - TaskCompletionSource> _received; - [Test] public async Task Should_publish_the_completed_event_and_redeliver() { @@ -43,9 +29,24 @@ public async Task Should_publish_the_completed_event_and_redeliver() await completed; var message = (await _received.Task).Message; - Assert.AreEqual("Data", message.Variables.Test); + Assert.That(message.Variables.Test, Is.EqualTo("Data")); } + + class MessageVariables + { + public string Test { get; set; } + } + + + class MessageWithVariables + { + public MessageVariables Variables { get; set; } + } + + + TaskCompletionSource> _received; + protected override void SetupActivities(BusTestHarness testHarness) { AddActivityContext(() => new SetVariablesFaultyActivity()); diff --git a/tests/MassTransit.Tests/Courier/ItinerarySubscription_Specs.cs b/tests/MassTransit.Tests/Courier/ItinerarySubscription_Specs.cs index 166b9bd4aeb..76208b0020d 100644 --- a/tests/MassTransit.Tests/Courier/ItinerarySubscription_Specs.cs +++ b/tests/MassTransit.Tests/Courier/ItinerarySubscription_Specs.cs @@ -5,7 +5,6 @@ using MassTransit.Courier.Contracts; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Courier; @@ -30,7 +29,7 @@ public async Task Should_continue_with_the_source_itinerary() await _reviseActivityCompleted; ConsumeContext testActivityResult = await _testActivityCompleted; - testActivityResult.GetArgument("Value").ShouldBe("Added"); + Assert.That(testActivityResult.GetArgument("Value"), Is.EqualTo("Added")); ConsumeContext consumeContext = await _handled; diff --git a/tests/MassTransit.Tests/Courier/MessageDataArguments_Specs.cs b/tests/MassTransit.Tests/Courier/MessageDataArguments_Specs.cs index e9277fe0c6c..dc8114fc57f 100644 --- a/tests/MassTransit.Tests/Courier/MessageDataArguments_Specs.cs +++ b/tests/MassTransit.Tests/Courier/MessageDataArguments_Specs.cs @@ -20,9 +20,12 @@ public async Task Should_override_variables_in_the_routing_slip() { ConsumeContext context = await _completed; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.Multiple(() => + { + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); - Assert.AreEqual("Frank", context.GetVariable("Name")); + Assert.That(context.GetVariable("Name"), Is.EqualTo("Frank")); + }); } [Test] @@ -30,7 +33,7 @@ public async Task Should_receive_the_routing_slip_activity_completed_event() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } public Using_message_data_arguments() diff --git a/tests/MassTransit.Tests/Courier/ObjectGraph_Specs.cs b/tests/MassTransit.Tests/Courier/ObjectGraph_Specs.cs index 28416c18543..bf0ba65cc16 100644 --- a/tests/MassTransit.Tests/Courier/ObjectGraph_Specs.cs +++ b/tests/MassTransit.Tests/Courier/ObjectGraph_Specs.cs @@ -8,7 +8,6 @@ using MassTransit.Courier.Contracts; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Courier; @@ -53,13 +52,11 @@ public async Task Should_work_for_activity_arguments() if (faulted.Status == TaskStatus.RanToCompletion) { - Assert.Fail("Failed due to exception {0}", faulted.Result.Message.ActivityExceptions.Any() - ? faulted.Result.Message.ActivityExceptions.First() - .ExceptionInfo.Message - : "VisitUnknownFilter"); + Assert.Fail( + $"Failed due to exception {(faulted.Result.Message.ActivityExceptions.Any() ? faulted.Result.Message.ActivityExceptions.First().ExceptionInfo.Message : "VisitUnknownFilter")}"); } - completed.Status.ShouldBe(TaskStatus.RanToCompletion); + Assert.That(completed.Status, Is.EqualTo(TaskStatus.RanToCompletion)); } int _intValue; @@ -98,13 +95,11 @@ public async Task Should_not_lose_their_default_value() if (faulted.Status == TaskStatus.RanToCompletion) { - Assert.Fail("Failed due to exception {0}", faulted.Result.Message.ActivityExceptions.Any() - ? faulted.Result.Message.ActivityExceptions.First() - .ExceptionInfo.Message - : "VisitUnknownFilter"); + Assert.Fail( + $"Failed due to exception {(faulted.Result.Message.ActivityExceptions.Any() ? faulted.Result.Message.ActivityExceptions.First().ExceptionInfo.Message : "VisitUnknownFilter")}"); } - completed.Status.ShouldBe(TaskStatus.RanToCompletion); + Assert.That(completed.Status, Is.EqualTo(TaskStatus.RanToCompletion)); } protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) diff --git a/tests/MassTransit.Tests/Courier/ReviseItinerary_Specs.cs b/tests/MassTransit.Tests/Courier/ReviseItinerary_Specs.cs index c9de74a2bbf..109325c0634 100644 --- a/tests/MassTransit.Tests/Courier/ReviseItinerary_Specs.cs +++ b/tests/MassTransit.Tests/Courier/ReviseItinerary_Specs.cs @@ -5,7 +5,6 @@ using MassTransit.Courier.Contracts; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Courier; @@ -44,7 +43,7 @@ public async Task Should_complete_the_additional_item() await reviseActivityCompleted; ConsumeContext revisions = await revised; - Assert.AreEqual(0, revisions.Message.DiscardedItinerary.Length); + Assert.That(revisions.Message.DiscardedItinerary, Is.Empty); } [Test] @@ -73,7 +72,7 @@ public async Task Should_continue_with_the_source_itinerary() await reviseActivityCompleted; ConsumeContext testActivityResult = await testActivityCompleted; - testActivityResult.GetArgument("Value").ShouldBe("Added"); + Assert.That(testActivityResult.GetArgument("Value"), Is.EqualTo("Added")); } [Test] @@ -106,10 +105,13 @@ public async Task Should_immediately_complete_an_empty_list() await reviseActivityCompleted; ConsumeContext revisions = await revised; - Assert.AreEqual(1, revisions.Message.DiscardedItinerary.Length); - Assert.AreEqual(0, revisions.Message.Itinerary.Length); + Assert.Multiple(() => + { + Assert.That(revisions.Message.DiscardedItinerary, Has.Length.EqualTo(1)); + Assert.That(revisions.Message.Itinerary, Is.Empty); - testActivityCompleted.Wait(TimeSpan.FromSeconds(3)).ShouldBe(false); + Assert.That(testActivityCompleted.Wait(TimeSpan.FromSeconds(3)), Is.False); + }); } protected override void SetupActivities(BusTestHarness testHarness) diff --git a/tests/MassTransit.Tests/Courier/SendEvent_Specs.cs b/tests/MassTransit.Tests/Courier/SendEvent_Specs.cs index 81e69eccfe2..3b094c031b4 100644 --- a/tests/MassTransit.Tests/Courier/SendEvent_Specs.cs +++ b/tests/MassTransit.Tests/Courier/SendEvent_Specs.cs @@ -100,8 +100,11 @@ await builder.AddSubscription(Bus.Address, RoutingSlipEvents.Completed, x => x.S ConsumeContext context = await myCompleted; - Assert.That(context.Message.Timestamp, Is.GreaterThanOrEqualTo(startTime)); - Assert.That(context.Message.SomeValue, Is.EqualTo("Hello")); + Assert.Multiple(() => + { + Assert.That(context.Message.Timestamp, Is.GreaterThanOrEqualTo(startTime)); + Assert.That(context.Message.SomeValue, Is.EqualTo("Hello")); + }); } protected override void SetupActivities(BusTestHarness testHarness) diff --git a/tests/MassTransit.Tests/Courier/SingleActivityEvent_Specs.cs b/tests/MassTransit.Tests/Courier/SingleActivityEvent_Specs.cs index 476959446d4..66849a8d2a4 100644 --- a/tests/MassTransit.Tests/Courier/SingleActivityEvent_Specs.cs +++ b/tests/MassTransit.Tests/Courier/SingleActivityEvent_Specs.cs @@ -18,7 +18,7 @@ public async Task Should_receive_the_routing_slip_activity_completed_event() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -26,7 +26,7 @@ public async Task Should_receive_the_routing_slip_activity_log() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_originalValue, context.GetResult("OriginalValue")); + Assert.That(context.GetResult("OriginalValue"), Is.EqualTo(_originalValue)); } [Test] @@ -34,7 +34,7 @@ public async Task Should_receive_the_routing_slip_activity_variable() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual("Knife", context.GetVariable("Variable")); + Assert.That(context.GetVariable("Variable"), Is.EqualTo("Knife")); } [Test] @@ -42,7 +42,7 @@ public async Task Should_receive_the_routing_slip_completed_event() { ConsumeContext context = await _completed; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -51,7 +51,7 @@ public async Task Should_receive_the_routing_slip_timestamps() ConsumeContext context = await _activityCompleted; ConsumeContext completeContext = await _completed; - Assert.AreEqual(completeContext.Message.Timestamp, context.Message.Timestamp + context.Message.Duration); + Assert.That(context.Message.Timestamp + context.Message.Duration, Is.EqualTo(completeContext.Message.Timestamp)); Console.WriteLine("Duration: {0}", context.Message.Duration); } @@ -61,7 +61,7 @@ public async Task Should_receive_the_routing_slip_variable() { ConsumeContext context = await _completed; - Assert.AreEqual("Knife", context.GetVariable("Variable")); + Assert.That(context.GetVariable("Variable"), Is.EqualTo("Knife")); } Task> _completed; @@ -106,7 +106,7 @@ public async Task Should_receive_the_routing_slip_activity_completed_event() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -114,7 +114,7 @@ public async Task Should_receive_the_routing_slip_activity_log() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual("Hello", context.GetResult("OriginalValue")); + Assert.That(context.GetResult("OriginalValue"), Is.EqualTo("Hello")); } [Test] @@ -122,7 +122,7 @@ public async Task Should_receive_the_routing_slip_activity_variable() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual("Knife", context.GetVariable("Variable")); + Assert.That(context.GetVariable("Variable"), Is.EqualTo("Knife")); } [Test] @@ -130,7 +130,7 @@ public async Task Should_receive_the_routing_slip_completed_event() { ConsumeContext context = await _completed; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -139,7 +139,7 @@ public async Task Should_receive_the_routing_slip_timestamps() ConsumeContext context = await _activityCompleted; ConsumeContext completeContext = await _completed; - Assert.AreEqual(completeContext.Message.Timestamp, context.Message.Timestamp + context.Message.Duration); + Assert.That(context.Message.Timestamp + context.Message.Duration, Is.EqualTo(completeContext.Message.Timestamp)); Console.WriteLine("Duration: {0}", context.Message.Duration); } @@ -149,7 +149,7 @@ public async Task Should_receive_the_routing_slip_variable() { ConsumeContext context = await _completed; - Assert.AreEqual("Knife", context.GetVariable("Variable")); + Assert.That(context.GetVariable("Variable"), Is.EqualTo("Knife")); } Task> _completed; diff --git a/tests/MassTransit.Tests/Courier/Subscription_Specs.cs b/tests/MassTransit.Tests/Courier/Subscription_Specs.cs index 2ded6e75125..32dc50369e1 100644 --- a/tests/MassTransit.Tests/Courier/Subscription_Specs.cs +++ b/tests/MassTransit.Tests/Courier/Subscription_Specs.cs @@ -6,7 +6,9 @@ using MassTransit.Courier.Contracts; using MassTransit.Serialization; using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; + using Scenario; using TestFramework; using TestFramework.Courier; @@ -42,7 +44,7 @@ public void Should_serialize_properly() var loaded = TestExtensionsForJson.GetRoutingSlip(jsonString); - Assert.AreEqual(RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted, loaded.Subscriptions[0].Events); + Assert.That(loaded.Subscriptions[0].Events, Is.EqualTo(RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted)); } } @@ -56,7 +58,7 @@ public async Task Should_not_receive_the_routing_slip_activity_log() { ConsumeContext context = await _activityCompleted; - Assert.IsFalse(context.Message.Data.ContainsKey("OriginalValue")); + Assert.That(context.Message.Data.ContainsKey("OriginalValue"), Is.False); } [Test] @@ -64,7 +66,7 @@ public async Task Should_not_receive_the_routing_slip_activity_variable() { ConsumeContext context = await _activityCompleted; - Assert.IsFalse(context.Message.Variables.ContainsKey("Variable")); + Assert.That(context.Message.Variables.ContainsKey("Variable"), Is.False); } [Test] @@ -72,7 +74,7 @@ public async Task Should_receive_the_routing_slip_activity_completed_event() { ConsumeContext context = await _activityCompleted; - Assert.AreEqual(_trackingNumber, context.Message.TrackingNumber); + Assert.That(context.Message.TrackingNumber, Is.EqualTo(_trackingNumber)); } [Test] @@ -89,7 +91,7 @@ public void Should_serialize_and_deserialize_properly() var loaded = TestExtensionsForJson.GetRoutingSlip(jsonString); - Assert.AreEqual(RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted, loaded.Subscriptions[0].Events); + Assert.That(loaded.Subscriptions[0].Events, Is.EqualTo(RoutingSlipEvents.Completed | RoutingSlipEvents.Faulted)); } Task> _completed; @@ -119,4 +121,63 @@ protected override void SetupActivities(BusTestHarness testHarness) AddActivityContext(() => new TestActivity()); } } + + + [TestFixture(typeof(Json))] + [TestFixture(typeof(RawJson))] + [TestFixture(typeof(NewtonsoftJson))] + [TestFixture(typeof(NewtonsoftRawJson))] + public class Adding_a_custom_routing_slip_event_subscription + where T : new() + { + [Test] + public async Task Should_be_sent() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddHandler(); + x.AddActivity(); + + x.UsingInMemory(((context, cfg) => + { + _configuration?.ConfigureBus(context, cfg); + + cfg.ConfigureEndpoints(context); + })); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var trackingNumber = NewId.NextGuid(); + var builder = new RoutingSlipBuilder(trackingNumber); + await builder.AddSubscription(harness.GetHandlerAddress(), RoutingSlipEvents.Completed, RoutingSlipEventContents.All, + x => x.Send(new { Value = "Secret Value" })); + + builder.AddActivity("TestActivity", harness.GetExecuteActivityAddress(), new { Value = "Hello" }); + + await harness.Bus.Execute(builder.Build()); + + IReceivedMessage completed = await harness.Consumed + .SelectAsync(x => x.Context.Message.TrackingNumber == trackingNumber).FirstOrDefault(); + Assert.That(completed, Is.Not.Null); + + Assert.That(completed.Context.Message.Value, Is.EqualTo("Secret Value")); + } + + readonly ITestBusConfiguration _configuration; + + public Adding_a_custom_routing_slip_event_subscription() + { + _configuration = new T() as ITestBusConfiguration; + } + + + public interface RegistrationCompleted : + RoutingSlipCompleted + { + string Value { get; } + } + } } diff --git a/tests/MassTransit.Tests/Courier/TwoActivityEvent_Specs.cs b/tests/MassTransit.Tests/Courier/TwoActivityEvent_Specs.cs index e16a3218cf9..57791d5dfd3 100644 --- a/tests/MassTransit.Tests/Courier/TwoActivityEvent_Specs.cs +++ b/tests/MassTransit.Tests/Courier/TwoActivityEvent_Specs.cs @@ -18,7 +18,7 @@ public async Task Should_include_the_activity_log_data() { ConsumeContext activityCompleted = await _firstActivityCompleted; - Assert.AreEqual("Hello", activityCompleted.GetResult("OriginalValue")); + Assert.That(activityCompleted.GetResult("OriginalValue"), Is.EqualTo("Hello")); } [Test] @@ -26,7 +26,7 @@ public async Task Should_include_the_variable_set_by_the_activity() { ConsumeContext completed = await _completed; - Assert.AreEqual("Hello, World!", completed.GetVariable("Value")); + Assert.That(completed.GetVariable("Value"), Is.EqualTo("Hello, World!")); } [Test] @@ -34,7 +34,7 @@ public async Task Should_include_the_variables_of_the_completed_routing_slip() { ConsumeContext completed = await _completed; - Assert.AreEqual("Knife", completed.GetVariable("Variable")); + Assert.That(completed.GetVariable("Variable"), Is.EqualTo("Knife")); } [Test] @@ -42,7 +42,7 @@ public async Task Should_include_the_variables_with_the_activity_log() { ConsumeContext activityCompleted = await _firstActivityCompleted; - Assert.AreEqual("Knife", activityCompleted.GetVariable("Variable")); + Assert.That(activityCompleted.GetVariable("Variable"), Is.EqualTo("Knife")); } [Test] @@ -50,7 +50,7 @@ public async Task Should_receive_the_first_routing_slip_activity_completed_event { var activityCompleted = (await _firstActivityCompleted).Message; - Assert.AreEqual(_routingSlip.TrackingNumber, activityCompleted.TrackingNumber); + Assert.That(activityCompleted.TrackingNumber, Is.EqualTo(_routingSlip.TrackingNumber)); } [Test] @@ -58,11 +58,11 @@ public async Task Should_receive_the_routing_slip_completed_event() { var completed = (await _completed).Message; - Assert.AreEqual(_routingSlip.TrackingNumber, completed.TrackingNumber); + Assert.That(completed.TrackingNumber, Is.EqualTo(_routingSlip.TrackingNumber)); Console.WriteLine("Duration: {0}", completed.Duration); - Assert.IsFalse(completed.Variables.ContainsKey("ToBeRemoved")); + Assert.That(completed.Variables.ContainsKey("ToBeRemoved"), Is.False); } [Test] @@ -70,7 +70,7 @@ public async Task Should_receive_the_second_routing_slip_activity_completed_even { var activityCompleted = (await _secondActivityCompleted).Message; - Assert.AreEqual(_routingSlip.TrackingNumber, activityCompleted.TrackingNumber); + Assert.That(activityCompleted.TrackingNumber, Is.EqualTo(_routingSlip.TrackingNumber)); } Task> _completed; diff --git a/tests/MassTransit.Tests/Courier/UriArgument_Specs.cs b/tests/MassTransit.Tests/Courier/UriArgument_Specs.cs index 8e8fa056f09..de32bad2792 100644 --- a/tests/MassTransit.Tests/Courier/UriArgument_Specs.cs +++ b/tests/MassTransit.Tests/Courier/UriArgument_Specs.cs @@ -34,11 +34,11 @@ public async Task Should_compensate_with_the_log() ConsumeContext consumeContext = await activity; - Assert.AreEqual(new Uri("http://google.com/"), consumeContext.GetResult("UsedAddress")); + Assert.That(consumeContext.GetResult("UsedAddress"), Is.EqualTo("http://google.com/")); ConsumeContext context = await activityCompensated; - Assert.AreEqual(new Uri("http://google.com/"), context.GetResult("UsedAddress")); + Assert.That(context.GetResult("UsedAddress"), Is.EqualTo("http://google.com/")); } [Test] @@ -61,7 +61,7 @@ public async Task Should_publish_the_completed_event() ConsumeContext consumeContext = await activity; - Assert.AreEqual(new Uri("http://google.com/"), consumeContext.GetResult("UsedAddress")); + Assert.That(consumeContext.GetResult("UsedAddress"), Is.EqualTo("http://google.com/")); } protected override void SetupActivities(BusTestHarness testHarness) diff --git a/tests/MassTransit.Tests/CronExpressionTests.cs b/tests/MassTransit.Tests/CronExpressionTests.cs new file mode 100644 index 00000000000..e38f0a6f9dd --- /dev/null +++ b/tests/MassTransit.Tests/CronExpressionTests.cs @@ -0,0 +1,972 @@ +namespace MassTransit.Tests; + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using JobService.Scheduling; +using NUnit.Framework; + + +public class CronExpressionTest +{ + static readonly TimeZoneInfo TestTimeZone = TimeZoneInfo.Local; + + /// + /// Test method for 'CronExpression.IsSatisfiedBy(DateTime)'. + /// + [Test] + public void TestIsSatisfiedBy() + { + var cronExpression = new CronExpression("0 15 10 * * ? 2005"); + + var cal = new DateTime(2005, 6, 1, 10, 15, 0).ToUniversalTime(); + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.True); + + cal = cal.AddYears(1); + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.False); + + cal = new DateTime(2005, 6, 1, 10, 16, 0).ToUniversalTime(); + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.False); + + cal = new DateTime(2005, 6, 1, 10, 14, 0).ToUniversalTime(); + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.False); + + cronExpression = new CronExpression("0 15 10 ? * MON-FRI"); + + // weekends + cal = new DateTime(2007, 6, 9, 10, 15, 0).ToUniversalTime(); + Assert.Multiple(() => + { + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.False); + Assert.That(cronExpression.IsSatisfiedBy(cal.AddDays(1)), Is.False); + }); + } + + [Test] + public void TestLastDayOffset() + { + var cronExpression = new CronExpression("0 15 10 L-2 * ? 2010"); + + var cal = new DateTime(2010, 10, 29, 10, 15, 0).ToUniversalTime(); // last day - 2 + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.True); + + cal = new DateTime(2010, 10, 28, 10, 15, 0).ToUniversalTime(); + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.False); + + cronExpression = new CronExpression("0 15 10 L-5W * ? 2010"); + + cal = new DateTime(2010, 10, 26, 10, 15, 0).ToUniversalTime(); // last day - 5 + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.True); + + cronExpression = new CronExpression("0 15 10 L-1 * ? 2010"); + + cal = new DateTime(2010, 10, 30, 10, 15, 0).ToUniversalTime(); // last day - 1 + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.True); + + cronExpression = new CronExpression("0 15 10 L-1W * ? 2010"); + + cal = new DateTime(2010, 10, 29, 10, 15, 0).ToUniversalTime(); // nearest weekday to last day - 1 (29th is a friday in 2010) + Assert.That(cronExpression.IsSatisfiedBy(cal), Is.True); + } + + [TestCase("0 15 10 6,15 * ? 2010", "0 15 10 6,15 * ? 2010")] + public void ExpressionToString(string cronExpression, string expected) + { + var expr = new CronExpression(cronExpression); + Assert.That(expr.ToString(), Is.EqualTo(expected)); + } + + [TestCase("0 15 10 L-1,L-2 * ? 2010", new[] { 31 - 1, 31 - 2 })] //Multiple L Not supported + public void CannotUseMultipleLastDayOfMonthInArray(string cronExpression, int[] expectedDays, string scenario = "") + { + Action act = () => new CronExpression(cronExpression); //10:15am October 2010 + Assert.That(() => act(), Throws.InstanceOf().With.Message.EqualTo( + "Support for specifying 'L' with other days of the month is limited to one instance of L")); + } + + [TestCase("0 15 10 6,15,LW * ? 2010", new[] { 6, 15, 29 })] //31 oct 2010 is a Sunday, week day would be 29 + [TestCase("0 15 10 6,15,L * ? 2010", new[] { 6, 15, 31 })] + [TestCase("0 15 10 15,L * ? 2010", new[] { 15, 31 })] + [TestCase("0 15 10 15,31 * ? 2010", new[] { 15, 31 })] + [TestCase("0 15 10 15,L-2 * ? 2010", new[] { 15, 31 - 2 })] + [TestCase("0 15 10 31,L-2 * ? 2010", new[] { 31 }, "duplicate day specified + last are equal")] + [TestCase("0 15 10 1,3,6,15,L * ? 2010", new[] { 1, 3, 6, 15, 31 })] + [TestCase("0 15 10 15,LW-2 * ? 2010", new[] { 15, 29 - 2 })] //29 is last week day + public void CanUseLastDayOfMonthInArray(string cronExpression, int[] expectedDays, string scenario = "") + { + var expr = new CronExpression(cronExpression); //10:15am October 2010 + + foreach (var expectedDay in expectedDays) + { + var date = new DateTime(2010, 10, expectedDay, 10, 15, 0).ToUniversalTime(); // last day + Assert.That(expr.IsSatisfiedBy(date), Is.True, $"expected day of {expectedDay}, {scenario}"); + } + } + + int[] CreateArrayOfDays(int year, int month) + { + var daysInMonth = DateTime.DaysInMonth(year, month); + var numbers = new List(); + for (var i = 0; i < daysInMonth; i++) + numbers.Add(i + 1); + + return numbers.ToArray(); + } + + [TestCase("0 15 10 5/5 * MON 2010", new[] { 4, 11, 18, 25, 5, 10, 15, 20, 25, 30 }, + "10:15am every 5th day of the month from 5 to 31, and on Mondays in October 2010")] + [TestCase("0 15 10 3 * MON,THU,FRI 2010", new[] { 1, 3, 4, 11, 18, 25, 7, 14, 21, 28, 8, 15, 22, 29 }, + "10:15am 3rd of month and every mon,thu,fri October 2010")] + [TestCase("0 15 10 1,2,3,4,5,6 * MON,THU,FRI 2010", new[] { 1, 2, 3, 4, 5, 6, 11, 18, 25, 7, 14, 21, 28, 8, 15, 22, 29 }, + "10:15am 1-6th of mon and every Mon,Thu,Fri October 2010")] + [TestCase("0 15 10 * * MON,THU,FRI 2010", + new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 }, + "10:15am EveryDay of Month October 2010, Wildcard specified")] + [TestCase("0 15 10 1 * * 2010", new[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31 }, + "10:15am Every Day of Month October 2010, Wildcard specified")] + public void CanUse_DayOfMonth_And_DayOfWeek_Together(string cronExpression, int[] expectedDays, string scenario = "") + { + var expr = new CronExpression(cronExpression); + var templateDate = new DateTime(2010, 10, 1, 10, 15, 0).ToUniversalTime(); + + foreach (var day in expectedDays) + { + var date = new DateTime(templateDate.Year, templateDate.Month, day, templateDate.Hour, templateDate.Minute, templateDate.Second, templateDate.Kind); + Assert.That(expr.IsSatisfiedBy(date), Is.True, $"expected day of {day}, {scenario}"); + } + + IEnumerable invalidDays = CreateArrayOfDays(2010, 10).Except(expectedDays); + + foreach (var day in invalidDays) + { + var date = new DateTime(templateDate.Year, templateDate.Month, day, templateDate.Hour, templateDate.Minute, templateDate.Second, templateDate.Kind); + Assert.That(expr.IsSatisfiedBy(date), Is.False, $"invalid day of {day}, {scenario}"); + } + } + + [TestCase("0 15 10 LW-2 * ? 2010", 27, "31 Oct 2010 is Sunday, last-weekday (LW) is 29 (FRI) -2 Offset")] + [TestCase("0 15 10 LW-5 * ? 2010", 24, "31 Oct 2010 is Sunday, last-weekday (LW) is 29 (FRI) -5 Offset")] + [TestCase("0 15 10 LW-7 * ? 2010", 22, "31 Oct 2010 is Sunday, last-weekday (LW) is 29 (FRI) -7 Offset")] + [TestCase("0 15 10 LW-28 * ? 2010", 1, "31 Oct 2010 is Sunday, last-weekday (LW) is 29 (FRI) -28 Offset")] + [TestCase("0 15 10 LW-29 * ? 2010", 1, "31 Oct 2010 is Sunday, last-weekday (LW) is 29 (FRI) -29 Offset fallback to 1st of month")] + [TestCase("0 15 10 LW-30 * ? 2010", 1, "31 Oct 2010 is Sunday, last-weekday (LW) is 29 (FRI) -30 Offset fallback to 1st of month")] + public void LastWeekDayWithOffset(string cronExpression, int expectedDay, string reason) + { + var expr = new CronExpression(cronExpression); + var date = new DateTime(2010, 10, expectedDay, 10, 15, 0).ToUniversalTime(); // last day + Assert.That(expr.IsSatisfiedBy(date), Is.True, reason); + } + + [TestCase("0 15 10 ? * 1#0 2010", false)] + [TestCase("0 15 10 ? * 1#1 2010", true)] + [TestCase("0 15 10 ? * 1#2 2010", true)] + [TestCase("0 15 10 ? * 1#3 2010", true)] + [TestCase("0 15 10 ? * 1#4 2010", true)] + [TestCase("0 15 10 ? * 1#5 2010", true)] + [TestCase("0 15 10 ? * 1#6 2010", false)] + public void Ensure_NthWeek_IsBetween1And5(string expression, bool isValid) + { + Action act = () => new CronExpression(expression); //10:15am October 2010 + if (isValid) + Assert.That(() => act(), Throws.Nothing); + else + Assert.That(() => act(), Throws.InstanceOf()); + } + + [TestCase("0 15 10 ? * 0#1 2010", false)] + [TestCase("0 15 10 ? * 1#1 2010", true, "2010-01-03T10:15:00")] + [TestCase("0 15 10 ? * 2#1 2010", true, "2010-01-04T10:15:00")] + [TestCase("0 15 10 ? * 3#1 2010", true, "2010-01-05T10:15:00")] + [TestCase("0 15 10 ? * 4#1 2010", true, "2010-01-06T10:15:00")] + [TestCase("0 15 10 ? * 5#1 2010", true, "2010-01-07T10:15:00")] + [TestCase("0 15 10 ? * 6#1 2010", true, "2010-01-01T10:15:00")] + [TestCase("0 15 10 ? * 7#1 2010", true, "2010-01-02T10:15:00")] + [TestCase("0 15 10 ? * 8#1 2010", false)] + [TestCase("0 15 10 ? * 14#1 2010", false)] + public void Ensure_NthWeek_Day_IsBetween1And7(string expression, bool isValid, string shouldSatisfyDate = null) + { + Action act = () => new CronExpression(expression); + if (isValid) + { + Assert.That(() => act(), Throws.Nothing); + + var exp = new CronExpression(expression); + if (!string.IsNullOrEmpty(shouldSatisfyDate)) + { + var dt = DateTime.Parse(shouldSatisfyDate); + Assert.That(exp.IsSatisfiedBy(new DateTimeOffset(dt))); + } + } + else + Assert.That(() => act(), Throws.InstanceOf()); + } + + [TestCase("0 15 10 6,15,LW * ? 2010")] + [TestCase("0 15 10 6,15,L * ? 2010")] + [TestCase("0 15 10 15,L * ? 2010")] + [TestCase("0 15 10 15,31 * ? 2010")] + [TestCase("0 15 10 15,L-2 * ? 2010")] + [TestCase("0 15 10 31,L-2 * ? 2010")] + [TestCase("0 15 10 1,3,6,15,L * ? 2010")] + public void ExpressionEquality(string expression) + { + var expr1 = new CronExpression(expression); + var expr2 = new CronExpression(expression); + Assert.That(expr1, Is.EqualTo(expr2)); + + Assert.That((object)expr1, Is.EqualTo(expr2)); + } + + [TestCase("0 15 10 15,L-31 * ? 2010")] + public void OffSetValue_CannontBe_GreaterThan30(string expression) + { + Action act = () => new CronExpression(expression); + Assert.That(() => act(), Throws.InstanceOf().With.Message.EqualTo("Offset from last day must be <= 30")); + } + + [TestCase("L 15 10 15 * ? 2010", false)] + [TestCase("0 L 10 15 * ? 2010", false)] + [TestCase("0 15 L 15 * ? 2010", false)] + [TestCase("0 15 10 L * ? 2010", true, "Valid for day of month")] + [TestCase("0 15 10 15 L ? 2010", false)] + [TestCase("0 15 10 ? * L 2010", true, "Valid for day of week")] + [TestCase("0 15 10 15 * ? L", false)] + public void Ensure_L_Token_CanOnlyBeUsedIn_DayOfWeek_ORDayOfMonth(string expression, bool isValid, string description = "") + { + Action act = () => new CronExpression(expression); + if (isValid) + Assert.That(() => act(), Throws.Nothing, description); + else + Assert.That(() => act(), Throws.InstanceOf(), description); + } + + [Test] + public void CronExpression_Throw_Error_Constructed_With_Null() + { + Action act = () => new CronExpression(null); + Assert.That(() => act(), Throws.InstanceOf()); + } + + [TestCase('h')] + [TestCase('?')] + [TestCase('*')] + public void Should_Throw_Error_When_Extra_NonWhitespace_Character_After_QuestionMark(char invalidChar) + { + Action act = () => new CronExpression($"0 0 * * * ?{invalidChar}"); + Assert.That(() => act(), Throws.InstanceOf()); + } + + [TestCase(' ')] + [TestCase('\t')] + public void QuestionMark_With_ExtraWhitespace_Should_Be_Valid(char allowedChar) + { + Action act = () => new CronExpression($"0 0 * * * ?{allowedChar}"); + Assert.That(() => act(), Throws.Nothing); + } + + [Test] + public void TestCronExpressionPassingMidnight() + { + var cronExpression = new CronExpression("0 15 23 * * ?"); + DateTimeOffset cal = new DateTime(2005, 6, 1, 23, 16, 0).ToUniversalTime(); + DateTimeOffset nextExpectedFireTime = new DateTime(2005, 6, 2, 23, 15, 0).ToUniversalTime(); + Assert.That(cronExpression.GetTimeAfter(cal).Value, Is.EqualTo(nextExpectedFireTime)); + } + + [Test] + public void TestCronExpressionPassingYear() + { + DateTimeOffset start = new DateTime(2007, 12, 1, 23, 59, 59).ToUniversalTime(); + + var ce = new CronExpression("0 55 15 1 * ?"); + DateTimeOffset expected = new DateTime(2008, 1, 1, 15, 55, 0).ToUniversalTime(); + var d = ce.GetNextValidTimeAfter(start).Value; + Assert.That(d, Is.EqualTo(expected), "Got wrong date and time when passed year"); + } + + [Test] + public void TestCronExpressionWeekdaysMonFri() + { + var cronExpression = new CronExpression("0 0 12 ? * MON-FRI"); + int[] arrJuneDaysThatShouldFire = + [1, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 18, 19, 20, 22, 21, 25, 26, 27, 28, 29]; + var juneDays = new List(arrJuneDaysThatShouldFire); + + TestCorrectWeekFireDays(cronExpression, juneDays); + } + + [Test] + public void TestCronExpressionWeekdaysFriday() + { + var cronExpression = new CronExpression("0 0 12 ? * FRI"); + DateTimeOffset? nextRunTime = cronExpression.GetTimeAfter(DateTimeOffset.Now); + DateTimeOffset? nextRunTime2 = cronExpression.GetTimeAfter((DateTimeOffset)nextRunTime); + + int[] arrJuneDaysThatShouldFire = { 1, 8, 15, 22, 29 }; + var juneDays = new List(arrJuneDaysThatShouldFire); + + TestCorrectWeekFireDays(cronExpression, juneDays); + } + + [Test] + public void TestCronExpressionWeekdaysFridayEveryTwoWeeks() + { + var cronExpression = new CronExpression("0 0 12 ? * FRI/2"); + DateTimeOffset? nextRunTime = cronExpression.GetTimeAfter(DateTimeOffset.Now); + DateTimeOffset? nextRunTime2 = cronExpression.GetTimeAfter((DateTimeOffset)nextRunTime); + + int[] arrJuneDaysThatShouldFire = { 1, 15, 29 }; + var juneDays = new List(arrJuneDaysThatShouldFire); + + TestCorrectWeekFireDays(cronExpression, juneDays); + } + + [Test] + public void TestCronExpressionWeekdaysThirsdayAndFridayEveryTwoWeeks() + { + var cronExpression = new CronExpression("0 0 12 ? * THU,FRI/2"); + DateTimeOffset? nextRunTime = cronExpression.GetTimeAfter(DateTimeOffset.Now); + DateTimeOffset? nextRunTime2 = cronExpression.GetTimeAfter((DateTimeOffset)nextRunTime); + + int[] arrJuneDaysThatShouldFire = { 1, 14, 15, 28, 29 }; + var juneDays = new List(arrJuneDaysThatShouldFire); + + TestCorrectWeekFireDays(cronExpression, juneDays); + } + + [Test] + public void TestCronExpressionLastDayOfMonth() + { + var cronExpression = new CronExpression("0 0 12 L * ?"); + int[] arrJuneDaysThatShouldFire = { 30 }; + var juneDays = new List(arrJuneDaysThatShouldFire); + + TestCorrectWeekFireDays(cronExpression, juneDays); + } + + [Test] + public void TestHourShift() + { + var cronExpression = new CronExpression("0/5 * * * * ?"); + var cal = new DateTimeOffset(2005, 6, 1, 1, 59, 55, TimeSpan.Zero); + var nextExpectedFireTime = new DateTimeOffset(2005, 6, 1, 2, 0, 0, TimeSpan.Zero); + Assert.That(cronExpression.GetTimeAfter(cal).Value, Is.EqualTo(nextExpectedFireTime)); + } + + [Test] + public void TestEveryMinute() + { + var cronExpression = new CronExpression("* * * * * ?"); + var cal = new DateTimeOffset(2005, 6, 1, 1, 59, 55, TimeSpan.Zero); + var nextExpectedFireTime = new DateTimeOffset(2005, 6, 1, 1, 59, 56, TimeSpan.Zero); + Assert.That(cronExpression.GetTimeAfter(cal).Value, Is.EqualTo(nextExpectedFireTime)); + } + + [Test] + public void TestMonthShift() + { + var cronExpression = new CronExpression("* * 1 * * ?"); + DateTimeOffset cal = new DateTime(2005, 7, 31, 22, 59, 57).ToUniversalTime(); + DateTimeOffset nextExpectedFireTime = new DateTime(2005, 8, 1, 1, 0, 0).ToUniversalTime(); + Assert.That(cronExpression.GetTimeAfter(cal).Value, Is.EqualTo(nextExpectedFireTime)); + } + + [Test] + public void TestYearChange() + { + var cronExpression = new CronExpression("0 12 4 ? * 3"); + cronExpression.GetNextValidTimeAfter(new DateTime(2007, 12, 28)); + } + + [Test] + public void TestCronExpressionParsingIncorrectDayOfWeek() + { + try + { + var expr = $" * * * * * {DateTime.Now.Year}"; + var ce = new CronExpression(expr); + ce.IsSatisfiedBy(DateTime.UtcNow.AddMinutes(2)); + Assert.Fail("Accepted wrong format"); + } + catch (FormatException fe) + { + Assert.That(fe.Message, Is.EqualTo("Day-of-Week values must be between 1 and 7")); + } + } + + [Test] + public void TestCronExpressionWithExtraWhiteSpace() + { + var expr = " 30 * * * * ? "; + var calendar = new CronExpression(expr); + Assert.That(calendar.IsSatisfiedBy(DateTime.UtcNow.Date.AddMinutes(2)), Is.False, "Time was included"); + } + + static void TestCorrectWeekFireDays(CronExpression cronExpression, IList correctFireDays) + { + var fireDays = new List(); + + var cal = new DateTime(2007, 6, 1, 11, 0, 0).ToUniversalTime(); + DateTimeOffset? nextFireTime = cal; + + for (var i = 0; i < DateTime.DaysInMonth(2007, 6); ++i) + { + nextFireTime = cronExpression.GetTimeAfter((DateTimeOffset)nextFireTime); + if (!fireDays.Contains(nextFireTime.Value.Day) && nextFireTime.Value.Month == 6 && nextFireTime.Value.Year == 2007) + fireDays.Add(nextFireTime.Value.Day); + } + + for (var i = 0; i < fireDays.Count; ++i) + { + var idx = correctFireDays.IndexOf(fireDays[i]); + Assert.That(idx, Is.GreaterThan(-1), $"CronExpression evaluated true for {fireDays[i]} even when it shouldn't have"); + correctFireDays.RemoveAt(idx); + } + + Assert.That(correctFireDays, Is.Empty, $"CronExpression did not evaluate true for all expected days (count: {correctFireDays.Count})."); + } + + [Test] + public void TestNthWeekDayPassingMonth() + { + var ce = new CronExpression("0 30 10-13 ? * FRI#3"); + var start = new DateTime(2008, 12, 19, 0, 0, 0); + for (var i = 0; i < 200; ++i) + { + var shouldFire = start.Hour >= 10 && start.Hour <= 13 && start.Minute == 30 + && (start.DayOfWeek == DayOfWeek.Wednesday || start.DayOfWeek == DayOfWeek.Friday); + shouldFire = shouldFire && start.Day > 15 && start.Day < 28; + + var satisfied = ce.IsSatisfiedBy(start.ToUniversalTime()); + Assert.That(satisfied, Is.EqualTo(shouldFire)); + + // cycle with half hour precision + start = start.AddHours(0.5); + } + } + + [Test] + public void TestNormal() + { + for (var i = 0; i < 6; i++) + AssertParsesForField("0 15 10 * * ? 2005", i); + } + + [Test] + public void TestSecond() + { + AssertParsesForField("58-4 5 21 ? * MON-FRI", 0); + } + + [Test] + public void TestMinute() + { + AssertParsesForField("0 58-4 21 ? * MON-FRI", 1); + } + + [Test] + public void TestHour() + { + AssertParsesForField("0 0/5 21-3 ? * MON-FRI", 2); + } + + [Test] + public void TestDayOfWeekNumber() + { + AssertParsesForField("58 5 21 ? * 6-2", 5); + } + + [Test] + public void TestDayOfWeek() + { + AssertParsesForField("58 5 21 ? * FRI-TUE", 5); + } + + [Test] + public void TestDayOfMonth() + { + AssertParsesForField("58 5 21 28-5 1 ?", 3); + } + + [Test] + public void TestMonth() + { + AssertParsesForField("58 5 21 ? 11-2 FRI", 4); + } + + [Test] + public void TestAmbiguous() + { + AssertParsesForField("0 0 14-6 ? * FRI-MON", 2); + AssertParsesForField("0 0 14-6 ? * FRI-MON", 5); + + AssertParsesForField("55-3 56-2 6 ? * FRI", 0); + AssertParsesForField("55-3 56-2 6 ? * FRI", 1); + } + + static void AssertParsesForField(string expression, int constant) + { + try + { + var cronExpression = new CronExpression(expression); + var set = cronExpression.GetSet(constant); + if (set.Count == 0) + Assert.Fail("Empty field [" + constant + "] returned for " + expression); + } + catch (FormatException pe) + { + Assert.Fail("Exception thrown during parsing: " + pe); + } + } + + [Test] + public void TestQuartz640() + { + try + { + new CronExpression("0 43 9 ? * SAT,SUN,L"); + Assert.Fail("Expected FormatException did not fire for L combined with other days of the week"); + } + catch (FormatException pe) + { + Assert.That( + pe.Message, + Does.StartWith("Support for specifying 'L' with other days of the week is not implemented"), + "Incorrect FormatException thrown"); + } + + try + { + new CronExpression("0 43 9 ? * 6,7,L"); + Assert.Fail("Expected FormatException did not fire for L combined with other days of the week"); + } + catch (FormatException pe) + { + Assert.That( + pe.Message, + Does.StartWith("Support for specifying 'L' with other days of the week is not implemented"), + "Incorrect FormatException thrown"); + } + + try + { + new CronExpression("0 43 9 ? * 5L"); + } + catch (FormatException) + { + Assert.Fail("Unexpected ParseException thrown for supported '5L' expression."); + } + } + + [Test] + public void TestGetTimeAfter_QRTZNET149() + { + var expression = new CronExpression("0 0 0 29 * ?"); + DateTimeOffset? after = expression.GetNextValidTimeAfter(new DateTime(2009, 1, 30, 0, 0, 0).ToUniversalTime()); + Assert.Multiple(() => + { + Assert.That(after.HasValue, Is.True); + Assert.That(after.Value.DateTime, Is.EqualTo(new DateTime(2009, 3, 29, 0, 0, 0).ToUniversalTime())); + }); + + after = expression.GetNextValidTimeAfter(new DateTime(2009, 12, 30).ToUniversalTime()); + Assert.Multiple(() => + { + Assert.That(after.HasValue, Is.True); + Assert.That(after.Value.DateTime, Is.EqualTo(new DateTime(2010, 1, 29, 0, 0, 0).ToUniversalTime())); + }); + } + + [Test] + public void TestQRTZNET152_Nearest_Weekday_Expression_W_Does_Not_Work_In_CronTrigger() + { + var expression = new CronExpression("0 5 13 5W 1-12 ?"); + var test = new DateTimeOffset(2009, 3, 8, 0, 0, 0, TimeSpan.Zero); //Sunday + var d = expression.GetNextValidTimeAfter(test).Value; + // 2009-04-06 is Monday, Sunday is invalid for W + Assert.That(d, Is.EqualTo(new DateTimeOffset(2009, 4, 6, 13, 5, 0, TimeZoneUtil.GetUtcOffset(d, TimeZoneInfo.Local)).ToUniversalTime())); + d = expression.GetNextValidTimeAfter(d).Value; + Assert.That(d, Is.EqualTo(new DateTimeOffset(2009, 5, 5, 13, 5, 0, TimeZoneUtil.GetUtcOffset(d, TimeZoneInfo.Local)))); + } + + [Test] + public void ShouldThrowExceptionIfWParameterMakesNoSense() + { + try + { + new CronExpression("0/5 * * 32W 1 ?"); + Assert.Fail("Expected FormatException did not fire for W with value larger than 31"); + } + catch (FormatException pe) + { + Assert.That(pe.Message, Does.StartWith("The 'W' option does not make sense with values larger than"), "Incorrect ParseException thrown"); + } + } + + [Test] + [Platform("WIN")] + public void TestDaylightSaving_QRTZNETZ186() + { + var expression = new CronExpression("0 15 * * * ?"); + if (!TimeZoneInfo.Local.SupportsDaylightSavingTime) + return; + + var daylightChange = TimeZone.CurrentTimeZone.GetDaylightChanges(2012); + DateTimeOffset before = daylightChange.Start.ToUniversalTime().AddMinutes(-5); // keep outside the potentially undefined interval + DateTimeOffset? after = expression.GetNextValidTimeAfter(before); + Assert.That(after.HasValue, Is.True); + DateTimeOffset expected = daylightChange.Start.Add(daylightChange.Delta).AddMinutes(15).ToUniversalTime(); + Assert.That(after.Value, Is.EqualTo(expected)); + } + + [Test] + public void TestDaylightSavingsDoesNotMatchAnHourBefore() + { + var est = TimeZoneUtil.FindTimeZoneById("Eastern Standard Time"); + var expression = new CronExpression("0 15 15 5 11 ?"); + expression.TimeZone = est; + + var startTime = new DateTimeOffset(2012, 11, 4, 0, 0, 0, TimeSpan.Zero); + + DateTimeOffset? actualTime = expression.GetTimeAfter(startTime); + var expected = new DateTimeOffset(2012, 11, 5, 15, 15, 0, TimeSpan.FromHours(-5)); + + Assert.That(actualTime.Value, Is.EqualTo(expected)); + } + + [Test] + public void TestDaylightSavingsDoesNotMatchAnHourBefore2() + { + //another case + var est = TimeZoneUtil.FindTimeZoneById("Eastern Standard Time"); + var expression = new CronExpression("0 0 0 ? * THU"); + expression.TimeZone = est; + + var startTime = new DateTimeOffset(2012, 11, 4, 0, 0, 0, TimeSpan.Zero); + + DateTimeOffset? actualTime = expression.GetTimeAfter(startTime); + var expected = new DateTimeOffset(2012, 11, 8, 0, 0, 0, TimeSpan.FromHours(-5)); + Assert.That(actualTime, Is.EqualTo(expected)); + } + + [Test] + public void TestSecRangeIntervalAfterSlash() + { + // Test case 1 + var e = + Assert.Throws(() => new CronExpression("/120 0 8-18 ? * 2-6"), "Cron did not validate bad range interval in '_blank/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 59 : 120")); + + // Test case 2 + e = Assert.Throws(() => new CronExpression("0/120 0 8-18 ? * 2-6"), "Cron did not validate bad range interval in in '0/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 59 : 120")); + + // Test case 3 + e = Assert.Throws(() => new CronExpression("/ 0 8-18 ? * 2-6"), "Cron did not validate bad range interval in '_blank/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + + // Test case 4 + e = Assert.Throws(() => new CronExpression("0/ 0 8-18 ? * 2-6"), "Cron did not validate bad range interval in '0/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + } + + [Test] + public void TestMinRangeIntervalAfterSlash() + { + // Test case 1 + var e = + Assert.Throws(() => new CronExpression("0 /120 8-18 ? * 2-6"), "Cron did not validate bad range interval in '_blank/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 59 : 120")); + + // Test case 2 + e = Assert.Throws(() => new CronExpression("0 0/120 8-18 ? * 2-6"), "Cron did not validate bad range interval in in '0/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 59 : 120")); + + // Test case 3 + e = Assert.Throws(() => new CronExpression("0 / 8-18 ? * 2-6"), "Cron did not validate bad range interval in '_blank/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + + // Test case 4 + e = Assert.Throws(() => new CronExpression("0 0/ 8-18 ? * 2-6"), "Cron did not validate bad range interval in '0/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + } + + [Test] + public void TestHourRangeIntervalAfterSlash() + { + // Test case 1 + var e = Assert.Throws(() => new CronExpression("0 0 /120 ? * 2-6"), "Cron did not validate bad range interval in '_blank/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 23 : 120")); + + // Test case 2 + e = Assert.Throws(() => new CronExpression("0 0 0/120 ? * 2-6"), "Cron did not validate bad range interval in in '0/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 23 : 120")); + + // Test case 3 + e = Assert.Throws(() => new CronExpression("0 0 / ? * 2-6"), "Cron did not validate bad range interval in '_blank/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + + // Test case 4 + e = Assert.Throws(() => new CronExpression("0 0 0/ ? * 2-6"), "Cron did not validate bad range interval in '0/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + } + + [Test] + public void TestDayOfMonthRangeIntervalAfterSlash() + { + // Test case 1 + var e = Assert.Throws(() => new CronExpression("0 0 0 /120 * 2-6"), "Cron did not validate bad range interval in '_blank/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 31 : 120")); + + // Test case 2 + e = Assert.Throws(() => new CronExpression("0 0 0 0/120 * 2-6"), "Cron did not validate bad range interval in in '0/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 31 : 120")); + + // Test case 3 + e = Assert.Throws(() => new CronExpression("0 0 0 / * 2-6"), "Cron did not validate bad range interval in '_blank/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + + // Test case 4 + e = Assert.Throws(() => new CronExpression("0 0 0 0/ * 2-6"), "Cron did not validate bad range interval in '0/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + } + + [Test] + public void TestMonthRangeIntervalAfterSlash() + { + // Test case 1 + var e = Assert.Throws(() => new CronExpression("0 0 0 ? /120 2-6"), "Cron did not validate bad range interval in '_blank/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 12 : 120")); + + // Test case 2 + e = Assert.Throws(() => new CronExpression("0 0 0 ? 0/120 2-6"), "Cron did not validate bad range interval in in '0/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 12 : 120")); + + // Test case 3 + e = Assert.Throws(() => new CronExpression("0 0 0 ? / 2-6"), "Cron did not validate bad range interval in '_blank/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + + // Test case 4 + e = Assert.Throws(() => new CronExpression("0 0 0 ? 0/ 2-6"), "Cron did not validate bad range interval in '0/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + } + + [Test] + public void TestDayOfWeekRangeIntervalAfterSlash() + { + // Test case 1 + var e = Assert.Throws(() => new CronExpression("0 0 0 ? * /120"), "Cron did not validate bad range interval in '_blank/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 7 : 120")); + + // Test case 2 + e = Assert.Throws(() => new CronExpression("0 0 0 ? * 0/120"), "Cron did not validate bad range interval in in '0/xxx' form"); + Assert.That(e.Message, Is.EqualTo("Increment > 7 : 120")); + + // Test case 3 + e = Assert.Throws(() => new CronExpression("0 0 0 ? * /"), "Cron did not validate bad range interval in '_blank/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + + // Test case 4 + e = Assert.Throws(() => new CronExpression("0 0 0 ? * 0/"), "Cron did not validate bad range interval in '0/_blank'"); + Assert.That(e.Message, Is.EqualTo("'/' must be followed by an integer.")); + } + + [Test] + public void TestInvalidCharactersAfterAsterisk() + { + Assert.Multiple(() => + { + Assert.That(CronExpression.IsValidExpression("* * * ? * *A&/5:"), Is.False); + Assert.That(CronExpression.IsValidExpression("* * * ? *14 "), Is.False); + Assert.That(CronExpression.IsValidExpression(" * * ? *A&/5 *"), Is.False); + Assert.That(CronExpression.IsValidExpression("* * ? */5 *"), Is.False); + Assert.That(CronExpression.IsValidExpression("* * ? */52 *"), Is.False); + + Assert.That(CronExpression.IsValidExpression("0 0/30 * * * ?"), Is.True); + Assert.That(CronExpression.IsValidExpression("0 0/1 * * * ?"), Is.True); + Assert.That(CronExpression.IsValidExpression("0 0/30 * * */2 ?"), Is.True); + }); + } + + [Test] + public void TestExtraCharactersAfterWeekDay() + { + Assert.That(CronExpression.IsValidExpression("0 0 15 ? * FRI*"), Is.False); + } + + [Test] + public void TestHourRangeAndSlash() + { + CronExpression.ValidateExpression("0 0 18-21/1 ? * MON,TUE,WED,THU,FRI,SAT,SUN"); + } + + [Test] + [Explicit] + public void PerformanceTest() + { + var quartz = new CronExpression("* * * * * ?"); + + var sw = new Stopwatch(); + sw.Start(); + + DateTimeOffset? next = new DateTimeOffset(2012, 1, 1, 0, 0, 0, TimeSpan.Zero); + + for (var i = 0; i < 1000000; i++) + { + next = quartz.GetNextValidTimeAfter(next.Value); + + if (next is null) + break; + } + + Console.WriteLine("{0}ms", sw.ElapsedMilliseconds); + } + + [Test] + public void CanGetHashCode() + { + var expression = new CronExpression("0 15 15 5 11 ?"); + var expression2 = new CronExpression("0 15 15 5 11 ?"); + Assert.That(expression.GetHashCode(), Is.EqualTo(expression2.GetHashCode())); + } + + [Test] + public void CanGetExpressionSummary() + { + var expression = new CronExpression("0 15 15 5 11 ?"); + var sut = expression.GetExpressionSummary(); + Assert.That(sut, Is.EqualTo( + @"seconds: 0 +minutes: 15 +hours: 15 +daysOfMonth: 5 +months: 11 +daysOfWeek: ? +lastdayOfWeek: False +nearestWeekday: False +NthDayOfWeek: 0 +lastdayOfMonth: False +calendardayOfWeek: False +calendardayOfMonth: False +years: * +")); + } + + [TestCase("OCT", 10)] + [TestCase("NOV", 11)] + [TestCase("DEC", 12)] + public void GivenMonthAbbreviation_ShouldGetTimeAfter(string monthAbbr, int monthNumber) + { + var expression = $"0 0 0 1 {monthAbbr} ? *"; + + CronExpression ce = new(expression) { TimeZone = TimeZoneInfo.Utc }; + var startTime = new DateTimeOffset(2024, 7, 22, 12, 0, 0, TimeSpan.Zero); + var expectedTimeAfter = new DateTimeOffset(2024, monthNumber, 1, 0, 0, 0, TimeSpan.Zero); + + DateTimeOffset? actualTimeAfter = ce.GetTimeAfter(startTime); + + Assert.That(actualTimeAfter, Is.EqualTo(expectedTimeAfter)); + } + + [TestCaseSource(typeof(CronTestScenarios), nameof(CronTestScenarios.TestCases))] + public void CronExpressionReturnsExpectedNextFireTime(CronExpression cronExpression, DateTimeOffset timeAfterDate, DateTimeOffset expectedNextFireTime) + { + DateTimeOffset? nextFireTime = cronExpression.GetTimeAfter(timeAfterDate); + Assert.That(nextFireTime.Value.Date, Is.EqualTo(expectedNextFireTime.Date)); + } +} + + +public class CronTestScenarios +{ + static IEnumerable TestCaseData => + [ + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 15W * ?"){ TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2024, 5, 15, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2024, 6, 14, 12, 0, 0, TimeSpan.Zero), + TestCase = "Run on Weekday 15th Every Month - 2024-06-15 is a Sat, schedule should be Fri 14th" + }, + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 15W * ?") { TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2024, 8, 15, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2024, 9, 16, 12, 0, 0, TimeSpan.Zero), + TestCase = "Run on Weekday 15th Every Month - 2024-09-15 is a Sunday, expect schedule to be Mon 16th" + }, + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 15W * ?"){ TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2023, 12, 15, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2024, 1, 15, 12, 0, 0, TimeSpan.Zero), + TestCase = "Run on Weekday 15th Every Month - 2024-01-15 is Monday, should run on Monday" + }, + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 31W * ?") { TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2025, 1, 31, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2025, 2, 28, 12, 0, 0, TimeSpan.Zero), + TestCase = "Test that next fire time to be in next month with less days in month - Issue #2330" + }, + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 LW * ?"){ TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2023, 2, 28, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2023, 3, 31, 12, 0, 0, TimeSpan.Zero), + TestCase = "Run on last weekday of the month - 2023-03-31 is a Friday" + }, + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 L-2 * ?") { TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2023, 4, 28, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2023, 5, 29, 12, 0, 0, TimeSpan.Zero), + TestCase = "Run on the second-to-last day of the month" + }, + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 ? * 6L") { TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2023, 6, 24, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2023, 6, 30, 12, 0, 0, TimeSpan.Zero), + TestCase = "Run on the last Friday of the month - 2023-06-30 is the last Friday" + }, + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 ? * 6#3") { TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2023, 7, 21, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2023, 8, 18, 12, 0, 0, TimeSpan.Zero), + TestCase = "Run on the third Friday of the month" + }, + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 ? * 2/2"){ TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2023, 9, 5, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2023, 9, 6, 12, 0, 0, TimeSpan.Zero), + TestCase = "Run every second day (/2) starting Monday (2)" + }, + new TestCaseProps + { + CronExpression = new CronExpression("0 0 12 1W * ?") { TimeZone = TimeZoneInfo.Utc }, + TimeAfterDate = new DateTimeOffset(2023, 10, 1, 12, 0, 0, TimeSpan.Zero), + ExpectedNextFireTime = new DateTimeOffset(2023, 10, 2, 12, 0, 0, TimeSpan.Zero), + TestCase = "Run on the first weekday of the month - 2023-10-01 is a Sunday, expect schedule to be Mon 2nd" + } + ]; + + public static IEnumerable TestCases => + TestCaseData.Select(model => new TestCaseData(model.CronExpression, model.TimeAfterDate, model.ExpectedNextFireTime)); + + + class TestCaseProps + { + public CronExpression CronExpression { get; set; } + + public DateTimeOffset TimeAfterDate { get; set; } + + public DateTimeOffset ExpectedNextFireTime { get; set; } + + public string TestCase { get; set; } + } +} diff --git a/tests/MassTransit.Tests/DelayedRedelivery_Specs.cs b/tests/MassTransit.Tests/DelayedRedelivery_Specs.cs index f03605213f6..c7eb4aa6553 100644 --- a/tests/MassTransit.Tests/DelayedRedelivery_Specs.cs +++ b/tests/MassTransit.Tests/DelayedRedelivery_Specs.cs @@ -20,9 +20,12 @@ public async Task Should_use_the_correct_intervals_for_each_redelivery() await Task.WhenAll(_received.Select(x => x.Task)); - Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(0.9))); - Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.9))); - Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2.9))); + Assert.Multiple(() => + { + Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(0.9))); + Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.9))); + Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2.9))); + }); TestContext.Out.WriteLine("Interval: {0}", _timestamps[1] - _timestamps[0]); TestContext.Out.WriteLine("Interval: {0}", _timestamps[2] - _timestamps[1]); @@ -74,9 +77,12 @@ public async Task Should_use_the_correct_intervals_for_each_redelivery() await Task.WhenAll(_received.Select(x => x.Task)); - Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(0.9))); - Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.9))); - Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2.9))); + Assert.Multiple(() => + { + Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(0.9))); + Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.9))); + Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2.9))); + }); TestContext.Out.WriteLine("Interval: {0}", _timestamps[1] - _timestamps[0]); TestContext.Out.WriteLine("Interval: {0}", _timestamps[2] - _timestamps[1]); @@ -113,6 +119,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } } + [TestFixture] [Category("Flaky")] public class Using_delayed_redelivery_with_new_message_id : @@ -127,22 +134,28 @@ public async Task Should_use_the_correct_intervals_for_each_redelivery() await Task.WhenAll(_received.Select(x => x.Task)); - Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(0.9))); - Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.9))); - Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2.9))); + Assert.Multiple(() => + { + Assert.That(_timestamps[1] - _timestamps[0], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(0.9))); + Assert.That(_timestamps[2] - _timestamps[1], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(1.9))); + Assert.That(_timestamps[3] - _timestamps[2], Is.GreaterThanOrEqualTo(TimeSpan.FromSeconds(2.9))); + }); TestContext.Out.WriteLine("Interval: {0}", _timestamps[1] - _timestamps[0]); TestContext.Out.WriteLine("Interval: {0}", _timestamps[2] - _timestamps[1]); TestContext.Out.WriteLine("Interval: {0}", _timestamps[3] - _timestamps[2]); - Assert.That(_received[0].Task.Result.MessageId.Value, Is.EqualTo(messageId)); - Assert.That(_received[1].Task.Result.MessageId.Value, Is.Not.EqualTo(messageId)); - Assert.That(_received[2].Task.Result.MessageId.Value, Is.Not.EqualTo(messageId)); - Assert.That(_received[3].Task.Result.MessageId.Value, Is.Not.EqualTo(messageId)); - - Assert.That(_received[1].Task.Result.GetHeader(MessageHeaders.OriginalMessageId, default(Guid?)), Is.EqualTo((Guid?)messageId)); - Assert.That(_received[2].Task.Result.GetHeader(MessageHeaders.OriginalMessageId, default(Guid?)), Is.EqualTo((Guid?)messageId)); - Assert.That(_received[3].Task.Result.GetHeader(MessageHeaders.OriginalMessageId, default(Guid?)), Is.EqualTo((Guid?)messageId)); + Assert.Multiple(() => + { + Assert.That(_received[0].Task.Result.MessageId.Value, Is.EqualTo(messageId)); + Assert.That(_received[1].Task.Result.MessageId.Value, Is.Not.EqualTo(messageId)); + Assert.That(_received[2].Task.Result.MessageId.Value, Is.Not.EqualTo(messageId)); + Assert.That(_received[3].Task.Result.MessageId.Value, Is.Not.EqualTo(messageId)); + + Assert.That(_received[1].Task.Result.GetHeader(MessageHeaders.OriginalMessageId, default(Guid?)), Is.EqualTo((Guid?)messageId)); + Assert.That(_received[2].Task.Result.GetHeader(MessageHeaders.OriginalMessageId, default(Guid?)), Is.EqualTo((Guid?)messageId)); + Assert.That(_received[3].Task.Result.GetHeader(MessageHeaders.OriginalMessageId, default(Guid?)), Is.EqualTo((Guid?)messageId)); + }); } TaskCompletionSource>[] _received; diff --git a/tests/MassTransit.Tests/Encryption/EncryptedFallbackDeserializerTests.cs b/tests/MassTransit.Tests/Encryption/EncryptedFallbackDeserializerTests.cs new file mode 100644 index 00000000000..555609b79cf --- /dev/null +++ b/tests/MassTransit.Tests/Encryption/EncryptedFallbackDeserializerTests.cs @@ -0,0 +1,147 @@ +#nullable enable +namespace MassTransit.Tests.Encryption +{ + using System; + using System.Runtime.Serialization; + using System.Security.Cryptography; + using InMemoryTransport; + using MassTransit.Serialization; + using NUnit.Framework; + + + [TestFixture] + public class EncryptedFallbackDeserializerTests + { + [Test] + public void MessageEncryptionTest_HavingMessageIsEncrypted_WhenMessageIsReceived_MessageCannotBeDecryptedWithDifferentKeySet() + { + // Arrange + Tuple firstServiceKeySet = GenerateEncryptionKeys(); + + // second factory, representing another service. + Tuple secondServiceKeySet = GenerateEncryptionKeys(); + + var firstFactory = CreateSerializerFactory(firstServiceKeySet); + var firstSerializer = firstFactory.CreateSerializer(); + + var secondFactory = CreateSerializerFactory(secondServiceKeySet); + var secondDeserializer = secondFactory.CreateDeserializer(); + + var inputMessage = CreateTestMessage(out var inputTestEvent); + + // Act + var inMemorySendContext = new InMemorySendContext(inputTestEvent); + var messageBody = firstSerializer.GetMessageBody(inMemorySendContext); + var encryptedMessage = messageBody.GetString(); + + void DeserializeAction() + { + secondDeserializer.Deserialize(messageBody, EmptyHeaders.Instance); + } + + // Assert + + Assert.That(encryptedMessage, Is.Not.EqualTo(inputMessage)); + Assert.Throws(DeserializeAction); + } + + [Test] + public void MessageEncryptionTest_HavingMessageIsEncrypted_WhenMessageIsReceived_MessageIsDecryptedWithPrimaryKey() + { + // Arrange + Tuple keys = GenerateEncryptionKeys(); + + var factory = CreateSerializerFactory(keys); + + var serializer = factory.CreateSerializer(); + var deserializer = factory.CreateDeserializer(); + + var inputMessage = CreateTestMessage(out var inputTestEvent); + + // Act + var inMemorySendContext = new InMemorySendContext(inputTestEvent); + var messageBody = serializer.GetMessageBody(inMemorySendContext); + var encryptedMessage = messageBody.GetString(); + + var deserializerContext = deserializer.Deserialize(messageBody, EmptyHeaders.Instance); + var canDecryptMessage = deserializerContext.TryGetMessage(out TestMessage? decryptedReceivedTestEvent); + + Assert.Multiple(() => + { + // Assert + Assert.That(encryptedMessage, Is.Not.EqualTo(inputMessage)); + Assert.That(canDecryptMessage, Is.True); + Assert.That(decryptedReceivedTestEvent, Is.EqualTo(inputTestEvent)); + }); + } + + [Test] + public void MessageEncryptionTest_HavingMessageIsEncrypted_WhenMessageIsReceived_MessageIsDecryptedWithSecondaryKey() + { + // Arrange + Tuple firstServiceKeySet = GenerateEncryptionKeys(); + + // second factory, representing another service. + var keyNotInUse = GenerateEncryptionKeys().Item1; + var secondServiceKeySet = new Tuple(keyNotInUse, firstServiceKeySet.Item1); + + var firstFactory = CreateSerializerFactory(firstServiceKeySet); + var firstSerializer = firstFactory.CreateSerializer(); + + var secondFactory = CreateSerializerFactory(secondServiceKeySet); + var secondDeserializer = secondFactory.CreateDeserializer(); + + var inputMessage = CreateTestMessage(out var inputTestEvent); + + // Act + var inMemorySendContext = new InMemorySendContext(inputTestEvent); + var messageBody = firstSerializer.GetMessageBody(inMemorySendContext); + var encryptedMessage = messageBody.GetString(); + + var deserializerContext = secondDeserializer.Deserialize(messageBody, EmptyHeaders.Instance); + var canDecryptMessage = deserializerContext.TryGetMessage(out TestMessage? decryptedReceivedTestEvent); + + Assert.Multiple(() => + { + // Assert + Assert.That(encryptedMessage, Is.Not.EqualTo(inputMessage)); + Assert.That(canDecryptMessage, Is.True); + Assert.That(decryptedReceivedTestEvent, Is.EqualTo(inputTestEvent)); + }); + } + + static string CreateTestMessage(out TestMessage inputTestEvent) + { + var inputMessage = "test message"; + inputTestEvent = new TestMessage(Guid.NewGuid(), inputMessage); + + return inputMessage; + } + + static EncryptedFallbackSerializerFactoryV2 CreateSerializerFactory(Tuple keys) + { + var primaryKey = Convert.FromBase64String(keys.Item1); + var keyProvider = new ConstantSecureKeyProvider(primaryKey); + var primaryCrypto = new AesCryptoStreamProviderV2(keyProvider); + + var secondaryKey = Convert.FromBase64String(keys.Item2); + var keyProvider2 = new ConstantSecureKeyProvider(secondaryKey); + var secondaryCrypto = new AesCryptoStreamProviderV2(keyProvider2); + + return new EncryptedFallbackSerializerFactoryV2(primaryCrypto, secondaryCrypto); + } + + static Tuple GenerateEncryptionKeys() + { + var aes = Aes.Create(); + aes.GenerateKey(); + + var key1 = Convert.ToBase64String(aes.Key); + + aes.GenerateKey(); + var key2 = Convert.ToBase64String(aes.Key); + + return new Tuple(key1, key2); + } + } +} diff --git a/tests/MassTransit.Tests/Encryption/TestMessage.cs b/tests/MassTransit.Tests/Encryption/TestMessage.cs new file mode 100644 index 00000000000..06dcd15a815 --- /dev/null +++ b/tests/MassTransit.Tests/Encryption/TestMessage.cs @@ -0,0 +1,44 @@ +namespace MassTransit.Tests.Encryption +{ + using System; + + public class TestMessage + { + public Guid Id { get; } + public string TestValue { get; } + + public TestMessage(Guid id, string testValue) + { + Id = id; + TestValue = testValue; + } + + #region Equality members + + protected bool Equals(TestMessage other) + { + return Id.Equals(other.Id) && TestValue == other.TestValue; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) + return false; + if (ReferenceEquals(this, obj)) + return true; + if (obj.GetType() != this.GetType()) + return false; + return Equals((TestMessage)obj); + } + + public override int GetHashCode() + { + unchecked + { + return (Id.GetHashCode() * 397) ^ TestValue.GetHashCode(); + } + } + + #endregion + } +} diff --git a/tests/MassTransit.Tests/EndpointName_Specs.cs b/tests/MassTransit.Tests/EndpointName_Specs.cs index e0f7a27d50b..9ffb7996afe 100644 --- a/tests/MassTransit.Tests/EndpointName_Specs.cs +++ b/tests/MassTransit.Tests/EndpointName_Specs.cs @@ -1,5 +1,6 @@ namespace MassTransit.Tests { + using System; using System.Threading.Tasks; using MassTransit.Configuration; using NUnit.Framework; @@ -59,6 +60,34 @@ public void Should_include_the_namespace_and_prefix() Assert.That(name, Is.EqualTo("dev-mass-transit-tests-endpoint-name-specs-some-really-cool")); } + [Test] + public void Should_include_the_namespace_and_prefix_with_generic_consumer() + { + var formatter = new KebabCaseEndpointNameFormatter("Dev", true); + + var name = formatter.Consumer>(); + + Assert.That(name, Is.EqualTo("dev-mass-transit-test-framework-messages-ping-message")); + } + + [Test] + public void Should_include_the_namespace_and_prefix_with_message_name() + { + var formatter = new KebabCaseEndpointNameFormatter("Dev", true); + + var name = formatter.Message(); + + Assert.That(name, Is.EqualTo("dev-mass-transit-test-framework-messages-ping-message")); + } + + [Test] + public void Should_only_include_the_message_name() + { + var name = KebabCaseEndpointNameFormatter.Instance.Message(); + + Assert.That(name, Is.EqualTo("ping-message")); + } + [Test] public void Should_include_the_prefix() { @@ -110,6 +139,37 @@ public void Should_format_instance_id_properly() Assert.That(name, Is.EqualTo("some-really-cool-top-shelf")); } + [Test] + public void Should_throw_exception_when_class_is_called_consumer() + { + var formatter = DefaultEndpointNameFormatter.Instance; + + Assert.Throws(() => formatter.Consumer()); + } + + [Test] + public void Should_throw_exception_when_class_is_called_saga() + { + var formatter = DefaultEndpointNameFormatter.Instance; + + Assert.Throws(() => formatter.Saga()); + } + + [Test] + public void Should_throw_exception_when_class_is_called_activity_for_execute() + { + var formatter = DefaultEndpointNameFormatter.Instance; + + Assert.Throws(() => formatter.ExecuteActivity()); + } + + [Test] + public void Should_throw_exception_when_class_is_called_activity_for_compensate() + { + var formatter = DefaultEndpointNameFormatter.Instance; + + Assert.Throws(() => formatter.CompensateActivity()); + } class SomeReallyCoolConsumer : IConsumer @@ -136,5 +196,41 @@ public async Task Consume(ConsumeContext context) { } } + + class SomeGenericConsumer : + IConsumer + where T : class + { + public async Task Consume(ConsumeContext context) + { + } + } + + + class Consumer : IConsumer + { + public async Task Consume(ConsumeContext context) + { + } + } + + + class Saga : ISaga + { + public Guid CorrelationId { get; set; } + } + + class Activity : IExecuteActivity, ICompensateActivity + { + public Task Execute(ExecuteContext context) + { + return Task.FromResult(context.Completed()); + } + + public Task Compensate(CompensateContext context) + { + return Task.FromResult(context.Compensated()); + } + } } } diff --git a/tests/MassTransit.Tests/ExceptionInfo_Specs.cs b/tests/MassTransit.Tests/ExceptionInfo_Specs.cs index 9783b361965..fd11d8720c9 100644 --- a/tests/MassTransit.Tests/ExceptionInfo_Specs.cs +++ b/tests/MassTransit.Tests/ExceptionInfo_Specs.cs @@ -22,14 +22,17 @@ public async Task Should_publish_a_single_fault_when_retried() ConsumeContext> fault = await faulted; - Assert.That(fault.Message.Exceptions.Length, Is.EqualTo(1)); + Assert.That(fault.Message.Exceptions, Has.Length.EqualTo(1)); var exceptionInfo = fault.Message.Exceptions.Single(); - Assert.That(exceptionInfo.ExceptionType, Is.EqualTo(TypeCache.ShortName)); + Assert.Multiple(() => + { + Assert.That(exceptionInfo.ExceptionType, Is.EqualTo(TypeCache.ShortName)); - Assert.That(exceptionInfo.Data.TryGetValue("Username", out string username) ? username : "", Is.EqualTo("Frank")); - Assert.That(exceptionInfo.Data.TryGetValue("CustomerId", out long? customerId) ? customerId : 0, Is.EqualTo(27)); + Assert.That(exceptionInfo.Data.TryGetValue("Username", out string username) ? username : "", Is.EqualTo("Frank")); + Assert.That(exceptionInfo.Data.TryGetValue("CustomerId", out long? customerId) ? customerId : 0, Is.EqualTo(27)); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -73,14 +76,17 @@ public async Task Should_publish_a_single_fault_when_retried() ConsumeContext> fault = await faulted; - Assert.That(fault.Message.Exceptions.Length, Is.EqualTo(1)); + Assert.That(fault.Message.Exceptions, Has.Length.EqualTo(1)); var exceptionInfo = fault.Message.Exceptions.Single(); - Assert.That(exceptionInfo.ExceptionType, Is.EqualTo(TypeCache.ShortName)); + Assert.Multiple(() => + { + Assert.That(exceptionInfo.ExceptionType, Is.EqualTo(TypeCache.ShortName)); - Assert.That(exceptionInfo.Data.TryGetValue("Username", out string username) ? username : "", Is.EqualTo("Frank")); - Assert.That(exceptionInfo.Data.TryGetValue("CustomerId", out long? customerId) ? customerId : 0, Is.EqualTo(27)); + Assert.That(exceptionInfo.Data.TryGetValue("Username", out string username) ? username : "", Is.EqualTo("Frank")); + Assert.That(exceptionInfo.Data.TryGetValue("CustomerId", out long? customerId) ? customerId : 0, Is.EqualTo(27)); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/ExcessiveAsyncFault_Specs.cs b/tests/MassTransit.Tests/ExcessiveAsyncFault_Specs.cs index 2a85760d26d..0fb0ef6cbd3 100644 --- a/tests/MassTransit.Tests/ExcessiveAsyncFault_Specs.cs +++ b/tests/MassTransit.Tests/ExcessiveAsyncFault_Specs.cs @@ -25,10 +25,10 @@ public async Task Should_not_explode_the_task_library() IReceivedMessage>[] messages = _consumer.Received.Select>().Take(limit).ToArray(); - Assert.AreEqual(limit, messages.Length); + Assert.That(messages, Has.Length.EqualTo(limit)); - Assert.AreEqual(limit, - messages.Select(x => x.Context.Message.Exceptions[0].ExceptionType == TypeCache.ShortName).Count()); + Assert.That(messages.Select(x => x.Context.Message.Exceptions[0].ExceptionType == TypeCache.ShortName).Count(), + Is.EqualTo(limit)); } PingConsumer _consumer; diff --git a/tests/MassTransit.Tests/FastProperty_Specs.cs b/tests/MassTransit.Tests/FastProperty_Specs.cs index dcab2e2f590..45ad3b2b109 100644 --- a/tests/MassTransit.Tests/FastProperty_Specs.cs +++ b/tests/MassTransit.Tests/FastProperty_Specs.cs @@ -25,7 +25,7 @@ public void Should_be_able_to_access_a_private_setter() const string expectedValue = "Chris"; fastProperty.Set(instance, expectedValue); - Assert.AreEqual(expectedValue, fastProperty.Get(instance)); + Assert.That(fastProperty.Get(instance), Is.EqualTo(expectedValue)); } [Test] @@ -38,7 +38,7 @@ public void Should_cache_properties_nicely() const string expectedValue = "Chris"; cache["Name"].Set(instance, expectedValue); - Assert.AreEqual(expectedValue, instance.Name); + Assert.That(instance.Name, Is.EqualTo(expectedValue)); } diff --git a/tests/MassTransit.Tests/FaultPoly_Specs.cs b/tests/MassTransit.Tests/FaultPoly_Specs.cs index 7f6a7c5577f..33d5f1b6561 100644 --- a/tests/MassTransit.Tests/FaultPoly_Specs.cs +++ b/tests/MassTransit.Tests/FaultPoly_Specs.cs @@ -12,26 +12,29 @@ public class A_fault_message [Test] public void Should_have_the_fault_base_message_class_type() { - Assert.That(MessageTypeCache>.MessageTypeNames, Contains.Item(MessageUrn.ForType(typeof(Fault)))); + Assert.That(MessageTypeCache>.MessageTypeNames, + Contains.Item(MessageUrn.ForTypeString(typeof(Fault)))); } [Test] public void Should_have_the_fault_base_message_type() { - Assert.That(MessageTypeCache>.MessageTypeNames, Contains.Item(MessageUrn.ForType(typeof(Fault)))); + Assert.That(MessageTypeCache>.MessageTypeNames, + Contains.Item(MessageUrn.ForTypeString(typeof(Fault)))); } [Test] public void Should_have_the_fault_message_class_type() { Assert.That(MessageTypeCache>.MessageTypeNames, - Contains.Item(MessageUrn.ForType(typeof(Fault)))); + Contains.Item(MessageUrn.ForTypeString(typeof(Fault)))); } [Test] public void Should_have_the_fault_message_type() { - Assert.That(MessageTypeCache>.MessageTypeNames, Contains.Item(MessageUrn.ForType(typeof(Fault)))); + Assert.That(MessageTypeCache>.MessageTypeNames, + Contains.Item(MessageUrn.ForTypeString(typeof(Fault)))); } } @@ -56,7 +59,7 @@ await InputQueueSendEndpoint.Send(new protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { - configurator.Handler(async context => throw new IntentionalTestException()); + configurator.Handler(async _ => throw new IntentionalTestException()); } } diff --git a/tests/MassTransit.Tests/FaultPublish_Specs.cs b/tests/MassTransit.Tests/FaultPublish_Specs.cs index 6d7b4689270..e97f685cd0c 100644 --- a/tests/MassTransit.Tests/FaultPublish_Specs.cs +++ b/tests/MassTransit.Tests/FaultPublish_Specs.cs @@ -6,7 +6,6 @@ using System.Threading.Tasks; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -20,7 +19,7 @@ public async Task Should_publish_a_single_fault_when_retried() { await InputQueueSendEndpoint.Send(new PingMessage()); - _consumer.Faults.Select().Count().ShouldBe(1); + Assert.That(_consumer.Faults.Select().Count(), Is.EqualTo(1)); } PingConsumer _consumer; @@ -29,7 +28,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin { _consumer = new PingConsumer(TimeSpan.FromSeconds(5), InMemoryTestHarness.InactivityToken); - configurator.UseRetry(r => r.Immediate(5)); + configurator.UseMessageRetry(r => r.Immediate(5)); _consumer.Configure(configurator); } @@ -62,7 +61,7 @@ public async Task Should_publish_a_single_fault_when_retried() { await InputQueueSendEndpoint.Send(new PingMessage()); - _consumer.Faults.Select().Count().ShouldBe(0); + Assert.That(_consumer.Faults.Select().Count(), Is.EqualTo(0)); } PingConsumer _consumer; @@ -73,7 +72,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin _consumer = new PingConsumer(TimeSpan.FromSeconds(5), InMemoryTestHarness.InactivityToken); - configurator.UseRetry(r => r.Immediate(5)); + configurator.UseMessageRetry(r => r.Immediate(5)); _consumer.Configure(configurator); } @@ -106,7 +105,7 @@ public async Task Should_include_the_faulted_message_types() { await InputQueueSendEndpoint.Send(new { }); - _consumer.Faults.Select().Count().ShouldBe(1); + Assert.That(_consumer.Faults.Select().Count(), Is.EqualTo(1)); IReceivedMessage> fault = _consumer.Faults.Select().FirstOrDefault(); Assert.That(fault, Is.Not.Null); @@ -114,10 +113,13 @@ public async Task Should_include_the_faulted_message_types() await TestContext.Out.WriteLineAsync(string.Join(Environment.NewLine, fault.Context.Message.FaultMessageTypes)); await TestContext.Out.WriteLineAsync(string.Join(Environment.NewLine, fault.Context.SupportedMessageTypes)); - Assert.That(fault.Context.Message.FaultMessageTypes.Contains(MessageUrn.ForTypeString())); - Assert.That(fault.Context.Message.FaultMessageTypes.Contains(MessageUrn.ForTypeString())); + Assert.Multiple(() => + { + Assert.That(fault.Context.Message.FaultMessageTypes, Does.Contain(MessageUrn.ForTypeString())); + Assert.That(fault.Context.Message.FaultMessageTypes, Does.Contain(MessageUrn.ForTypeString())); - Assert.That(fault.Context.SupportedMessageTypes.Contains(MessageUrn.ForTypeString>())); + Assert.That(fault.Context.SupportedMessageTypes, Does.Contain(MessageUrn.ForTypeString>())); + }); } BaseMessageConsumer _consumer; @@ -167,7 +169,7 @@ public async Task Should_include_the_faulted_message_types() { await InputQueueSendEndpoint.Send(new { }); - _consumer.Faults.Select().Count().ShouldBe(1); + Assert.That(_consumer.Faults.Select().Count(), Is.EqualTo(1)); IReceivedMessage> fault = _consumer.Faults.Select().FirstOrDefault(); Assert.That(fault, Is.Not.Null); @@ -175,11 +177,14 @@ public async Task Should_include_the_faulted_message_types() await TestContext.Out.WriteLineAsync(string.Join(Environment.NewLine, fault.Context.Message.FaultMessageTypes)); await TestContext.Out.WriteLineAsync(string.Join(Environment.NewLine, fault.Context.SupportedMessageTypes)); - Assert.That(fault.Context.Message.FaultMessageTypes.Contains(MessageUrn.ForTypeString())); - Assert.That(fault.Context.Message.FaultMessageTypes.Contains(MessageUrn.ForTypeString())); + Assert.Multiple(() => + { + Assert.That(fault.Context.Message.FaultMessageTypes, Does.Contain(MessageUrn.ForTypeString())); + Assert.That(fault.Context.Message.FaultMessageTypes, Does.Contain(MessageUrn.ForTypeString())); - Assert.That(fault.Context.SupportedMessageTypes.Contains(MessageUrn.ForTypeString>())); - Assert.That(fault.Context.SupportedMessageTypes.Contains(MessageUrn.ForTypeString>())); + Assert.That(fault.Context.SupportedMessageTypes, Does.Contain(MessageUrn.ForTypeString>())); + Assert.That(fault.Context.SupportedMessageTypes, Does.Contain(MessageUrn.ForTypeString>())); + }); } BaseMessageConsumer _consumer; @@ -199,7 +204,7 @@ class FaultConsumer : { public Task Consume(ConsumeContext context) { - if (context.TryGetMessage(out ConsumeContext actualContext)) + if (context.TryGetMessage(out ConsumeContext actualContext)) return actualContext.NotifyFaulted(TimeSpan.Zero, TypeCache.ShortName, new IntentionalTestException()); throw new InvalidOperationException("This was not expected, but gets the job done"); diff --git a/tests/MassTransit.Tests/FutureLocation_Specs.cs b/tests/MassTransit.Tests/FutureLocation_Specs.cs index 2c995d95e11..38c6eba5b2d 100644 --- a/tests/MassTransit.Tests/FutureLocation_Specs.cs +++ b/tests/MassTransit.Tests/FutureLocation_Specs.cs @@ -19,8 +19,11 @@ public void Should_round_trip_without_issue() var returnedLocation = new FutureLocation(location); - Assert.That(returnedLocation.Id, Is.EqualTo(id)); - Assert.That(returnedLocation.Address, Is.EqualTo(new Uri("queue:input-queue"))); + Assert.Multiple(() => + { + Assert.That(returnedLocation.Id, Is.EqualTo(id)); + Assert.That(returnedLocation.Address, Is.EqualTo(new Uri("queue:input-queue"))); + }); } } } diff --git a/tests/MassTransit.Tests/Groups/Group_Specs.cs b/tests/MassTransit.Tests/Groups/Group_Specs.cs index 90d80d2aa1a..7eabe9bc741 100644 --- a/tests/MassTransit.Tests/Groups/Group_Specs.cs +++ b/tests/MassTransit.Tests/Groups/Group_Specs.cs @@ -19,7 +19,7 @@ public void It_should_be_easy_to_build_and_send_a_group_of_messages() CorrelatedMessageGroup messageGroup = createOrder.CombineWith(orderItemList.ToArray()); - Assert.AreEqual(1, messageGroup.Count()); + Assert.That(messageGroup.Count(), Is.EqualTo(1)); } [Test] @@ -35,7 +35,7 @@ public void It_should_be_easy_to_build_and_send_a_group_of_messages_with_multipl CorrelatedMessageGroup messageGroup = createOrder.CombineWith(orderItemList.ToArray()); - Assert.AreEqual(3, messageGroup.Count()); + Assert.That(messageGroup.Count(), Is.EqualTo(3)); } [Test] @@ -55,7 +55,7 @@ public void Multiple_groups_should_be_combinable_into_a_single_group() messageGroup.CombineWith(secondGroup); - Assert.AreEqual(5, messageGroup.Count()); + Assert.That(messageGroup.Count(), Is.EqualTo(5)); } [SetUp] @@ -73,7 +73,7 @@ public static class OIUWERO public static CorrelatedMessageGroup CombineWith(this CorrelatedBy message, params TMessage[] messages) where TMessage : CorrelatedBy { - var group = new CorrelatedMessageGroup {message}; + var group = new CorrelatedMessageGroup { message }; group.AddRange(messages); diff --git a/tests/MassTransit.Tests/HeaderObject_Specs.cs b/tests/MassTransit.Tests/HeaderObject_Specs.cs index a52876d52d8..982c03ba577 100644 --- a/tests/MassTransit.Tests/HeaderObject_Specs.cs +++ b/tests/MassTransit.Tests/HeaderObject_Specs.cs @@ -27,8 +27,11 @@ await InputQueueSendEndpoint.Send(new PingMessage(), context => var identity = await _header.Task; - Assert.AreEqual(27, identity.IdentityId); - Assert.AreEqual("AAD:Claims", identity.IdentityType); + Assert.Multiple(() => + { + Assert.That(identity.IdentityId, Is.EqualTo(27)); + Assert.That(identity.IdentityType, Is.EqualTo("AAD:Claims")); + }); } Task> _handled; diff --git a/tests/MassTransit.Tests/HostInfo_Specs.cs b/tests/MassTransit.Tests/HostInfo_Specs.cs index b94125516cd..b9d7445c716 100644 --- a/tests/MassTransit.Tests/HostInfo_Specs.cs +++ b/tests/MassTransit.Tests/HostInfo_Specs.cs @@ -18,16 +18,19 @@ public async Task Should_match_the_sending_host_information() ConsumeContext context = await _handled; - Assert.IsNotNull(context.Host); + Assert.That(context.Host, Is.Not.Null); - Assert.AreEqual(HostMetadataCache.Host.MachineName, context.Host.MachineName); - Assert.AreEqual(HostMetadataCache.Host.Assembly, context.Host.Assembly); - Assert.AreEqual(HostMetadataCache.Host.AssemblyVersion, context.Host.AssemblyVersion); - Assert.AreEqual(HostMetadataCache.Host.FrameworkVersion, context.Host.FrameworkVersion); - Assert.AreEqual(HostMetadataCache.Host.MassTransitVersion, context.Host.MassTransitVersion); - Assert.AreEqual(HostMetadataCache.Host.OperatingSystemVersion, context.Host.OperatingSystemVersion); - Assert.AreEqual(HostMetadataCache.Host.ProcessName, context.Host.ProcessName); - Assert.AreEqual(HostMetadataCache.Host.ProcessId, context.Host.ProcessId); + Assert.Multiple(() => + { + Assert.That(context.Host.MachineName, Is.EqualTo(HostMetadataCache.Host.MachineName)); + Assert.That(context.Host.Assembly, Is.EqualTo(HostMetadataCache.Host.Assembly)); + Assert.That(context.Host.AssemblyVersion, Is.EqualTo(HostMetadataCache.Host.AssemblyVersion)); + Assert.That(context.Host.FrameworkVersion, Is.EqualTo(HostMetadataCache.Host.FrameworkVersion)); + Assert.That(context.Host.MassTransitVersion, Is.EqualTo(HostMetadataCache.Host.MassTransitVersion)); + Assert.That(context.Host.OperatingSystemVersion, Is.EqualTo(HostMetadataCache.Host.OperatingSystemVersion)); + Assert.That(context.Host.ProcessName, Is.EqualTo(HostMetadataCache.Host.ProcessName)); + Assert.That(context.Host.ProcessId, Is.EqualTo(HostMetadataCache.Host.ProcessId)); + }); } Task> _handled; diff --git a/tests/MassTransit.Tests/ITestBusConfiguration.cs b/tests/MassTransit.Tests/ITestBusConfiguration.cs new file mode 100644 index 00000000000..ddb2f7deb11 --- /dev/null +++ b/tests/MassTransit.Tests/ITestBusConfiguration.cs @@ -0,0 +1,54 @@ +namespace MassTransit.Tests +{ + public interface ITestBusConfiguration + { + void ConfigureBus(IBusRegistrationContext context, IBusFactoryConfigurator configurator) + where T : IReceiveEndpointConfigurator; + } + + + namespace Scenario + { + public class Json : + ITestBusConfiguration + { + public void ConfigureBus(IBusRegistrationContext context, IBusFactoryConfigurator configurator) + where T : IReceiveEndpointConfigurator + { + } + } + + + public class RawJson : + ITestBusConfiguration + { + public void ConfigureBus(IBusRegistrationContext context, IBusFactoryConfigurator configurator) + where T : IReceiveEndpointConfigurator + { + configurator.UseRawJsonSerializer(); + } + } + + + public class NewtonsoftJson : + ITestBusConfiguration + { + public void ConfigureBus(IBusRegistrationContext context, IBusFactoryConfigurator configurator) + where T : IReceiveEndpointConfigurator + { + configurator.UseNewtonsoftJsonSerializer(); + } + } + + + public class NewtonsoftRawJson : + ITestBusConfiguration + { + public void ConfigureBus(IBusRegistrationContext context, IBusFactoryConfigurator configurator) + where T : IReceiveEndpointConfigurator + { + configurator.UseNewtonsoftRawJsonSerializer(); + } + } + } +} diff --git a/tests/MassTransit.Tests/InMemoryDuo_Specs.cs b/tests/MassTransit.Tests/InMemoryDuo_Specs.cs index 2185f01dc13..25171246309 100644 --- a/tests/MassTransit.Tests/InMemoryDuo_Specs.cs +++ b/tests/MassTransit.Tests/InMemoryDuo_Specs.cs @@ -6,7 +6,6 @@ using MassTransit.Middleware; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -31,7 +30,7 @@ public async Task Should_keep_em_separated() { await externalHarness.Bus.Publish(new A()); - realConsumer.Consumed.Select().Any().ShouldBeTrue(); + Assert.That(realConsumer.Consumed.Select().Any(), Is.True); } finally { diff --git a/tests/MassTransit.Tests/InMemoryOutboxRedelivery_Specs.cs b/tests/MassTransit.Tests/InMemoryOutboxRedelivery_Specs.cs index f2e0997e947..72fdcfc0ced 100644 --- a/tests/MassTransit.Tests/InMemoryOutboxRedelivery_Specs.cs +++ b/tests/MassTransit.Tests/InMemoryOutboxRedelivery_Specs.cs @@ -182,7 +182,7 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { configurator.UseDelayedRedelivery(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); - configurator.UseRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); + configurator.UseMessageRetry(r => r.Interval(1, TimeSpan.FromMilliseconds(100))); configurator.UseInMemoryOutbox(); configurator.Consumer(); diff --git a/tests/MassTransit.Tests/Initializers/Class_Specs.cs b/tests/MassTransit.Tests/Initializers/Class_Specs.cs index ab365d6f570..c9f2e71c416 100644 --- a/tests/MassTransit.Tests/Initializers/Class_Specs.cs +++ b/tests/MassTransit.Tests/Initializers/Class_Specs.cs @@ -26,8 +26,11 @@ public async Task Should_initialize_a_fault() Assert.That(context.Message.Fault, Is.Not.Null); Assert.That(context.Message.Fault.Host, Is.Not.Null); - Assert.That(context.Message.Fault.Host.MachineName, Is.EqualTo(HostMetadataCache.Host.MachineName)); - Assert.That(context.Message.Fault.Message, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(context.Message.Fault.Host.MachineName, Is.EqualTo(HostMetadataCache.Host.MachineName)); + Assert.That(context.Message.Fault.Message, Is.Not.Null); + }); Assert.That(context.Message.Fault.Message.Text, Is.EqualTo("Hello")); } @@ -45,11 +48,17 @@ public async Task Should_initialize_all_the_properties() }); Assert.That(context.Message, Is.Not.Null); - Assert.That(context.Message.Name, Is.EqualTo("Frank")); + Assert.Multiple(() => + { + Assert.That(context.Message.Name, Is.EqualTo("Frank")); - Assert.That(context.Message.Address, Is.Not.Null); - Assert.That(context.Message.Address.Street, Is.EqualTo("123 American Way")); - Assert.That(context.Message.Address.City, Is.EqualTo("Dallas")); + Assert.That(context.Message.Address, Is.Not.Null); + }); + Assert.Multiple(() => + { + Assert.That(context.Message.Address.Street, Is.EqualTo("123 American Way")); + Assert.That(context.Message.Address.City, Is.EqualTo("Dallas")); + }); } [Test] @@ -61,19 +70,19 @@ public async Task Should_initialize_interface_with_readonly_property() } [Test] - public async Task Should_initialize_object_with_readonly_property() + public async Task Should_initialize_interface_with_readonly_property_from_subclass() { - var model1 = new { ReadWrite = "Some Property Value" }; + var model2 = new ReadWriteReadOnly { ReadWrite = "Some Property Value" }; - await MessageInitializerCache.Initialize(model1); + await MessageInitializerCache.Initialize(model2); } [Test] - public async Task Should_initialize_interface_with_readonly_property_from_subclass() + public async Task Should_initialize_object_with_readonly_property() { - var model2 = new ReadWriteReadOnly { ReadWrite = "Some Property Value" }; + var model1 = new { ReadWrite = "Some Property Value" }; - await MessageInitializerCache.Initialize(model2); + await MessageInitializerCache.Initialize(model1); } [Test] diff --git a/tests/MassTransit.Tests/Initializers/Expando_Specs.cs b/tests/MassTransit.Tests/Initializers/Expando_Specs.cs index 326c121620b..95f1a651fdd 100644 --- a/tests/MassTransit.Tests/Initializers/Expando_Specs.cs +++ b/tests/MassTransit.Tests/Initializers/Expando_Specs.cs @@ -28,35 +28,40 @@ public async Task Should_do_the_right_thing() InitializeContext message = await MessageInitializerCache.Initialize(dto); - Assert.That(message.Message.Id, Is.EqualTo(27)); - Assert.That(message.Message.CustomerId, Is.EqualTo("SuperMart")); - Assert.That(message.Message.UniqueId, Is.EqualTo(uniqueId)); - Assert.That(message.Message.CustomerType, Is.EqualTo(CustomerType.Public)); - Assert.That(message.Message.TypeByName, Is.EqualTo(CustomerType.Internal)); + Assert.Multiple(() => + { + Assert.That(message.Message.Id, Is.EqualTo(27)); + Assert.That(message.Message.CustomerId, Is.EqualTo("SuperMart")); + Assert.That(message.Message.UniqueId, Is.EqualTo(uniqueId)); + Assert.That(message.Message.CustomerType, Is.EqualTo(CustomerType.Public)); + Assert.That(message.Message.TypeByName, Is.EqualTo(CustomerType.Internal)); + }); } [Test] public void Should_have_an_interface_from_dictionary_converter() { var factory = new PropertyProviderFactory>(); - Assert.IsTrue(factory.TryGetPropertyConverter(out IPropertyConverter converter)); + Assert.That(factory.TryGetPropertyConverter(out IPropertyConverter converter), Is.True); } [Test] public async Task Should_properly_handle_the_big_dto() { - var order = new OrderDto(); - order.Amount = 123.45m; - order.Id = 27; - order.CustomerId = "FRANK01"; - order.ItemType = "Crayon"; - order.OrderState = new OrderState(OrderStatus.Validated); - order.TokenizedCreditCard = new TokenizedCreditCardDto + var order = new OrderDto { - ExpirationMonth = "12", - ExpirationYear = "2019", - PublicKey = new JObject(new JProperty("key", "12345")), - Token = new JObject(new JProperty("value", "Token123")) + Amount = 123.45m, + Id = 27, + CustomerId = "FRANK01", + ItemType = "Crayon", + OrderState = new OrderState(OrderStatus.Validated), + TokenizedCreditCard = new TokenizedCreditCardDto + { + ExpirationMonth = "12", + ExpirationYear = "2019", + PublicKey = new JObject(new JProperty("key", "12345")), + Token = new JObject(new JProperty("value", "Token123")) + } }; var correlationId = Guid.NewGuid(); @@ -68,11 +73,17 @@ public async Task Should_properly_handle_the_big_dto() ConsumerProcessed = true }); - Assert.That(message.Message.CorrelationId, Is.EqualTo(correlationId)); - Assert.That(message.Message.Order, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Message.CorrelationId, Is.EqualTo(correlationId)); + Assert.That(message.Message.Order, Is.Not.Null); + }); Assert.That(message.Message.Order.OrderState, Is.Not.Null); - Assert.That(message.Message.Order.OrderState.Status, Is.EqualTo(OrderStatus.Validated)); - Assert.That(message.Message.Order.TokenizedCreditCard, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Message.Order.OrderState.Status, Is.EqualTo(OrderStatus.Validated)); + Assert.That(message.Message.Order.TokenizedCreditCard, Is.Not.Null); + }); Assert.That(message.Message.Order.TokenizedCreditCard.ExpirationMonth, Is.EqualTo("12")); } @@ -87,9 +98,12 @@ public async Task Should_work_with_a_dictionary() InitializeContext message = await MessageInitializerCache.Initialize(dto); - Assert.That(message.Message.Id, Is.EqualTo(27)); - Assert.That(message.Message.CustomerId, Is.EqualTo("SuperMart")); - Assert.That(message.Message.UniqueId, Is.EqualTo(uniqueId)); + Assert.Multiple(() => + { + Assert.That(message.Message.Id, Is.EqualTo(27)); + Assert.That(message.Message.CustomerId, Is.EqualTo("SuperMart")); + Assert.That(message.Message.UniqueId, Is.EqualTo(uniqueId)); + }); } [Test] @@ -101,12 +115,15 @@ public async Task Should_work_with_a_dictionary_sourced_object_property() dto.Add(nameof(MessageContract.CustomerId), "SuperMart"); dto.Add(nameof(MessageContract.UniqueId), uniqueId); - InitializeContext message = await MessageInitializerCache.Initialize(new {Contract = dto}); + InitializeContext message = await MessageInitializerCache.Initialize(new { Contract = dto }); Assert.That(message.Message.Contract, Is.Not.Null); - Assert.That(message.Message.Contract.Id, Is.EqualTo(27)); - Assert.That(message.Message.Contract.CustomerId, Is.EqualTo("SuperMart")); - Assert.That(message.Message.Contract.UniqueId, Is.EqualTo(uniqueId)); + Assert.Multiple(() => + { + Assert.That(message.Message.Contract.Id, Is.EqualTo(27)); + Assert.That(message.Message.Contract.CustomerId, Is.EqualTo("SuperMart")); + Assert.That(message.Message.Contract.UniqueId, Is.EqualTo(uniqueId)); + }); } [Test] @@ -142,8 +159,11 @@ public async Task Should_work_with_a_list() InitializeContext message = await MessageInitializerCache.Initialize(expando); // doesn't work (orders not included) - Assert.That(message.Message.Id, Is.EqualTo(32)); - Assert.That(message.Message.Orders, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(message.Message.Id, Is.EqualTo(32)); + Assert.That(message.Message.Orders, Is.Not.Null); + }); } diff --git a/tests/MassTransit.Tests/Initializers/HeaderInitializer_Specs.cs b/tests/MassTransit.Tests/Initializers/HeaderInitializer_Specs.cs index 3f89bea3c07..ec2239f9546 100644 --- a/tests/MassTransit.Tests/Initializers/HeaderInitializer_Specs.cs +++ b/tests/MassTransit.Tests/Initializers/HeaderInitializer_Specs.cs @@ -30,16 +30,23 @@ await Bus.Publish(new ConsumeContext context = await _handled; - Assert.That(context.ResponseAddress, Is.EqualTo(new Uri(responseAddress))); - Assert.That(context.RequestId, Is.EqualTo(requestId)); - Assert.That(context.ExpirationTime.HasValue, Is.True); - Assert.That(context.ExpirationTime.Value, Is.GreaterThanOrEqualTo(now + TimeSpan.FromSeconds(5))); - - Assert.That(context.Headers.TryGetHeader("Custom-Header-Value", out var value), Is.True); - Assert.That(value, Is.EqualTo("Frankie Say Relax")); - - Assert.That(context.Headers.TryGetHeader("Custom-Header-Value2", out value), Is.True); - Assert.That(value, Is.EqualTo(27)); + Assert.Multiple(() => + { + Assert.That(context.ResponseAddress, Is.EqualTo(new Uri(responseAddress))); + Assert.That(context.RequestId, Is.EqualTo(requestId)); + Assert.That(context.ExpirationTime.HasValue, Is.True); + Assert.That(context.ExpirationTime.Value, Is.GreaterThanOrEqualTo(now + TimeSpan.FromSeconds(5))); + }); + Assert.Multiple(() => + { + Assert.That(context.Headers.TryGetHeader("Custom-Header-Value", out var value), Is.True); + Assert.That(value, Is.EqualTo("Frankie Say Relax")); + }); + Assert.Multiple(() => + { + Assert.That(context.Headers.TryGetHeader("Custom-Header-Value2", out var value), Is.True); + Assert.That(value, Is.EqualTo(27)); + }); } Task> _handled; diff --git a/tests/MassTransit.Tests/Initializers/Initializer_Specs.cs b/tests/MassTransit.Tests/Initializers/Initializer_Specs.cs index a1080b26b19..3cc9eaaeb77 100644 --- a/tests/MassTransit.Tests/Initializers/Initializer_Specs.cs +++ b/tests/MassTransit.Tests/Initializers/Initializer_Specs.cs @@ -13,7 +13,7 @@ public class Initializer_Specs [Test] public async Task Should_convert_value_types_to_strings() { - InitializeContext context = await MessageInitializerCache.Initialize(new {Text = _intValue}); + InitializeContext context = await MessageInitializerCache.Initialize(new { Text = _intValue }); Assert.That(context.Message.Text, Is.EqualTo(_intValue.ToString())); } @@ -40,19 +40,22 @@ public async Task Should_copy_the_property_values() var message = context.Message; - Assert.That(message.StringValue, Is.EqualTo(_stringValue)); - Assert.That(message.BoolValue, Is.EqualTo(_boolValue)); - Assert.That(message.ByteValue, Is.EqualTo(_byteValue)); - Assert.That(message.ShortValue, Is.EqualTo(_shortValue)); - Assert.That(message.IntValue, Is.EqualTo(_intValue)); - Assert.That(message.LongValue, Is.EqualTo(_longValue)); - Assert.That(message.DoubleValue, Is.EqualTo(_doubleValue)); - Assert.That(message.DecimalValue, Is.EqualTo(_decimalValue)); - Assert.That(message.DateTimeValue, Is.EqualTo(_dateTimeValue)); - Assert.That(message.DateTimeOffsetValue, Is.EqualTo(_dateTimeOffsetValue)); - Assert.That(message.TimeSpanValue, Is.EqualTo(_timeSpanValue)); - Assert.That(message.DayValue, Is.EqualTo(_dayValue)); - Assert.That(message.ObjectValue, Is.EqualTo(_objectValue)); + Assert.Multiple(() => + { + Assert.That(message.StringValue, Is.EqualTo(_stringValue)); + Assert.That(message.BoolValue, Is.EqualTo(_boolValue)); + Assert.That(message.ByteValue, Is.EqualTo(_byteValue)); + Assert.That(message.ShortValue, Is.EqualTo(_shortValue)); + Assert.That(message.IntValue, Is.EqualTo(_intValue)); + Assert.That(message.LongValue, Is.EqualTo(_longValue)); + Assert.That(message.DoubleValue, Is.EqualTo(_doubleValue)); + Assert.That(message.DecimalValue, Is.EqualTo(_decimalValue)); + Assert.That(message.DateTimeValue, Is.EqualTo(_dateTimeValue)); + Assert.That(message.DateTimeOffsetValue, Is.EqualTo(_dateTimeOffsetValue)); + Assert.That(message.TimeSpanValue, Is.EqualTo(_timeSpanValue)); + Assert.That(message.DayValue, Is.EqualTo(_dayValue)); + Assert.That(message.ObjectValue, Is.EqualTo(_objectValue)); + }); } [Test] @@ -76,18 +79,21 @@ public async Task Should_copy_the_property_values_from_nullable_types() var message = context.Message; - Assert.That(message.StringValue, Is.EqualTo(_stringValue)); - Assert.That(message.BoolValue, Is.EqualTo(_boolValue)); - Assert.That(message.ByteValue, Is.EqualTo(_byteValue)); - Assert.That(message.ShortValue, Is.EqualTo(_shortValue)); - Assert.That(message.IntValue, Is.EqualTo(_intValue)); - Assert.That(message.LongValue, Is.EqualTo(_longValue)); - Assert.That(message.DoubleValue, Is.EqualTo(_doubleValue)); - Assert.That(message.DecimalValue, Is.EqualTo(_decimalValue)); - Assert.That(message.DateTimeValue, Is.EqualTo(_dateTimeValue)); - Assert.That(message.DateTimeOffsetValue, Is.EqualTo(_dateTimeOffsetValue)); - Assert.That(message.TimeSpanValue, Is.EqualTo(_timeSpanValue)); - Assert.That(message.DayValue, Is.EqualTo(_dayValue)); + Assert.Multiple(() => + { + Assert.That(message.StringValue, Is.EqualTo(_stringValue)); + Assert.That(message.BoolValue, Is.EqualTo(_boolValue)); + Assert.That(message.ByteValue, Is.EqualTo(_byteValue)); + Assert.That(message.ShortValue, Is.EqualTo(_shortValue)); + Assert.That(message.IntValue, Is.EqualTo(_intValue)); + Assert.That(message.LongValue, Is.EqualTo(_longValue)); + Assert.That(message.DoubleValue, Is.EqualTo(_doubleValue)); + Assert.That(message.DecimalValue, Is.EqualTo(_decimalValue)); + Assert.That(message.DateTimeValue, Is.EqualTo(_dateTimeValue)); + Assert.That(message.DateTimeOffsetValue, Is.EqualTo(_dateTimeOffsetValue)); + Assert.That(message.TimeSpanValue, Is.EqualTo(_timeSpanValue)); + Assert.That(message.DayValue, Is.EqualTo(_dayValue)); + }); } [Test] @@ -112,19 +118,22 @@ public async Task Should_copy_the_property_values_from_strings() var message = context.Message; - Assert.That(message.StringValue, Is.EqualTo(_stringValue)); - Assert.That(message.BoolValue, Is.EqualTo(_boolValue)); - Assert.That(message.ByteValue, Is.EqualTo(_byteValue)); - Assert.That(message.ShortValue, Is.EqualTo(_shortValue)); - Assert.That(message.IntValue, Is.EqualTo(_intValue)); - Assert.That(message.LongValue, Is.EqualTo(_longValue)); - Assert.That(message.DoubleValue, Is.EqualTo(_doubleValue)); - Assert.That(message.DecimalValue, Is.EqualTo(_decimalValue)); - Assert.That(message.DateTimeValue, Is.EqualTo(_dateTimeValue)); - Assert.That(message.DateTimeOffsetValue, Is.EqualTo(_dateTimeOffsetValue)); - Assert.That(message.TimeSpanValue, Is.EqualTo(_timeSpanValue)); - Assert.That(message.DayValue, Is.EqualTo(_dayValue)); - Assert.That(message.ObjectValue, Is.EqualTo(_objectValue)); + Assert.Multiple(() => + { + Assert.That(message.StringValue, Is.EqualTo(_stringValue)); + Assert.That(message.BoolValue, Is.EqualTo(_boolValue)); + Assert.That(message.ByteValue, Is.EqualTo(_byteValue)); + Assert.That(message.ShortValue, Is.EqualTo(_shortValue)); + Assert.That(message.IntValue, Is.EqualTo(_intValue)); + Assert.That(message.LongValue, Is.EqualTo(_longValue)); + Assert.That(message.DoubleValue, Is.EqualTo(_doubleValue)); + Assert.That(message.DecimalValue, Is.EqualTo(_decimalValue)); + Assert.That(message.DateTimeValue, Is.EqualTo(_dateTimeValue)); + Assert.That(message.DateTimeOffsetValue, Is.EqualTo(_dateTimeOffsetValue)); + Assert.That(message.TimeSpanValue, Is.EqualTo(_timeSpanValue)); + Assert.That(message.DayValue, Is.EqualTo(_dayValue)); + Assert.That(message.ObjectValue, Is.EqualTo(_objectValue)); + }); } [Test] @@ -148,18 +157,20 @@ public async Task Should_copy_the_property_values_to_nullable_types() var message = context.Message; - - Assert.That(message.BoolValue, Is.EqualTo(_boolValue)); - Assert.That(message.ByteValue, Is.EqualTo(_byteValue)); - Assert.That(message.ShortValue, Is.EqualTo(_shortValue)); - Assert.That(message.IntValue, Is.EqualTo(_intValue)); - Assert.That(message.LongValue, Is.EqualTo(_longValue)); - Assert.That(message.DoubleValue, Is.EqualTo(_doubleValue)); - Assert.That(message.DecimalValue, Is.EqualTo(_decimalValue)); - Assert.That(message.DateTimeValue, Is.EqualTo(_dateTimeValue)); - Assert.That(message.DateTimeOffsetValue, Is.EqualTo(_dateTimeOffsetValue)); - Assert.That(message.TimeSpanValue, Is.EqualTo(_timeSpanValue)); - Assert.That(message.DayValue, Is.EqualTo(_dayValue)); + Assert.Multiple(() => + { + Assert.That(message.BoolValue, Is.EqualTo(_boolValue)); + Assert.That(message.ByteValue, Is.EqualTo(_byteValue)); + Assert.That(message.ShortValue, Is.EqualTo(_shortValue)); + Assert.That(message.IntValue, Is.EqualTo(_intValue)); + Assert.That(message.LongValue, Is.EqualTo(_longValue)); + Assert.That(message.DoubleValue, Is.EqualTo(_doubleValue)); + Assert.That(message.DecimalValue, Is.EqualTo(_decimalValue)); + Assert.That(message.DateTimeValue, Is.EqualTo(_dateTimeValue)); + Assert.That(message.DateTimeOffsetValue, Is.EqualTo(_dateTimeOffsetValue)); + Assert.That(message.TimeSpanValue, Is.EqualTo(_timeSpanValue)); + Assert.That(message.DayValue, Is.EqualTo(_dayValue)); + }); } readonly bool _boolValue = true; diff --git a/tests/MassTransit.Tests/Initializers/MessageInitializer_Specs.cs b/tests/MassTransit.Tests/Initializers/MessageInitializer_Specs.cs index 97011648554..980db52b52e 100644 --- a/tests/MassTransit.Tests/Initializers/MessageInitializer_Specs.cs +++ b/tests/MassTransit.Tests/Initializers/MessageInitializer_Specs.cs @@ -20,17 +20,20 @@ public async Task Should_initialize_the_properties() { IRequestClient client = CreateRequestClient(); - Response response = await client.GetResponse(new {Name = "Hello"}); + Response response = await client.GetResponse(new { Name = "Hello" }); - Assert.That(response.Message.Name, Is.EqualTo("Hello")); - Assert.That(response.Message.Value, Is.EqualTo("World")); + Assert.Multiple(() => + { + Assert.That(response.Message.Name, Is.EqualTo("Hello")); + Assert.That(response.Message.Value, Is.EqualTo("World")); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { configurator.Handler(async context => { - await context.RespondAsync(new {Value = "World"}); + await context.RespondAsync(new { Value = "World" }); }); } } @@ -51,8 +54,11 @@ public async Task Should_initialize_the_properties() var message = context.Message; - Assert.That(message.Message, Is.EqualTo("Hello")); - Assert.That(message.ExceptionType, Is.EqualTo(TypeCache.ShortName)); + Assert.Multiple(() => + { + Assert.That(message.Message, Is.EqualTo("Hello")); + Assert.That(message.ExceptionType, Is.EqualTo(TypeCache.ShortName)); + }); } } @@ -66,10 +72,13 @@ public async Task Should_initialize_the_properties() { IRequestClient client = CreateRequestClient(); - Response response = await client.GetResponse(new {Name = "Hello"}); + Response response = await client.GetResponse(new { Name = "Hello" }); - Assert.That(response.Message.Name, Is.EqualTo("Hello")); - Assert.That(response.Message.Value, Is.Null); + Assert.Multiple(() => + { + Assert.That(response.Message.Name, Is.EqualTo("Hello")); + Assert.That(response.Message.Value, Is.Null); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -98,10 +107,13 @@ public async Task Should_initialize_the_properties() NullableIntValue = 42 }); - Assert.That(response.Message.Name, Is.EqualTo("Hello")); - Assert.That(response.Message.IntValue.HasValue, Is.True); - Assert.That(response.Message.IntValue.Value, Is.EqualTo(27)); - Assert.That(response.Message.NullableIntValue, Is.EqualTo(42)); + Assert.Multiple(() => + { + Assert.That(response.Message.Name, Is.EqualTo("Hello")); + Assert.That(response.Message.IntValue.HasValue, Is.True); + Assert.That(response.Message.IntValue.Value, Is.EqualTo(27)); + Assert.That(response.Message.NullableIntValue, Is.EqualTo(42)); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -122,18 +134,24 @@ public class Creating_a_super_complex_message : public void Should_handle_basic_dictionary() { Assert.That(_response.Message.Strings, Is.Not.Null); - Assert.That(_response.Message.Strings.Count, Is.EqualTo(2)); - Assert.That(_response.Message.Strings["Hello"], Is.EqualTo("World")); - Assert.That(_response.Message.Strings["Thank You"], Is.EqualTo("Next")); + Assert.That(_response.Message.Strings, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(_response.Message.Strings["Hello"], Is.EqualTo("World")); + Assert.That(_response.Message.Strings["Thank You"], Is.EqualTo("Next")); + }); } [Test] public void Should_handle_conversion_dictionary() { Assert.That(_response.Message.IntToStrings, Is.Not.Null); - Assert.That(_response.Message.IntToStrings.Count, Is.EqualTo(2)); - Assert.That(_response.Message.IntToStrings[100], Is.EqualTo("1000")); - Assert.That(_response.Message.IntToStrings[200], Is.EqualTo("2000")); + Assert.That(_response.Message.IntToStrings, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(_response.Message.IntToStrings[100], Is.EqualTo("1000")); + Assert.That(_response.Message.IntToStrings[200], Is.EqualTo("2000")); + }); } [Test] @@ -159,17 +177,23 @@ public void Should_handle_duplicate_input_property_names() public void Should_handle_enumerable_decimal() { Assert.That(_response.Message.Amounts, Is.Not.Null); - Assert.That(_response.Message.Amounts.Count, Is.EqualTo(2)); - Assert.That(_response.Message.Amounts[0], Is.EqualTo(98.6m)); - Assert.That(_response.Message.Amounts[1], Is.EqualTo(98.6m)); + Assert.That(_response.Message.Amounts, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(_response.Message.Amounts[0], Is.EqualTo(98.6m)); + Assert.That(_response.Message.Amounts[1], Is.EqualTo(98.6m)); + }); } [Test] public void Should_handle_enums() { - Assert.That(_response.Message.EngineStatus, Is.EqualTo(Status.Started)); - Assert.That(_response.Message.NumberStatus, Is.EqualTo(Status.Stopped)); - Assert.That(_response.Message.StringStatus, Is.EqualTo(Status.Started)); + Assert.Multiple(() => + { + Assert.That(_response.Message.EngineStatus, Is.EqualTo(Status.Started)); + Assert.That(_response.Message.NumberStatus, Is.EqualTo(Status.Stopped)); + Assert.That(_response.Message.StringStatus, Is.EqualTo(Status.Started)); + }); } [Test] @@ -182,8 +206,11 @@ public void Should_handle_exception() [Test] public async Task Should_handle_id_variable() { - Assert.That(_response.Message.CorrelationId, Is.EqualTo(_response.Message.Id)); - Assert.That(_response.Message.StringId, Is.EqualTo(_response.Message.Id)); + Assert.Multiple(() => + { + Assert.That(_response.Message.CorrelationId, Is.EqualTo(_response.Message.Id)); + Assert.That(_response.Message.StringId, Is.EqualTo(_response.Message.Id)); + }); } [Test] @@ -196,10 +223,13 @@ public void Should_handle_int() public void Should_handle_int_to_string_array() { Assert.That(_response.Message.Numbers, Is.Not.Null); - Assert.That(_response.Message.Numbers.Length, Is.EqualTo(3)); - Assert.That(_response.Message.Numbers[0], Is.EqualTo("12")); - Assert.That(_response.Message.Numbers[1], Is.EqualTo("24")); - Assert.That(_response.Message.Numbers[2], Is.EqualTo("36")); + Assert.That(_response.Message.Numbers, Has.Length.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(_response.Message.Numbers[0], Is.EqualTo("12")); + Assert.That(_response.Message.Numbers[1], Is.EqualTo("24")); + Assert.That(_response.Message.Numbers[2], Is.EqualTo("36")); + }); } [Test] @@ -212,18 +242,24 @@ public void Should_handle_interface_type() [Test] public void Should_handle_interface_type_array() { - Assert.That(_response.Message.SubValues.Length, Is.EqualTo(2)); - Assert.That(_response.Message.SubValues[0].Text, Is.EqualTo("Frank")); - Assert.That(_response.Message.SubValues[1].Text, Is.EqualTo("Lola")); + Assert.That(_response.Message.SubValues, Has.Length.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(_response.Message.SubValues[0].Text, Is.EqualTo("Frank")); + Assert.That(_response.Message.SubValues[1].Text, Is.EqualTo("Lola")); + }); } [Test] public void Should_handle_lists() { Assert.That(_response.Message.StringList, Is.Not.Null); - Assert.That(_response.Message.StringList.Count, Is.EqualTo(2)); - Assert.That(_response.Message.StringList[0], Is.EqualTo("Frank")); - Assert.That(_response.Message.StringList[1], Is.EqualTo("Estelle")); + Assert.That(_response.Message.StringList, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(_response.Message.StringList[0], Is.EqualTo("Frank")); + Assert.That(_response.Message.StringList[1], Is.EqualTo("Estelle")); + }); } [Test] @@ -248,9 +284,12 @@ public void Should_handle_nullable_long() public void Should_handle_object_dictionary() { Assert.That(_response.Message.StringSubValues, Is.Not.Null); - Assert.That(_response.Message.StringSubValues.Count, Is.EqualTo(2)); - Assert.That(_response.Message.StringSubValues["A"].Text, Is.EqualTo("Eh")); - Assert.That(_response.Message.StringSubValues["B"].Text, Is.EqualTo("Bee")); + Assert.That(_response.Message.StringSubValues, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(_response.Message.StringSubValues["A"].Text, Is.EqualTo("Eh")); + Assert.That(_response.Message.StringSubValues["B"].Text, Is.EqualTo("Bee")); + }); } [Test] @@ -263,10 +302,13 @@ public void Should_handle_string() public void Should_handle_string_array() { Assert.That(_response.Message.Names, Is.Not.Null); - Assert.That(_response.Message.Names.Length, Is.EqualTo(3)); - Assert.That(_response.Message.Names[0], Is.EqualTo("Curly")); - Assert.That(_response.Message.Names[1], Is.EqualTo("Larry")); - Assert.That(_response.Message.Names[2], Is.EqualTo("Moe")); + Assert.That(_response.Message.Names, Has.Length.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(_response.Message.Names[0], Is.EqualTo("Curly")); + Assert.That(_response.Message.Names[1], Is.EqualTo("Larry")); + Assert.That(_response.Message.Names[2], Is.EqualTo("Moe")); + }); } [Test] @@ -278,16 +320,22 @@ public void Should_handle_task_of_task_of_int() [Test] public void Should_handle_timestamp_variable() { - Assert.That(_response.Message.Timestamp.HasValue, Is.True); - Assert.That(_response.Message.Timestamp, Is.GreaterThanOrEqualTo(_now)); + Assert.Multiple(() => + { + Assert.That(_response.Message.Timestamp.HasValue, Is.True); + Assert.That(_response.Message.Timestamp, Is.GreaterThanOrEqualTo(_now)); + }); } [Test] public void Should_handle_uris() { - Assert.That(_response.Message.ServiceAddress, Is.EqualTo(new Uri("http://masstransit-project.com"))); - Assert.That(_response.Message.OtherAddress, Is.EqualTo(new Uri("http://github.com"))); - Assert.That(_response.Message.StringAddress, Is.EqualTo("loopback://localhost/")); + Assert.Multiple(() => + { + Assert.That(_response.Message.ServiceAddress, Is.EqualTo(new Uri("http://masstransit-project.com"))); + Assert.That(_response.Message.OtherAddress, Is.EqualTo(new Uri("http://github.com"))); + Assert.That(_response.Message.StringAddress, Is.EqualTo("loopback://localhost/")); + }); } DateTime _now; @@ -312,11 +360,11 @@ public async Task Setup() NullableValue = 42, NotNullableValue = (int?)69, NullableDecimalValue = 123.45m, - Numbers = new[] {12, 24, 36}, - Names = new[] {"Curly", "Larry", "Moe"}, + Numbers = new[] { 12, 24, 36 }, + Names = new[] { "Curly", "Larry", "Moe" }, Exception = new IntentionalTestException("It Happens"), - SubValue = new {Text = "Mary"}, - SubValues = new object[] {new {Text = "Frank"}, new {Text = "Lola"}}, + SubValue = new { Text = "Mary" }, + SubValues = new object[] { new { Text = "Frank" }, new { Text = "Lola" } }, Amount = 867.53m, Amounts = Enumerable.Repeat(98.6m, 2), AsyncValue = GetIntResult().Select(x => x.Number), @@ -326,22 +374,22 @@ public async Task Setup() ServiceAddress = new Uri("http://masstransit-project.com"), OtherAddress = "http://github.com", StringAddress = new Uri("loopback://localhost"), - StringList = new[] {"Frank", "Estelle"}, - NewProperty = new SubProperty {NewProperty = "Hello"}, + StringList = new[] { "Frank", "Estelle" }, + NewProperty = new SubProperty { NewProperty = "Hello" }, Strings = new Dictionary { - {"Hello", "World"}, - {"Thank You", "Next"} + { "Hello", "World" }, + { "Thank You", "Next" } }, StringSubValues = new Dictionary { - {"A", new {Text = "Eh"}}, - {"B", new {Text = "Bee"}} + { "A", new { Text = "Eh" } }, + { "B", new { Text = "Bee" } } }, IntToStrings = new Dictionary { - {100, 1000}, - {200, 2000} + { 100, 1000 }, + { 200, 2000 } } }); } diff --git a/tests/MassTransit.Tests/Initializers/PropertyProvider_Specs.cs b/tests/MassTransit.Tests/Initializers/PropertyProvider_Specs.cs index 054eb9fa6da..9778e1d39b9 100644 --- a/tests/MassTransit.Tests/Initializers/PropertyProvider_Specs.cs +++ b/tests/MassTransit.Tests/Initializers/PropertyProvider_Specs.cs @@ -21,43 +21,43 @@ public class The_property_providers [Test] public async Task Should_support_async_arrays() { - var input = new {IntArray = new[] {Task.FromResult(1), Task.FromResult(2), Task.FromResult(3)}}; + var input = new { IntArray = new[] { Task.FromResult(1), Task.FromResult(2), Task.FromResult(3) } }; var provider = GetPropertyProvider(input, x => x.IntArray, (long[])default); long[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new long[] {1, 2, 3})); + Assert.That(arrayValue, Is.EqualTo(new long[] { 1, 2, 3 })); } [Test] public async Task Should_support_async_arrays_with_nulls() { - var input = new {IntArray = new[] {Task.FromResult(1), Task.FromResult(2), null, Task.FromResult(3)}}; + var input = new { IntArray = new[] { Task.FromResult(1), Task.FromResult(2), null, Task.FromResult(3) } }; var provider = GetPropertyProvider(input, x => x.IntArray, (long[])default); long[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new long[] {1, 2, 0, 3})); + Assert.That(arrayValue, Is.EqualTo(new long[] { 1, 2, 0, 3 })); } [Test] public async Task Should_support_async_convertible_arrays() { - var input = new {IntArray = Task.FromResult(new[] {1, 2, 3})}; + var input = new { IntArray = Task.FromResult(new[] { 1, 2, 3 }) }; var provider = GetPropertyProvider(input, x => x.IntArray, (long[])default); long[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new long[] {1, 2, 3})); + Assert.That(arrayValue, Is.EqualTo(new long[] { 1, 2, 3 })); } [Test] public async Task Should_support_async_input_types() { - var input = new {IntValue = Task.FromResult(27)}; + var input = new { IntValue = Task.FromResult(27) }; var provider = GetPropertyProvider(input, x => x.IntValue, 69); @@ -69,19 +69,19 @@ public async Task Should_support_async_input_types() [Test] public async Task Should_support_async_matching_arrays() { - var input = new {IntArray = Task.FromResult(new[] {1, 2, 3})}; + var input = new { IntArray = Task.FromResult(new[] { 1, 2, 3 }) }; var provider = GetPropertyProvider(input, x => x.IntArray, (int[])default); int[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new[] {1, 2, 3})); + Assert.That(arrayValue, Is.EqualTo(new[] { 1, 2, 3 })); } [Test] public async Task Should_support_async_matching_enumerable() { - var input = new {IntArray = Task.FromResult(new[] {1, 2, 3})}; + var input = new { IntArray = Task.FromResult(new[] { 1, 2, 3 }) }; var provider = GetPropertyProvider(input, x => x.IntArray, (IEnumerable)default); @@ -98,7 +98,7 @@ public async Task Should_support_async_matching_enumerable() [Test] public async Task Should_support_async_matching_lists() { - var input = new {IntArray = Task.FromResult(new[] {1, 2, 3})}; + var input = new { IntArray = Task.FromResult(new[] { 1, 2, 3 }) }; var provider = GetPropertyProvider(input, x => x.IntArray, (List)default); @@ -115,7 +115,7 @@ public async Task Should_support_async_matching_lists() [Test] public async Task Should_support_async_matching_readonly_lists() { - var input = new {IntArray = Task.FromResult(new[] {1, 2, 3})}; + var input = new { IntArray = Task.FromResult(new[] { 1, 2, 3 }) }; var provider = GetPropertyProvider(input, x => x.IntArray, (IReadOnlyList)default); @@ -132,7 +132,7 @@ public async Task Should_support_async_matching_readonly_lists() [Test] public async Task Should_support_async_result_convertible_arrays() { - var input = new {IntArray = new[] {1, 2, 3}}; + var input = new { IntArray = new[] { 1, 2, 3 } }; var provider = GetPropertyProvider(input, x => x.IntArray, (Task)default); @@ -140,13 +140,13 @@ public async Task Should_support_async_result_convertible_arrays() long[] arrayValue = await arrayTask; - Assert.That(arrayValue, Is.EqualTo(new long[] {1, 2, 3})); + Assert.That(arrayValue, Is.EqualTo(new long[] { 1, 2, 3 })); } [Test] public async Task Should_support_async_result_types() { - var input = new {IntValue = 27}; + var input = new { IntValue = 27 }; var provider = GetPropertyProvider(input, x => x.IntValue, Task.FromResult(69)); @@ -159,67 +159,67 @@ public async Task Should_support_async_result_types() [Test] public async Task Should_support_convertible_arrays() { - var input = new {IntArray = new[] {1, 2, 3}}; + var input = new { IntArray = new[] { 1, 2, 3 } }; var provider = GetPropertyProvider(input, x => x.IntArray, (long[])default); long[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new long[] {1, 2, 3})); + Assert.That(arrayValue, Is.EqualTo(new long[] { 1, 2, 3 })); } [Test] public async Task Should_support_convertible_arrays_from_strings() { - var input = new {StringArray = new[] {"1", "2", "", "3"}}; + var input = new { StringArray = new[] { "1", "2", "", "3" } }; var provider = GetPropertyProvider(input, x => x.StringArray, (int?[])default); int?[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new int?[] {1, 2, default, 3})); + Assert.That(arrayValue, Is.EqualTo(new int?[] { 1, 2, default, 3 })); } [Test] public async Task Should_support_convertible_arrays_to_nullable_types() { - var input = new {IntArray = new[] {1, 2, 3}}; + var input = new { IntArray = new[] { 1, 2, 3 } }; var provider = GetPropertyProvider(input, x => x.IntArray, (long?[])default); long?[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new long?[] {1, 2, 3})); + Assert.That(arrayValue, Is.EqualTo(new long?[] { 1, 2, 3 })); } [Test] public async Task Should_support_convertible_arrays_to_strings() { - var input = new {IntArray = new[] {1, 2, 3}}; + var input = new { IntArray = new[] { 1, 2, 3 } }; var provider = GetPropertyProvider(input, x => x.IntArray, (string[])default); string[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new[] {"1", "2", "3"})); + Assert.That(arrayValue, Is.EqualTo(new[] { "1", "2", "3" })); } [Test] public async Task Should_support_convertible_arrays_with_nullable_types() { - var input = new {IntArray = new int?[] {1, 2, 3}}; + var input = new { IntArray = new int?[] { 1, 2, 3 } }; var provider = GetPropertyProvider(input, x => x.IntArray, (long[])default); long[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new long[] {1, 2, 3})); + Assert.That(arrayValue, Is.EqualTo(new long[] { 1, 2, 3 })); } [Test] public async Task Should_support_convertible_property_types() { - var input = new {IntValue = 27}; + var input = new { IntValue = 27 }; var longProvider = GetPropertyProvider(input, x => x.IntValue, 69L); @@ -235,8 +235,8 @@ public async Task Should_support_dictionary() { Strings = new Dictionary { - {"Hello", "World"}, - {"Thank You", "Next"} + { "Hello", "World" }, + { "Thank You", "Next" } } }; @@ -245,9 +245,12 @@ public async Task Should_support_dictionary() IDictionary dictionary = await provider.GetProperty(CreateInitializeContext(input)); Assert.That(dictionary, Is.Not.Null); - Assert.That(dictionary.Count, Is.EqualTo(2)); - Assert.That(dictionary["Hello"], Is.EqualTo("World")); - Assert.That(dictionary["Thank You"], Is.EqualTo("Next")); + Assert.That(dictionary, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(dictionary["Hello"], Is.EqualTo("World")); + Assert.That(dictionary["Thank You"], Is.EqualTo("Next")); + }); } [Test] @@ -257,8 +260,8 @@ public async Task Should_support_dictionary_key_conversion() { Strings = new Dictionary { - {"1", "One"}, - {"2", "Two"} + { "1", "One" }, + { "2", "Two" } } }; @@ -267,9 +270,12 @@ public async Task Should_support_dictionary_key_conversion() IDictionary dictionary = await provider.GetProperty(CreateInitializeContext(input)); Assert.That(dictionary, Is.Not.Null); - Assert.That(dictionary.Count, Is.EqualTo(2)); - Assert.That(dictionary[1], Is.EqualTo("One")); - Assert.That(dictionary[2], Is.EqualTo("Two")); + Assert.That(dictionary, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(dictionary[1], Is.EqualTo("One")); + Assert.That(dictionary[2], Is.EqualTo("Two")); + }); } [Test] @@ -279,8 +285,8 @@ public async Task Should_support_dictionary_type_conversion() { Strings = new Dictionary { - {"Hello", "World"}, - {"Thank You", "Next"} + { "Hello", "World" }, + { "Thank You", "Next" } } }; @@ -289,9 +295,12 @@ public async Task Should_support_dictionary_type_conversion() IDictionary dictionary = await provider.GetProperty(CreateInitializeContext(input)); Assert.That(dictionary, Is.Not.Null); - Assert.That(dictionary.Count, Is.EqualTo(2)); - Assert.That(dictionary["Hello"], Is.EqualTo("World")); - Assert.That(dictionary["Thank You"], Is.EqualTo("Next")); + Assert.That(dictionary, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(dictionary["Hello"], Is.EqualTo("World")); + Assert.That(dictionary["Thank You"], Is.EqualTo("Next")); + }); } [Test] @@ -301,8 +310,8 @@ public async Task Should_support_enumerable_key_value_pair() { Strings = (IEnumerable>)new Dictionary { - {"Hello", "World"}, - {"Thank You", "Next"} + { "Hello", "World" }, + { "Thank You", "Next" } } }; @@ -311,15 +320,18 @@ public async Task Should_support_enumerable_key_value_pair() IDictionary dictionary = await provider.GetProperty(CreateInitializeContext(input)); Assert.That(dictionary, Is.Not.Null); - Assert.That(dictionary.Count, Is.EqualTo(2)); - Assert.That(dictionary["Hello"], Is.EqualTo("World")); - Assert.That(dictionary["Thank You"], Is.EqualTo("Next")); + Assert.That(dictionary, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(dictionary["Hello"], Is.EqualTo("World")); + Assert.That(dictionary["Thank You"], Is.EqualTo("Next")); + }); } [Test] public async Task Should_support_enums() { - var input = new {Status = TaskStatus.RanToCompletion}; + var input = new { Status = TaskStatus.RanToCompletion }; var provider = GetPropertyProvider(input, x => x.Status, TaskStatus.Canceled); @@ -331,7 +343,7 @@ public async Task Should_support_enums() [Test] public async Task Should_support_enums_from_int() { - var input = new {Status = (int)TaskStatus.RanToCompletion}; + var input = new { Status = (int)TaskStatus.RanToCompletion }; var provider = GetPropertyProvider(input, x => x.Status, TaskStatus.Canceled); @@ -343,7 +355,7 @@ public async Task Should_support_enums_from_int() [Test] public async Task Should_support_enums_from_long() { - var input = new {Status = (long)TaskStatus.RanToCompletion}; + var input = new { Status = (long)TaskStatus.RanToCompletion }; var provider = GetPropertyProvider(input, x => x.Status, TaskStatus.Canceled); @@ -355,7 +367,7 @@ public async Task Should_support_enums_from_long() [Test] public async Task Should_support_enums_from_strings() { - var input = new {Status = TaskStatus.RanToCompletion.ToString()}; + var input = new { Status = TaskStatus.RanToCompletion.ToString() }; var provider = GetPropertyProvider(input, x => x.Status, TaskStatus.Canceled); @@ -367,7 +379,7 @@ public async Task Should_support_enums_from_strings() [Test] public async Task Should_support_exceptions() { - var input = new {Exception = new IntentionalTestException("It Happens")}; + var input = new { Exception = new IntentionalTestException("It Happens") }; var provider = GetPropertyProvider(input, x => x.Exception, (ExceptionInfo)default); @@ -382,7 +394,7 @@ public async Task Should_support_initializer_variables() { var id = NewId.NextGuid(); - var input = new {IdValue = new IdVariable(id)}; + var input = new { IdValue = new IdVariable(id) }; var provider = GetPropertyProvider(input, x => x.IdValue, Guid.Empty); @@ -396,7 +408,7 @@ public async Task Should_support_initializer_variables_to_strings() { var id = NewId.NextGuid(); - var input = new {IdValue = new IdVariable(id)}; + var input = new { IdValue = new IdVariable(id) }; var provider = GetPropertyProvider(input, x => x.IdValue, (string)default); @@ -408,7 +420,7 @@ public async Task Should_support_initializer_variables_to_strings() [Test] public async Task Should_support_interface_initializer() { - var input = new {Message = new {Text = "Hello"}}; + var input = new { Message = new { Text = "Hello" } }; var provider = GetPropertyProvider(input, x => x.Message, (MessageContract)default); @@ -447,28 +459,37 @@ public async Task Should_support_interface_initializer_nested() var message = await provider.GetProperty(CreateInitializeContext(input)); Assert.That(message, Is.Not.Null); - Assert.That(message.Text, Is.EqualTo("Hello")); - Assert.That(message.Headers, Is.Not.Null); - Assert.That(message.Headers.Length, Is.EqualTo(2)); - Assert.That(message.Headers[0].Key, Is.EqualTo("Format")); - Assert.That(message.Headers[0].Value, Is.EqualTo("CSV")); - Assert.That(message.Headers[1].Key, Is.EqualTo("Length")); - Assert.That(message.Headers[1].Value, Is.EqualTo("2457")); + Assert.Multiple(() => + { + Assert.That(message.Text, Is.EqualTo("Hello")); + Assert.That(message.Headers, Is.Not.Null); + }); + Assert.That(message.Headers, Has.Length.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(message.Headers[0].Key, Is.EqualTo("Format")); + Assert.That(message.Headers[0].Value, Is.EqualTo("CSV")); + Assert.That(message.Headers[1].Key, Is.EqualTo("Length")); + Assert.That(message.Headers[1].Value, Is.EqualTo("2457")); + }); } [Test] public async Task Should_support_interface_initializer_with_array() { - var input = new {Messages = new[] {new {Text = "Hello"}, new {Text = "World"}}}; + var input = new { Messages = new[] { new { Text = "Hello" }, new { Text = "World" } } }; var provider = GetPropertyProvider(input, x => x.Messages, (MessageContract[])default); MessageContract[] message = await provider.GetProperty(CreateInitializeContext(input)); Assert.That(message, Is.Not.Null); - Assert.That(message.Length, Is.EqualTo(2)); - Assert.That(message[0].Text, Is.EqualTo("Hello")); - Assert.That(message[1].Text, Is.EqualTo("World")); + Assert.That(message, Has.Length.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(message[0].Text, Is.EqualTo("Hello")); + Assert.That(message[1].Text, Is.EqualTo("World")); + }); } [Test] @@ -478,8 +499,8 @@ public async Task Should_support_interface_initializer_with_dictionary() { Messages = new Dictionary { - {"Hello", new {Text = "Hello"}}, - {"World", new {Text = "World"}} + { "Hello", new { Text = "Hello" } }, + { "World", new { Text = "World" } } } }; @@ -488,39 +509,42 @@ public async Task Should_support_interface_initializer_with_dictionary() IDictionary message = await provider.GetProperty(CreateInitializeContext(input)); Assert.That(message, Is.Not.Null); - Assert.That(message.Count, Is.EqualTo(2)); - Assert.That(message["Hello"].Text, Is.EqualTo("Hello")); - Assert.That(message["World"].Text, Is.EqualTo("World")); + Assert.That(message, Has.Count.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(message["Hello"].Text, Is.EqualTo("Hello")); + Assert.That(message["World"].Text, Is.EqualTo("World")); + }); } [Test] public async Task Should_support_matching_arrays() { - var input = new {IntArray = new[] {1, 2, 3}}; + var input = new { IntArray = new[] { 1, 2, 3 } }; var provider = GetPropertyProvider(input, x => x.IntArray, (int[])default); int[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new[] {1, 2, 3})); + Assert.That(arrayValue, Is.EqualTo(new[] { 1, 2, 3 })); } [Test] public async Task Should_support_matching_enumerable() { - var input = new {Decimals = Enumerable.Repeat(98.7m, 2)}; + var input = new { Decimals = Enumerable.Repeat(98.7m, 2) }; var provider = GetPropertyProvider(input, x => x.Decimals, (decimal[])default); decimal[] arrayValue = await provider.GetProperty(CreateInitializeContext(input)); - Assert.That(arrayValue, Is.EqualTo(new[] {98.7m, 98.7m})); + Assert.That(arrayValue, Is.EqualTo(new[] { 98.7m, 98.7m })); } [Test] public async Task Should_support_matching_property_types() { - var input = new {IntValue = 27}; + var input = new { IntValue = 27 }; var provider = GetPropertyProvider(input, x => x.IntValue); @@ -532,7 +556,7 @@ public async Task Should_support_matching_property_types() [Test] public async Task Should_support_multiple_result_types_for_single_property() { - var input = new {IntValue = 27}; + var input = new { IntValue = 27 }; var provider = GetPropertyProvider(input, x => x.IntValue); @@ -550,7 +574,7 @@ public async Task Should_support_multiple_result_types_for_single_property() [Test] public async Task Should_support_nullable_input_types() { - var input = new {IntValue = (int?)27}; + var input = new { IntValue = (int?)27 }; var provider = GetPropertyProvider(input, x => x.IntValue, 69); @@ -568,7 +592,7 @@ public async Task Should_support_nullable_input_types() [Test] public async Task Should_support_nullable_result_types() { - var input = new {IntValue = 27}; + var input = new { IntValue = 27 }; var provider = GetPropertyProvider(input, x => x.IntValue, (int?)69); @@ -586,7 +610,7 @@ public async Task Should_support_nullable_result_types() [Test] public async Task Should_support_object() { - var input = new {IntValue = 27}; + var input = new { IntValue = 27 }; var provider = GetPropertyProvider(input, x => x.IntValue, (object)default); @@ -598,7 +622,7 @@ public async Task Should_support_object() [Test] public async Task Should_support_string_to_uri() { - var input = new {Address = "http://localhost/"}; + var input = new { Address = "http://localhost/" }; var uriProvider = GetPropertyProvider(input, x => x.Address, (Uri)default); @@ -610,7 +634,7 @@ public async Task Should_support_string_to_uri() [Test] public async Task Should_support_uri() { - var input = new {Address = new Uri("http://localhost/")}; + var input = new { Address = new Uri("http://localhost/") }; var uriProvider = GetPropertyProvider(input, x => x.Address, (Uri)default); @@ -622,7 +646,7 @@ public async Task Should_support_uri() [Test] public async Task Should_support_uri_to_string() { - var input = new {Address = new Uri("http://localhost/")}; + var input = new { Address = new Uri("http://localhost/") }; var stringProvider = GetPropertyProvider(input, x => x.Address, (string)default); @@ -634,7 +658,7 @@ public async Task Should_support_uri_to_string() [Test] public async Task Should_support_value_to_string_types() { - var input = new {IntValue = 27}; + var input = new { IntValue = 27 }; var stringProvider = GetPropertyProvider(input, x => x.IntValue, (string)default); diff --git a/tests/MassTransit.Tests/Initializers/State_Specs.cs b/tests/MassTransit.Tests/Initializers/State_Specs.cs index 9e564eb417d..aaa562d30f8 100644 --- a/tests/MassTransit.Tests/Initializers/State_Specs.cs +++ b/tests/MassTransit.Tests/Initializers/State_Specs.cs @@ -21,10 +21,13 @@ public async Task Should_handle_a_double_state() ConsumeContext stateUpdated = await handler; - Assert.That(stateUpdated.Message.CurrentState, Is.EqualTo(_machine.Running.Name)); + Assert.Multiple(() => + { + Assert.That(stateUpdated.Message.CurrentState, Is.EqualTo(_machine.Running.Name)); - Assert.That(stateUpdated.Headers.TryGetHeader("Custom-Header-Value", out var value), Is.True); - Assert.That(value, Is.EqualTo("Frankie Say Relax")); + Assert.That(stateUpdated.Headers.TryGetHeader("Custom-Header-Value", out var value), Is.True); + Assert.That(value, Is.EqualTo("Frankie Say Relax")); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -33,7 +36,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } readonly IntStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; public Initializing_a_property_using_state_machine_state() { diff --git a/tests/MassTransit.Tests/InterfaceProxy_Specs.cs b/tests/MassTransit.Tests/InterfaceProxy_Specs.cs index 6c3cf2f03f2..d31204b7e96 100644 --- a/tests/MassTransit.Tests/InterfaceProxy_Specs.cs +++ b/tests/MassTransit.Tests/InterfaceProxy_Specs.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -16,7 +15,7 @@ public async Task Should_have_address_value() { ConsumeContext message = await _handler; - message.Message.Address.OriginalString.ShouldBe(UriString); + Assert.That(message.Message.Address.OriginalString, Is.EqualTo(UriString)); } [Test] @@ -24,7 +23,7 @@ public async Task Should_have_correlation_id() { ConsumeContext message = await _handler; - message.Message.CorrelationId.ShouldBe(_correlationId); + Assert.That(message.Message.CorrelationId, Is.EqualTo(_correlationId)); } [Test] @@ -32,7 +31,7 @@ public async Task Should_have_integer_value() { ConsumeContext message = await _handler; - message.Message.IntValue.ShouldBe(IntValue); + Assert.That(message.Message.IntValue, Is.EqualTo(IntValue)); } [Test] @@ -46,7 +45,7 @@ public async Task Should_have_string_value() { ConsumeContext message = await _handler; - message.Message.StringValue.ShouldBe(StringValue); + Assert.That(message.Message.StringValue, Is.EqualTo(StringValue)); } const int IntValue = 42; diff --git a/tests/MassTransit.Tests/Introspection_Specs.cs b/tests/MassTransit.Tests/Introspection_Specs.cs index 6c0ef203c3d..b293fa1e209 100644 --- a/tests/MassTransit.Tests/Introspection_Specs.cs +++ b/tests/MassTransit.Tests/Introspection_Specs.cs @@ -18,7 +18,7 @@ public class Probing_the_bus : public void Should_extract_receive_endpoint_addresses() { List receiveAddresses = Bus.GetReceiveEndpointAddresses().ToList(); - Assert.Contains(InputQueueAddress, receiveAddresses); + Assert.That(receiveAddresses, Does.Contain(InputQueueAddress)); } [Test] diff --git a/tests/MassTransit.Tests/JobConsumerFault_Specs.cs b/tests/MassTransit.Tests/JobConsumerFault_Specs.cs index 425af503bca..3772b69d3ff 100644 --- a/tests/MassTransit.Tests/JobConsumerFault_Specs.cs +++ b/tests/MassTransit.Tests/JobConsumerFault_Specs.cs @@ -19,6 +19,12 @@ public interface ICustomDependency } + public class CustomDependency : + ICustomDependency + { + } + + public class OddJobFaultConsumer : IJobConsumer { @@ -28,7 +34,10 @@ public OddJobFaultConsumer(ICustomDependency dependency) public async Task Run(JobContext context) { - await Task.Delay(context.Job.Duration, context.CancellationToken); + if (context.RetryAttempt > 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + else + throw new InvalidOperationException("Failing the first time, for fun"); } } } @@ -40,7 +49,7 @@ public class JobConsumerFault_Specs [Test] public async Task Should_detect_the_faulted_job() { - await using var provider = SetupServiceCollection(); + await using var provider = SetupServiceCollection(x => x.AddConsumer()); var harness = provider.GetTestHarness(); @@ -56,25 +65,63 @@ public async Task Should_detect_the_faulted_job() Job = new { Duration = TimeSpan.FromSeconds(10) } }); - Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); - Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any>(), Is.True); + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); } - static ServiceProvider SetupServiceCollection() + [Test] + public async Task Should_retry_the_faulted_job_and_pass_the_second_time() + { + await using var provider = SetupServiceCollection(x => + { + x.AddConsumer(c => c.Options>(options => options.SetRetry(r => r.Immediate(2)))); + x.AddScoped(); + }); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + var duration = TimeSpan.FromSeconds(2); + + var submittedJobId = await client.SubmitJob(jobId, new { Duration = duration }, properties => properties.Set("Variable", "Knife")); + + await Assert.MultipleAsync(async () => + { + Assert.That(submittedJobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any>(), Is.True); + + IPublishedMessage> completed = await harness.Published.SelectAsync>().FirstOrDefault(); + + Assert.That(completed.Context.Message.Job.Duration, Is.EqualTo(duration)); + }); + } + + static ServiceProvider SetupServiceCollection(Action configure = null) { var provider = new ServiceCollection() .AddMassTransitTestHarness(x => { + configure?.Invoke(x); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); x.SetKebabCaseEndpointNameFormatter(); - x.AddConsumer(); - x.UsingInMemory((context, cfg) => { cfg.UseDelayedMessageScheduler(); diff --git a/tests/MassTransit.Tests/JobConsumer_Specs.cs b/tests/MassTransit.Tests/JobConsumer_Specs.cs index 8a52c23399e..c52f7ffdf1c 100644 --- a/tests/MassTransit.Tests/JobConsumer_Specs.cs +++ b/tests/MassTransit.Tests/JobConsumer_Specs.cs @@ -12,6 +12,8 @@ namespace MassTransit.Tests namespace JobConsumerTests { using System; + using System.Threading.Tasks; + using Contracts.JobService; public interface OddJob @@ -25,12 +27,43 @@ public class OddJobConsumer : { public async Task Run(JobContext context) { - if (context.RetryAttempt == 0) - await Task.Delay(context.Job.Duration, context.CancellationToken); + if (context.TryGetJobState(out var previousState)) + { + LogContext.Debug?.Log("Previous AttemptId: {LastAttemptId}", previousState.LastAttemptId); + } + + try + { + await context.SetJobProgress(0, 100); + + if (context.RetryAttempt == 0) + await Task.Delay(context.Job.Duration, context.CancellationToken); + + if (context.RetryAttempt > 0 && context.LastProgressValue is null) + { + throw new InvalidOperationException("The progress was not stored"); + } + + for (int i = 0; i < 100; i++) + { + await context.SetJobProgress(i, 100); + } + } + catch (OperationCanceledException) + { + await context.SaveJobState(new OddJobState { LastAttemptId = context.AttemptId }); + throw; + } } } + public class OddJobState + { + public Guid LastAttemptId { get; set; } + } + + public class OddJobCompletedConsumer : IConsumer> { @@ -43,104 +76,184 @@ public Task Consume(ConsumeContext> context) [TestFixture] - public class JobConsumer_Specs + public class Using_the_new_job_service_configuration { [Test] public async Task Should_complete_the_job() { - await using var provider = SetupServiceCollection(); + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); - var harness = provider.GetTestHarness(); - harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + x.SetKebabCaseEndpointNameFormatter(); - await harness.Start(); + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); - var jobId = NewId.NextGuid(); + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); - IRequestClient> client = harness.GetRequestClient>(); + x.SetInMemorySagaRepositoryProvider(); - Response response = await client.GetResponse(new + x.AddJobSagaStateMachines(); + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + try { - JobId = jobId, - Job = new { Duration = TimeSpan.FromSeconds(1) } - }); + var jobId = NewId.NextGuid(); - Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + IRequestClient> client = harness.GetRequestClient>(); - Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any(), Is.True); + var responseJobId = await client.SubmitJob(jobId, new { Duration = TimeSpan.FromSeconds(1) }); - Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any>(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(responseJobId, Is.EqualTo(jobId)); - await harness.Stop(); + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + } + finally + { + await harness.Stop(); + } } + } + + [TestFixture] + public class Using_outbound_scoped_filters_with_the_job_service + { [Test] - public async Task Should_cancel_the_job() + public async Task Should_complete_the_job() { - await using var provider = SetupServiceCollection(); + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(10)); - var harness = provider.GetTestHarness(); + x.SetKebabCaseEndpointNameFormatter(); - await harness.Start(); + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); - var jobId = NewId.NextGuid(); + x.AddConsumer() + .Endpoint(e => e.ConcurrentMessageLimit = 1); - IRequestClient> client = harness.GetRequestClient>(); + x.SetInMemorySagaRepositoryProvider(); - Response response = await client.GetResponse(new + x.AddJobSagaStateMachines(); + x.SetJobConsumerOptions(options => options.HeartbeatInterval = TimeSpan.FromSeconds(10)) + .Endpoint(e => e.PrefetchCount = 100); + + x.AddConfigureEndpointsCallback((context, name, cfg) => + { + cfg.UsePublishFilter(typeof(JobTestPublishFilter<>), context); + }); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + try { - JobId = jobId, - Job = new { Duration = TimeSpan.FromSeconds(10) } - }); + var jobId = NewId.NextGuid(); - Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + IRequestClient> client = harness.GetRequestClient>(); - Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any(), Is.True); + var responseJobId = await client.SubmitJob(jobId, new { Duration = TimeSpan.FromSeconds(1) }); - await harness.Bus.Publish(new { JobId = jobId }); + await Assert.MultipleAsync(async () => + { + Assert.That(responseJobId, Is.EqualTo(jobId)); - Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any(), Is.True); - await harness.Stop(); + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + } + finally + { + await harness.Stop(); + } } + + public class JobTestPublishFilter : + IFilter> + where T : class + { + public Task Send(PublishContext context, IPipe> next) + { + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + } + } + } + + + [TestFixture] + public class JobConsumer_Specs + { [Test] - public async Task Should_cancel_the_job_and_retry_it() + public async Task Should_cancel_the_job() { await using var provider = SetupServiceCollection(); var harness = provider.GetTestHarness(); await harness.Start(); - harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); var jobId = NewId.NextGuid(); IRequestClient> client = harness.GetRequestClient>(); - Response response = await client.GetResponse(new + var responseJobId = await client.SubmitJob(jobId, new { Duration = TimeSpan.FromSeconds(10) }); + + await Assert.MultipleAsync(async () => { - JobId = jobId, - Job = new { Duration = TimeSpan.FromSeconds(10) } - }); + Assert.That(responseJobId, Is.EqualTo(jobId)); - Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); - await harness.Bus.Publish(new { JobId = jobId }); + await harness.Bus.CancelJob(jobId); Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Sent.Any(), Is.True); - - await harness.Bus.Publish(new { JobId = jobId }); - Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any>(), Is.True); await harness.Stop(); } @@ -159,16 +272,17 @@ public async Task Should_cancel_the_job_and_get_the_status() IRequestClient> client = harness.GetRequestClient>(); - Response response = await client.GetResponse(new + var responseJobId = await client.SubmitJob(jobId, new { Duration = TimeSpan.FromSeconds(10) }); + + await Assert.MultipleAsync(async () => { - JobId = jobId, - Job = new { Duration = TimeSpan.FromSeconds(10) } - }); + Assert.That(responseJobId, Is.EqualTo(jobId)); - Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); IRequestClient stateClient = harness.GetRequestClient(); @@ -176,10 +290,13 @@ public async Task Should_cancel_the_job_and_get_the_status() Assert.That(jobState.Message.CurrentState, Is.EqualTo("Started")); - await harness.Bus.Publish(new { JobId = jobId }); + await harness.Bus.CancelJob(jobId); - Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Sent.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Sent.Any(), Is.True); + }); jobState = await stateClient.GetResponse(new { JobId = jobId }); @@ -189,22 +306,46 @@ public async Task Should_cancel_the_job_and_get_the_status() } [Test] - public async Task Should_return_not_found() + public async Task Should_cancel_the_job_and_retry_it() { await using var provider = SetupServiceCollection(); var harness = provider.GetTestHarness(); await harness.Start(); - harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); var jobId = NewId.NextGuid(); - IRequestClient stateClient = harness.GetRequestClient(); + IRequestClient> client = harness.GetRequestClient>(); - var jobState = await stateClient.GetJobState(jobId); + var responseJobId = await client.SubmitJob(jobId, new { Duration = TimeSpan.FromSeconds(10) }); - Assert.That(jobState.CurrentState, Is.EqualTo("NotFound")); + await Assert.MultipleAsync(async () => + { + Assert.That(responseJobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + await harness.Bus.CancelJob(jobId); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Sent.Any(), Is.True); + }); + + await harness.Bus.RetryJob(jobId); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); await harness.Stop(); } @@ -225,32 +366,114 @@ public async Task Should_cancel_the_job_while_waiting() IRequestClient> client = harness.GetRequestClient>(); - Response response = await client.GetResponse(new + await client.SubmitJob(previousJobId, new { Duration = TimeSpan.FromSeconds(10) }); + + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + + var responseJobId = await client.SubmitJob(jobId, new { Duration = TimeSpan.FromSeconds(10) }); + + await Assert.MultipleAsync(async () => { - JobId = previousJobId, - Job = new { Duration = TimeSpan.FromSeconds(10) } + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + + Assert.That(responseJobId, Is.EqualTo(jobId)); + + Assert.That(await harness.Sent.Any(x => x.Context.Message.JobId == jobId), Is.True); }); - Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + await harness.Bus.CancelJob(jobId); + + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + + await harness.Bus.CancelJob(previousJobId); + + Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + + await harness.Stop(); + } + + [Test] + public async Task Should_complete_the_job() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + await harness.Start(); - response = await client.GetResponse(new + var jobId = NewId.NextGuid(); + + IRequestClient> client = harness.GetRequestClient>(); + + Response response = await client.GetResponse(new { JobId = jobId, - Job = new { Duration = TimeSpan.FromSeconds(10) } + Job = new { Duration = TimeSpan.FromSeconds(1) } }); - Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.JobId, Is.EqualTo(jobId)); - Assert.That(response.Message.JobId, Is.EqualTo(jobId)); + Assert.That(await harness.Published.Any(), Is.True); - Assert.That(await harness.Sent.Any(x => x.Context.Message.JobId == jobId), Is.True); + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); - await harness.Bus.Publish(new { JobId = jobId }); + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); - Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == jobId), Is.True); + await harness.Stop(); + } - await harness.Bus.Publish(new { JobId = previousJobId }); - Assert.That(await harness.Published.Any(x => x.Context.Message.JobId == previousJobId), Is.True); + [Test] + public async Task Should_create_a_unique_job_id() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(5); + + await harness.Start(); + + await harness.Bus.Publish(new { Duration = TimeSpan.FromSeconds(1) }); + + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.Published.Any(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + + Assert.That(await harness.Published.Any(), Is.True); + Assert.That(await harness.Published.Any>(), Is.True); + }); + + IPublishedMessage publishedMessage = await harness.Published.SelectAsync().First(); + Assert.That(publishedMessage.Context.Message.JobId, Is.Not.EqualTo(Guid.Empty)); + + await harness.Stop(); + } + + [Test] + public async Task Should_return_not_found() + { + await using var provider = SetupServiceCollection(); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + harness.TestInactivityTimeout = TimeSpan.FromSeconds(10); + + var jobId = NewId.NextGuid(); + + IRequestClient stateClient = harness.GetRequestClient(); + + var jobState = await stateClient.GetJobState(jobId); + + Assert.That(jobState.CurrentState, Is.EqualTo("NotFound")); await harness.Stop(); } @@ -262,25 +485,18 @@ static ServiceProvider SetupServiceCollection() { x.SetKebabCaseEndpointNameFormatter(); - x.AddConsumer(); + x.AddConsumer() + .Endpoint(e => e.Name = "odd-job"); + x.AddConsumer() .Endpoint(e => e.ConcurrentMessageLimit = 1); + x.AddJobSagaStateMachines(options => options.SlotWaitTime = TimeSpan.FromSeconds(10)); + x.UsingInMemory((context, cfg) => { cfg.UseDelayedMessageScheduler(); - var options = new ServiceInstanceOptions() - .SetEndpointNameFormatter(context.GetService() ?? - DefaultEndpointNameFormatter.Instance); - - cfg.ServiceInstance(options, instance => - { - instance.ConfigureJobServiceEndpoints(); - - instance.ConfigureEndpoints(context, f => f.Include()); - }); - cfg.ConfigureEndpoints(context); }); }) diff --git a/tests/MassTransit.Tests/JsonToken_Specs.cs b/tests/MassTransit.Tests/JsonToken_Specs.cs index 269fb7ffb64..fc1ff6dca2d 100644 --- a/tests/MassTransit.Tests/JsonToken_Specs.cs +++ b/tests/MassTransit.Tests/JsonToken_Specs.cs @@ -17,7 +17,7 @@ public void Should_support_int() { var value = NewtonsoftJsonMessageSerializer.Deserializer.Deserialize(jsonReader); - Assert.AreEqual(27, value); + Assert.That(value, Is.EqualTo(27)); } } } diff --git a/tests/MassTransit.Tests/KillSwitch_Specs.cs b/tests/MassTransit.Tests/KillSwitch_Specs.cs index a0a2c8be2bd..127b77dfd74 100644 --- a/tests/MassTransit.Tests/KillSwitch_Specs.cs +++ b/tests/MassTransit.Tests/KillSwitch_Specs.cs @@ -20,15 +20,21 @@ public async Task Should_be_degraded_after_too_many_exceptions() await Task.WhenAll(Enumerable.Range(0, 20).Select(x => Bus.Publish(new BadMessage()))); - Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Degraded, TimeSpan.FromSeconds(15)), Is.EqualTo(BusHealthStatus.Degraded)); + await Assert.MultipleAsync(async () => + { + Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Degraded, TimeSpan.FromSeconds(15)), Is.EqualTo(BusHealthStatus.Degraded)); - Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Healthy, TimeSpan.FromSeconds(10)), Is.EqualTo(BusHealthStatus.Healthy)); + Assert.That(await BusControl.WaitForHealthStatus(BusHealthStatus.Healthy, TimeSpan.FromSeconds(10)), Is.EqualTo(BusHealthStatus.Healthy)); + }); await Task.WhenAll(Enumerable.Range(0, 20).Select(x => Bus.Publish(new GoodMessage()))); - Assert.That(await InMemoryTestHarness.Consumed.SelectAsync().Take(20).Count(), Is.EqualTo(20)); + await Assert.MultipleAsync(async () => + { + Assert.That(await InMemoryTestHarness.Consumed.SelectAsync().Take(20).Count(), Is.EqualTo(20)); - Assert.That(await InMemoryTestHarness.Consumed.SelectAsync().Take(20).Count(), Is.EqualTo(20)); + Assert.That(await InMemoryTestHarness.Consumed.SelectAsync().Take(20).Count(), Is.EqualTo(20)); + }); } protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) diff --git a/src/MassTransit.TestFramework/LocalDbConnectionStringProvider.cs b/tests/MassTransit.Tests/LocalDbConnectionStringProvider.cs similarity index 98% rename from src/MassTransit.TestFramework/LocalDbConnectionStringProvider.cs rename to tests/MassTransit.Tests/LocalDbConnectionStringProvider.cs index 9d69fd2b309..9159b8c1826 100644 --- a/src/MassTransit.TestFramework/LocalDbConnectionStringProvider.cs +++ b/tests/MassTransit.Tests/LocalDbConnectionStringProvider.cs @@ -1,4 +1,4 @@ -namespace MassTransit.TestFramework +namespace MassTransit.Tests { using System; using System.Collections.Generic; diff --git a/tests/MassTransit.Tests/MassTransit.Tests.csproj b/tests/MassTransit.Tests/MassTransit.Tests.csproj index 9bf4b600499..4457b16b9ed 100644 --- a/tests/MassTransit.Tests/MassTransit.Tests.csproj +++ b/tests/MassTransit.Tests/MassTransit.Tests.csproj @@ -1,14 +1,14 @@  - net6.0 + net8.0 - $(TargetFrameworks);net462 + $(TargetFrameworks);net472 - + @@ -18,14 +18,23 @@ + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - + + + diff --git a/tests/MassTransit.Tests/MessageContext_Specs.cs b/tests/MassTransit.Tests/MessageContext_Specs.cs index b3275678ffd..a2a4ac2ee33 100644 --- a/tests/MassTransit.Tests/MessageContext_Specs.cs +++ b/tests/MassTransit.Tests/MessageContext_Specs.cs @@ -3,7 +3,6 @@ namespace MassTransit.Tests using System; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -17,7 +16,7 @@ public async Task Should_have_an_empty_fault_address() { ConsumeContext ping = await _ping; - ping.FaultAddress.ShouldBe(null); + Assert.That(ping.FaultAddress, Is.Null); } [Test] @@ -25,7 +24,7 @@ public async Task Should_have_an_empty_response_address() { ConsumeContext ping = await _ping; - ping.ResponseAddress.ShouldBe(null); + Assert.That(ping.ResponseAddress, Is.Null); } [Test] @@ -33,7 +32,7 @@ public async Task Should_include_the_correlation_id() { ConsumeContext ping = await _ping; - ping.CorrelationId.ShouldBe(_correlationId); + Assert.That(ping.CorrelationId, Is.EqualTo(_correlationId)); } [Test] @@ -41,7 +40,7 @@ public async Task Should_include_the_destination_address() { ConsumeContext ping = await _ping; - ping.DestinationAddress.ShouldBe(InputQueueAddress); + Assert.That(ping.DestinationAddress, Is.EqualTo(InputQueueAddress)); } [Test] @@ -50,7 +49,7 @@ public async Task Should_include_the_header() ConsumeContext ping = await _ping; ping.Headers.TryGetHeader("One", out var header); - header.ShouldBe("1"); + Assert.That(header, Is.EqualTo("1")); } [Test] @@ -58,7 +57,7 @@ public async Task Should_include_the_source_address() { ConsumeContext ping = await _ping; - ping.SourceAddress.ShouldBe(BusAddress); + Assert.That(ping.SourceAddress, Is.EqualTo(BusAddress)); } Task> _ping; @@ -92,7 +91,7 @@ public async Task Should_have_received_the_response_on_the_handler() { Response message = await _request; - message.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(message.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } [Test] @@ -100,7 +99,7 @@ public async Task Should_have_the_matching_correlation_id() { ConsumeContext context = await _responseHandler; - context.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(context.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } [Test] @@ -108,7 +107,7 @@ public async Task Should_include_the_destination_address() { ConsumeContext ping = await _ping; - ping.DestinationAddress.ShouldBe(InputQueueAddress); + Assert.That(ping.DestinationAddress, Is.EqualTo(InputQueueAddress)); } [Test] @@ -116,7 +115,7 @@ public async Task Should_include_the_response_address() { ConsumeContext ping = await _ping; - ping.ResponseAddress.ShouldBe(BusAddress); + Assert.That(ping.ResponseAddress, Is.EqualTo(BusAddress)); } [Test] @@ -124,7 +123,7 @@ public async Task Should_include_the_source_address() { ConsumeContext ping = await _ping; - ping.SourceAddress.ShouldBe(BusAddress); + Assert.That(ping.SourceAddress, Is.EqualTo(BusAddress)); } [Test] @@ -132,7 +131,7 @@ public async Task Should_receive_the_response() { ConsumeContext context = await _responseHandler; - context.ConversationId.ShouldBe(_conversationId); + Assert.That(context.ConversationId, Is.EqualTo(_conversationId)); } Task> _ping; @@ -170,7 +169,7 @@ public async Task Should_have_received_the_actual_response() Response message = await notSupported; - message.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(message.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } [Test] @@ -218,7 +217,7 @@ public async Task Should_have_received_the_response_on_the_handler() { Response message = await _request; - message.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(message.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } Task> _ping; diff --git a/tests/MassTransit.Tests/MessageData/DataBus_Specs.cs b/tests/MassTransit.Tests/MessageData/DataBus_Specs.cs index 4c285ee76e0..12d8483b356 100644 --- a/tests/MassTransit.Tests/MessageData/DataBus_Specs.cs +++ b/tests/MassTransit.Tests/MessageData/DataBus_Specs.cs @@ -11,31 +11,110 @@ namespace DataBus_Specs using MassTransit.MessageData.Values; using MassTransit.Serialization; using NUnit.Framework; - using Shouldly; using TestFramework; [TestFixture] - public class Sending_a_large_message_through_the_file_system : + public class Sending_inlined_message_data : InMemoryTestFixture { [Test] - public async Task Should_be_able_to_write_stream_too() + public async Task Should_be_able_to_write_bytes_too() { - var data = new byte[10000]; - using MemoryStream ms = new MemoryStream(data); + var data = new byte[256]; - var message = new MessageWithStreamImpl { Stream = await _repository.PutStream(ms) }; + var message = new MessageWithByteArrayImpl { Bytes = await _repository.PutBytes(data) }; await InputQueueSendEndpoint.Send(message); - await _receivedStream; + ConsumeContext receivedBytesContext = await _receivedBytes; - using MemoryStream receivedMemoryStream = new MemoryStream(); + Assert.Multiple(() => + { + Assert.That(receivedBytesContext.Message.Bytes.Address, Is.Null); + Assert.That(_receivedBytesArray, Is.EqualTo(data)); + }); + } + + [Test] + public async Task Should_inline_the_string() + { + var data = new string('*', 256); + + var message = new SendMessageWithBigData { Body = await _repository.PutString(data) }; + + await InputQueueSendEndpoint.Send(message); + + var recievedContext = await _received; + Assert.Multiple(() => + { + Assert.That(recievedContext.Message.Body.Address, Is.Null); + Assert.That(_receivedBody, Is.EqualTo(data)); + }); + } + + [Test] + public async Task Should_not_inline_stream() + { + var data = new byte[256]; + using var ms = new MemoryStream(data); + + await InputQueueSendEndpoint.Send(new { Stream = ms }); + + ConsumeContext recievedStreamcontext = await _receivedStream; + Assert.That(recievedStreamcontext.Message.Stream.Address, Is.Not.Null); + + using var receivedMemoryStream = new MemoryStream(); await _receivedStreamData.CopyToAsync(receivedMemoryStream); - receivedMemoryStream.ToArray().ShouldBe(data); + Assert.That(receivedMemoryStream.ToArray(), Is.EqualTo(data)); } + IMessageDataRepository _repository; + Task> _received; + Task> _receivedBytes; + Task> _receivedStream; + string _receivedBody; + byte[] _receivedBytesArray; + Stream _receivedStreamData; + + protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) + { + var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + + var messageDataPath = Path.Combine(baseDirectory, "MessageData"); + + var dataDirectory = new DirectoryInfo(messageDataPath); + + _repository = new FileSystemMessageDataRepository(dataDirectory); + MessageDataDefaults.AlwaysWriteToRepository = false; + MessageDataDefaults.Threshold = 4096; + configurator.UseMessageData(_repository); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + _received = Handler(configurator, async context => + { + _receivedBody = await context.Message.Body.Value; + }); + + _receivedBytes = Handler(configurator, async context => + { + _receivedBytesArray = await context.Message.Bytes.Value; + }); + + _receivedStream = Handler(configurator, async context => + { + _receivedStreamData = await context.Message.Stream.Value; + }); + } + } + + + [TestFixture] + public class Sending_a_large_message_through_the_file_system : + InMemoryTestFixture + { [Test] public async Task Should_be_able_to_write_bytes_too() { @@ -47,7 +126,24 @@ public async Task Should_be_able_to_write_bytes_too() await _receivedBytes; - _receivedBytesArray.ShouldBe(data); + Assert.That(_receivedBytesArray, Is.EqualTo(data)); + } + + [Test] + public async Task Should_be_able_to_write_stream_too() + { + var data = new byte[10000]; + using MemoryStream ms = new MemoryStream(data); + + var message = new MessageWithStreamImpl { Stream = await _repository.PutStream(ms) }; + + await InputQueueSendEndpoint.Send(message); + + await _receivedStream; + + using MemoryStream receivedMemoryStream = new MemoryStream(); + await _receivedStreamData.CopyToAsync(receivedMemoryStream); + Assert.That(receivedMemoryStream.ToArray(), Is.EqualTo(data)); } [Test] @@ -61,7 +157,7 @@ public async Task Should_load_the_data_from_the_repository() await _received; - _receivedBody.ShouldBe(data); + Assert.That(_receivedBody, Is.EqualTo(data)); } IMessageDataRepository _repository; @@ -110,34 +206,34 @@ public class Sending_a_large_message_through_the_file_system_encrypted : InMemoryTestFixture { [Test] - public async Task Should_be_able_to_write_stream_too() + public async Task Should_be_able_to_write_bytes_too() { var data = new byte[10000]; - using MemoryStream ms = new MemoryStream(data); - var message = new MessageWithStreamImpl { Stream = await _repository.PutStream(ms) }; + var message = new MessageWithByteArrayImpl { Bytes = await _repository.PutBytes(data) }; await InputQueueSendEndpoint.Send(message); - await _receivedStream; + await _receivedBytes; - using MemoryStream receivedMemoryStream = new MemoryStream(); - await _receivedStreamData.CopyToAsync(receivedMemoryStream); - receivedMemoryStream.ToArray().ShouldBe(data); + Assert.That(_receivedBytesArray, Is.EqualTo(data)); } [Test] - public async Task Should_be_able_to_write_bytes_too() + public async Task Should_be_able_to_write_stream_too() { var data = new byte[10000]; + using MemoryStream ms = new MemoryStream(data); - var message = new MessageWithByteArrayImpl { Bytes = await _repository.PutBytes(data) }; + var message = new MessageWithStreamImpl { Stream = await _repository.PutStream(ms) }; await InputQueueSendEndpoint.Send(message); - await _receivedBytes; + await _receivedStream; - _receivedBytesArray.ShouldBe(data); + using MemoryStream receivedMemoryStream = new MemoryStream(); + await _receivedStreamData.CopyToAsync(receivedMemoryStream); + Assert.That(receivedMemoryStream.ToArray(), Is.EqualTo(data)); } [Test] @@ -151,7 +247,7 @@ public async Task Should_load_the_data_from_the_repository() await _received; - _receivedBody.ShouldBe(data); + Assert.That(_receivedBody, Is.EqualTo(data)); } IMessageDataRepository _repository; @@ -219,7 +315,7 @@ public async Task Should_load_the_data_from_the_repository() await _received; - _receivedBody.ShouldBe(data); + Assert.That(_receivedBody, Is.EqualTo(data)); } IMessageDataRepository _messageDataRepository; @@ -290,8 +386,11 @@ public async Task Should_load_the_data_from_the_repository() await _received; - Assert.That(_body, Is.Not.Null); - Assert.That(_body0, Is.Not.Null); + Assert.Multiple(() => + { + Assert.That(_body, Is.Not.Null); + Assert.That(_body0, Is.Not.Null); + }); byte[] result = await _body.Value; Assert.That(result, Is.EqualTo(buffer)); @@ -361,7 +460,7 @@ public async Task Should_load_the_data_from_the_repository() await _received; var newId = new NewId(_receivedBytesArray); - newId.ToString().ShouldBe(data); + Assert.That(newId.ToString(), Is.EqualTo(data)); } IMessageDataRepository _messageDataRepository; @@ -414,7 +513,7 @@ public async Task Should_load_the_data_from_the_repository() await _receivedStream.CopyToAsync(memoryStreamForReceivedStream); NewId newId = new NewId(memoryStreamForReceivedStream.ToArray()); - newId.ToString().ShouldBe(data); + Assert.That(newId.ToString(), Is.EqualTo(data)); } IMessageDataRepository _messageDataRepository; diff --git a/tests/MassTransit.Tests/MessageData/FileSystem_Specs.cs b/tests/MassTransit.Tests/MessageData/FileSystem_Specs.cs index 8af9ef38446..5e66167bf82 100644 --- a/tests/MassTransit.Tests/MessageData/FileSystem_Specs.cs +++ b/tests/MassTransit.Tests/MessageData/FileSystem_Specs.cs @@ -1,48 +1,52 @@ -namespace MassTransit.Tests.MessageData -{ - using System; - using System.IO; - using System.Linq; - using System.Threading.Tasks; - using MassTransit.MessageData; - using NUnit.Framework; - - - [TestFixture] - public class Storing_message_data_on_the_file_system - { - [Test] - public async Task Should_generate_the_folder_and_file() - { - MessageData property = await _repository.PutString(new string('8', 10000)); - - Console.WriteLine(property.Address); - - Console.WriteLine("Path: {0}", Path.Combine(property.Address.Segments.SelectMany(x => x.Split(new[] {':'})).ToArray())); - } - - [Test] - public async Task Should_generate_time_based_folder() - { - MessageData property = await _repository.PutString(new string('8', 10000), TimeSpan.FromDays(30)); - - MessageData loaded = await _repository.GetString(property.Address); - - Console.WriteLine(await loaded.Value); - } - - IMessageDataRepository _repository; - - [OneTimeSetUp] - public void Setup() - { - var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; - var messageDataPath = Path.Combine(baseDirectory, "MessageData"); - - var dataDirectory = new DirectoryInfo(messageDataPath); - Console.WriteLine("Using data directory: {0}", dataDirectory); - - _repository = new FileSystemMessageDataRepository(dataDirectory); - } - } -} +namespace MassTransit.Tests.MessageData +{ + using System; + using System.IO; + using System.Threading.Tasks; + using MassTransit.MessageData; + using NUnit.Framework; + + + [TestFixture] + public class Storing_message_data_on_the_file_system + { + [Test] + public async Task Should_generate_the_folder_and_file() + { + MessageData property = await _repository.PutString(new string('8', 10000)); + + MessageData loaded = await _repository.GetString(property.Address); + await Assert.MultipleAsync(async () => + { + Assert.That(property.Address, Is.Not.Null); + + + Assert.That(await loaded.Value, Is.Not.Null); + }); + } + + [Test] + public async Task Should_generate_time_based_folder() + { + MessageData property = await _repository.PutString(new string('8', 10000), TimeSpan.FromDays(30)); + + MessageData loaded = await _repository.GetString(property.Address); + + Assert.That(await loaded.Value, Is.Not.Null); + } + + IMessageDataRepository _repository; + + [OneTimeSetUp] + public void Setup() + { + var baseDirectory = AppDomain.CurrentDomain.BaseDirectory; + var messageDataPath = Path.Combine(baseDirectory, "MessageData"); + + var dataDirectory = new DirectoryInfo(messageDataPath); + Console.WriteLine("Using data directory: {0}", dataDirectory); + + _repository = new FileSystemMessageDataRepository(dataDirectory); + } + } +} diff --git a/tests/MassTransit.Tests/MessageData/InMemory_Specs.cs b/tests/MassTransit.Tests/MessageData/InMemory_Specs.cs new file mode 100644 index 00000000000..d44329a07d3 --- /dev/null +++ b/tests/MassTransit.Tests/MessageData/InMemory_Specs.cs @@ -0,0 +1,39 @@ +namespace MassTransit.Tests.MessageData +{ + using System.IO; + using System.Threading.Tasks; + using MassTransit.MessageData; + using NUnit.Framework; + + + public class InMemory_Specs + { + [TestFixture] + public class Storing_message_data_in_memory + { + [Test] + public async Task Should_set_and_get_data() + { + var data = new string('8', 10000); + MessageData property = await _repository.PutString(data); + + Assert.That(property.Address, Is.Not.Null); + + using var dataFromRepository = await _repository.Get(property.Address); + + using var reader = new StreamReader(dataFromRepository); + var stringFromRepository = await reader.ReadToEndAsync(); + + Assert.That(stringFromRepository, Is.EqualTo(data)); + } + + IMessageDataRepository _repository; + + [OneTimeSetUp] + public void Setup() + { + _repository = new InMemoryMessageDataRepository(); + } + } + } +} diff --git a/tests/MassTransit.Tests/MessageData/InitializerClassWithMessageData_Specs.cs b/tests/MassTransit.Tests/MessageData/InitializerClassWithMessageData_Specs.cs index dd161c148e8..b250f5954a3 100644 --- a/tests/MassTransit.Tests/MessageData/InitializerClassWithMessageData_Specs.cs +++ b/tests/MassTransit.Tests/MessageData/InitializerClassWithMessageData_Specs.cs @@ -55,42 +55,72 @@ public async Task Should_load_the_data_from_the_repository() }); Assert.That(response.Message.StringData, Is.Not.Null); - Assert.That(response.Message.StringData.HasValue, Is.True); - Assert.That(response.Message.StringData.Address, Is.EqualTo(_stringDataAddress), "Should use the existing message data address"); + Assert.Multiple(() => + { + Assert.That(response.Message.StringData.HasValue, Is.True); + Assert.That(response.Message.StringData.Address, Is.EqualTo(_stringDataAddress), "Should use the existing message data address"); + }); var text = await response.Message.StringData.Value; - Assert.That(text, Is.EqualTo(stringData)); + Assert.Multiple(() => + { + Assert.That(text, Is.EqualTo(stringData)); - Assert.That(response.Message.StringByteData, Is.Not.Null); - Assert.That(response.Message.StringByteData.HasValue, Is.True); - Assert.That(response.Message.StringByteData.Address, Is.EqualTo(_stringDataAddress), "Should use the existing message data address"); + Assert.That(response.Message.StringByteData, Is.Not.Null); + }); + Assert.Multiple(() => + { + Assert.That(response.Message.StringByteData.HasValue, Is.True); + Assert.That(response.Message.StringByteData.Address, Is.EqualTo(_stringDataAddress), "Should use the existing message data address"); + }); var bytes = await response.Message.StringByteData.Value; - Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(stringData)); + Assert.Multiple(() => + { + Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(stringData)); - Assert.That(response.Message.ByteData, Is.Not.Null); - Assert.That(response.Message.ByteData.HasValue, Is.True); - Assert.That(response.Message.ByteData.Address, Is.EqualTo(_byteDataAddress), "Should use the existing message data address"); + Assert.That(response.Message.ByteData, Is.Not.Null); + }); + Assert.Multiple(() => + { + Assert.That(response.Message.ByteData.HasValue, Is.True); + Assert.That(response.Message.ByteData.Address, Is.EqualTo(_byteDataAddress), "Should use the existing message data address"); + }); bytes = await response.Message.ByteData.Value; - Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(byteData)); + Assert.Multiple(() => + { + Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(byteData)); - Assert.That(response.Message.StreamData, Is.Not.Null); - Assert.That(response.Message.StreamData.HasValue, Is.True); - Assert.That(response.Message.StreamData.Address, Is.EqualTo(_streamDataAddress), "Should use the existing message data address"); + Assert.That(response.Message.StreamData, Is.Not.Null); + }); + Assert.Multiple(() => + { + Assert.That(response.Message.StreamData.HasValue, Is.True); + Assert.That(response.Message.StreamData.Address, Is.EqualTo(_streamDataAddress), "Should use the existing message data address"); + }); using var receivedStream = new MemoryStream(); var stream = await response.Message.StreamData.Value; await stream.CopyToAsync(receivedStream); - Assert.That(receivedStream.ToArray(), Is.EqualTo(streamBytes)); + Assert.Multiple(() => + { + Assert.That(receivedStream.ToArray(), Is.EqualTo(streamBytes)); - Assert.That(response.Message.StringValue, Is.Not.Null); + Assert.That(response.Message.StringValue, Is.Not.Null); + }); Assert.That(response.Message.StringValue.HasValue, Is.True); text = await response.Message.StringValue.Value; - Assert.That(text, Is.EqualTo(stringValue)); + Assert.Multiple(() => + { + Assert.That(text, Is.EqualTo(stringValue)); - Assert.That(response.Message.StringByteValue, Is.Not.Null); + Assert.That(response.Message.StringByteValue, Is.Not.Null); + }); Assert.That(response.Message.StringByteValue.HasValue, Is.True); bytes = await response.Message.StringByteValue.Value; - Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(stringValue)); + Assert.Multiple(() => + { + Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(stringValue)); - Assert.That(response.Message.ByteValue, Is.Not.Null); + Assert.That(response.Message.ByteValue, Is.Not.Null); + }); Assert.That(response.Message.ByteValue.HasValue, Is.True); bytes = await response.Message.ByteValue.Value; Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(byteValue)); diff --git a/tests/MassTransit.Tests/MessageData/InitializerMessageData_Specs.cs b/tests/MassTransit.Tests/MessageData/InitializerMessageData_Specs.cs index 91617fd4a70..1b8139d4310 100644 --- a/tests/MassTransit.Tests/MessageData/InitializerMessageData_Specs.cs +++ b/tests/MassTransit.Tests/MessageData/InitializerMessageData_Specs.cs @@ -55,42 +55,72 @@ public async Task Should_load_the_data_from_the_repository() }); Assert.That(response.Message.StringData, Is.Not.Null); - Assert.That(response.Message.StringData.HasValue, Is.True); - Assert.That(response.Message.StringData.Address, Is.EqualTo(_stringDataAddress), "Should use the existing message data address"); + Assert.Multiple(() => + { + Assert.That(response.Message.StringData.HasValue, Is.True); + Assert.That(response.Message.StringData.Address, Is.EqualTo(_stringDataAddress), "Should use the existing message data address"); + }); var text = await response.Message.StringData.Value; - Assert.That(text, Is.EqualTo(stringData)); + Assert.Multiple(() => + { + Assert.That(text, Is.EqualTo(stringData)); - Assert.That(response.Message.StringByteData, Is.Not.Null); - Assert.That(response.Message.StringByteData.HasValue, Is.True); - Assert.That(response.Message.StringByteData.Address, Is.EqualTo(_stringDataAddress), "Should use the existing message data address"); + Assert.That(response.Message.StringByteData, Is.Not.Null); + }); + Assert.Multiple(() => + { + Assert.That(response.Message.StringByteData.HasValue, Is.True); + Assert.That(response.Message.StringByteData.Address, Is.EqualTo(_stringDataAddress), "Should use the existing message data address"); + }); byte[] bytes = await response.Message.StringByteData.Value; - Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(stringData)); + Assert.Multiple(() => + { + Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(stringData)); - Assert.That(response.Message.ByteData, Is.Not.Null); - Assert.That(response.Message.ByteData.HasValue, Is.True); - Assert.That(response.Message.ByteData.Address, Is.EqualTo(_byteDataAddress), "Should use the existing message data address"); + Assert.That(response.Message.ByteData, Is.Not.Null); + }); + Assert.Multiple(() => + { + Assert.That(response.Message.ByteData.HasValue, Is.True); + Assert.That(response.Message.ByteData.Address, Is.EqualTo(_byteDataAddress), "Should use the existing message data address"); + }); bytes = await response.Message.ByteData.Value; - Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(byteData)); + Assert.Multiple(() => + { + Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(byteData)); - Assert.That(response.Message.StreamData, Is.Not.Null); - Assert.That(response.Message.StreamData.HasValue, Is.True); - Assert.That(response.Message.StreamData.Address, Is.EqualTo(_streamDataAddress), "Should use the existing message data address"); + Assert.That(response.Message.StreamData, Is.Not.Null); + }); + Assert.Multiple(() => + { + Assert.That(response.Message.StreamData.HasValue, Is.True); + Assert.That(response.Message.StreamData.Address, Is.EqualTo(_streamDataAddress), "Should use the existing message data address"); + }); using MemoryStream receivedStream = new MemoryStream(); var stream = await response.Message.StreamData.Value; await stream.CopyToAsync(receivedStream); - Assert.That(receivedStream.ToArray(), Is.EqualTo(streamBytes)); + Assert.Multiple(() => + { + Assert.That(receivedStream.ToArray(), Is.EqualTo(streamBytes)); - Assert.That(response.Message.StringValue, Is.Not.Null); + Assert.That(response.Message.StringValue, Is.Not.Null); + }); Assert.That(response.Message.StringValue.HasValue, Is.True); text = await response.Message.StringValue.Value; - Assert.That(text, Is.EqualTo(stringValue)); + Assert.Multiple(() => + { + Assert.That(text, Is.EqualTo(stringValue)); - Assert.That(response.Message.StringByteValue, Is.Not.Null); + Assert.That(response.Message.StringByteValue, Is.Not.Null); + }); Assert.That(response.Message.StringByteValue.HasValue, Is.True); bytes = await response.Message.StringByteValue.Value; - Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(stringValue)); + Assert.Multiple(() => + { + Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(stringValue)); - Assert.That(response.Message.ByteValue, Is.Not.Null); + Assert.That(response.Message.ByteValue, Is.Not.Null); + }); Assert.That(response.Message.ByteValue.HasValue, Is.True); bytes = await response.Message.ByteValue.Value; Assert.That(Encoding.UTF8.GetString(bytes), Is.EqualTo(byteValue)); diff --git a/tests/MassTransit.Tests/MessageData/MessageDataOfT_Specs.cs b/tests/MassTransit.Tests/MessageData/MessageDataOfT_Specs.cs index 0c14a0d6059..938a7164b99 100644 --- a/tests/MassTransit.Tests/MessageData/MessageDataOfT_Specs.cs +++ b/tests/MassTransit.Tests/MessageData/MessageDataOfT_Specs.cs @@ -36,13 +36,19 @@ public async Task Should_load_the_data_from_the_repository() RequestTimeout.After(s: 5)); Assert.That(response.Message.Payload, Is.Not.Null); - Assert.That(response.Message.Payload.HasValue, Is.True); - Assert.That(response.Message.Payload.Address, Is.EqualTo(_payloadAddress), "Should use the existing message data address"); + Assert.Multiple(() => + { + Assert.That(response.Message.Payload.HasValue, Is.True); + Assert.That(response.Message.Payload.Address, Is.EqualTo(_payloadAddress), "Should use the existing message data address"); + }); var responsePayload = await response.Message.Payload.Value; - Assert.That(responsePayload.Value, Is.EqualTo(payload.Value)); - Assert.That(responsePayload.Dictionary.ContainsKey("string"), Is.EqualTo(true)); // will pass - Assert.That(responsePayload.Dictionary.ContainsKey("bool_true"), Is.EqualTo(true)); // Will pass - Assert.That(responsePayload.Dictionary.ContainsKey("bool_false"), Is.EqualTo(true)); // Will fail + Assert.Multiple(() => + { + Assert.That(responsePayload.Value, Is.EqualTo(payload.Value)); + Assert.That(responsePayload.Dictionary.ContainsKey("string"), Is.EqualTo(true)); // will pass + Assert.That(responsePayload.Dictionary.ContainsKey("bool_true"), Is.EqualTo(true)); // Will pass + Assert.That(responsePayload.Dictionary.ContainsKey("bool_false"), Is.EqualTo(true)); // Will fail + }); } protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) diff --git a/tests/MassTransit.Tests/MessageData/NestedInitializer_Specs.cs b/tests/MassTransit.Tests/MessageData/NestedInitializer_Specs.cs index 9fdfdbcbbfa..503227c2305 100644 --- a/tests/MassTransit.Tests/MessageData/NestedInitializer_Specs.cs +++ b/tests/MassTransit.Tests/MessageData/NestedInitializer_Specs.cs @@ -69,16 +69,19 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin configurator.Handler(async context => { Assert.That(context.Message.Bodies, Is.Not.Null); - Assert.That(context.Message.Bodies.Length, Is.EqualTo(2)); + Assert.That(context.Message.Bodies, Has.Length.EqualTo(2)); Assert.That(context.Message.Bodies[0].Body, Is.Not.Null); Assert.That(context.Message.Bodies[0].Body.HasValue); byte[] bodyValue = await context.Message.Bodies[0].Body.Value; Assert.That(bodyValue, Is.Not.Null); - Assert.That(bodyValue.Length, Is.EqualTo(10000)); + Assert.Multiple(() => + { + Assert.That(bodyValue, Has.Length.EqualTo(10000)); - Assert.That(context.Message.Bodies[0].FileName, Is.Not.Null); + Assert.That(context.Message.Bodies[0].FileName, Is.Not.Null); + }); Assert.That(context.Message.Bodies[0].FileName, Is.EqualTo("first.txt")); _handled.SetResult(context); diff --git a/tests/MassTransit.Tests/MessageData/ResponseMessageData_Specs.cs b/tests/MassTransit.Tests/MessageData/ResponseMessageData_Specs.cs index b823a37face..b5f792033bf 100644 --- a/tests/MassTransit.Tests/MessageData/ResponseMessageData_Specs.cs +++ b/tests/MassTransit.Tests/MessageData/ResponseMessageData_Specs.cs @@ -22,9 +22,12 @@ public async Task Should_load_the_data_from_the_repository() Key = "Hello" }); - Assert.That(response.Message.Key, Is.EqualTo("Hello")); + await Assert.MultipleAsync(async () => + { + Assert.That(response.Message.Key, Is.EqualTo("Hello")); - Assert.That(await response.Message.Value.Value, Is.Not.Empty); + Assert.That(await response.Message.Value.Value, Is.Not.Empty); + }); } [Test] diff --git a/tests/MassTransit.Tests/MessageType_Specs.cs b/tests/MassTransit.Tests/MessageType_Specs.cs new file mode 100644 index 00000000000..bf7dad7e6f9 --- /dev/null +++ b/tests/MassTransit.Tests/MessageType_Specs.cs @@ -0,0 +1,67 @@ +namespace MassTransit.Tests +{ + using System.Threading.Tasks; + using MassTransit.Testing; + using MessageTypeSubjects; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using TestFramework.Messages; + + + [TestFixture] + public class MessageType_Specs + { + [Test] + public async Task Should_not_allow_array_message_types_but_does() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new[] { new PingMessage(), new PingMessage(), new PingMessage() }); + + Assert.That(await harness.Consumed.Any()); + } + + + class ArrayConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + } + + + [TestFixture] + public class Using_the_message_urn_attribute + { + [Test] + public async Task Should_be_respected_by_the_message_type() + { + Assert.That(MessageUrn.ForTypeString(), Is.EqualTo("urn:message:SomeMessage")); + } + + [Test] + public async Task Should_be_respected_by_the_message_type_as_an_array() + { + Assert.That(MessageUrn.ForTypeString(), Is.EqualTo("urn:message:SomeMessage[]")); + } + } + + + namespace MessageTypeSubjects + { + [MessageUrn("SomeMessage")] + class SomeClassMessage + { + } + } +} diff --git a/tests/MassTransit.Tests/MessageUrnSpecs.cs b/tests/MassTransit.Tests/MessageUrnSpecs.cs index 3d968e33e89..567ce61f7b4 100644 --- a/tests/MassTransit.Tests/MessageUrnSpecs.cs +++ b/tests/MassTransit.Tests/MessageUrnSpecs.cs @@ -1,49 +1,165 @@ -namespace MassTransit.Tests -{ - using System; - using NUnit.Framework; - using TestFramework.Messages; - - - [TestFixture] - public class MessageUrnSpecs - { - [Test] - public void ClosedGenericMessage() - { - var urn = MessageUrn.ForType(typeof(G)); - var expected = new Uri("urn:message:MassTransit.Tests:G[[MassTransit.TestFramework.Messages:PingMessage]]"); - Assert.AreEqual(expected.AbsolutePath, urn.AbsolutePath); - } - - [Test] - public void NestedMessage() - { - var urn = MessageUrn.ForType(typeof(X)); - Assert.AreEqual(urn.AbsolutePath, "message:MassTransit.Tests:MessageUrnSpecs+X"); - } - - [Test] - public void OpenGenericMessage() - { - Assert.That(() => MessageUrn.ForType(typeof(G<>)), Throws.TypeOf()); - } - - [Test] - public void SimpleMessage() - { - var urn = MessageUrn.ForType(typeof(PingMessage)); - Assert.AreEqual(urn.AbsolutePath, "message:MassTransit.TestFramework.Messages:PingMessage"); - } - - - class X - { - } - } - - - public class G - { - } -} +namespace MassTransit.Tests +{ + using System; + using NUnit.Framework; + using TestFramework.Messages; + + + [TestFixture] + public class MessageUrnSpecs + { + [Test] + public void AttributedMessage() + { + var urn = MessageUrn.ForType(typeof(Attributed)); + Assert.That(urn.AbsolutePath, Is.EqualTo("message:MyCustomName")); + } + + [Test] + public void AttributedMessage_with_default_prefix_throws_error() + { + Assert.That(() => MessageUrn.ForType(typeof(AttributedKnownPrefix)), + Throws.TypeOf() + .And.InnerException.TypeOf() + .And.InnerException.Message.StartsWith("Value should not contain the default prefix 'urn:message:'.")); + } + + [Test] + public void AttributedMessage_with_empty_throws_error() + { + Assert.That(() => MessageUrn.ForType(typeof(AttributedEmpty)), + Throws.TypeOf() + .And.InnerException.TypeOf() + .And.InnerException.Message.StartsWith("Value cannot be empty or whitespace only string.")); + } + + [Test] + public void AttributedMessage_with_null_throws_error() + { + Assert.That( + () => MessageUrn.ForType(typeof(AttributedNull)), + Throws.TypeOf() + .And.InnerException.TypeOf() + .And.InnerException.Message.StartsWith("Value cannot be null.")); + } + + [Test] + public void AttributedMessage_with_symbols() + { + var urn = MessageUrn.ForType(typeof(AttributedSymbols)); + Assert.That(urn, Is.EqualTo(new Uri("urn:message:\\|,./<>?;'#:@~[]{}�!\"�$%25^&*()_+`��"))); + // Assert.That(urn, Is.EqualTo("urn:message:\\|,./<>?;'#:@~[]{}�!\"�$%25^&*()_+`��")); + } + + [Test] + public void AttributedMessage_with_whitespace_throws_error() + { + Assert.That(() => MessageUrn.ForType(typeof(AttributedWhitespace)), + Throws.TypeOf() + .And.InnerException.TypeOf() + .And.InnerException.Message.StartsWith("Value cannot be empty or whitespace only string.")); + } + + [Test] + public void AttributedMessage_without_default_prefix() + { + var urn = MessageUrn.ForTypeString(typeof(AttributedNoDefaults)); + Assert.That(urn, Is.EqualTo("scheme:identifier")); + } + + [Test] + public void AttributedMessage_without_default_prefix_and_invalid_urn_throws_error() + { + Assert.That(() => MessageUrn.ForType(typeof(AttributedNoDefaultsInvalidUrn)), + Throws.TypeOf() + .And.InnerException.TypeOf() + .And.InnerException.Message.EqualTo("Invalid URN: scheme")); + } + + [Test] + public void ClosedGenericMessage() + { + var urn = MessageUrn.ForType(typeof(G)); + var expected = new Uri("urn:message:MassTransit.Tests:G[[MassTransit.TestFramework.Messages:PingMessage]]"); + Assert.That(urn.AbsolutePath, Is.EqualTo(expected.AbsolutePath)); + } + + [Test] + public void NestedMessage() + { + var urn = MessageUrn.ForType(typeof(X)); + Assert.That(urn.AbsolutePath, Is.EqualTo("message:MassTransit.Tests:MessageUrnSpecs+X")); + } + + [Test] + public void OpenGenericMessage() + { + Assert.That(() => MessageUrn.ForType(typeof(G<>)), Throws.TypeOf()); + } + + [Test] + public void SimpleMessage() + { + var urn = MessageUrn.ForType(typeof(PingMessage)); + Assert.That(urn.AbsolutePath, Is.EqualTo("message:MassTransit.TestFramework.Messages:PingMessage")); + } + + + class X + { + } + } + + + public class G + { + } + + + [MessageUrn("MyCustomName")] + public class Attributed + { + } + + + [MessageUrn(null)] + public class AttributedNull + { + } + + + [MessageUrn("")] + public class AttributedEmpty + { + } + + + [MessageUrn("\t\t ")] + public class AttributedWhitespace + { + } + + + [MessageUrn("urn:message:MyCustomName")] + public class AttributedKnownPrefix + { + } + + + [MessageUrn("\\|,./<>?;'#:@~[]{}�!\"�$%^&*()_+`��")] + public class AttributedSymbols + { + } + + + [MessageUrn("scheme:identifier", useDefaultPrefix: false)] + public class AttributedNoDefaults + { + } + + + [MessageUrn("scheme", useDefaultPrefix: false)] + public class AttributedNoDefaultsInvalidUrn + { + } +} diff --git a/tests/MassTransit.Tests/Middleware/Agents/Agent_Specs.cs b/tests/MassTransit.Tests/Middleware/Agents/Agent_Specs.cs index 73823f5bc26..ea761dc5a46 100644 --- a/tests/MassTransit.Tests/Middleware/Agents/Agent_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Agents/Agent_Specs.cs @@ -26,7 +26,7 @@ public async Task Should_fault_on_ready_faulted() supervisor.SetReady(); - Assert.That(async () => await supervisor.Ready.OrTimeout(s: 5), Throws.TypeOf()); + Assert.That(async () => await supervisor.Ready.OrTimeout(s: 5), Throws.TypeOf()); await supervisor.Stop().OrTimeout(s: 5); @@ -126,8 +126,11 @@ public async Task Should_allow_active_instances() Assert.That(async () => await supervisor.Send(pipe), Throws.TypeOf()); await supervisor.Send(pipe); - Assert.That(lastValue, Is.EqualTo("2")); - Assert.That(count, Is.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(lastValue, Is.EqualTo("2")); + Assert.That(count, Is.EqualTo(3)); + }); await supervisor.Stop(); @@ -154,8 +157,11 @@ public async Task Should_support_disconnection() await supervisor.Send(pipe); await supervisor.Send(pipe); - Assert.That(lastValue, Is.EqualTo("2")); - Assert.That(count, Is.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(lastValue, Is.EqualTo("2")); + Assert.That(count, Is.EqualTo(3)); + }); await supervisor.Stop(); diff --git a/tests/MassTransit.Tests/Middleware/Authentication_Specs.cs b/tests/MassTransit.Tests/Middleware/Authentication_Specs.cs index 0c1db97023b..e8f121e0ae6 100644 --- a/tests/MassTransit.Tests/Middleware/Authentication_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Authentication_Specs.cs @@ -57,9 +57,12 @@ public async Task Authenticated() await _thePipe.Send(request).ConfigureAwait(false); - Assert.That(protectedBusinessAction, Is.True); - Assert.That(cleanUp, Is.True); - Assert.That(rejected, Is.False); + Assert.Multiple(() => + { + Assert.That(protectedBusinessAction, Is.True); + Assert.That(cleanUp, Is.True); + Assert.That(rejected, Is.False); + }); } [Test] diff --git a/tests/MassTransit.Tests/Middleware/Caching/Bucket_Specs.cs b/tests/MassTransit.Tests/Middleware/Caching/Bucket_Specs.cs index c10ca2a85e8..0b1df23aa45 100644 --- a/tests/MassTransit.Tests/Middleware/Caching/Bucket_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Caching/Bucket_Specs.cs @@ -57,7 +57,7 @@ public async Task Should_fill_them_even_fuller() Task[] values = cache.GetAll().ToArray(); - Assert.That(values.Length, Is.EqualTo(101)); + Assert.That(values, Has.Length.EqualTo(101)); } [Test] @@ -89,7 +89,7 @@ public async Task Should_fill_up_a_bunch_of_buckets() Task[] values = cache.GetAll().ToArray(); - Assert.That(values.Length, Is.EqualTo(60)); + Assert.That(values, Has.Length.EqualTo(60)); } [Test] diff --git a/tests/MassTransit.Tests/Middleware/Caching/Index_Specs.cs b/tests/MassTransit.Tests/Middleware/Caching/Index_Specs.cs index a30d6d9ad96..363bbb7b418 100644 --- a/tests/MassTransit.Tests/Middleware/Caching/Index_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Caching/Index_Specs.cs @@ -34,8 +34,11 @@ public async Task Should_support_a_simple_addition() var value = await index.Get(helloKey, SimpleValueFactory.Healthy); Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo(helloKey)); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(value.Id, Is.EqualTo(helloKey)); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); + }); } [Test] @@ -50,14 +53,20 @@ public async Task Should_support_a_simple_addition_and_access() Task readValueTask = index.Get(helloKey); Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo(helloKey)); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(value.Id, Is.EqualTo(helloKey)); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); + }); var readValue = await readValueTask; Assert.That(readValue, Is.Not.Null); - Assert.That(readValue.Id, Is.EqualTo(helloKey)); - Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(readValue.Id, Is.EqualTo(helloKey)); + Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + }); } [Test] @@ -78,14 +87,20 @@ public async Task Should_support_access_to_eventual_success() var value = await goodValueTask; Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo(helloKey)); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(value.Id, Is.EqualTo(helloKey)); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); + }); var readValue = await readValueTask; Assert.That(readValue, Is.Not.Null); - Assert.That(readValue.Id, Is.EqualTo(helloKey)); - Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(readValue.Id, Is.EqualTo(helloKey)); + Assert.That(readValue.Value, Is.EqualTo("The key is Hello")); + }); } } } diff --git a/tests/MassTransit.Tests/Middleware/Caching/MissingValueFactory_Specs.cs b/tests/MassTransit.Tests/Middleware/Caching/MissingValueFactory_Specs.cs index bcda08d8ed5..9247c3a6611 100644 --- a/tests/MassTransit.Tests/Middleware/Caching/MissingValueFactory_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Caching/MissingValueFactory_Specs.cs @@ -25,10 +25,13 @@ public async Task Should_complete_with_the_second_factory_once_it_works() var value = await nodeValueFactory.CreateValue().ConfigureAwait(false); Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo(helloKey)); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + await Assert.MultipleAsync(async () => + { + Assert.That(value.Id, Is.EqualTo(helloKey)); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); - Assert.That(async () => await faultyValue.Value, Throws.TypeOf()); + Assert.That(async () => await faultyValue.Value, Throws.TypeOf()); + }); var healthy = await healthyValue.Value; } @@ -57,8 +60,11 @@ public async Task Should_return_the_created_value() var value = await SimpleValueFactory.Healthy("Hello"); Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo("Hello")); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(value.Id, Is.EqualTo("Hello")); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); + }); } [Test] @@ -73,8 +79,11 @@ public async Task Should_return_the_value_through_the_pending_value() var value = await nodeValueFactory.CreateValue().ConfigureAwait(false); Assert.That(value, Is.Not.Null); - Assert.That(value.Id, Is.EqualTo(helloKey)); - Assert.That(value.Value, Is.EqualTo("The key is Hello")); + Assert.Multiple(() => + { + Assert.That(value.Id, Is.EqualTo(helloKey)); + Assert.That(value.Value, Is.EqualTo("The key is Hello")); + }); } } } diff --git a/tests/MassTransit.Tests/Middleware/Caching/TestException.cs b/tests/MassTransit.Tests/Middleware/Caching/TestException.cs index e4b69576b77..c2011be7b17 100644 --- a/tests/MassTransit.Tests/Middleware/Caching/TestException.cs +++ b/tests/MassTransit.Tests/Middleware/Caching/TestException.cs @@ -17,6 +17,9 @@ public TestException(string message) { } +#if NET8_0_OR_GREATER + [Obsolete("Formatter-based serialization is obsolete and should not be used.")] +#endif protected TestException(SerializationInfo info, StreamingContext context) : base(info, context) { diff --git a/tests/MassTransit.Tests/Middleware/Caching/TwoIndex_Specs.cs b/tests/MassTransit.Tests/Middleware/Caching/TwoIndex_Specs.cs index 803c813f4b0..ed6d2ca6fcd 100644 --- a/tests/MassTransit.Tests/Middleware/Caching/TwoIndex_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Caching/TwoIndex_Specs.cs @@ -62,9 +62,12 @@ public async Task Should_honor_the_clear_and_still_allow_adding_nodes() var result = await valueIndex.Get("The key is key27"); - Assert.That(result.Id, Is.EqualTo("key27")); + Assert.Multiple(() => + { + Assert.That(result.Id, Is.EqualTo("key27")); - Assert.That(cache.Statistics.Hits, Is.EqualTo(1)); + Assert.That(cache.Statistics.Hits, Is.EqualTo(1)); + }); cache.Clear(); @@ -135,8 +138,11 @@ public async Task Should_update_the_second_index_once_removed() Assert.That(async () => await index.Get("key29"), Throws.TypeOf()); Assert.That(cache.Statistics.Count, Is.EqualTo(99)); - Assert.That(cache.Statistics.Hits, Is.EqualTo(1)); - Assert.That(cache.Statistics.Misses, Is.EqualTo(101)); + Assert.Multiple(() => + { + Assert.That(cache.Statistics.Hits, Is.EqualTo(1)); + Assert.That(cache.Statistics.Misses, Is.EqualTo(101)); + }); } } } diff --git a/tests/MassTransit.Tests/Middleware/Clone_a_payload_cache.cs b/tests/MassTransit.Tests/Middleware/Clone_a_payload_cache.cs index b6297981b6b..4686b61c1ba 100644 --- a/tests/MassTransit.Tests/Middleware/Clone_a_payload_cache.cs +++ b/tests/MassTransit.Tests/Middleware/Clone_a_payload_cache.cs @@ -20,11 +20,12 @@ public void Scoped_context_should_not_update_parent() p2.GetOrAddPayload(() => new Item2("bill")); - Item2 i2; - Assert.That(p.TryGetPayload(out i2), Is.False); - Assert.That(p2.TryGetPayload(out i2), Is.True); - Item i1; - Assert.That(p2.TryGetPayload(out i1), Is.True); + Assert.Multiple(() => + { + Assert.That(p.TryGetPayload(out Item2 _), Is.False); + Assert.That(p2.TryGetPayload(out Item2 _), Is.True); + }); + Assert.That(p2.TryGetPayload(out Item _), Is.True); } diff --git a/tests/MassTransit.Tests/Middleware/DynamicRouter_Specs.cs b/tests/MassTransit.Tests/Middleware/DynamicRouter_Specs.cs index e3ae4221584..44576a70a9a 100644 --- a/tests/MassTransit.Tests/Middleware/DynamicRouter_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/DynamicRouter_Specs.cs @@ -45,8 +45,11 @@ public async Task A() { await _router.Send(new Vendor("A")); - Assert.That(_aWasCalled, Is.True); - Assert.That(_bWasCalled, Is.False); + Assert.Multiple(() => + { + Assert.That(_aWasCalled, Is.True); + Assert.That(_bWasCalled, Is.False); + }); } [Test] @@ -54,8 +57,11 @@ public async Task B() { await _router.Send(new Vendor("B")); - Assert.That(_aWasCalled, Is.False); - Assert.That(_bWasCalled, Is.True); + Assert.Multiple(() => + { + Assert.That(_aWasCalled, Is.False); + Assert.That(_bWasCalled, Is.True); + }); } diff --git a/tests/MassTransit.Tests/Middleware/InterfaceExtensions.cs b/tests/MassTransit.Tests/Middleware/InterfaceExtensions.cs index bd112cc4e57..6c03ac36fcf 100644 --- a/tests/MassTransit.Tests/Middleware/InterfaceExtensions.cs +++ b/tests/MassTransit.Tests/Middleware/InterfaceExtensions.cs @@ -22,8 +22,7 @@ static InterfaceExtensions() /// True if the type is an open generic public static bool IsOpenGeneric(this Type type) { - var typeInfo = type.GetTypeInfo(); - return typeInfo.IsGenericTypeDefinition || typeInfo.ContainsGenericParameters; + return type.IsGenericTypeDefinition || type.ContainsGenericParameters; } public static Type GetInterface(this Type type, Type interfaceType) @@ -33,8 +32,7 @@ public static Type GetInterface(this Type type, Type interfaceType) if (interfaceType == null) throw new ArgumentNullException(nameof(interfaceType)); - var interfaceTypeInfo = interfaceType.GetTypeInfo(); - if (!interfaceTypeInfo.IsInterface) + if (!interfaceType.IsInterface) throw new ArgumentException("The interface type must be an interface: " + interfaceType.Name); return _cache.Get(type, interfaceType); @@ -50,7 +48,7 @@ public static IEnumerable GetClosingArguments(this Type type, Type openTyp if (!openType.IsOpenGeneric()) throw new ArgumentException("The interface type must be an open generic interface: " + openType.Name); - if (openType.GetTypeInfo().IsInterface) + if (openType.IsInterface) { if (!openType.IsOpenGeneric()) throw new ArgumentException("The interface type must be an open generic interface: " + openType.Name); @@ -59,20 +57,19 @@ public static IEnumerable GetClosingArguments(this Type type, Type openTyp if (interfaceType == null) throw new ArgumentException("The interface type is not implemented by: " + type.Name); - return interfaceType.GetTypeInfo().GetGenericArguments().Where(x => !x.IsGenericParameter); + return interfaceType.GetGenericArguments().Where(x => !x.IsGenericParameter); } var baseType = type; while (baseType != null && baseType != typeof(object)) { - var baseTypeInfo = baseType.GetTypeInfo(); - if (baseTypeInfo.IsGenericType && baseType.GetGenericTypeDefinition() == openType) - return baseTypeInfo.GetGenericArguments().Where(x => !x.IsGenericParameter); + if (baseType.IsGenericType && baseType.GetGenericTypeDefinition() == openType) + return baseType.GetGenericArguments().Where(x => !x.IsGenericParameter); - if (!baseTypeInfo.IsGenericType && baseType == openType) - return baseTypeInfo.GetGenericArguments().Where(x => !x.IsGenericParameter); + if (!baseType.IsGenericType && baseType == openType) + return baseType.GetGenericArguments().Where(x => !x.IsGenericParameter); - baseType = baseTypeInfo.BaseType; + baseType = baseType.BaseType; } throw new ArgumentException("Could not find open type in type: " + type.Name); diff --git a/tests/MassTransit.Tests/Middleware/InterfaceReflectionCache.cs b/tests/MassTransit.Tests/Middleware/InterfaceReflectionCache.cs index 1deb905e3d7..b45eb35ad82 100644 --- a/tests/MassTransit.Tests/Middleware/InterfaceReflectionCache.cs +++ b/tests/MassTransit.Tests/Middleware/InterfaceReflectionCache.cs @@ -17,7 +17,7 @@ public InterfaceReflectionCache() public Type GetGenericInterface(Type type, Type interfaceType) { - if (!interfaceType.GetTypeInfo().IsGenericTypeDefinition) + if (!interfaceType.IsGenericTypeDefinition) { throw new ArgumentException( "The interface must be a generic interface definition: " + interfaceType.Name, @@ -28,7 +28,7 @@ public Type GetGenericInterface(Type type, Type interfaceType) if (type == interfaceType) return null; - if (type.GetTypeInfo().IsGenericType) + if (type.IsGenericType) { if (type.GetGenericTypeDefinition() == interfaceType) return type; @@ -36,7 +36,7 @@ public Type GetGenericInterface(Type type, Type interfaceType) Type[] interfaces = type.GetTypeInfo().ImplementedInterfaces.ToArray(); - return interfaces.Where(t => t.GetTypeInfo().IsGenericType) + return interfaces.Where(t => t.IsGenericType) .FirstOrDefault(t => t.GetGenericTypeDefinition() == interfaceType); } @@ -49,7 +49,7 @@ public Type Get(Type type, Type interfaceType) Type GetInterfaceInternal(Type type, Type interfaceType) { - if (interfaceType.GetTypeInfo().IsGenericTypeDefinition) + if (interfaceType.IsGenericTypeDefinition) return GetGenericInterface(type, interfaceType); Type[] interfaces = type.GetTypeInfo().ImplementedInterfaces.ToArray(); diff --git a/tests/MassTransit.Tests/Middleware/Internals/FastProperty_Specs.cs b/tests/MassTransit.Tests/Middleware/Internals/FastProperty_Specs.cs index 562a29863ab..0e0a3af06c0 100644 --- a/tests/MassTransit.Tests/Middleware/Internals/FastProperty_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Internals/FastProperty_Specs.cs @@ -25,7 +25,7 @@ public void Should_be_able_to_access_a_private_setter() const string expectedValue = "Chris"; fastProperty.Set(instance, expectedValue); - Assert.AreEqual(expectedValue, fastProperty.Get(instance)); + Assert.That(fastProperty.Get(instance), Is.EqualTo(expectedValue)); } [Test] @@ -38,7 +38,7 @@ public void Should_cache_properties_nicely() const string expectedValue = "Chris"; cache["Name"].Set(instance, expectedValue); - Assert.AreEqual(expectedValue, instance.Name); + Assert.That(instance.Name, Is.EqualTo(expectedValue)); } diff --git a/tests/MassTransit.Tests/Middleware/Internals/TypeExtensionGeneric_Specs.cs b/tests/MassTransit.Tests/Middleware/Internals/TypeExtensionGeneric_Specs.cs index 698157538a6..91342e2556e 100644 --- a/tests/MassTransit.Tests/Middleware/Internals/TypeExtensionGeneric_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Internals/TypeExtensionGeneric_Specs.cs @@ -13,25 +13,25 @@ public class When_getting_the_generic_types_from_an_interface [Test] public void Should_close_generic_type() { - Assert.IsTrue(typeof(GenericClass).ClosesType(typeof(IGeneric<>))); + Assert.That(typeof(GenericClass).ClosesType(typeof(IGeneric<>)), Is.True); } [Test] public void Should_not_close_nested_open_generic_base_class() { - Assert.IsFalse(typeof(SuperGenericBaseClass<>).ClosesType(typeof(GenericBaseClass<>))); + Assert.That(typeof(SuperGenericBaseClass<>).ClosesType(typeof(GenericBaseClass<>)), Is.False); } [Test] public void Should_not_close_nested_open_generic_interface_in_base_class() { - Assert.IsFalse(typeof(SuperGenericBaseClass<>).ClosesType(typeof(IGeneric<>))); + Assert.That(typeof(SuperGenericBaseClass<>).ClosesType(typeof(IGeneric<>)), Is.False); } [Test] public void Should_not_close_open_generic_type() { - Assert.IsFalse(typeof(GenericBaseClass<>).ClosesType(typeof(IGeneric<>))); + Assert.That(typeof(GenericBaseClass<>).ClosesType(typeof(IGeneric<>)), Is.False); } [Test] @@ -39,43 +39,55 @@ public void Should_not_have_closing_arguments_for_a_class_that_isnt_closed() { IEnumerable types = typeof(SuperGenericBaseClass<>).GetClosingArguments(typeof(IGeneric<>)); - Assert.AreEqual(0, types.Count()); + Assert.That(types.Count(), Is.EqualTo(0)); } [Test] public void Should_return_the_appropriate_generic_type() { - IEnumerable types = typeof(GenericClass).GetClosingArguments(typeof(IGeneric<>)); + IEnumerable types = typeof(GenericClass).GetClosingArguments(typeof(IGeneric<>)).ToArray(); - Assert.AreEqual(1, types.Count()); - Assert.AreEqual(typeof(int), types.First()); + Assert.Multiple(() => + { + Assert.That(types.Count(), Is.EqualTo(1)); + Assert.That(types.First(), Is.EqualTo(typeof(int))); + }); } [Test] public void Should_return_the_appropriate_generic_type_for_a_subclass_non_generic() { - IEnumerable types = typeof(SubClass).GetClosingArguments(typeof(IGeneric<>)); + IEnumerable types = typeof(SubClass).GetClosingArguments(typeof(IGeneric<>)).ToArray(); - Assert.AreEqual(1, types.Count()); - Assert.AreEqual(typeof(int), types.First()); + Assert.Multiple(() => + { + Assert.That(types.Count(), Is.EqualTo(1)); + Assert.That(types.First(), Is.EqualTo(typeof(int))); + }); } [Test] public void Should_return_the_appropriate_generic_type_with_a_generic_base_class() { - IEnumerable types = typeof(NonGenericSubClass).GetClosingArguments(typeof(IGeneric<>)); + IEnumerable types = typeof(NonGenericSubClass).GetClosingArguments(typeof(IGeneric<>)).ToArray(); - Assert.AreEqual(1, types.Count()); - Assert.AreEqual(typeof(int), types.First()); + Assert.Multiple(() => + { + Assert.That(types.Count(), Is.EqualTo(1)); + Assert.That(types.First(), Is.EqualTo(typeof(int))); + }); } [Test] public void Should_return_the_generic_type_from_a_class() { - IEnumerable types = typeof(NonGenericSubClass).GetClosingArguments(typeof(GenericBaseClass<>)); + IEnumerable types = typeof(NonGenericSubClass).GetClosingArguments(typeof(GenericBaseClass<>)).ToArray(); - Assert.AreEqual(1, types.Count()); - Assert.AreEqual(typeof(int), types.First()); + Assert.Multiple(() => + { + Assert.That(types.Count(), Is.EqualTo(1)); + Assert.That(types.First(), Is.EqualTo(typeof(int))); + }); } diff --git a/tests/MassTransit.Tests/Middleware/Internals/TypeExtensionMoreGeneric_Specs.cs b/tests/MassTransit.Tests/Middleware/Internals/TypeExtensionMoreGeneric_Specs.cs index 1ea8b6230db..c0f11412c9a 100644 --- a/tests/MassTransit.Tests/Middleware/Internals/TypeExtensionMoreGeneric_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Internals/TypeExtensionMoreGeneric_Specs.cs @@ -13,7 +13,7 @@ public class Reflecting_over_a_generic_type [Test] public void Should_not_close_open_generic() { - Assert.IsFalse(typeof(ISingleGeneric<>).ClosesType(typeof(ISingleGeneric<>))); + Assert.That(typeof(ISingleGeneric<>).ClosesType(typeof(ISingleGeneric<>)), Is.False); } [Test] @@ -21,9 +21,12 @@ public void Should_return_an_enumeration_of_a_constraint_based_generic_interface { Type[] types = typeof(NestedDoubleGenericInterface).GetClosingArguments(typeof(INestedDoubleGeneric<,>)).ToArray(); - Assert.AreEqual(2, types.Length); - Assert.AreEqual(typeof(SingleGenericInterface), types[0]); - Assert.AreEqual(typeof(int), types[1]); + Assert.That(types, Has.Length.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(types[0], Is.EqualTo(typeof(SingleGenericInterface))); + Assert.That(types[1], Is.EqualTo(typeof(int))); + }); } [Test] @@ -31,9 +34,12 @@ public void Should_return_an_enumeration_of_a_deep_double_nested_generic_type() { Type[] types = typeof(DeepDoubleNestedGeneric).GetClosingArguments(typeof(Dictionary<,>)).ToArray(); - Assert.AreEqual(2, types.Length); - Assert.AreEqual(typeof(int), types[0]); - Assert.AreEqual(typeof(string), types[1]); + Assert.That(types, Has.Length.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(types[0], Is.EqualTo(typeof(int))); + Assert.That(types[1], Is.EqualTo(typeof(string))); + }); } [Test] @@ -41,8 +47,8 @@ public void Should_return_an_enumeration_of_a_deep_single_nested_generic_type() { Type[] types = typeof(DeepSingleNestedGeneric).GetClosingArguments(typeof(List<>)).ToArray(); - Assert.AreEqual(1, types.Length); - Assert.AreEqual(typeof(string), types[0]); + Assert.That(types, Has.Length.EqualTo(1)); + Assert.That(types[0], Is.EqualTo(typeof(string))); } [Test] @@ -50,9 +56,12 @@ public void Should_return_an_enumeration_of_a_double_generic_interface() { Type[] types = typeof(DoubleGenericInterface).GetClosingArguments(typeof(IDoubleGeneric<,>)).ToArray(); - Assert.AreEqual(2, types.Length); - Assert.AreEqual(typeof(int), types[0]); - Assert.AreEqual(typeof(string), types[1]); + Assert.That(types, Has.Length.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(types[0], Is.EqualTo(typeof(int))); + Assert.That(types[1], Is.EqualTo(typeof(string))); + }); } [Test] @@ -60,9 +69,12 @@ public void Should_return_an_enumeration_of_a_double_generic_type() { Type[] types = typeof(Dictionary).GetClosingArguments(typeof(Dictionary<,>)).ToArray(); - Assert.AreEqual(2, types.Length); - Assert.AreEqual(typeof(int), types[0]); - Assert.AreEqual(typeof(string), types[1]); + Assert.That(types, Has.Length.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(types[0], Is.EqualTo(typeof(int))); + Assert.That(types[1], Is.EqualTo(typeof(string))); + }); } [Test] @@ -70,9 +82,12 @@ public void Should_return_an_enumeration_of_a_double_nested_generic_type() { Type[] types = typeof(DoubleNestedGeneric).GetClosingArguments(typeof(Dictionary<,>)).ToArray(); - Assert.AreEqual(2, types.Length); - Assert.AreEqual(typeof(int), types[0]); - Assert.AreEqual(typeof(string), types[1]); + Assert.That(types, Has.Length.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(types[0], Is.EqualTo(typeof(int))); + Assert.That(types[1], Is.EqualTo(typeof(string))); + }); } [Test] @@ -80,8 +95,8 @@ public void Should_return_an_enumeration_of_a_single_generic_interface() { Type[] types = typeof(SingleGenericInterface).GetClosingArguments(typeof(ISingleGeneric<>)).ToArray(); - Assert.AreEqual(1, types.Length); - Assert.AreEqual(typeof(int), types[0]); + Assert.That(types, Has.Length.EqualTo(1)); + Assert.That(types[0], Is.EqualTo(typeof(int))); } [Test] @@ -89,8 +104,8 @@ public void Should_return_an_enumeration_of_a_single_generic_type() { Type[] types = typeof(List).GetClosingArguments(typeof(List<>)).ToArray(); - Assert.AreEqual(1, types.Length); - Assert.AreEqual(typeof(string), types[0]); + Assert.That(types, Has.Length.EqualTo(1)); + Assert.That(types[0], Is.EqualTo(typeof(string))); } [Test] @@ -98,8 +113,8 @@ public void Should_return_an_enumeration_of_a_single_nested_generic_type() { Type[] types = typeof(SingleNestedGeneric).GetClosingArguments(typeof(List<>)).ToArray(); - Assert.AreEqual(1, types.Length); - Assert.AreEqual(typeof(string), types[0]); + Assert.That(types, Has.Length.EqualTo(1)); + Assert.That(types[0], Is.EqualTo(typeof(string))); } diff --git a/tests/MassTransit.Tests/Middleware/Internals/TypeExtension_Specs.cs b/tests/MassTransit.Tests/Middleware/Internals/TypeExtension_Specs.cs index 690932d9a35..2d58ea8a9da 100644 --- a/tests/MassTransit.Tests/Middleware/Internals/TypeExtension_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Internals/TypeExtension_Specs.cs @@ -11,67 +11,67 @@ public class An_object_that_implements_a_generic_interface [Test] public void Should_match_a_generic_base_class_implementation_of_the_interface() { - Assert.IsTrue(typeof(NonGenericSubClass).HasInterface>()); + Assert.That(typeof(NonGenericSubClass).HasInterface>(), Is.True); } [Test] public void Should_match_a_generic_interface() { - Assert.IsTrue(typeof(GenericClass).HasInterface>()); + Assert.That(typeof(GenericClass).HasInterface>(), Is.True); } [Test] public void Should_match_a_regular_interface_by_type_argument_on_an_object() { - Assert.IsTrue(typeof(GenericClass).HasInterface(typeof(INotGeneric))); + Assert.That(typeof(GenericClass).HasInterface(typeof(INotGeneric)), Is.True); } [Test] public void Should_match_a_regular_interface_on_an_object() { - Assert.IsTrue(typeof(GenericClass).HasInterface()); + Assert.That(typeof(GenericClass).HasInterface(), Is.True); } [Test] public void Should_match_a_regular_interface_using_the_generic_argument() { - Assert.IsTrue(typeof(GenericClass).HasInterface()); + Assert.That(typeof(GenericClass).HasInterface(), Is.True); } [Test] public void Should_match_a_regular_interface_using_the_generic_argument_on_a_subclass() { - Assert.IsTrue(typeof(GenericSubClass).HasInterface()); + Assert.That(typeof(GenericSubClass).HasInterface(), Is.True); } [Test] public void Should_match_a_regular_interface_using_the_type_argument() { - Assert.IsTrue(typeof(GenericClass).HasInterface(typeof(INotGeneric))); + Assert.That(typeof(GenericClass).HasInterface(typeof(INotGeneric)), Is.True); } [Test] public void Should_match_a_regular_interface_using_the_type_argument_on_a_subclass() { - Assert.IsTrue(typeof(GenericSubClass).HasInterface(typeof(INotGeneric))); + Assert.That(typeof(GenericSubClass).HasInterface(typeof(INotGeneric)), Is.True); } [Test] public void Should_match_an_open_generic_interface() { - Assert.IsTrue(typeof(GenericClass).HasInterface(typeof(IGeneric<>))); + Assert.That(typeof(GenericClass).HasInterface(typeof(IGeneric<>)), Is.True); } [Test] public void Should_match_an_open_generic_interface_in_a_base_class() { - Assert.IsTrue(typeof(NonGenericSubClass).HasInterface(typeof(IGeneric<>))); + Assert.That(typeof(NonGenericSubClass).HasInterface(typeof(IGeneric<>)), Is.True); } [Test] public void Should_not_match_a_regular_interface_that_is_not_implemented() { - Assert.IsFalse(typeof(GenericClass).HasInterface()); + Assert.That(typeof(GenericClass).HasInterface(), Is.False); } diff --git a/tests/MassTransit.Tests/Middleware/Internals/TypeProperty_Specs.cs b/tests/MassTransit.Tests/Middleware/Internals/TypeProperty_Specs.cs index 4c9d8a042ac..bc42cdefebe 100644 --- a/tests/MassTransit.Tests/Middleware/Internals/TypeProperty_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Internals/TypeProperty_Specs.cs @@ -15,7 +15,7 @@ public void Should_include_properties_from_the_base_class() { IEnumerable properties = typeof(B).GetAllProperties(); - Assert.AreEqual(3, properties.Count()); + Assert.That(properties.Count(), Is.EqualTo(3)); } [Test] @@ -23,7 +23,7 @@ public void Should_include_properties_from_the_interface() { IEnumerable properties = typeof(C).GetAllProperties(); - Assert.AreEqual(2, properties.Count()); + Assert.That(properties.Count(), Is.EqualTo(2)); } [Test] @@ -31,7 +31,7 @@ public void Should_include_properties_from_the_interface_and_base_interfaces() { IEnumerable properties = typeof(D).GetAllProperties(); - Assert.AreEqual(4, properties.Count()); + Assert.That(properties.Count(), Is.EqualTo(4)); } [Test] @@ -39,7 +39,7 @@ public void Should_include_properties_from_the_interface_and_such() { IEnumerable properties = typeof(ID).GetAllProperties(); - Assert.AreEqual(4, properties.Count()); + Assert.That(properties.Count(), Is.EqualTo(4)); } [Test] @@ -47,7 +47,7 @@ public void Should_include_static_properties() { IEnumerable properties = typeof(A).GetAllStaticProperties(); - Assert.AreEqual(1, properties.Count()); + Assert.That(properties.Count(), Is.EqualTo(1)); } [Test] @@ -55,7 +55,7 @@ public void Should_include_static_properties_of_base_classes() { IEnumerable properties = typeof(B).GetAllStaticProperties(); - Assert.AreEqual(1, properties.Count()); + Assert.That(properties.Count(), Is.EqualTo(1)); } [Test] @@ -63,7 +63,7 @@ public void Should_include_the_properties_on_the_class() { IEnumerable properties = typeof(A).GetAllProperties(); - Assert.AreEqual(2, properties.Count()); + Assert.That(properties.Count(), Is.EqualTo(2)); } diff --git a/tests/MassTransit.Tests/Middleware/OneTime_Specs.cs b/tests/MassTransit.Tests/Middleware/OneTime_Specs.cs index 84b48c79c53..1faddf8a277 100644 --- a/tests/MassTransit.Tests/Middleware/OneTime_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/OneTime_Specs.cs @@ -19,10 +19,10 @@ public async Task Should_only_invoke_the_method_once() { cfg.UseExecuteAsync(async x => { - await x.OneTimeSetup>(async key => + await x.OneTimeSetup>(async () => { Interlocked.Increment(ref callCount); - }, () => new MySecret()); + }); Interlocked.Increment(ref totalCount); }); @@ -36,8 +36,11 @@ await x.OneTimeSetup>(async key => await Task.WhenAll(tasks); - Assert.That(callCount, Is.EqualTo(1)); - Assert.That(totalCount, Is.EqualTo(50)); + Assert.Multiple(() => + { + Assert.That(callCount, Is.EqualTo(1)); + Assert.That(totalCount, Is.EqualTo(50)); + }); } diff --git a/tests/MassTransit.Tests/Middleware/OrCanceled_Specs.cs b/tests/MassTransit.Tests/Middleware/OrCanceled_Specs.cs index 4d1980b1e3a..49339d62ae4 100644 --- a/tests/MassTransit.Tests/Middleware/OrCanceled_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/OrCanceled_Specs.cs @@ -13,6 +13,7 @@ namespace MassTransit.Tests.Middleware public class When_using_or_canceled_should_not_have_an_unhandled_task_exception { [Test] + [Explicit] public async Task Should_fault_on_ready_faulted() { List unhandledExceptions = new List(); @@ -47,8 +48,11 @@ async Task DoSomething() GC.WaitForPendingFinalizers(); GC.Collect(); - Assert.That(unhandledExceptions, Is.Empty); - Assert.That(unobservedTaskExceptions, Is.Empty); + Assert.Multiple(() => + { + Assert.That(unhandledExceptions, Is.Empty); + Assert.That(unobservedTaskExceptions, Is.Empty); + }); } } } diff --git a/tests/MassTransit.Tests/Middleware/Parent_Child_Pipes.cs b/tests/MassTransit.Tests/Middleware/Parent_Child_Pipes.cs index 235787e5438..ff0ea3e3a25 100644 --- a/tests/MassTransit.Tests/Middleware/Parent_Child_Pipes.cs +++ b/tests/MassTransit.Tests/Middleware/Parent_Child_Pipes.cs @@ -35,8 +35,11 @@ public async Task Should_compose_pipes() await pipe2.Send(new InitialContext()).ConfigureAwait(false); - Assert.That(count1, Is.EqualTo(1)); - Assert.That(count2, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(count1, Is.EqualTo(1)); + Assert.That(count2, Is.EqualTo(1)); + }); } [Test] @@ -67,8 +70,11 @@ public async Task ShouldDeliverToBoth() await pipe2.Send(new InitialContext()).ConfigureAwait(false); - Assert.That(count1, Is.EqualTo(1)); - Assert.That(count2, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(count1, Is.EqualTo(1)); + Assert.That(count2, Is.EqualTo(1)); + }); } diff --git a/tests/MassTransit.Tests/Middleware/PartitionByKey_Specs.cs b/tests/MassTransit.Tests/Middleware/PartitionByKey_Specs.cs index 589fff2ee14..8dc3b25c3d3 100644 --- a/tests/MassTransit.Tests/Middleware/PartitionByKey_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/PartitionByKey_Specs.cs @@ -30,7 +30,7 @@ public async Task Should_use_a_partitioner_for_consistency() await completed.Task; - Assert.AreEqual(Limit, count); + Assert.That(count, Is.EqualTo(Limit)); Console.WriteLine("Processed: {0}", count); } diff --git a/tests/MassTransit.Tests/Middleware/Retry_Specs.cs b/tests/MassTransit.Tests/Middleware/Retry_Specs.cs index 6bebc30646c..588baeee90d 100644 --- a/tests/MassTransit.Tests/Middleware/Retry_Specs.cs +++ b/tests/MassTransit.Tests/Middleware/Retry_Specs.cs @@ -43,8 +43,11 @@ public async Task Should_be_observable_at_each_retry() await observer.PostFault; - Assert.That(observer.PostFaultCount, Is.EqualTo(4)); - Assert.That(observer.RetryFaultCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PostFaultCount, Is.EqualTo(4)); + Assert.That(observer.RetryFaultCount, Is.EqualTo(1)); + }); var retryFault = await observer.RetryFault; @@ -285,10 +288,13 @@ public async Task Should_retry_and_then_succeed_without_repeating_forever() await pipe.Send(context); - Assert.That(count, Is.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(count, Is.EqualTo(2)); - Assert.That(observer.PostFault.IsCompleted); - Assert.That(observer.RetryComplete.IsCompleted); + Assert.That(observer.PostFault.IsCompleted); + Assert.That(observer.RetryComplete.IsCompleted); + }); } [Test] diff --git a/tests/MassTransit.Tests/MultiBusRequest_Specs.cs b/tests/MassTransit.Tests/MultiBusRequest_Specs.cs index ff4fe450ea9..4023099f800 100644 --- a/tests/MassTransit.Tests/MultiBusRequest_Specs.cs +++ b/tests/MassTransit.Tests/MultiBusRequest_Specs.cs @@ -47,8 +47,11 @@ public async Task Should_handle_responses_properly() __Header_Test_Value = "Hello, World" }, timeout: harness.TestInactivityTimeout); - Assert.That(response.Message.Value, Is.EqualTo("Key: Hello")); - Assert.That(response.Headers.Get("Test-Value"), Is.EqualTo("Hello, World")); + Assert.Multiple(() => + { + Assert.That(response.Message.Value, Is.EqualTo("Key: Hello")); + Assert.That(response.Headers.Get("Test-Value"), Is.EqualTo("Hello, World")); + }); } diff --git a/tests/MassTransit.Tests/MultiTestConsumer_Specs.cs b/tests/MassTransit.Tests/MultiTestConsumer_Specs.cs index a1c6bac1a0a..db1f33f1dea 100644 --- a/tests/MassTransit.Tests/MultiTestConsumer_Specs.cs +++ b/tests/MassTransit.Tests/MultiTestConsumer_Specs.cs @@ -27,8 +27,13 @@ public async Task Should_distinguish_multiple_events() await Bus.Publish(pingMessage); await Bus.Publish(pingMessage2); - Assert.IsTrue(consumer.Received.Select(received => received.Context.Message.CorrelationId == pingMessage.CorrelationId).Any()); - Assert.IsTrue(consumer.Received.Select(received => received.Context.Message.CorrelationId == pingMessage2.CorrelationId).Any()); + Assert.Multiple(() => + { + Assert.That(consumer.Received.Select(received => received.Context.Message.CorrelationId == pingMessage.CorrelationId).Any(), + Is.True); + Assert.That(consumer.Received.Select(received => received.Context.Message.CorrelationId == pingMessage2.CorrelationId).Any(), + Is.True); + }); } finally { @@ -49,7 +54,7 @@ public async Task Should_show_that_the_message_was_received_by_the_consumer() { await Bus.Publish(new PingMessage()); - Assert.IsTrue(received.Select().Any()); + Assert.That(received.Select().Any(), Is.True); } finally { @@ -71,8 +76,12 @@ public async Task Should_show_that_the_specified_type_was_received() await Bus.Publish(pingMessage); await Bus.Publish(new PongMessage(pingMessage.CorrelationId)); - Assert.IsTrue(consumer.Received.Select().Any()); - Assert.IsTrue(consumer.Received.Select(received => received.Context.Message.CorrelationId == pingMessage.CorrelationId).Any()); + Assert.Multiple(() => + { + Assert.That(consumer.Received.Select().Any(), Is.True); + Assert.That(consumer.Received.Select(received => received.Context.Message.CorrelationId == pingMessage.CorrelationId).Any(), + Is.True); + }); } finally { diff --git a/tests/MassTransit.Tests/Pipeline/CircuitBreaker_Specs.cs b/tests/MassTransit.Tests/Pipeline/CircuitBreaker_Specs.cs index e5af7cae420..5085b84e33d 100644 --- a/tests/MassTransit.Tests/Pipeline/CircuitBreaker_Specs.cs +++ b/tests/MassTransit.Tests/Pipeline/CircuitBreaker_Specs.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -35,7 +34,7 @@ public async Task Should_allow_the_first_call() for (var i = 0; i < 100; i++) Assert.That(async () => await pipe.Send(context), Throws.TypeOf()); - count.ShouldBe(6); + Assert.That(count, Is.EqualTo(6)); } } } diff --git a/tests/MassTransit.Tests/Pipeline/Concurrency_Specs.cs b/tests/MassTransit.Tests/Pipeline/Concurrency_Specs.cs index c6b4b9b4bbf..3f13697180d 100644 --- a/tests/MassTransit.Tests/Pipeline/Concurrency_Specs.cs +++ b/tests/MassTransit.Tests/Pipeline/Concurrency_Specs.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -42,7 +41,7 @@ public async Task Should_allow_just_enough_threads_at_once() await Task.WhenAll(tasks); - maxCount.ShouldBe(32); + Assert.That(maxCount, Is.EqualTo(32)); } [Test] @@ -74,7 +73,7 @@ public async Task Should_prevent_too_many_threads_at_one_time() await Task.WhenAll(tasks); - maxCount.ShouldBe(1); + Assert.That(maxCount, Is.EqualTo(1)); } } @@ -119,7 +118,7 @@ public async Task Should_count_success_and_failure_as_same() timer.Stop(); - timer.ElapsedMilliseconds.ShouldBeGreaterThan(9500); + Assert.That(timer.ElapsedMilliseconds, Is.GreaterThan(9500)); } [Test] @@ -148,7 +147,7 @@ public async Task Should_only_do_n_messages_per_interval() timer.Stop(); - timer.ElapsedMilliseconds.ShouldBeGreaterThan(9500); + Assert.That(timer.ElapsedMilliseconds, Is.GreaterThan(9500)); } } } diff --git a/tests/MassTransit.Tests/Pipeline/ContentFilter_Specs.cs b/tests/MassTransit.Tests/Pipeline/ContentFilter_Specs.cs index 33302c352cc..4fc851e3456 100644 --- a/tests/MassTransit.Tests/Pipeline/ContentFilter_Specs.cs +++ b/tests/MassTransit.Tests/Pipeline/ContentFilter_Specs.cs @@ -3,7 +3,6 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -22,7 +21,7 @@ public async Task Should_only_get_one_message() await Task.Delay(50); - _consumer.Count.ShouldBe(1); + Assert.That(_consumer.Count, Is.EqualTo(1)); } MyConsumer _consumer; diff --git a/tests/MassTransit.Tests/Pipeline/PartitionByKey_Specs.cs b/tests/MassTransit.Tests/Pipeline/PartitionByKey_Specs.cs index cdee472aa98..6d080f13740 100644 --- a/tests/MassTransit.Tests/Pipeline/PartitionByKey_Specs.cs +++ b/tests/MassTransit.Tests/Pipeline/PartitionByKey_Specs.cs @@ -19,7 +19,7 @@ public async Task Should_use_a_partitioner_for_consistency() var count = await _completed.Task; - Assert.AreEqual(Limit, count); + Assert.That(count, Is.EqualTo(Limit)); Console.WriteLine("Processed: {0}", count); } @@ -81,7 +81,7 @@ public async Task Should_use_a_partitioner_for_consistency() var count = await _completed.Task; - Assert.AreEqual(Limit, count); + Assert.That(count, Is.EqualTo(Limit)); Console.WriteLine("Processed: {0}", count); } diff --git a/tests/MassTransit.Tests/Pipeline/Retry_Specs.cs b/tests/MassTransit.Tests/Pipeline/Retry_Specs.cs index cc86ebc0a51..cfd41715231 100644 --- a/tests/MassTransit.Tests/Pipeline/Retry_Specs.cs +++ b/tests/MassTransit.Tests/Pipeline/Retry_Specs.cs @@ -3,7 +3,6 @@ using System; using MassTransit.Middleware; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -28,7 +27,7 @@ public void Should_retry_the_specified_times_and_fail() Assert.That(async () => await pipe.Send(context), Throws.TypeOf()); - count.ShouldBe(5); + Assert.That(count, Is.EqualTo(5)); } [Test] @@ -50,7 +49,7 @@ public void Should_support_overloading_downstream() Assert.That(async () => await pipe.Send(context), Throws.TypeOf()); - count.ShouldBe(1); + Assert.That(count, Is.EqualTo(1)); } [Test] @@ -72,7 +71,7 @@ public void Should_support_overloading_downstream_either_way() Assert.That(async () => await pipe.Send(context), Throws.TypeOf()); - count.ShouldBe(5); + Assert.That(count, Is.EqualTo(5)); } [Test] @@ -108,7 +107,7 @@ public void Should_support_overloading_downstream_on_cc() Assert.That(async () => await pipe.Send(context), Throws.TypeOf()); - count.ShouldBe(1); + Assert.That(count, Is.EqualTo(1)); } diff --git a/tests/MassTransit.Tests/Pipeline/Transaction_Specs.cs b/tests/MassTransit.Tests/Pipeline/Transaction_Specs.cs index 9ff4f1142ef..e8d9074d3ee 100644 --- a/tests/MassTransit.Tests/Pipeline/Transaction_Specs.cs +++ b/tests/MassTransit.Tests/Pipeline/Transaction_Specs.cs @@ -1,186 +1,186 @@ -namespace MassTransit.Tests.Pipeline -{ - using System; - using System.Linq; - using System.Threading; - using System.Threading.Tasks; - using System.Transactions; - using NUnit.Framework; - using TestFramework; - using TestFramework.Messages; - - - [TestFixture] - public class When_a_transaction_spans_threads - { - [Test] - public void Should_properly_fail() - { - IPipe pipe = Pipe.New(x => - { - x.UseTransaction(); - x.UseExecute(payload => Console.WriteLine("Execute: {0}", Thread.CurrentThread.ManagedThreadId)); - x.UseExecuteAsync(payload => Task.Run(() => - { - using (var scope = payload.CreateTransactionScope()) - { - Console.WriteLine("ExecuteAsync: {0}", Thread.CurrentThread.ManagedThreadId); - - Assert.IsNotNull(Transaction.Current); - Console.WriteLine("Isolation Level: {0}", Transaction.Current.IsolationLevel); - } - })); - }); - - var context = new TestConsumeContext(new PingMessage()); - - Assert.That(async () => await pipe.Send(context), Throws.TypeOf()); - } - - [Test] - public void Should_properly_handle_exception() - { - IPipe pipe = Pipe.New(x => - { - x.UseTransaction(); - x.UseExecute(payload => Console.WriteLine("Execute: {0}", Thread.CurrentThread.ManagedThreadId)); - x.UseExecuteAsync(async payload => - { - await Task.Yield(); - - using (var scope = payload.CreateTransactionScope()) - { - Console.WriteLine("ExecuteAsync: {0}", Thread.CurrentThread.ManagedThreadId); - - Assert.IsNotNull(Transaction.Current); - Console.WriteLine("Isolation Level: {0}", Transaction.Current.IsolationLevel); - - scope.Complete(); - } - - throw new InvalidOperationException("This is a friendly boom"); - }); - x.UseExecute(payload => Console.WriteLine("After Transaction: {0}", Thread.CurrentThread.ManagedThreadId)); - }); - - var context = new TestConsumeContext(new PingMessage()); - - Assert.That(async () => await pipe.Send(context), Throws.InvalidOperationException); - } - - [Test] - public async Task Should_properly_succeed() - { - IPipe pipe = Pipe.New(x => - { - x.UseTransaction(); - x.UseExecute(payload => Console.WriteLine("Execute: {0}", Thread.CurrentThread.ManagedThreadId)); - x.UseExecuteAsync(payload => Task.Run(() => - { - using (var scope = payload.CreateTransactionScope()) - { - Console.WriteLine("ExecuteAsync: {0}", Thread.CurrentThread.ManagedThreadId); - - Assert.IsNotNull(Transaction.Current); - Console.WriteLine("Isolation Level: {0}", Transaction.Current.IsolationLevel); - scope.Complete(); - } - })); - }); - - var context = new TestConsumeContext(new PingMessage()); - - await pipe.Send(context); - } - - [Test] - public void Should_timeout() - { - IPipe pipe = Pipe.New(x => - { - x.UseTransaction(t => t.Timeout = TimeSpan.FromSeconds(1)); - x.UseExecute(payload => Console.WriteLine("Execute: {0}", Thread.CurrentThread.ManagedThreadId)); - x.UseExecuteAsync(async payload => - { - await Task.Yield(); - - using (var scope = payload.CreateTransactionScope()) - { - Console.WriteLine("ExecuteAsync: {0}", Thread.CurrentThread.ManagedThreadId); - - Assert.IsNotNull(Transaction.Current); - Console.WriteLine("Isolation Level: {0}", Transaction.Current.IsolationLevel); - - Thread.Sleep(5000); - - scope.Complete(); - } - - Console.WriteLine("Exited Scope"); - }); - x.UseExecute(payload => Console.WriteLine("After Transaction: {0}", Thread.CurrentThread.ManagedThreadId)); - }); - - var context = new TestConsumeContext(new PingMessage()); - - Assert.That(async () => await pipe.Send(context), Throws.TypeOf()); - } - } - - - [TestFixture] - public class When_a_transaction_throws_an_exception_with_retry : - InMemoryTestFixture - { - [Test] - public async Task Should_not_reuse_transaction_context_between_retries() - { - ConsumeContext> fault = await _faultReceived; - - Assert.That(fault.Message.Exceptions.First().ExceptionType, Does.Contain(nameof(IntentionalTestException))); - } - - Task>> _faultReceived; - - [OneTimeSetUp] - public async Task Setup() - { - await InputQueueSendEndpoint.Send(new Message { Id = NewId.NextGuid() }); - } - - protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) - { - configurator.ReceiveEndpoint("errors", x => - { - _faultReceived = Handled>(x); - }); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.UseRetry(x => x.Immediate(1)); - configurator.UseTransaction(); - - configurator.Consumer(); - } - - - class Consumer : IConsumer - { - public Task Consume(ConsumeContext context) - { - var transactionContext = context.GetPayload(); - - using var ts = new TransactionScope(transactionContext.Transaction); - - throw new IntentionalTestException("Then, you, shall, die!"); - } - } - - - public class Message - { - public Guid Id { get; set; } - } - } -} +namespace MassTransit.Tests.Pipeline +{ + using System; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using System.Transactions; + using NUnit.Framework; + using TestFramework; + using TestFramework.Messages; + + + [TestFixture] + public class When_a_transaction_spans_threads + { + [Test] + public void Should_properly_fail() + { + IPipe pipe = Pipe.New(x => + { + x.UseTransaction(); + x.UseExecute(payload => Console.WriteLine("Execute: {0}", Thread.CurrentThread.ManagedThreadId)); + x.UseExecuteAsync(payload => Task.Run(() => + { + using (var scope = payload.CreateTransactionScope()) + { + Console.WriteLine("ExecuteAsync: {0}", Thread.CurrentThread.ManagedThreadId); + + Assert.That(Transaction.Current, Is.Not.Null); + Console.WriteLine("Isolation Level: {0}", Transaction.Current.IsolationLevel); + } + })); + }); + + var context = new TestConsumeContext(new PingMessage()); + + Assert.That(async () => await pipe.Send(context), Throws.TypeOf()); + } + + [Test] + public void Should_properly_handle_exception() + { + IPipe pipe = Pipe.New(x => + { + x.UseTransaction(); + x.UseExecute(payload => Console.WriteLine("Execute: {0}", Thread.CurrentThread.ManagedThreadId)); + x.UseExecuteAsync(async payload => + { + await Task.Yield(); + + using (var scope = payload.CreateTransactionScope()) + { + Console.WriteLine("ExecuteAsync: {0}", Thread.CurrentThread.ManagedThreadId); + + Assert.That(Transaction.Current, Is.Not.Null); + Console.WriteLine("Isolation Level: {0}", Transaction.Current.IsolationLevel); + + scope.Complete(); + } + + throw new InvalidOperationException("This is a friendly boom"); + }); + x.UseExecute(payload => Console.WriteLine("After Transaction: {0}", Thread.CurrentThread.ManagedThreadId)); + }); + + var context = new TestConsumeContext(new PingMessage()); + + Assert.That(async () => await pipe.Send(context), Throws.InvalidOperationException); + } + + [Test] + public async Task Should_properly_succeed() + { + IPipe pipe = Pipe.New(x => + { + x.UseTransaction(); + x.UseExecute(payload => Console.WriteLine("Execute: {0}", Thread.CurrentThread.ManagedThreadId)); + x.UseExecuteAsync(payload => Task.Run(() => + { + using (var scope = payload.CreateTransactionScope()) + { + Console.WriteLine("ExecuteAsync: {0}", Thread.CurrentThread.ManagedThreadId); + + Assert.That(Transaction.Current, Is.Not.Null); + Console.WriteLine("Isolation Level: {0}", Transaction.Current.IsolationLevel); + scope.Complete(); + } + })); + }); + + var context = new TestConsumeContext(new PingMessage()); + + await pipe.Send(context); + } + + [Test] + public void Should_timeout() + { + IPipe pipe = Pipe.New(x => + { + x.UseTransaction(t => t.Timeout = TimeSpan.FromSeconds(1)); + x.UseExecute(payload => Console.WriteLine("Execute: {0}", Thread.CurrentThread.ManagedThreadId)); + x.UseExecuteAsync(async payload => + { + await Task.Yield(); + + using (var scope = payload.CreateTransactionScope()) + { + Console.WriteLine("ExecuteAsync: {0}", Thread.CurrentThread.ManagedThreadId); + + Assert.That(Transaction.Current, Is.Not.Null); + Console.WriteLine("Isolation Level: {0}", Transaction.Current.IsolationLevel); + + Thread.Sleep(5000); + + scope.Complete(); + } + + Console.WriteLine("Exited Scope"); + }); + x.UseExecute(payload => Console.WriteLine("After Transaction: {0}", Thread.CurrentThread.ManagedThreadId)); + }); + + var context = new TestConsumeContext(new PingMessage()); + + Assert.That(async () => await pipe.Send(context), Throws.TypeOf()); + } + } + + + [TestFixture] + public class When_a_transaction_throws_an_exception_with_retry : + InMemoryTestFixture + { + [Test] + public async Task Should_not_reuse_transaction_context_between_retries() + { + ConsumeContext> fault = await _faultReceived; + + Assert.That(fault.Message.Exceptions.First().ExceptionType, Does.Contain(nameof(IntentionalTestException))); + } + + Task>> _faultReceived; + + [OneTimeSetUp] + public async Task Setup() + { + await InputQueueSendEndpoint.Send(new Message { Id = NewId.NextGuid() }); + } + + protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) + { + configurator.ReceiveEndpoint("errors", x => + { + _faultReceived = Handled>(x); + }); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.UseMessageRetry(x => x.Immediate(1)); + configurator.UseTransaction(); + + configurator.Consumer(); + } + + + class Consumer : IConsumer + { + public Task Consume(ConsumeContext context) + { + var transactionContext = context.GetPayload(); + + using var ts = new TransactionScope(transactionContext.Transaction); + + throw new IntentionalTestException("Then, you, shall, die!"); + } + } + + + public class Message + { + public Guid Id { get; set; } + } + } +} diff --git a/tests/MassTransit.Tests/PollingAlgorithm_Specs.cs b/tests/MassTransit.Tests/PollingAlgorithm_Specs.cs index d4ac935ddb6..c322fce1216 100644 --- a/tests/MassTransit.Tests/PollingAlgorithm_Specs.cs +++ b/tests/MassTransit.Tests/PollingAlgorithm_Specs.cs @@ -87,8 +87,11 @@ async Task ProcessMessage(Message result, CancellationToken cancellationToken) await algorithm.Run(GetMessages, ProcessMessage, GroupMessages, OrderMessages); await algorithm.Run(GetMessages, ProcessMessage, GroupMessages, OrderMessages); - Assert.That(algorithm.ActiveRequestCount, Is.EqualTo(0)); - Assert.That(algorithm.MaxActiveRequestCount, Is.EqualTo(10)); + Assert.Multiple(() => + { + Assert.That(algorithm.ActiveRequestCount, Is.EqualTo(0)); + Assert.That(algorithm.MaxActiveRequestCount, Is.EqualTo(10)); + }); } [Test] @@ -120,9 +123,12 @@ public async Task Should_easily_configure() request = await state.BeginRequest(); await request.Complete(state.ResultLimit); - Assert.That(state.RequestCount, Is.EqualTo(10)); + Assert.Multiple(() => + { + Assert.That(state.RequestCount, Is.EqualTo(10)); - Assert.That(state.ActiveRequestCount, Is.EqualTo(0)); + Assert.That(state.ActiveRequestCount, Is.EqualTo(0)); + }); } [Test] @@ -134,8 +140,11 @@ public void Should_limit_results_to_prefetch_count() RequestResultLimit = 10 }); - Assert.That(algorithm.RequestCount, Is.EqualTo(1)); - Assert.That(algorithm.ResultLimit, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(algorithm.RequestCount, Is.EqualTo(1)); + Assert.That(algorithm.ResultLimit, Is.EqualTo(1)); + }); } [Test] @@ -147,14 +156,20 @@ public async Task Should_one_ever_have_a_single_request() RequestResultLimit = 100 }); - Assert.That(algorithm.RequestCount, Is.EqualTo(1)); - Assert.That(algorithm.ResultLimit, Is.EqualTo(100)); + Assert.Multiple(() => + { + Assert.That(algorithm.RequestCount, Is.EqualTo(1)); + Assert.That(algorithm.ResultLimit, Is.EqualTo(100)); + }); var request = await algorithm.BeginRequest(); await request.Complete(algorithm.ResultLimit); - Assert.That(algorithm.RequestCount, Is.EqualTo(1)); - Assert.That(algorithm.ResultLimit, Is.EqualTo(100)); + Assert.Multiple(() => + { + Assert.That(algorithm.RequestCount, Is.EqualTo(1)); + Assert.That(algorithm.ResultLimit, Is.EqualTo(100)); + }); } [Test] @@ -166,14 +181,20 @@ public async Task Should_one_ever_have_a_single_request_even_when_empty() RequestResultLimit = 100 }); - Assert.That(algorithm.RequestCount, Is.EqualTo(1)); - Assert.That(algorithm.ResultLimit, Is.EqualTo(100)); + Assert.Multiple(() => + { + Assert.That(algorithm.RequestCount, Is.EqualTo(1)); + Assert.That(algorithm.ResultLimit, Is.EqualTo(100)); + }); var request = await algorithm.BeginRequest(); await request.Complete(0); - Assert.That(algorithm.RequestCount, Is.EqualTo(1)); - Assert.That(algorithm.ResultLimit, Is.EqualTo(100)); + Assert.Multiple(() => + { + Assert.That(algorithm.RequestCount, Is.EqualTo(1)); + Assert.That(algorithm.ResultLimit, Is.EqualTo(100)); + }); } diff --git a/tests/MassTransit.Tests/PublishHeader_Specs.cs b/tests/MassTransit.Tests/PublishHeader_Specs.cs index 1e9a1bb70f9..300d471087b 100644 --- a/tests/MassTransit.Tests/PublishHeader_Specs.cs +++ b/tests/MassTransit.Tests/PublishHeader_Specs.cs @@ -2,7 +2,6 @@ { using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -22,7 +21,7 @@ public async Task Should_source_address_from_the_endpoint() ConsumeContext responseContext = await responseHandled; - responseContext.SourceAddress.ShouldBe(InputQueueAddress); + Assert.That(responseContext.SourceAddress, Is.EqualTo(InputQueueAddress)); } Task> _handled; diff --git a/tests/MassTransit.Tests/PublishObserver_Specs.cs b/tests/MassTransit.Tests/PublishObserver_Specs.cs index 5b5b4045a3b..f88f1dad623 100644 --- a/tests/MassTransit.Tests/PublishObserver_Specs.cs +++ b/tests/MassTransit.Tests/PublishObserver_Specs.cs @@ -7,7 +7,6 @@ namespace ObserverTests using System.Threading.Tasks; using Internals; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; using Util; @@ -67,7 +66,7 @@ public async Task Should_not_invoke_post_sent_on_exception() await observer.SendFaulted; - observer.PostSent.Status.ShouldBe(TaskStatus.WaitingForActivation); + Assert.That(observer.PostSent.Status, Is.EqualTo(TaskStatus.WaitingForActivation)); } } diff --git a/tests/MassTransit.Tests/PublishSubscribe_Specs.cs b/tests/MassTransit.Tests/PublishSubscribe_Specs.cs index a6d2113da80..1bafc94ac2b 100644 --- a/tests/MassTransit.Tests/PublishSubscribe_Specs.cs +++ b/tests/MassTransit.Tests/PublishSubscribe_Specs.cs @@ -3,7 +3,6 @@ namespace MassTransit.Tests using System; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -64,8 +63,11 @@ public async Task Should_be_received_properly() ConsumeContext context = await _received; - context.RequestId.HasValue.ShouldBe(true); - context.RequestId.Value.ShouldBe(_requestId); + Assert.Multiple(() => + { + Assert.That(context.RequestId.HasValue, Is.True); + Assert.That(context.RequestId.Value, Is.EqualTo(_requestId)); + }); } Task> _received; @@ -90,8 +92,11 @@ public async Task Should_be_received_properly() ConsumeContext context = await _received; - context.RequestId.HasValue.ShouldBe(true); - context.RequestId.Value.ShouldBe(_requestId); + Assert.Multiple(() => + { + Assert.That(context.RequestId.HasValue, Is.True); + Assert.That(context.RequestId.Value, Is.EqualTo(_requestId)); + }); } Task> _received; @@ -116,7 +121,7 @@ public async Task Should_be_received_properly() ConsumeContext consumeContext = await _received; - consumeContext.RequestId.ShouldBe(_requestId); + Assert.That(consumeContext.RequestId, Is.EqualTo(_requestId)); } Task> _received; @@ -161,8 +166,11 @@ public async Task Should_be_received_properly() ConsumeContext context = await _received; - context.RequestId.HasValue.ShouldBe(true); - context.RequestId.ShouldBe(_requestId); + Assert.Multiple(() => + { + Assert.That(context.RequestId.HasValue, Is.True); + Assert.That(context.RequestId, Is.EqualTo(_requestId)); + }); } Task> _received; diff --git a/tests/MassTransit.Tests/ReceiveObserver_Specs.cs b/tests/MassTransit.Tests/ReceiveObserver_Specs.cs index 5cbdb88e371..67ed0ca6905 100644 --- a/tests/MassTransit.Tests/ReceiveObserver_Specs.cs +++ b/tests/MassTransit.Tests/ReceiveObserver_Specs.cs @@ -24,7 +24,7 @@ public async Task Should_call_the_post_consume_notification() Tuple context = await observer.PostConsumed; - Assert.AreEqual(TypeCache>.ShortName, context.Item2); + Assert.That(context.Item2, Is.EqualTo(TypeCache>.ShortName)); } } @@ -80,7 +80,7 @@ public async Task Should_call_the_post_consume_notification() Tuple context = await observer.PostConsumed; - Assert.AreEqual(TypeCache.ShortName, context.Item2); + Assert.That(context.Item2, Is.EqualTo(TypeCache.ShortName)); } } diff --git a/tests/MassTransit.Tests/RecurringJobConsumer_Specs.cs b/tests/MassTransit.Tests/RecurringJobConsumer_Specs.cs new file mode 100644 index 00000000000..abe40fef94a --- /dev/null +++ b/tests/MassTransit.Tests/RecurringJobConsumer_Specs.cs @@ -0,0 +1,181 @@ +namespace MassTransit.Tests; + +using System; +using System.Threading.Tasks; +using Contracts.JobService; +using MassTransit.Testing; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NUnit.Framework; + + +[TestFixture] +public class Configuring_a_recurring_job_consumer +{ + [Test] + public async Task Should_support_configuration_of_the_job() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer(); + + x.AddJobSagaStateMachines(options => options.SlotWaitTime = TimeSpan.FromSeconds(1)); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(15)); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var client = harness.GetRequestClient>(); + + var jobId = await client.AddOrUpdateRecurringJob(nameof(RecurringJobConsumer), new RecurringJobMessage(), x => x.Every(seconds: 5), + harness.CancellationToken); + + Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); + + await harness.Stop(); + } + + [Test] + public async Task Should_support_reconfiguration_of_the_job() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer(); + + x.AddJobSagaStateMachines(options => options.SlotWaitTime = TimeSpan.FromSeconds(1)); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(15), testTimeout: TimeSpan.FromSeconds(30)); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var client = harness.GetRequestClient>(); + + var jobId = await client.AddOrUpdateRecurringJob(nameof(RecurringJobConsumer), new RecurringJobMessage(), "*/5 * * * * ?", + harness.CancellationToken); + + Assert.That(await harness.Published.SelectAsync>().Take(1).Count(), Is.EqualTo(1)); + + await client.AddOrUpdateRecurringJob(nameof(RecurringJobConsumer), new RecurringJobMessage(), "*/10 * * * * ?", + harness.CancellationToken); + + Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); + + await harness.Stop(); + } + + [Test] + public async Task Should_support_reconfiguration_of_the_job_with_no_changes() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer(); + + x.AddJobSagaStateMachines(options => options.SlotWaitTime = TimeSpan.FromSeconds(1)); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(15)); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var client = harness.GetRequestClient>(); + + var jobId = await client.AddOrUpdateRecurringJob(nameof(RecurringJobConsumer), new RecurringJobMessage(), "*/5 * * * * ?", + harness.CancellationToken); + + Assert.That(await harness.Published.SelectAsync>().Take(1).Count(), Is.EqualTo(1)); + + await client.AddOrUpdateRecurringJob(nameof(RecurringJobConsumer), new RecurringJobMessage(), "*/5 * * * * ?", + harness.CancellationToken); + + Assert.That(await harness.Published.SelectAsync>().Take(2).Count(), Is.EqualTo(2)); + + await harness.Stop(); + } + + [Test] + public async Task Should_support_scheduling_a_single_run_job() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetKebabCaseEndpointNameFormatter(); + + x.AddConsumer(); + + x.AddJobSagaStateMachines(options => options.SlotWaitTime = TimeSpan.FromSeconds(1)); + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(15)); + + x.UsingInMemory((context, cfg) => + { + cfg.UseDelayedMessageScheduler(); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var client = harness.GetRequestClient>(); + + var jobId = await client.ScheduleJob(DateTimeOffset.UtcNow.AddSeconds(3), new RecurringJobMessage(), harness.CancellationToken); + + Assert.That(await harness.Published.Any>(), Is.True); + + await harness.Stop(); + } + + + public record RecurringJobConsumer : + IJobConsumer + { + readonly ILogger _logger; + + public RecurringJobConsumer(ILogger logger) + { + _logger = logger; + } + + public Task Run(JobContext context) + { + _logger.LogInformation("Every minute"); + + return Task.CompletedTask; + } + } +} + + +public record RecurringJobMessage; diff --git a/tests/MassTransit.Tests/RecurringSchedule_Specs.cs b/tests/MassTransit.Tests/RecurringSchedule_Specs.cs new file mode 100644 index 00000000000..8ab10084032 --- /dev/null +++ b/tests/MassTransit.Tests/RecurringSchedule_Specs.cs @@ -0,0 +1,44 @@ +namespace MassTransit.Tests; + +using JobService.Messages; +using NUnit.Framework; + + +public class RecurringSchedule_Specs +{ + [Test] + public void Should_return_every_five_seconds() + { + var info = new RecurringJobScheduleInfo(); + info.Every(seconds: 5); + + Assert.That(info.CronExpression, Is.EqualTo("0/5 * * * * *")); + } + + [Test] + public void Should_return_every_five_seconds_from_noon_until_one() + { + var info = new RecurringJobScheduleInfo(); + info.Every(seconds: 5, hour: 12); + + Assert.That(info.CronExpression, Is.EqualTo("0/5 * 12 * * *")); + } + + [Test] + public void Should_return_every_five_seconds_from_noon_until_one_ish() + { + var info = new RecurringJobScheduleInfo(); + info.Every(seconds: 5, hour: 12, minute: 15); + + Assert.That(info.CronExpression, Is.EqualTo("0/5 15 12 * * *")); + } + + [Test] + public void Should_return_every_ten_minutes() + { + var info = new RecurringJobScheduleInfo(); + info.Every(minutes: 10); + + Assert.That(info.CronExpression, Is.EqualTo("0 0/10 * * * *")); + } +} diff --git a/tests/MassTransit.Tests/RedeliveryHeader_Specs.cs b/tests/MassTransit.Tests/RedeliveryHeader_Specs.cs new file mode 100644 index 00000000000..ced3840b843 --- /dev/null +++ b/tests/MassTransit.Tests/RedeliveryHeader_Specs.cs @@ -0,0 +1,61 @@ +namespace MassTransit.Tests +{ + using System.Threading.Tasks; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using Middleware.Caching; + using NUnit.Framework; + + + [TestFixture] + public class When_the_redelivery_header_is_present + { + [Test] + public async Task Should_not_exist_on_outgoing_messages() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddHandler(async (ConsumeContext context) => + { + if (context.GetRedeliveryCount() == 1) + { + await context.Publish(new OutboundMessage()); + return; + } + + throw new TestException("Ouch!"); + }); + + x.AddHandler(async (ConsumeContext context) => + { + }); + + x.AddConfigureEndpointsCallback((context, name, cfg) => + { + cfg.UseDelayedRedelivery(r => r.Interval(10, 100)); + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new InboundMessage()); + + IReceivedMessage message = await harness.Consumed.SelectAsync().Take(1).FirstOrDefault(); + Assert.That(message, Is.Not.Null); + + Assert.That(message.Context.GetHeader(MessageHeaders.RedeliveryCount, default(int?)), Is.Null); + } + + + class InboundMessage + { + } + + + class OutboundMessage + { + } + } +} diff --git a/tests/MassTransit.Tests/ReliableMessaging/InboxLock_Specs.cs b/tests/MassTransit.Tests/ReliableMessaging/InboxLock_Specs.cs index dbb42458ce5..35304e82a4c 100644 --- a/tests/MassTransit.Tests/ReliableMessaging/InboxLock_Specs.cs +++ b/tests/MassTransit.Tests/ReliableMessaging/InboxLock_Specs.cs @@ -52,7 +52,9 @@ public static IServiceCollection AddInboxLockInMemoryTestHarness(this IServiceCo .AddInMemoryInboxOutbox() .AddMassTransitTestHarness(x => { - x.AddHandler(async (Event message) => {}); + x.AddHandler(async (Event message) => + { + }); x.AddConsumer(); }); @@ -65,7 +67,6 @@ public static IServiceCollection AddInboxLockInMemoryTestHarness(this IServiceCo namespace InboxLock { - using System; using System.Linq; @@ -87,19 +88,12 @@ await Task.WhenAll(Enumerable.Range(0, 100).Select(index => public class InboxLockInMemoryConsumerDefinition : ConsumerDefinition { - readonly IServiceProvider _provider; - - public InboxLockInMemoryConsumerDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(10, 50, 100, 100, 100, 100, 100, 100)); - endpointConfigurator.UseInMemoryInboxOutbox(_provider); + endpointConfigurator.UseInMemoryInboxOutbox(context); } } } diff --git a/tests/MassTransit.Tests/ReliableMessaging/ReliableConsumer.cs b/tests/MassTransit.Tests/ReliableMessaging/ReliableConsumer.cs index 4c952a1c369..27ed0a71907 100644 --- a/tests/MassTransit.Tests/ReliableMessaging/ReliableConsumer.cs +++ b/tests/MassTransit.Tests/ReliableMessaging/ReliableConsumer.cs @@ -75,7 +75,8 @@ public class ReliableEventConsumerDefinition : ConsumerDefinition { protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, + IRegistrationContext context) { if (endpointConfigurator is IInMemoryReceiveEndpointConfigurator configurator) { diff --git a/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemoryConsumerDefinition.cs b/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemoryConsumerDefinition.cs index 96109237d34..3e1ffa5e82e 100644 --- a/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemoryConsumerDefinition.cs +++ b/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemoryConsumerDefinition.cs @@ -1,24 +1,14 @@ namespace MassTransit.Tests.ReliableMessaging { - using System; - - public class ReliableInMemoryConsumerDefinition : ConsumerDefinition { - readonly IServiceProvider _provider; - - public ReliableInMemoryConsumerDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureConsumer(IReceiveEndpointConfigurator endpointConfigurator, - IConsumerConfigurator consumerConfigurator) + IConsumerConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(10, 50, 100, 100, 100, 100, 100, 100)); - endpointConfigurator.UseInMemoryInboxOutbox(_provider); + endpointConfigurator.UseInMemoryInboxOutbox(context); } } } diff --git a/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemoryStateDefinition.cs b/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemoryStateDefinition.cs index 89a3abcf7e9..aaee32d41ec 100644 --- a/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemoryStateDefinition.cs +++ b/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemoryStateDefinition.cs @@ -1,25 +1,15 @@ namespace MassTransit.Tests.ReliableMessaging { - using System; - - public class ReliableInMemoryStateDefinition : SagaDefinition { - readonly IServiceProvider _provider; - - public ReliableInMemoryStateDefinition(IServiceProvider provider) - { - _provider = provider; - } - protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, - ISagaConfigurator consumerConfigurator) + ISagaConfigurator consumerConfigurator, IRegistrationContext context) { endpointConfigurator.UseMessageRetry(r => r.Intervals(10, 50, 100, 100, 100, 100, 100, 100)); - endpointConfigurator.UseMessageScope(_provider); - endpointConfigurator.UseInMemoryInboxOutbox(_provider); + endpointConfigurator.UseMessageScope(context); + endpointConfigurator.UseInMemoryInboxOutbox(context); } } } diff --git a/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemory_Specs.cs b/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemory_Specs.cs index 9c1e5011a5c..abf307dc423 100644 --- a/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemory_Specs.cs +++ b/tests/MassTransit.Tests/ReliableMessaging/ReliableInMemory_Specs.cs @@ -92,9 +92,12 @@ public async Task Should_handle_the_saga_successfully() ISagaStateMachineTestHarness? sagaHarness = harness.GetSagaStateMachineHarness(); - Assert.That(await sagaHarness.Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any(), Is.True); - Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + }); } [Test] @@ -121,9 +124,12 @@ await harness.Bus.Publish(new CreateState ISagaStateMachineTestHarness? sagaHarness = harness.GetSagaStateMachineHarness(); - Assert.That(await sagaHarness.Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any(), Is.True); - Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + }); } [Test] @@ -150,9 +156,12 @@ await harness.Bus.Publish(new CreateState ISagaStateMachineTestHarness? sagaHarness = harness.GetSagaStateMachineHarness(); - Assert.That(await sagaHarness.Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any(), Is.True); - Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + Assert.That(await sagaHarness.Exists(sagaId, x => x.Verified), Is.Not.Null); + }); } [Test] diff --git a/tests/MassTransit.Tests/RequestClientNew_Specs.cs b/tests/MassTransit.Tests/RequestClientNew_Specs.cs index 56786bbdbdd..3b8b7d64768 100644 --- a/tests/MassTransit.Tests/RequestClientNew_Specs.cs +++ b/tests/MassTransit.Tests/RequestClientNew_Specs.cs @@ -38,9 +38,12 @@ public async Task Should_be_awesome_with_a_side_of_sourdough_toast() response = await request.GetResponse(); } - Assert.That(response.RequestId.HasValue, Is.True); - Assert.That(response.Headers.TryGetHeader("Frank", out var value), Is.True); - Assert.That(value, Is.EqualTo("Mary")); + Assert.Multiple(() => + { + Assert.That(response.RequestId.HasValue, Is.True); + Assert.That(response.Headers.TryGetHeader("Frank", out var value), Is.True); + Assert.That(value, Is.EqualTo("Mary")); + }); } [Test] @@ -104,9 +107,12 @@ public async Task Should_throw_the_request_exception_including_fault() } catch (RequestFaultException exception) { - Assert.That(exception.Fault.Exceptions.First().ExceptionType, Is.EqualTo(TypeCache.ShortName)); - Assert.That(exception.RequestType, Is.EqualTo(TypeCache.ShortName)); - Assert.That(exception.Fault.FaultMessageTypes, Is.EqualTo(MessageTypeCache.MessageTypeNames)); + Assert.Multiple(() => + { + Assert.That(exception.Fault.Exceptions.First().ExceptionType, Is.EqualTo(TypeCache.ShortName)); + Assert.That(exception.RequestType, Is.EqualTo(TypeCache.ShortName)); + Assert.That(exception.Fault.FaultMessageTypes, Is.EqualTo(MessageTypeCache.MessageTypeNames)); + }); } catch { @@ -163,9 +169,12 @@ public async Task Should_match_the_first_result() Response response = await client.GetResponse(new RegisterMember()); - Assert.That(response.Is(out Response _), Is.True, "Should have been registered"); + Assert.Multiple(() => + { + Assert.That(response.Is(out Response _), Is.True, "Should have been registered"); - Assert.That(response.Is(out Response _), Is.False, "Should not have been an existing member"); + Assert.That(response.Is(out Response _), Is.False, "Should not have been an existing member"); + }); } [Test] @@ -176,9 +185,12 @@ public async Task Should_match_the_second_result() Response response = await client.GetResponse(new RegisterMember { MemberId = "Johnny5" }); - Assert.That(response.Is(out Response _), Is.False, "Should not have been registered"); + Assert.Multiple(() => + { + Assert.That(response.Is(out Response _), Is.False, "Should not have been registered"); - Assert.That(response.Is(out Response _), Is.True, "Should have been an existing member"); + Assert.That(response.Is(out Response _), Is.True, "Should have been an existing member"); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/RequestClient_Specs.cs b/tests/MassTransit.Tests/RequestClient_Specs.cs index 61ddf6d0bd5..2cae7852f06 100644 --- a/tests/MassTransit.Tests/RequestClient_Specs.cs +++ b/tests/MassTransit.Tests/RequestClient_Specs.cs @@ -7,7 +7,6 @@ using MassTransit.Testing; using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -21,7 +20,7 @@ public async Task Should_receive_the_response() { Response message = await _response; - message.Message.CorrelationId.ShouldBe(_ping.Result.Message.CorrelationId); + Assert.That(message.Message.CorrelationId, Is.EqualTo(_ping.Result.Message.CorrelationId)); } Task> _ping; @@ -87,26 +86,116 @@ public async Task Should_timeout_without_exceptions() unobservedTaskExceptions.Add(eventArgs.Exception); }; - Assert.That(async () => await _response, Throws.TypeOf()); + IRequestClient requestClient = Bus.CreateRequestClient(InputQueueAddress, TimeSpan.FromSeconds(1)); + + Task> response = requestClient.GetResponse(new PingMessage()); + + Assert.That(async () => await response, Throws.TypeOf()); GC.Collect(); await Task.Delay(1000); GC.WaitForPendingFinalizers(); GC.Collect(); - Assert.That(unhandledExceptions, Is.Empty); - Assert.That(unobservedTaskExceptions, Is.Empty); + Assert.Multiple(() => + { + Assert.That(unhandledExceptions, Is.Empty); + Assert.That(unobservedTaskExceptions, Is.Empty); + }); } + } - Task> _response; - IRequestClient _requestClient; - [OneTimeSetUp] - public void Setup() + [TestFixture] + [Explicit] + public class Sending_a_request_using_mediator_to_a_missing_service_that_times_out : + InMemoryTestFixture + { + [Test] + public async Task Should_timeout_without_exceptions() { - _requestClient = Bus.CreateRequestClient(InputQueueAddress, TimeSpan.FromSeconds(1)); + var mediator = MassTransit.Bus.Factory.CreateMediator(x => + { + x.Handler(async context => + { + }); + }); - _response = _requestClient.GetResponse(new PingMessage()); + List unhandledExceptions = new List(); + List unobservedTaskExceptions = new List(); + + AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => + { + unhandledExceptions.Add(eventArgs.ExceptionObject); + }; + + TaskScheduler.UnobservedTaskException += (sender, eventArgs) => + { + unobservedTaskExceptions.Add(eventArgs.Exception); + }; + + IRequestClient requestClient = mediator.CreateRequestClient(InputQueueAddress, TimeSpan.FromSeconds(1)); + + Task> response = requestClient.GetResponse(new PingMessage()); + + Assert.That(async () => await response, Throws.TypeOf()); + + GC.Collect(); + await Task.Delay(1000); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.Multiple(() => + { + Assert.That(unhandledExceptions, Is.Empty, "Unhandled"); + Assert.That(unobservedTaskExceptions, Is.Empty, "Unobserved"); + }); + } + } + + + [TestFixture] + [Explicit] + public class Sending_a_request_using_mediator_that_faults : + InMemoryTestFixture + { + [Test] + public async Task Should_observe_all_exceptions() + { + var mediator = MassTransit.Bus.Factory.CreateMediator(x => + { + }); + + List unhandledExceptions = new List(); + List unobservedTaskExceptions = new List(); + + AppDomain.CurrentDomain.UnhandledException += (sender, eventArgs) => + { + unhandledExceptions.Add(eventArgs.ExceptionObject); + }; + + TaskScheduler.UnobservedTaskException += (sender, eventArgs) => + { + eventArgs.SetObserved(); + unobservedTaskExceptions.Add(eventArgs.Exception); + }; + + IRequestClient requestClient = mediator.CreateRequestClient(InputQueueAddress, TimeSpan.FromSeconds(1)); + + Task> response = requestClient.GetResponse(new PingMessage()); + + Assert.That(async () => await response, Throws.TypeOf()); + + GC.Collect(); + await Task.Delay(1000); + GC.WaitForPendingFinalizers(); + GC.Collect(); + + Assert.Multiple(() => + { + Assert.That(unhandledExceptions, Is.Empty, "Unhandled"); + Assert.That(unobservedTaskExceptions, Is.Empty, "Unobserved"); + }); } } diff --git a/tests/MassTransit.Tests/Retry_Specs.cs b/tests/MassTransit.Tests/Retry_Specs.cs index 9c73f629d94..4f74045bb76 100644 --- a/tests/MassTransit.Tests/Retry_Specs.cs +++ b/tests/MassTransit.Tests/Retry_Specs.cs @@ -4,7 +4,6 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; using Util; @@ -27,14 +26,14 @@ await InputQueueSendEndpoint.Send(new PingMessage(), context => await fault; - _attempts.ShouldBe(1); + Assert.That(_attempts, Is.EqualTo(1)); } int _attempts; protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { - configurator.UseRetry(x => x.None()); + configurator.UseMessageRetry(x => x.None()); Handler(configurator, async context => { @@ -64,7 +63,7 @@ await InputQueueSendEndpoint.Send(new PingMessage(), context => await fault; - _attempts.ShouldBe(1); + Assert.That(_attempts, Is.EqualTo(1)); } int _attempts; @@ -99,14 +98,14 @@ await InputQueueSendEndpoint.Send(new PingMessage(), context => await fault; - Consumer.Attempts.ShouldBe(6); + Assert.That(Consumer.Attempts, Is.EqualTo(6)); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { configurator.Consumer(() => new Consumer(), x => { - x.UseRetry(r => r.Immediate(5)); + x.UseMessageRetry(r => r.Immediate(5)); }); } @@ -143,14 +142,14 @@ await InputQueueSendEndpoint.Send(new PingMessage(), context => await fault; - _attempts.ShouldBe(2); + Assert.That(_attempts, Is.EqualTo(2)); } int _attempts; protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { - configurator.UseRetry(x => x.Immediate(1)); + configurator.UseMessageRetry(x => x.Immediate(1)); base.ConfigureInMemoryBus(configurator); } @@ -183,14 +182,14 @@ await InputQueueSendEndpoint.Send(new BaseMessage(), context => await fault; - _attempts.ShouldBe(2); + Assert.That(_attempts, Is.EqualTo(2)); } int _attempts; protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { - configurator.UseRetry(x => x.Immediate(1)); + configurator.UseMessageRetry(x => x.Immediate(1)); base.ConfigureInMemoryBus(configurator); } @@ -234,10 +233,13 @@ await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute + { + Assert.That(_attempts, Is.EqualTo(4)); - _lastCount.ShouldBe(2); - _lastAttempt.ShouldBe(3); + Assert.That(_lastCount, Is.EqualTo(2)); + Assert.That(_lastAttempt, Is.EqualTo(3)); + }); } int _attempts; @@ -246,14 +248,14 @@ await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute x.Immediate(1)); + configurator.UseMessageRetry(x => x.Immediate(1)); base.ConfigureInMemoryBus(configurator); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { - configurator.UseRetry(x => x.Immediate(3)); + configurator.UseMessageRetry(x => x.Immediate(3)); Handler(configurator, async context => { Interlocked.Increment(ref _attempts); @@ -284,10 +286,13 @@ await InputQueueSendEndpoint.Send(new YourMessage { Text = "Hi" }, Pipe.Execute< await fault; - _attempts.ShouldBe(6); + Assert.Multiple(() => + { + Assert.That(_attempts, Is.EqualTo(6)); - _lastCount.ShouldBe(4); - _lastAttempt.ShouldBe(5); + Assert.That(_lastCount, Is.EqualTo(4)); + Assert.That(_lastAttempt, Is.EqualTo(5)); + }); } static int _attempts; @@ -327,27 +332,27 @@ class YourMessageConsumer : IConsumer, IConsumer> { - public Task Consume(ConsumeContext context) + public Task Consume(ConsumeContext> context) { - Interlocked.Increment(ref _attempts); + var faultRetryCount = context.Headers.Get(MessageHeaders.FaultRetryCount, default(int?)) ?? 0; - _lastAttempt = context.GetRetryAttempt(); - _lastCount = context.GetRetryCount(); + TestContext.Out.WriteLine($"Attempt (from fault consumer): {faultRetryCount}"); - TestContext.Out.WriteLine($"Attempt: {context.GetRetryAttempt()}"); + TestContext.Out.WriteLine(@"Faulted, won't retry any more."); - throw new Exception("Big bad exception"); + return Task.CompletedTask; } - public Task Consume(ConsumeContext> context) + public Task Consume(ConsumeContext context) { - var faultRetryCount = context.Headers.Get(MessageHeaders.FaultRetryCount, default(int?)) ?? 0; + Interlocked.Increment(ref _attempts); - TestContext.Out.WriteLine($"Attempt (from fault consumer): {faultRetryCount}"); + _lastAttempt = context.GetRetryAttempt(); + _lastCount = context.GetRetryCount(); - TestContext.Out.WriteLine(@"Faulted, won't retry any more."); + TestContext.Out.WriteLine($"Attempt: {context.GetRetryAttempt()}"); - return Task.CompletedTask; + throw new Exception("Big bad exception"); } } } @@ -370,17 +375,20 @@ await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute + { + Assert.That(Consumer.Attempts, Is.EqualTo(4)); - Consumer.LastCount.ShouldBe(2); - Consumer.LastAttempt.ShouldBe(3); + Assert.That(Consumer.LastCount, Is.EqualTo(2)); + Assert.That(Consumer.LastAttempt, Is.EqualTo(3)); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { configurator.Consumer(cfg => { - cfg.UseRetry(x => x.Immediate(3)); + cfg.UseMessageRetry(x => x.Immediate(3)); }); } @@ -424,10 +432,13 @@ await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute + { + Assert.That(_attempts, Is.EqualTo(1)); - _lastAttempt.ShouldBe(0); - _lastCount.ShouldBe(0); + Assert.That(_lastAttempt, Is.EqualTo(0)); + Assert.That(_lastCount, Is.EqualTo(0)); + }); } int _attempts; @@ -436,7 +447,7 @@ await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute + configurator.UseMessageRetry(x => { x.Ignore(); x.Immediate(1); @@ -477,9 +488,12 @@ await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute + { + Assert.That(_attempts, Is.EqualTo(1)); - _lastAttempt.ShouldBe(0); + Assert.That(_lastAttempt, Is.EqualTo(0)); + }); } int _attempts; @@ -487,7 +501,7 @@ await InputQueueSendEndpoint.Send(new PingMessage(), Pipe.Execute + configurator.UseMessageRetry(x => { x.Ignore(); x.Immediate(1); @@ -523,7 +537,7 @@ public async Task After_try_trying_again() await Task.Delay(100); - _attempts.ShouldBe(2); + Assert.That(_attempts, Is.EqualTo(2)); } int _attempts; @@ -531,7 +545,7 @@ public async Task After_try_trying_again() protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { - configurator.UseRetry(x => x.Immediate(1)); + configurator.UseMessageRetry(x => x.Immediate(1)); base.ConfigureInMemoryBus(configurator); } @@ -572,11 +586,14 @@ public async Task Should_call_the_observer() var payload = await _payload.Task; - Assert.That(payload.PostCreateCount, Is.EqualTo(0), "PostCreateCount"); - Assert.That(payload.RetryCompletedCount, Is.EqualTo(1), "RetryCompletedCount"); - Assert.That(payload.PreRetryCount, Is.EqualTo(1), "PreRetryCount"); - Assert.That(payload.PostFaultCount, Is.EqualTo(1), "PostFaultCount"); - Assert.That(payload.RetryFaultCount, Is.EqualTo(0), "RetryFaultCount"); + Assert.Multiple(() => + { + Assert.That(payload.PostCreateCount, Is.EqualTo(0), "PostCreateCount"); + Assert.That(payload.RetryCompletedCount, Is.EqualTo(1), "RetryCompletedCount"); + Assert.That(payload.PreRetryCount, Is.EqualTo(1), "PreRetryCount"); + Assert.That(payload.PostFaultCount, Is.EqualTo(1), "PostFaultCount"); + Assert.That(payload.RetryFaultCount, Is.EqualTo(0), "RetryFaultCount"); + }); } int _attempts; @@ -588,7 +605,7 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con { _observed = GetTask(); _payload = GetTask(); - configurator.UseRetry(x => + configurator.UseMessageRetry(x => { x.Immediate(5); x.ConnectRetryObserver(new RetryObserver(_observed, _payload)); @@ -705,7 +722,7 @@ public async Task Should_cancel_the_retry_and_give_it_up() await Task.Delay(100); - _attempts.ShouldBe(1); + Assert.That(_attempts, Is.EqualTo(1)); } int _attempts; @@ -716,7 +733,7 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con { _retryObserver = new RetryObserver(); - configurator.UseRetry(x => + configurator.UseMessageRetry(x => { x.Interval(1, TimeSpan.FromMinutes(1)); x.ConnectRetryObserver(_retryObserver); diff --git a/tests/MassTransit.Tests/Saga/InitiateSaga_Specs.cs b/tests/MassTransit.Tests/Saga/InitiateSaga_Specs.cs index fb773c84086..4be78a4c581 100644 --- a/tests/MassTransit.Tests/Saga/InitiateSaga_Specs.cs +++ b/tests/MassTransit.Tests/Saga/InitiateSaga_Specs.cs @@ -5,7 +5,6 @@ namespace MassTransit.Tests.Saga using MassTransit.Testing; using Messages; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -22,7 +21,7 @@ public async Task The_saga_should_be_created_when_an_initiating_message_is_recei Guid? sagaId = await _repository.ShouldContainSaga(_sagaId, TestTimeout); - sagaId.HasValue.ShouldBe(true); + Assert.That(sagaId.HasValue, Is.True); } public When_an_initiating_message_for_a_saga_arrives() @@ -40,17 +39,17 @@ protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator con { base.ConfigureInMemoryBus(configurator); - configurator.UseRetry(x => x.None()); + configurator.UseMessageRetry(x => x.None()); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { - configurator.UseRetry(x => x.Immediate(2)); + configurator.UseMessageRetry(x => x.Immediate(2)); configurator.Saga(_repository); } Guid _sagaId; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; } @@ -69,7 +68,7 @@ public async Task The_message_should_fault() Guid? sagaId = await _repository.ShouldContainSaga(_sagaId, TestTimeout); - sagaId.HasValue.ShouldBe(true); + Assert.That(sagaId.HasValue, Is.True); await InputQueueSendEndpoint.Send(message); @@ -93,7 +92,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } Guid _sagaId; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; } @@ -112,7 +111,7 @@ public async Task The_saga_should_be_loaded() sagaId = await _repository.ShouldContainSaga(x => x.Completed, TestTimeout); - sagaId.HasValue.ShouldBe(true); + Assert.That(sagaId.HasValue, Is.True); } public When_an_initiating_and_orchestrated_message_for_a_saga_arrives() @@ -132,7 +131,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } Guid _sagaId; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; } @@ -151,7 +150,7 @@ public async Task The_saga_should_be_loaded() sagaId = await _repository.ShouldContainSaga(x => x.Observed, TestTimeout); - sagaId.HasValue.ShouldBe(true); + Assert.That(sagaId.HasValue, Is.True); } public When_an_initiating_and_observed_message_for_a_saga_arrives() @@ -171,7 +170,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } Guid _sagaId; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; } @@ -193,7 +192,7 @@ public async Task An_exception_should_be_thrown() } catch (SagaException sex) { - Assert.AreEqual(sex.MessageType, typeof(InitiateSimpleSaga)); + Assert.That(sex.MessageType, Is.EqualTo(typeof(InitiateSimpleSaga))); } } diff --git a/tests/MassTransit.Tests/Saga/Injecting_Specs.cs b/tests/MassTransit.Tests/Saga/Injecting_Specs.cs index cfb72378f53..eb39534fbdc 100644 --- a/tests/MassTransit.Tests/Saga/Injecting_Specs.cs +++ b/tests/MassTransit.Tests/Saga/Injecting_Specs.cs @@ -5,7 +5,6 @@ using MassTransit.Testing; using Messages; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -22,7 +21,7 @@ public async Task The_saga_should_be_created_when_an_initiating_message_is_recei Guid? sagaId = await _repository.ShouldContainSaga(_sagaId, TestTimeout); - sagaId.HasValue.ShouldBe(true); + Assert.That(sagaId.HasValue, Is.True); } ISagaRepository _repository; diff --git a/tests/MassTransit.Tests/Saga/Locator/SagaExpression_Specs.cs b/tests/MassTransit.Tests/Saga/Locator/SagaExpression_Specs.cs index 82c223fa5e6..eca576edf97 100644 --- a/tests/MassTransit.Tests/Saga/Locator/SagaExpression_Specs.cs +++ b/tests/MassTransit.Tests/Saga/Locator/SagaExpression_Specs.cs @@ -1,96 +1,96 @@ -namespace MassTransit.Tests.Saga.Locator -{ - using System; - using System.Diagnostics; - using System.Linq.Expressions; - using System.Threading.Tasks; - using MassTransit.Saga; - using MassTransit.Testing; - using Messages; - using NUnit.Framework; - using TestFramework; - - - [TestFixture] - public class SagaExpression_Specs : - InMemoryTestFixture - { - [Test] - public async Task Matching_by_property_should_be_happy() - { - Expression> selector = (s, m) => s.Name == m.Name; - - Expression> filter = - new SagaFilterExpressionConverter(_observeSaga).Convert(selector); - Trace.WriteLine(filter.ToString()); - - Guid? matches = await _repository.ShouldContainSaga(filter, TestTimeout); - - Assert.IsTrue(matches.HasValue); - } - - [Test] - public async Task The_saga_expression_should_be_converted_down_to_a_saga_only_filter() - { - Expression> selector = - (s, m) => s.CorrelationId == m.CorrelationId; - - Expression> filter = - new SagaFilterExpressionConverter(_initiateSaga).Convert(selector); - Trace.WriteLine(filter.ToString()); - - Guid? matches = await _repository.ShouldContainSaga(filter, TestTimeout); - - Assert.IsTrue(matches.HasValue); - } - - public SagaExpression_Specs() - { - _repository = new InMemorySagaRepository(); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - configurator.Saga(_repository); - } - - [OneTimeSetUp] - public void Setup() - { - _sagaId = NewId.NextGuid(); - _initiateSaga = new InitiateSimpleSaga - { - CorrelationId = _sagaId, - Name = "Chris" - }; - - InputQueueSendEndpoint.Send(_initiateSaga) - .Wait(TestCancellationToken); - - _repository.ShouldContainSaga(_sagaId, TestTimeout) - .Wait(TestCancellationToken); - - _otherSagaId = Guid.NewGuid(); - _initiateOtherSaga = new InitiateSimpleSaga - { - CorrelationId = _otherSagaId, - Name = "Dru" - }; - - InputQueueSendEndpoint.Send(_initiateOtherSaga) - .Wait(TestCancellationToken); - - _repository.ShouldContainSaga(_otherSagaId, TestTimeout) - .Wait(TestCancellationToken); - - _observeSaga = new ObservableSagaMessage {Name = "Chris"}; - } - - Guid _sagaId; - InitiateSimpleSaga _initiateSaga; - readonly InMemorySagaRepository _repository; - Guid _otherSagaId; - ObservableSagaMessage _observeSaga; - InitiateSimpleSaga _initiateOtherSaga; - } -} +namespace MassTransit.Tests.Saga.Locator +{ + using System; + using System.Diagnostics; + using System.Linq.Expressions; + using System.Threading.Tasks; + using MassTransit.Saga; + using MassTransit.Testing; + using Messages; + using NUnit.Framework; + using TestFramework; + + + [TestFixture] + public class SagaExpression_Specs : + InMemoryTestFixture + { + [Test] + public async Task Matching_by_property_should_be_happy() + { + Expression> selector = (s, m) => s.Name == m.Name; + + Expression> filter = + new SagaFilterExpressionConverter(_observeSaga).Convert(selector); + Trace.WriteLine(filter.ToString()); + + Guid? matches = await _repository.ShouldContainSaga(filter, TestTimeout); + + Assert.That(matches.HasValue, Is.True); + } + + [Test] + public async Task The_saga_expression_should_be_converted_down_to_a_saga_only_filter() + { + Expression> selector = + (s, m) => s.CorrelationId == m.CorrelationId; + + Expression> filter = + new SagaFilterExpressionConverter(_initiateSaga).Convert(selector); + Trace.WriteLine(filter.ToString()); + + Guid? matches = await _repository.ShouldContainSaga(filter, TestTimeout); + + Assert.That(matches.HasValue, Is.True); + } + + public SagaExpression_Specs() + { + _repository = new InMemorySagaRepository(); + } + + protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) + { + configurator.Saga(_repository); + } + + [OneTimeSetUp] + public void Setup() + { + _sagaId = NewId.NextGuid(); + _initiateSaga = new InitiateSimpleSaga + { + CorrelationId = _sagaId, + Name = "Chris" + }; + + InputQueueSendEndpoint.Send(_initiateSaga) + .Wait(TestCancellationToken); + + _repository.ShouldContainSaga(_sagaId, TestTimeout) + .Wait(TestCancellationToken); + + _otherSagaId = Guid.NewGuid(); + _initiateOtherSaga = new InitiateSimpleSaga + { + CorrelationId = _otherSagaId, + Name = "Dru" + }; + + InputQueueSendEndpoint.Send(_initiateOtherSaga) + .Wait(TestCancellationToken); + + _repository.ShouldContainSaga(_otherSagaId, TestTimeout) + .Wait(TestCancellationToken); + + _observeSaga = new ObservableSagaMessage { Name = "Chris" }; + } + + Guid _sagaId; + InitiateSimpleSaga _initiateSaga; + readonly ISagaRepository _repository; + Guid _otherSagaId; + ObservableSagaMessage _observeSaga; + InitiateSimpleSaga _initiateOtherSaga; + } +} diff --git a/tests/MassTransit.Tests/Saga/NewOrExisting_Specs.cs b/tests/MassTransit.Tests/Saga/NewOrExisting_Specs.cs index 338ded519c8..7cbb463b911 100644 --- a/tests/MassTransit.Tests/Saga/NewOrExisting_Specs.cs +++ b/tests/MassTransit.Tests/Saga/NewOrExisting_Specs.cs @@ -21,9 +21,12 @@ public async Task Should_initiate() await InputQueueSendEndpoint.Send(message); var saga = _sagaHarness.Sagas.Contains(sagaId); - Assert.That(saga, Is.Not.Null); + await Assert.MultipleAsync(async () => + { + Assert.That(saga, Is.Not.Null); - Assert.That(await _sagaHarness.Consumed.Any()); + Assert.That(await _sagaHarness.Consumed.Any()); + }); } [Test] @@ -36,9 +39,12 @@ public async Task Should_orchestrate() await InputQueueSendEndpoint.Send(message); var saga = _sagaHarness.Sagas.Contains(sagaId); - Assert.That(saga, Is.Not.Null); + await Assert.MultipleAsync(async () => + { + Assert.That(saga, Is.Not.Null); - Assert.That(await _sagaHarness.Consumed.Any()); + Assert.That(await _sagaHarness.Consumed.Any()); + }); await InputQueueSendEndpoint.Send(new EventMessage(sagaId)); diff --git a/tests/MassTransit.Tests/Saga/PartitionSaga_Specs.cs b/tests/MassTransit.Tests/Saga/PartitionSaga_Specs.cs index 2db13f02e42..67170d3cc79 100644 --- a/tests/MassTransit.Tests/Saga/PartitionSaga_Specs.cs +++ b/tests/MassTransit.Tests/Saga/PartitionSaga_Specs.cs @@ -28,7 +28,7 @@ public async Task Should_initiate_the_saga() for (var i = 0; i < Limit; i++) { Guid? guid = await _repository.ShouldContainSaga(ids[i], TestTimeout); - Assert.IsTrue(guid.HasValue); + Assert.That(guid.HasValue, Is.True); } timer.Stop(); @@ -36,7 +36,7 @@ public async Task Should_initiate_the_saga() Console.WriteLine("Total time: {0}", timer.Elapsed); } - InMemorySagaRepository _repository; + ISagaRepository _repository; const int Limit = 100; diff --git a/tests/MassTransit.Tests/Saga/RepositoryContext_Specs.cs b/tests/MassTransit.Tests/Saga/RepositoryContext_Specs.cs index 9a10d64f97e..cf9df31499d 100644 --- a/tests/MassTransit.Tests/Saga/RepositoryContext_Specs.cs +++ b/tests/MassTransit.Tests/Saga/RepositoryContext_Specs.cs @@ -49,26 +49,30 @@ ISagaRepository CreateInMemorySagaRepository() ISagaConsumeContextFactory, T> factory = new InMemorySagaConsumeContextFactory(); - ISagaRepositoryContextFactory repositoryContextFactory = new InMemorySagaRepositoryContextFactory(dictionary, factory); + var repositoryContextFactory = new InMemorySagaRepositoryContextFactory(dictionary, factory); - return new SagaRepository(repositoryContextFactory); + return new SagaRepository(repositoryContextFactory, repositoryContextFactory, repositoryContextFactory); } + public interface Create : CorrelatedBy { } + public interface CreateCompleted : CorrelatedBy { } + public interface FinallyCompleted : CorrelatedBy { } + public class Instance : SagaStateMachineInstance { @@ -76,6 +80,7 @@ public class Instance : public Guid CorrelationId { get; set; } } + class InsertOnInitialTestStateMachine : MassTransitStateMachine { @@ -101,7 +106,7 @@ public InsertOnInitialTestStateMachine() When(Destroyed) .Finalize()); - Finally(binder => binder.PublishAsync(x=> x.Init(x.Instance))); + Finally(binder => binder.PublishAsync(x => x.Init(x.Instance))); } public State Active { get; private set; } @@ -110,6 +115,7 @@ public InsertOnInitialTestStateMachine() } } + [TestFixture] public class Using_the_universal_saga_repository : InMemoryTestFixture @@ -120,7 +126,7 @@ public async Task Should_reach_the_saga() Task> createCompleted = await ConnectPublishHandler(); Task> destroyCompleted = await ConnectPublishHandler(); - var values = new {InVar.CorrelationId}; + var values = new { InVar.CorrelationId }; await InputQueueSendEndpoint.Send(values); ConsumeContext createContext = await createCompleted; @@ -148,9 +154,9 @@ ISagaRepository CreateInMemorySagaRepository() ISagaConsumeContextFactory, T> factory = new InMemorySagaConsumeContextFactory(); - ISagaRepositoryContextFactory repositoryContextFactory = new InMemorySagaRepositoryContextFactory(dictionary, factory); + var repositoryContextFactory = new InMemorySagaRepositoryContextFactory(dictionary, factory); - return new SagaRepository(repositoryContextFactory); + return new SagaRepository(repositoryContextFactory, repositoryContextFactory, repositoryContextFactory); } @@ -185,6 +191,7 @@ public class Instance : public Guid CorrelationId { get; set; } } + class TestStateMachine : MassTransitStateMachine { diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Activity_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Activity_Specs.cs index 3cb11806275..8e1df4114f8 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Activity_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Activity_Specs.cs @@ -10,7 +10,7 @@ public class When_specifying_an_event_activity [Test] public void Should_transition_to_the_proper_state() { - Assert.AreEqual(_machine.Running, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Running)); } Instance _instance; @@ -60,7 +60,7 @@ public class When_specifying_an_event_activity_using_initially [Test] public void Should_transition_to_the_proper_state() { - Assert.AreEqual(_machine.Running, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Running)); } Instance _instance; @@ -107,13 +107,13 @@ public class When_specifying_an_event_activity_using_finally [Test] public void Should_have_called_the_finally_activity() { - Assert.AreEqual(InstanceStateMachine.Finalized, _instance.Value); + Assert.That(_instance.Value, Is.EqualTo(InstanceStateMachine.Finalized)); } [Test] public void Should_transition_to_the_proper_state() { - Assert.AreEqual(_machine.Final, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Final)); } Instance _instance; @@ -166,25 +166,25 @@ public class When_hooking_the_initial_enter_state_event [Test] public void Should_call_the_activity() { - Assert.AreEqual(_machine.Final, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Final)); } [Test] public void Should_have_trigger_the_final_before_enter_event() { - Assert.AreEqual(_machine.Running, _instance.FinalState); + Assert.That(_instance.FinalState, Is.EqualTo(_machine.Running)); } [Test] public void Should_have_triggered_the_after_leave_event() { - Assert.AreEqual(_machine.Initial, _instance.LeftState); + Assert.That(_instance.LeftState, Is.EqualTo(_machine.Initial)); } [Test] public void Should_have_triggered_the_before_enter_event() { - Assert.AreEqual(_machine.Initializing, _instance.EnteredState); + Assert.That(_instance.EnteredState, Is.EqualTo(_machine.Initializing)); } Instance _instance; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/AnyStateTransition_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/AnyStateTransition_Specs.cs index 8a8c5710c65..a5eb8f707ff 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/AnyStateTransition_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/AnyStateTransition_Specs.cs @@ -10,19 +10,19 @@ public class When_any_state_transition_occurs [Test] public void Should_be_running() { - Assert.AreEqual(_machine.Running, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Running)); } [Test] public void Should_have_entered_running() { - Assert.AreEqual(_machine.Running, _instance.LastEntered); + Assert.That(_instance.LastEntered, Is.EqualTo(_machine.Running)); } [Test] public void Should_have_left_initial() { - Assert.AreEqual(_machine.Initial, _instance.LastLeft); + Assert.That(_instance.LastLeft, Is.EqualTo(_machine.Initial)); } Instance _instance; @@ -40,13 +40,13 @@ public void Setup() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public State LastEntered { get; set; } public State LastLeft { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Anytime_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Anytime_Specs.cs index 65ef46920fa..03fff855424 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Anytime_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Anytime_Specs.cs @@ -16,8 +16,11 @@ public async Task Should_be_called_regardless_of_state() await _machine.RaiseEvent(instance, x => x.Init); await _machine.RaiseEvent(instance, x => x.Hello); - Assert.IsTrue(instance.HelloCalled); - Assert.AreEqual(_machine.Final, instance.CurrentState); + Assert.Multiple(() => + { + Assert.That(instance.HelloCalled, Is.True); + Assert.That(instance.CurrentState, Is.EqualTo(_machine.Final)); + }); } [Test] @@ -26,13 +29,13 @@ public async Task Should_have_value_of_event_data() var instance = new Instance(); await _machine.RaiseEvent(instance, x => x.Init); - await _machine.RaiseEvent(instance, x => x.EventA, new A + await _machine.RaiseEvent(instance, x => x.EventA, new A { Value = "Test" }); + + Assert.Multiple(() => { - Value = "Test" + Assert.That(instance.AValue, Is.EqualTo("Test")); + Assert.That(instance.CurrentState, Is.EqualTo(_machine.Final)); }); - - Assert.AreEqual("Test", instance.AValue); - Assert.AreEqual(_machine.Final, instance.CurrentState); } [Test] @@ -42,8 +45,11 @@ public void Should_not_be_handled_on_initial() Assert.That(async () => await _machine.RaiseEvent(instance, x => x.Hello), Throws.TypeOf()); - Assert.IsFalse(instance.HelloCalled); - Assert.AreEqual(_machine.Initial, instance.CurrentState); + Assert.Multiple(() => + { + Assert.That(instance.HelloCalled, Is.False); + Assert.That(instance.CurrentState, Is.EqualTo(_machine.Initial)); + }); } TestStateMachine _machine; @@ -62,12 +68,12 @@ class A class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public bool HelloCalled { get; set; } public string AValue { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/AsyncActivity_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/AsyncActivity_Specs.cs index e6ada860dbc..7c01dbbbaa4 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/AsyncActivity_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/AsyncActivity_Specs.cs @@ -16,16 +16,16 @@ public async Task Should_capture_the_value() await machine.RaiseEvent(claim, machine.Create, new CreateInstance()); - Assert.AreEqual("ExecuteAsync", claim.Value); + Assert.That(claim.Value, Is.EqualTo("ExecuteAsync")); } class TestInstance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public string Value { get; set; } + public Guid CorrelationId { get; set; } } @@ -38,7 +38,8 @@ async Task IStateMachineActivity.Execute(BehaviorC context.Instance.Value = "ExecuteAsync"; } - Task IStateMachineActivity.Faulted(BehaviorExceptionContext context, + Task IStateMachineActivity.Faulted( + BehaviorExceptionContext context, IBehavior next) { return next.Faulted(context); @@ -56,11 +57,11 @@ public void Probe(ProbeContext context) class CreateInstance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public int X { get; set; } public int Y { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Combine_Assigned_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Combine_Assigned_Specs.cs index 283a14a9c1c..9e0ba9be260 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Combine_Assigned_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Combine_Assigned_Specs.cs @@ -19,8 +19,25 @@ public async Task Should_have_called_combined_event() await _machine.RaiseEvent(_instance, _machine.First); await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsTrue(_instance.Called); - Assert.IsEmpty(_machine.NextEvents(_instance.CurrentState)); + Assert.Multiple(() => + { + Assert.That(_instance.Called, Is.True); + Assert.That(_machine.NextEvents(_instance.CurrentState), Is.Empty); + }); + } + + [Test] + public async Task Should_have_correct_events() + { + _machine = new TestStateMachine(); + _instance = new Instance(); + + Assert.Multiple(() => + { + Assert.That(_machine.NextEvents(_machine.Initial).Count(), Is.EqualTo(1)); + Assert.That(_machine.NextEvents(_machine.Waiting).Count(), Is.EqualTo(3)); + Assert.That(_machine.NextEvents(_machine.Final).Count(), Is.EqualTo(0)); + }); } [Test] @@ -32,12 +49,15 @@ public async Task Should_not_call_for_one_event() await _machine.RaiseEvent(_instance, _machine.First); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); - var events = _machine.NextEvents(_instance.CurrentState); + Event[] events = _machine.NextEvents(_instance.CurrentState).ToArray(); - Assert.AreEqual(3, events.Count()); - Assert.AreEqual(2, events.Count(e => !_machine.IsCompositeEvent(e))); + Assert.Multiple(() => + { + Assert.That(events, Has.Length.EqualTo(3)); + Assert.That(events.Count(e => !_machine.IsCompositeEvent(e)), Is.EqualTo(2)); + }); } [Test] @@ -49,21 +69,10 @@ public async Task Should_not_call_for_one_other_event() await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsFalse(_instance.Called); - Assert.AreEqual(3, _machine.NextEvents(_instance.CurrentState).Count()); - } - - [Test] - public async Task Should_have_correct_events() - { - _machine = new TestStateMachine(); - _instance = new Instance(); - Assert.Multiple(() => { - Assert.AreEqual(1, _machine.NextEvents(_machine.Initial).Count()); - Assert.AreEqual(3, _machine.NextEvents(_machine.Waiting).Count()); - Assert.AreEqual(0, _machine.NextEvents(_machine.Final).Count()); + Assert.That(_instance.Called, Is.False); + Assert.That(_machine.NextEvents(_instance.CurrentState).Count(), Is.EqualTo(3)); }); } @@ -72,12 +81,12 @@ public async Task Should_have_correct_events() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public CompositeEventStatus CompositeStatus { get; set; } public bool Called { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -96,8 +105,6 @@ public TestStateMachine() When(Third) .Then(context => context.Instance.Called = true) .Finalize()); - - } public State Waiting { get; private set; } @@ -121,68 +128,80 @@ public async Task Should_have_called_combined_event() _instance = new Instance(); await _machine.RaiseEvent(_instance, _machine.Start); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); await _machine.RaiseEvent(_instance, _machine.First); await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsTrue(_instance.Called); + Assert.Multiple(() => + { + Assert.That(_instance.Called, Is.True); - Assert.AreEqual(2, _instance.CurrentState); - Assert.IsEmpty(_machine.NextEvents(_machine.GetState("Final"))); + Assert.That(_instance.CurrentState, Is.EqualTo(2)); + Assert.That(_machine.NextEvents(_machine.GetState("Final")), Is.Empty); + }); } [Test] - public async Task Should_have_initial_state_with_zero() + public async Task Should_have_correct_events() { _machine = new TestStateMachine(); _instance = new Instance(); - await _machine.RaiseEvent(_instance, _machine.Start); - Assert.AreEqual(3, _instance.CurrentState); - Assert.IsEmpty(_machine.NextEvents(_machine.GetState("Final"))); + Assert.Multiple(() => + { + Assert.That(_machine.NextEvents(_machine.Initial).Count(), Is.EqualTo(1)); + Assert.That(_machine.NextEvents(_machine.GetState("Initial")).Count(), Is.EqualTo(1)); + Assert.That(_machine.NextEvents(_machine.Waiting).Count(), Is.EqualTo(3)); + Assert.That(_machine.NextEvents(_machine.GetState("Waiting")).Count(), Is.EqualTo(3)); + Assert.That(_machine.NextEvents(_machine.Final).Count(), Is.EqualTo(0)); + Assert.That(_machine.NextEvents(_machine.GetState("Final")).Count(), Is.EqualTo(0)); + }); } [Test] - public async Task Should_not_call_for_one_event() + public async Task Should_have_initial_state_with_zero() { _machine = new TestStateMachine(); _instance = new Instance(); await _machine.RaiseEvent(_instance, _machine.Start); - await _machine.RaiseEvent(_instance, _machine.First); - - Assert.IsFalse(_instance.Called); - Assert.IsEmpty(_machine.NextEvents(_machine.GetState("Final"))); + Assert.Multiple(() => + { + Assert.That(_instance.CurrentState, Is.EqualTo(3)); + Assert.That(_machine.NextEvents(_machine.GetState("Final")), Is.Empty); + }); } [Test] - public async Task Should_not_call_for_one_other_event() + public async Task Should_not_call_for_one_event() { _machine = new TestStateMachine(); _instance = new Instance(); await _machine.RaiseEvent(_instance, _machine.Start); - await _machine.RaiseEvent(_instance, _machine.Second); + await _machine.RaiseEvent(_instance, _machine.First); - Assert.IsFalse(_instance.Called); - Assert.IsEmpty(_machine.NextEvents(_machine.GetState("Final"))); + Assert.Multiple(() => + { + Assert.That(_instance.Called, Is.False); + Assert.That(_machine.NextEvents(_machine.GetState("Final")), Is.Empty); + }); } [Test] - public async Task Should_have_correct_events() + public async Task Should_not_call_for_one_other_event() { _machine = new TestStateMachine(); _instance = new Instance(); + await _machine.RaiseEvent(_instance, _machine.Start); + + await _machine.RaiseEvent(_instance, _machine.Second); Assert.Multiple(() => { - Assert.AreEqual(1, _machine.NextEvents(_machine.Initial).Count()); - Assert.AreEqual(1, _machine.NextEvents(_machine.GetState("Initial")).Count()); - Assert.AreEqual(3, _machine.NextEvents(_machine.Waiting).Count()); - Assert.AreEqual(3, _machine.NextEvents(_machine.GetState("Waiting")).Count()); - Assert.AreEqual(0, _machine.NextEvents(_machine.Final).Count()); - Assert.AreEqual(0, _machine.NextEvents(_machine.GetState("Final")).Count()); + Assert.That(_instance.Called, Is.False); + Assert.That(_machine.NextEvents(_machine.GetState("Final")), Is.Empty); }); } @@ -192,12 +211,12 @@ public async Task Should_have_correct_events() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public int CompositeStatus { get; set; } public bool Called { get; set; } public int CurrentState { get; set; } public bool CalledFirst { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Combine_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Combine_Specs.cs index ce84d7b4231..05ac7e22442 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Combine_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Combine_Specs.cs @@ -18,7 +18,7 @@ public async Task Should_have_called_combined_event() await _machine.RaiseEvent(_instance, _machine.First); await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] @@ -30,7 +30,7 @@ public async Task Should_not_call_for_one_event() await _machine.RaiseEvent(_instance, _machine.First); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } [Test] @@ -42,7 +42,7 @@ public async Task Should_not_call_for_one_other_event() await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } TestStateMachine _machine; @@ -50,12 +50,12 @@ public async Task Should_not_call_for_one_other_event() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public CompositeEventStatus CompositeStatus { get; set; } public bool Called { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -97,14 +97,17 @@ public async Task Should_have_called_combined_event() _instance = new Instance(); await _machine.RaiseEvent(_instance, _machine.Start); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); await _machine.RaiseEvent(_instance, _machine.First); await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsTrue(_instance.Called); + Assert.Multiple(() => + { + Assert.That(_instance.Called, Is.True); - Assert.AreEqual(2, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(2)); + }); } [Test] @@ -114,7 +117,7 @@ public async Task Should_have_initial_state_with_zero() _instance = new Instance(); await _machine.RaiseEvent(_instance, _machine.Start); - Assert.AreEqual(3, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(3)); } [Test] @@ -126,7 +129,7 @@ public async Task Should_not_call_for_one_event() await _machine.RaiseEvent(_instance, _machine.First); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } [Test] @@ -138,7 +141,7 @@ public async Task Should_not_call_for_one_other_event() await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } TestStateMachine _machine; @@ -146,12 +149,12 @@ public async Task Should_not_call_for_one_other_event() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public int CompositeStatus { get; set; } public bool Called { get; set; } public int CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -183,4 +186,103 @@ public TestStateMachine() public Event Third { get; private set; } } } + + + [TestFixture] + public class When_multiple_events_trigger_a_composite_event + { + [Test] + public async Task Should_call_once_for_duplicate_events() + { + var machine = new TestStateMachine(CompositeEventOptions.RaiseOnce); + var instance = new Instance(); + + await machine.RaiseEvent(instance, machine.Start); + + await machine.RaiseEvent(instance, machine.First); + await machine.RaiseEvent(instance, machine.Second); + await machine.RaiseEvent(instance, machine.Second); + + Assert.That(instance.TriggerCount, Is.EqualTo(1)); + } + + [Test] + public async Task Should_call_twice_for_duplicate_events() + { + var machine = new TestStateMachine(CompositeEventOptions.None); + var instance = new Instance(); + + await machine.RaiseEvent(instance, machine.Start); + + await machine.RaiseEvent(instance, machine.First); + await machine.RaiseEvent(instance, machine.Second); + await machine.RaiseEvent(instance, machine.Second); + + Assert.That(instance.TriggerCount, Is.EqualTo(2)); + } + + [Test] + public async Task Should_have_called_combined_event() + { + var machine = new TestStateMachine(CompositeEventOptions.None); + var instance = new Instance(); + + await machine.RaiseEvent(instance, machine.Start); + + await machine.RaiseEvent(instance, machine.First); + await machine.RaiseEvent(instance, machine.Second); + + Assert.That(instance.TriggerCount, Is.EqualTo(1)); + } + + [Test] + public async Task Should_not_call_for_one_event() + { + var machine = new TestStateMachine(CompositeEventOptions.None); + var instance = new Instance(); + await machine.RaiseEvent(instance, machine.Start); + + await machine.RaiseEvent(instance, machine.First); + + Assert.That(instance.TriggerCount, Is.EqualTo(0)); + } + + + class Instance : + SagaStateMachineInstance + { + public int CurrentState { get; set; } + public int CompositeStatus { get; set; } + public int TriggerCount { get; set; } + public Guid CorrelationId { get; set; } + } + + + sealed class TestStateMachine : + MassTransitStateMachine + { + public TestStateMachine(CompositeEventOptions options) + { + InstanceState(x => x.CurrentState, Waiting); + + CompositeEvent(() => Third, x => x.CompositeStatus, options, First, Second); + + Initially( + When(Start) + .TransitionTo(Waiting)); + + During(Waiting, + When(Third) + .Then(context => context.Instance.TriggerCount++)); + } // ReSharper disable UnassignedGetOnlyAutoProperty + // ReSharper disable MemberCanBePrivate.Local + public State Waiting { get; } + + public Event Start { get; } + + public Event First { get; } + public Event Second { get; } + public Event Third { get; } + } + } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeCondition_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeCondition_Specs.cs index 0a0b980cce9..12469f0ac31 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeCondition_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeCondition_Specs.cs @@ -18,8 +18,11 @@ public async Task Should_call_when_met() await _machine.RaiseEvent(_instance, _machine.Second); await _machine.RaiseEvent(_instance, _machine.First); - Assert.IsTrue(_instance.Called); - Assert.IsTrue(_instance.SecondFirst); + Assert.Multiple(() => + { + Assert.That(_instance.Called, Is.True); + Assert.That(_instance.SecondFirst, Is.True); + }); } [Test] @@ -32,19 +35,20 @@ public async Task Should_skip_when_not_met() await _machine.RaiseEvent(_instance, _machine.First); await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsFalse(_instance.Called); - Assert.IsFalse(_instance.SecondFirst); + Assert.Multiple(() => + { + Assert.That(_instance.Called, Is.False); + Assert.That(_instance.SecondFirst, Is.False); + }); } - TestStateMachine _machine; Instance _instance; class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public CompositeEventStatus CompositeStatus { get; set; } public bool Called { get; set; } public bool CalledAfterAll { get; set; } @@ -52,6 +56,7 @@ class Instance : public bool SecondFirst { get; set; } public bool First { get; set; } public bool Second { get; set; } + public Guid CorrelationId { get; set; } } @@ -78,7 +83,7 @@ public TestStateMachine() context.Instance.Second = true; context.Instance.CalledAfterAll = false; }) - ); + ); CompositeEvent(() => Third, x => x.CompositeStatus, First, Second); diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeEventMultipleStates_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeEventMultipleStates_Specs.cs index 144ab23bfe9..b0eef09fca4 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeEventMultipleStates_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeEventMultipleStates_Specs.cs @@ -8,9 +8,6 @@ [TestFixture] public class When_combining_events_into_a_single_event_into_a_single_event { - TestStateMachine _machine; - Instance _instance; - [Test] public async Task Should_have_called_combined_event_when_compositeevent_defined_before() { @@ -21,7 +18,7 @@ public async Task Should_have_called_combined_event_when_compositeevent_defined_ await _machine.RaiseEvent(_instance, _machine.First); await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] @@ -34,9 +31,12 @@ public async Task Should_have_called_combined_event_when_compositeevent_defined_ await _machine.RaiseEvent(_instance, _machine.First); await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } + TestStateMachine _machine; + Instance _instance; + sealed class TestStateMachine : MassTransitStateMachine @@ -75,6 +75,7 @@ public TestStateMachine(string testName) public Event Third { get; private set; } } + class Instance : SagaStateMachineInstance { @@ -92,13 +93,13 @@ protected Instance() public int CompositeStatus { get; set; } public int CurrentState { get; set; } - public Guid CorrelationId { get; set; } - public bool? Called { get { return _called; } set { _called = value; } } + + public Guid CorrelationId { get; set; } } } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeOrder_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeOrder_Specs.cs index b6f1b2b620d..252be247d35 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeOrder_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/CompositeOrder_Specs.cs @@ -18,7 +18,7 @@ public async Task Should_have_called_combined_event() await _machine.RaiseEvent(_instance, _machine.First); await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] @@ -31,7 +31,7 @@ public async Task Should_have_called_combined_event_after_all_events() await _machine.RaiseEvent(_instance, _machine.First); await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsTrue(_instance.CalledAfterAll); + Assert.That(_instance.CalledAfterAll, Is.True); } [Test] @@ -43,7 +43,7 @@ public async Task Should_not_call_for_one_event() await _machine.RaiseEvent(_instance, _machine.First); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } [Test] @@ -55,7 +55,7 @@ public async Task Should_not_call_for_one_other_event() await _machine.RaiseEvent(_instance, _machine.Second); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } TestStateMachine _machine; @@ -63,13 +63,13 @@ public async Task Should_not_call_for_one_other_event() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public CompositeEventStatus CompositeStatus { get; set; } public bool Called { get; set; } public bool CalledAfterAll { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -84,9 +84,15 @@ public TestStateMachine() During(Waiting, When(First) - .Then(context => { context.Instance.CalledAfterAll = false; }), + .Then(context => + { + context.Instance.CalledAfterAll = false; + }), When(Second) - .Then(context => { context.Instance.CalledAfterAll = false; })); + .Then(context => + { + context.Instance.CalledAfterAll = false; + })); CompositeEvent(() => Third, x => x.CompositeStatus, First, Second); diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/DataActivity_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/DataActivity_Specs.cs index 0c0b90a408b..764c24344af 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/DataActivity_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/DataActivity_Specs.cs @@ -10,13 +10,13 @@ public class When_specifying_an_event_activity_with_data [Test] public void Should_have_the_proper_value() { - Assert.AreEqual("Hello", _instance.Value); + Assert.That(_instance.Value, Is.EqualTo("Hello")); } [Test] public void Should_transition_to_the_proper_state() { - Assert.AreEqual(_machine.Running, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Running)); } Instance _instance; @@ -33,7 +33,7 @@ public void Specifying_an_event_activity_with_data() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { public string Value { get; set; } public int OtherValue { get; set; } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Declarative_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Declarative_Specs.cs index 9ba66bce8ad..6b45fced758 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Declarative_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Declarative_Specs.cs @@ -10,8 +10,11 @@ public class When_an_instance_has_multiple_states [Test] public void Should_handle_both_states() { - Assert.AreEqual(_top.Greeted, _instance.Top); - Assert.AreEqual(_bottom.Ignored, _instance.Bottom); + Assert.Multiple(() => + { + Assert.That(_instance.Top, Is.EqualTo(_top.Greeted)); + Assert.That(_instance.Bottom, Is.EqualTo(_bottom.Ignored)); + }); } MyState _instance; @@ -26,24 +29,18 @@ public void Specifying_an_event_activity_with_data() _top = new TopInstanceStateMachine(); _bottom = new BottomInstanceStateMachine(); - _top.RaiseEvent(_instance, _top.Initialized, new Init - { - Value = "Hello" - }).Wait(); + _top.RaiseEvent(_instance, _top.Initialized, new Init { Value = "Hello" }).Wait(); - _bottom.RaiseEvent(_instance, _bottom.Initialized, new Init - { - Value = "Goodbye" - }).Wait(); + _bottom.RaiseEvent(_instance, _bottom.Initialized, new Init { Value = "Goodbye" }).Wait(); } class MyState : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State Top { get; set; } public State Bottom { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Dependency_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Dependency_Specs.cs index 0f923ffd96a..5de524262a6 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Dependency_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Dependency_Specs.cs @@ -12,7 +12,7 @@ public class Having_a_dependency_available [Test] public void Should_capture_the_value() { - Assert.AreEqual("79", _claim.Value); + Assert.That(_claim.Value, Is.EqualTo("79")); } ClaimAdjustmentInstance _claim; @@ -37,11 +37,11 @@ public void Specifying_an_event_activity() class ClaimAdjustmentInstance : ClaimAdjustment, -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public string Value { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/EventObservable_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/EventObservable_Specs.cs index 58fd374cbe9..acfa3537b53 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/EventObservable_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/EventObservable_Specs.cs @@ -10,13 +10,13 @@ public class When_an_event_is_raised_on_an_instance [Test] public void Should_have_raised_the_initialized_event() { - Assert.AreEqual(_machine.Initialized, _observer.Events[0].Event); + Assert.That(_observer.Events[0].Event, Is.EqualTo(_machine.Initialized)); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(1, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(1)); } Instance _instance; @@ -36,10 +36,10 @@ public void Specifying_an_event_activity() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/EventRaisedObserver.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/EventRaisedObserver.cs index c58b6668cc2..d970ae62ef1 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/EventRaisedObserver.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/EventRaisedObserver.cs @@ -7,7 +7,7 @@ namespace MassTransit.Tests.SagaStateMachineTests.Automatonymous class EventRaisedObserver : IEventObserver - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { public EventRaisedObserver() { diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Event_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Event_Specs.cs index 07f3c67a138..1e00e82ea66 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Event_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Event_Specs.cs @@ -11,25 +11,25 @@ public class When_an_event_is_declared [Test] public void It_should_capture_a_simple_event_name() { - Assert.AreEqual("Hello", _machine.Hello.Name); + Assert.That(_machine.Hello.Name, Is.EqualTo("Hello")); } [Test] public void It_should_capture_the_data_event_name() { - Assert.AreEqual("EventA", _machine.EventA.Name); + Assert.That(_machine.EventA.Name, Is.EqualTo("EventA")); } [Test] public void It_should_create_the_proper_event_type_for_data_events() { - Assert.IsInstanceOf>(_machine.EventA); + Assert.That(_machine.EventA, Is.InstanceOf>()); } [Test] public void It_should_create_the_proper_event_type_for_simple_events() { - Assert.IsInstanceOf(_machine.Hello); + Assert.That(_machine.Hello, Is.InstanceOf()); } TestStateMachine _machine; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Exception_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Exception_Specs.cs index 6e90d2e3724..ce3c1f6db77 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Exception_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Exception_Specs.cs @@ -11,79 +11,79 @@ public class When_an_action_throws_an_exception [Test] public void Should_capture_the_exception_message() { - Assert.AreEqual("Boom!", _instance.ExceptionMessage); + Assert.That(_instance.ExceptionMessage, Is.EqualTo("Boom!")); } [Test] public void Should_capture_the_exception_type() { - Assert.AreEqual(typeof(ApplicationException), _instance.ExceptionType); + Assert.That(_instance.ExceptionType, Is.EqualTo(typeof(ApplicationException))); } [Test] public void Should_have_called_the_async_if_block() { - Assert.IsTrue(_instance.CalledThenClauseAsync); + Assert.That(_instance.CalledThenClauseAsync, Is.True); } [Test] public void Should_have_called_the_async_then_block() { - Assert.IsTrue(_instance.ThenAsyncShouldBeCalled); + Assert.That(_instance.ThenAsyncShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_exception_handler() { - Assert.AreEqual(_machine.Failed, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Failed)); } [Test] public void Should_have_called_the_false_async_condition_else_block() { - Assert.IsTrue(_instance.ElseAsyncShouldBeCalled); + Assert.That(_instance.ElseAsyncShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_false_condition_else_block() { - Assert.IsTrue(_instance.ElseShouldBeCalled); + Assert.That(_instance.ElseShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_first_action() { - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] public void Should_have_called_the_first_if_block() { - Assert.IsTrue(_instance.CalledThenClause); + Assert.That(_instance.CalledThenClause, Is.True); } [Test] public void Should_not_have_called_the_false_async_condition_then_block() { - Assert.IsFalse(_instance.ThenAsyncShouldNotBeCalled); + Assert.That(_instance.ThenAsyncShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_false_condition_then_block() { - Assert.IsFalse(_instance.ThenShouldNotBeCalled); + Assert.That(_instance.ThenShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_regular_exception() { - Assert.IsFalse(_instance.ShouldNotBeCalled); + Assert.That(_instance.ShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_second_action() { - Assert.IsTrue(_instance.NotCalled); + Assert.That(_instance.NotCalled, Is.True); } Instance _instance; @@ -184,31 +184,31 @@ public class When_the_exception_does_not_match_the_type [Test] public void Should_capture_the_exception_message() { - Assert.AreEqual("Boom!", _instance.ExceptionMessage); + Assert.That(_instance.ExceptionMessage, Is.EqualTo("Boom!")); } [Test] public void Should_capture_the_exception_type() { - Assert.AreEqual(typeof(ApplicationException), _instance.ExceptionType); + Assert.That(_instance.ExceptionType, Is.EqualTo(typeof(ApplicationException))); } [Test] public void Should_have_called_the_exception_handler() { - Assert.AreEqual(_machine.Failed, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Failed)); } [Test] public void Should_have_called_the_first_action() { - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] public void Should_not_have_called_the_second_action() { - Assert.IsTrue(_instance.NotCalled); + Assert.That(_instance.NotCalled, Is.True); } Instance _instance; @@ -275,7 +275,7 @@ public class When_the_exception_is_caught [Test] public void Should_have_called_the_subsequent_action() { - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } Instance _instance; @@ -329,7 +329,7 @@ public class When_the_exception_is_caught_within_an_else [Test] public void Should_have_called_the_subsequent_action() { - Assert.AreEqual(_instance.CurrentState, _machine.Failed); + Assert.That(_machine.Failed, Is.EqualTo(_instance.CurrentState)); } Instance _instance; @@ -386,67 +386,67 @@ public class When_an_action_throws_an_exception_on_data_events [Test] public void Should_capture_the_exception_message() { - Assert.AreEqual("Boom!", _instance.ExceptionMessage); + Assert.That(_instance.ExceptionMessage, Is.EqualTo("Boom!")); } [Test] public void Should_capture_the_exception_type() { - Assert.AreEqual(typeof(ApplicationException), _instance.ExceptionType); + Assert.That(_instance.ExceptionType, Is.EqualTo(typeof(ApplicationException))); } [Test] public void Should_have_called_the_async_if_block() { - Assert.IsTrue(_instance.CalledSecondThenClause); + Assert.That(_instance.CalledSecondThenClause, Is.True); } [Test] public void Should_have_called_the_exception_handler() { - Assert.AreEqual(_machine.Failed, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Failed)); } [Test] public void Should_have_called_the_false_async_condition_else_block() { - Assert.IsTrue(_instance.ElseAsyncShouldBeCalled); + Assert.That(_instance.ElseAsyncShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_false_condition_else_block() { - Assert.IsTrue(_instance.ElseShouldBeCalled); + Assert.That(_instance.ElseShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_first_action() { - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] public void Should_have_called_the_first_if_block() { - Assert.IsTrue(_instance.CalledThenClause); + Assert.That(_instance.CalledThenClause, Is.True); } [Test] public void Should_not_have_called_the_false_async_condition_then_block() { - Assert.IsFalse(_instance.ThenAsyncShouldNotBeCalled); + Assert.That(_instance.ThenAsyncShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_false_condition_then_block() { - Assert.IsFalse(_instance.ThenShouldNotBeCalled); + Assert.That(_instance.ThenShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_second_action() { - Assert.IsTrue(_instance.NotCalled); + Assert.That(_instance.NotCalled, Is.True); } Instance _instance; @@ -540,7 +540,7 @@ public class When_an_action_throws_an_exception_and_catches_it [Test] public void Should_finalize_in_catch_block() { - Assert.AreEqual(_machine.Final, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Final)); } Instance _instance; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Faulted_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Faulted_Specs.cs index c8dc04c2078..dd8e5d7ba7c 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Faulted_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Faulted_Specs.cs @@ -20,7 +20,7 @@ public void Should_capture_the_value() Assert.That(async () => await _machine.RaiseEvent(_claim, _machine.Create, data), Throws.TypeOf()); - Assert.AreEqual(default, _claim.Value); + Assert.That(_claim.Value, Is.EqualTo(default)); } ClaimAdjustmentInstance _claim; @@ -36,11 +36,11 @@ public void Specifying_an_event_activity() class ClaimAdjustmentInstance : ClaimAdjustment, -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public string Value { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/FilterExpression_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/FilterExpression_Specs.cs index aea9f8dcc2a..425b3185fee 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/FilterExpression_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/FilterExpression_Specs.cs @@ -14,22 +14,22 @@ public async Task Should_transition_to_the_proper_state() var instance = new Instance(); var machine = new InstanceStateMachine(); - await machine.RaiseEvent(instance, machine.Thing, new Data {Condition = true}); - Assert.AreEqual(machine.True, instance.CurrentState); + await machine.RaiseEvent(instance, machine.Thing, new Data { Condition = true }); + Assert.That(instance.CurrentState, Is.EqualTo(machine.True)); // reset instance.CurrentState = machine.Initial; - await machine.RaiseEvent(instance, machine.Thing, new Data {Condition = false}); - Assert.AreEqual(machine.False, instance.CurrentState); + await machine.RaiseEvent(instance, machine.Thing, new Data { Condition = false }); + Assert.That(instance.CurrentState, Is.EqualTo(machine.False)); } class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Group_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Group_Specs.cs index 0d333e37a30..65e1d92d6da 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Group_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Group_Specs.cs @@ -15,8 +15,11 @@ public void Should_allow_parallel_execution_of_events() [Test] public void Should_have_captured_initial_data() { - Assert.AreEqual("Audi", _instance.VehicleMake); - Assert.AreEqual("A6", _instance.VehicleModel); + Assert.Multiple(() => + { + Assert.That(_instance.VehicleMake, Is.EqualTo("Audi")); + Assert.That(_instance.VehicleModel, Is.EqualTo("A6")); + }); } PitStop _machine; @@ -39,9 +42,8 @@ public void Setup() class PitStopInstance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State OverallState { get; private set; } public State FuelState { get; private set; } public State OilState { get; private set; } @@ -56,6 +58,7 @@ class PitStopInstance : public decimal OilQuarts { get; set; } public decimal OilPricePerQuart { get; set; } public decimal OilCost { get; set; } + public Guid CorrelationId { get; set; } } @@ -74,12 +77,12 @@ public PitStop() context.Instance.VehicleModel = context.Data.Model; }) .TransitionTo(BeingServiced) -// .RunParallel(p => -// { -// p.Start(x => x.BeginFilling); -// p.Start(x => x.BeginChecking); -// })) - ); + // .RunParallel(p => + // { + // p.Start(x => x.BeginFilling); + // p.Start(x => x.BeginChecking); + // })) + ); } public State BeingServiced { get; private set; } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Introspection_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Introspection_Specs.cs index 7d1789a00aa..612bd03fc7a 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Introspection_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Introspection_Specs.cs @@ -13,7 +13,7 @@ public class Introspection_Specs [Test] public void The_machine_shoud_report_its_instance_type() { - Assert.AreEqual(typeof(Instance), ((StateMachine)_machine).InstanceType); + Assert.That(((StateMachine)_machine).InstanceType, Is.EqualTo(typeof(Instance))); } [Test] @@ -21,29 +21,32 @@ public void The_machine_should_expose_all_events() { List events = _machine.Events.ToList(); - Assert.AreEqual(4, events.Count); - Assert.Contains(_machine.Ignored, events); - Assert.Contains(_machine.Handshake, events); - Assert.Contains(_machine.Hello, events); - Assert.Contains(_machine.YelledAt, events); + Assert.That(events, Has.Count.EqualTo(4)); + Assert.That(events, Does.Contain(_machine.Ignored)); + Assert.That(events, Does.Contain(_machine.Handshake)); + Assert.That(events, Does.Contain(_machine.Hello)); + Assert.That(events, Does.Contain(_machine.YelledAt)); } [Test] public void The_machine_should_expose_all_states() { - Assert.AreEqual(5, ((StateMachine)_machine).States.Count()); - Assert.Contains(_machine.Initial, _machine.States.ToList()); - Assert.Contains(_machine.Final, _machine.States.ToList()); - Assert.Contains(_machine.Greeted, _machine.States.ToList()); - Assert.Contains(_machine.Loved, _machine.States.ToList()); - Assert.Contains(_machine.Pissed, _machine.States.ToList()); + Assert.Multiple(() => + { + Assert.That(((StateMachine)_machine).States.Count(), Is.EqualTo(5)); + Assert.That(_machine.States.ToList(), Does.Contain(_machine.Initial)); + }); + Assert.That(_machine.States.ToList(), Does.Contain(_machine.Final)); + Assert.That(_machine.States.ToList(), Does.Contain(_machine.Greeted)); + Assert.That(_machine.States.ToList(), Does.Contain(_machine.Loved)); + Assert.That(_machine.States.ToList(), Does.Contain(_machine.Pissed)); } [Test] public async Task The_next_events_should_be_known() { List events = (await _machine.NextEvents(_instance)).ToList(); - Assert.AreEqual(3, events.Count); + Assert.That(events, Has.Count.EqualTo(3)); } Instance _instance; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/JsonStateSerializer.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/JsonStateSerializer.cs index 50d0d8a48d9..8ef79e9de5d 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/JsonStateSerializer.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/JsonStateSerializer.cs @@ -8,7 +8,7 @@ public class JsonStateSerializer where TStateMachine : StateMachine - where TInstance : class, ISaga + where TInstance : class, SagaStateMachineInstance { readonly TStateMachine _machine; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Observable_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Observable_Specs.cs index b68f25db657..af73f0bbf2b 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Observable_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Observable_Specs.cs @@ -10,21 +10,27 @@ public class Observing_state_machine_instance_state_changes [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_second_switched_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(_machine.Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(_machine.Running)); + }); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(3, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(3)); } Instance _instance; @@ -49,8 +55,8 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -81,53 +87,65 @@ public class Observing_events_with_substates [Test] public void Should_have_all_events() { - Assert.AreEqual(2, _eventObserver.Events.Count); + Assert.That(_eventObserver.Events, Has.Count.EqualTo(2)); } [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_fourth_switched_to_finished() { - Assert.AreEqual(_machine.Resting, _observer.Events[3].Previous); - Assert.AreEqual(_machine.Final, _observer.Events[3].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[3].Previous, Is.EqualTo(_machine.Resting)); + Assert.That(_observer.Events[3].Current, Is.EqualTo(_machine.Final)); + }); } [Test] public void Should_have_second_switched_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(_machine.Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(_machine.Running)); + }); } [Test] public void Should_have_third_switched_to_resting() { - Assert.AreEqual(_machine.Running, _observer.Events[2].Previous); - Assert.AreEqual(_machine.Resting, _observer.Events[2].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[2].Previous, Is.EqualTo(_machine.Running)); + Assert.That(_observer.Events[2].Current, Is.EqualTo(_machine.Resting)); + }); } [Test] public void Should_have_transition_1() { - Assert.AreEqual("Initialized", _eventObserver.Events[0].Event.Name); + Assert.That(_eventObserver.Events[0].Event.Name, Is.EqualTo("Initialized")); } [Test] public void Should_have_transition_2() { - Assert.AreEqual("LegCramped", _eventObserver.Events[1].Event.Name); + Assert.That(_eventObserver.Events[1].Event.Name, Is.EqualTo("LegCramped")); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(4, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(4)); } Instance _instance; @@ -157,8 +175,8 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -208,60 +226,75 @@ public class Observing_events_with_substates_part_deux [Test] public void Should_have_all_events() { - Assert.AreEqual(2, _eventObserver.Events.Count); + Assert.That(_eventObserver.Events, Has.Count.EqualTo(2)); } [Test] public void Should_have_fifth_switched_to_finished() { - Assert.AreEqual(_machine.Running, _observer.Events[4].Previous); - Assert.AreEqual(_machine.Final, _observer.Events[4].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[4].Previous, Is.EqualTo(_machine.Running)); + Assert.That(_observer.Events[4].Current, Is.EqualTo(_machine.Final)); + }); } [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_fourth_switched_to_finished() { - Assert.AreEqual(_machine.Resting, _observer.Events[3].Previous); - Assert.AreEqual(_machine.Running, _observer.Events[3].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[3].Previous, Is.EqualTo(_machine.Resting)); + Assert.That(_observer.Events[3].Current, Is.EqualTo(_machine.Running)); + }); } [Test] public void Should_have_second_switched_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(_machine.Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(_machine.Running)); + }); } [Test] public void Should_have_third_switched_to_resting() { - Assert.AreEqual(_machine.Running, _observer.Events[2].Previous); - Assert.AreEqual(_machine.Resting, _observer.Events[2].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[2].Previous, Is.EqualTo(_machine.Running)); + Assert.That(_observer.Events[2].Current, Is.EqualTo(_machine.Resting)); + }); } [Test] public void Should_have_transition_1() { - Assert.AreEqual("Running.BeforeEnter", _eventObserver.Events[0].Event.Name); + Assert.That(_eventObserver.Events[0].Event.Name, Is.EqualTo("Running.BeforeEnter")); } [Test] public void Should_have_transition_2() { - Assert.AreEqual("Running.AfterLeave", _eventObserver.Events[1].Event.Name); + Assert.That(_eventObserver.Events[1].Event.Name, Is.EqualTo("Running.AfterLeave")); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(5, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(5)); } Instance _instance; @@ -292,8 +325,8 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/RaiseEvent_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/RaiseEvent_Specs.cs index e037f5e5fe7..233e169383e 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/RaiseEvent_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/RaiseEvent_Specs.cs @@ -14,21 +14,21 @@ public async Task Should_include_payload() var instance = new Instance(); var machine = new InstanceStateMachine(); - await machine.RaiseEvent(instance, machine.Thing, new Data + await machine.RaiseEvent(instance, machine.Thing, new Data { Condition = true }); + Assert.Multiple(() => { - Condition = true + Assert.That(instance.CurrentState, Is.EqualTo(machine.True)); + Assert.That(instance.Initialized.HasValue, Is.True); }); - Assert.AreEqual(machine.True, instance.CurrentState); - Assert.IsTrue(instance.Initialized.HasValue); } class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public DateTime? Initialized { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/SerializeState_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/SerializeState_Specs.cs index 431f3d5275d..24d50665de0 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/SerializeState_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/SerializeState_Specs.cs @@ -14,11 +14,8 @@ public async Task Should_properly_handle_the_state_property() var instance = new Instance(); var machine = new InstanceStateMachine(); - await machine.RaiseEvent(instance, machine.Thing, new Data - { - Condition = true - }); - Assert.AreEqual(machine.True, instance.CurrentState); + await machine.RaiseEvent(instance, machine.Thing, new Data { Condition = true }); + Assert.That(instance.CurrentState, Is.EqualTo(machine.True)); var serializer = new JsonStateSerializer(machine); @@ -27,15 +24,15 @@ public async Task Should_properly_handle_the_state_property() Console.WriteLine("Body: {0}", body); var reInstance = serializer.Deserialize(body); - Assert.AreEqual(machine.True, reInstance.CurrentState); + Assert.That(reInstance.CurrentState, Is.EqualTo(machine.True)); } class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/StateChangeObserver.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/StateChangeObserver.cs index 4fe11f3b726..fe455187ee8 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/StateChangeObserver.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/StateChangeObserver.cs @@ -6,7 +6,7 @@ class StateChangeObserver : IStateObserver - where T : class, ISaga + where T : class, SagaStateMachineInstance { public StateChangeObserver() { diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/State_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/State_Specs.cs index 64a1de911b2..aee763149df 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/State_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/State_Specs.cs @@ -2,7 +2,6 @@ { using System; using NUnit.Framework; - using SagaStateMachine; [TestFixture] @@ -11,33 +10,33 @@ public class When_a_state_is_declared [Test] public void It_should_capture_the_name_of_final() { - Assert.AreEqual("Final", _machine.Final.Name); + Assert.That(_machine.Final.Name, Is.EqualTo("Final")); } [Test] public void It_should_capture_the_name_of_initial() { - Assert.AreEqual("Initial", _machine.Initial.Name); + Assert.That(_machine.Initial.Name, Is.EqualTo("Initial")); } [Test] public void It_should_capture_the_name_of_running() { - Assert.AreEqual("Running", _machine.Running.Name); + Assert.That(_machine.Running.Name, Is.EqualTo("Running")); } [Test] public void Should_be_an_instance_of_the_proper_type() { - Assert.IsInstanceOf>(_machine.Initial); + Assert.That(_machine.Initial, Is.InstanceOf.StateMachineState>()); } class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -69,7 +68,7 @@ public class When_a_state_is_stored_another_way [Test] public void It_should_get_the_name_right() { - Assert.AreEqual("Running", _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo("Running")); } TestStateMachine _machine; @@ -93,13 +92,14 @@ public void A_state_is_declared() /// an ORM that doesn't support user types (cough, EF, cough). /// class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } /// /// The CurrentState is exposed as a string for the ORM /// public string CurrentState { get; private set; } + + public Guid CorrelationId { get; set; } } @@ -127,7 +127,7 @@ public class When_storing_state_as_an_int [Test] public void It_should_get_the_name_right() { - Assert.AreEqual(_machine.Running, _machine.GetState(_instance).Result); + Assert.That(_machine.GetState(_instance).Result, Is.EqualTo(_machine.Running)); } TestStateMachine _machine; @@ -151,13 +151,14 @@ public void A_state_is_declared() /// an ORM that doesn't support user types (cough, EF, cough). /// class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } /// /// The CurrentState is exposed as a string for the ORM /// public int CurrentState { get; private set; } + + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/SubStateOnEnter_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/SubStateOnEnter_Specs.cs index 95d677b3e5e..fd744fa010e 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/SubStateOnEnter_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/SubStateOnEnter_Specs.cs @@ -26,7 +26,7 @@ public async Task Should_raise_both_enter_events() await machine.RaiseEvent(instance, machine.ToSub); // go to s21 --> Enter s2 is missing here! await machine.RaiseEvent(instance, machine.Quit); - Assert.That(eventObserver.Events.Count, Is.EqualTo(2)); + Assert.That(eventObserver.Events, Has.Count.EqualTo(2)); } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Telephone_Sample.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Telephone_Sample.cs index 57bb3e5363e..3c5bd083afe 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Telephone_Sample.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Telephone_Sample.cs @@ -26,8 +26,11 @@ public async Task Should_be_short_and_sweet() await _machine.RaiseEvent(phone, x => x.HungUp); - Assert.AreEqual(_machine.OffHook.Name, phone.CurrentState); - Assert.GreaterOrEqual(phone.CallTimer.ElapsedMilliseconds, 45); + Assert.Multiple(() => + { + Assert.That(phone.CurrentState, Is.EqualTo(_machine.OffHook.Name)); + Assert.That(phone.CallTimer.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(45)); + }); } PhoneStateMachine _machine; @@ -74,8 +77,11 @@ public async Task Should_be_short_and_sweet() await _machine.RaiseEvent(phone, x => x.TakenOffHold); await _machine.RaiseEvent(phone, x => x.HungUp); - Assert.AreEqual(_machine.OffHook.Name, phone.CurrentState); - Assert.GreaterOrEqual(phone.CallTimer.ElapsedMilliseconds, 45); + Assert.Multiple(() => + { + Assert.That(phone.CurrentState, Is.EqualTo(_machine.OffHook.Name)); + Assert.That(phone.CallTimer.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(45)); + }); } PhoneStateMachine _machine; @@ -105,8 +111,11 @@ public async Task Should_end__badly() await _machine.RaiseEvent(phone, x => x.HungUp); - Assert.AreEqual(_machine.OffHook.Name, phone.CurrentState); - Assert.GreaterOrEqual(phone.CallTimer.ElapsedMilliseconds, 45); + Assert.Multiple(() => + { + Assert.That(phone.CurrentState, Is.EqualTo(_machine.OffHook.Name)); + Assert.That(phone.CallTimer.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(45)); + }); } PhoneStateMachine _machine; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Transition_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Transition_Specs.cs index be07c034d62..a59969e933d 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Transition_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Transition_Specs.cs @@ -10,27 +10,33 @@ public class Explicitly_transitioning_to_a_state [Test] public void Should_call_the_enter_event() { - Assert.IsTrue(_instance.EnterCalled); + Assert.That(_instance.EnterCalled, Is.True); } [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_second_moved_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(_machine.Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(_machine.Running)); + }); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(2, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(2)); } Instance _instance; @@ -53,11 +59,11 @@ public void Specifying_an_event_activity() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public bool EnterCalled { get; set; } + public Guid CorrelationId { get; set; } } @@ -91,40 +97,49 @@ public class Transitioning_to_a_state_from_a_state [Test] public void Should_call_the_enter_event() { - Assert.IsTrue(_instance.EnterCalled); + Assert.That(_instance.EnterCalled, Is.True); } [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_invoked_final_entered() { - Assert.IsTrue(_instance.FinalEntered); + Assert.That(_instance.FinalEntered, Is.True); } [Test] public void Should_have_second_moved_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(_machine.Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(_machine.Running)); + }); } [Test] public void Should_have_third_moved_to_final() { - Assert.AreEqual(_machine.Running, _observer.Events[2].Previous); - Assert.AreEqual(_machine.Final, _observer.Events[2].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[2].Previous, Is.EqualTo(_machine.Running)); + Assert.That(_observer.Events[2].Current, Is.EqualTo(_machine.Final)); + }); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(3, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(3)); } Instance _instance; @@ -148,13 +163,13 @@ public void Specifying_an_event_activity() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public bool EnterCalled { get; set; } public bool FinalEntered { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/UnobservedEvent_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/UnobservedEvent_Specs.cs index f7c439dbd65..7ffca25045e 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/UnobservedEvent_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/UnobservedEvent_Specs.cs @@ -127,7 +127,7 @@ public async Task Should_also_ignore_yet_process_invalid_events() await _machine.RaiseEvent(instance, x => x.Charge, new A { Volts = 12 }); - Assert.AreEqual(0, instance.Volts); + Assert.That(instance.Volts, Is.EqualTo(0)); } [Test] @@ -137,11 +137,11 @@ public async Task Should_have_the_next_event_even_though_ignored() await _machine.RaiseEvent(instance, x => x.Start); - Assert.AreEqual(_machine.Running, await _machine.GetState(instance)); + Assert.That(await _machine.GetState(instance), Is.EqualTo(_machine.Running)); var nextEvents = await _machine.NextEvents(instance); - Assert.IsTrue(nextEvents.Any(x => x.Name.Equals("Charge"))); + Assert.That(nextEvents.Any(x => x.Name.Equals("Charge")), Is.True); } [Test] diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Visualizer_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Visualizer_Specs.cs index febe74db93b..9b4e0359b5d 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Visualizer_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Automatonymous/Visualizer_Specs.cs @@ -12,35 +12,35 @@ public class When_visualizing_a_state_machine [Test] public void Should_parse_the_graph() { - Assert.IsNotNull(_graph); + Assert.That(_graph, Is.Not.Null); } [Test] - public void Should_show_the_goods() + public void Should_show_the_differences() { - var generator = new StateMachineGraphvizGenerator(_graph); - - string dots = generator.CreateDotFile(); + var dotsAssigned = new StateMachineGraphvizGenerator(new TestStateMachine().GetGraph()).CreateDotFile(); - Console.WriteLine(dots); + Console.WriteLine(dotsAssigned); - var expected = Expected.Replace("\r", "").Replace("\n", Environment.NewLine); + var expectedAssigned = ExpectedAssigned.Replace("\r", "").Replace("\n", Environment.NewLine); + var expectedNotAssigned = ExpectedNotAssigned.Replace("\r", "").Replace("\n", Environment.NewLine); - Assert.AreEqual(expected, dots); + Assert.That(dotsAssigned, Is.EqualTo(expectedAssigned)); + Assert.That(dotsAssigned, Is.Not.EqualTo(expectedNotAssigned)); } [Test] - public void Should_show_the_differences() + public void Should_show_the_goods() { - var dotsAssigned = new StateMachineGraphvizGenerator(new TestStateMachine().GetGraph()).CreateDotFile(); + var generator = new StateMachineGraphvizGenerator(_graph); - Console.WriteLine(dotsAssigned); + var dots = generator.CreateDotFile(); - var expectedAssigned = ExpectedAssigned.Replace("\r", "").Replace("\n", Environment.NewLine); - var expectedNotAssigned = ExpectedNotAssigned.Replace("\r", "").Replace("\n", Environment.NewLine); + Console.WriteLine(dots); + + var expected = Expected.Replace("\r", "").Replace("\n", Environment.NewLine); - Assert.AreEqual(expectedAssigned, dotsAssigned); - Assert.AreNotEqual(expectedNotAssigned, dotsAssigned); + Assert.That(dots, Is.EqualTo(expected)); } InstanceStateMachine _machine; @@ -116,11 +116,12 @@ public void Setup() 6 -> 5; }"; + class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -168,10 +169,10 @@ class RestartData public string Name { get; set; } } + class CompositeInstance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public CompositeEventStatus CompositeStatus { get; set; } public bool Called { get; set; } public bool CalledAfterAll { get; set; } @@ -179,6 +180,7 @@ class CompositeInstance : public bool SecondFirst { get; set; } public bool First { get; set; } public bool Second { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/BaseClass_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/BaseClass_Specs.cs new file mode 100644 index 00000000000..fcd10695a7e --- /dev/null +++ b/tests/MassTransit.Tests/SagaStateMachineTests/BaseClass_Specs.cs @@ -0,0 +1,123 @@ +namespace MassTransit.Tests.SagaStateMachineTests +{ + using System.Threading.Tasks; + using BaseStateMachineTestSubjects; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + + + [TestFixture] + public class Using_a_base_state_machine + { + [Test] + public async Task Should_initialize_all_states_and_events() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddSagaStateMachine(); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var id = NewId.NextGuid(); + + await harness.Bus.Publish(new HappyEvent(id)); + + Assert.That(await harness.Consumed.Any()); + + await harness.Bus.Publish(new EndItAllEvent(id)); + + Assert.That(await harness.Consumed.Any()); + } + } + + + namespace BaseStateMachineTestSubjects + { + using System; + + + public class HappyEvent + { + public HappyEvent(Guid correlationId) + { + CorrelationId = correlationId; + } + + public Guid CorrelationId { get; set; } + } + + + public class GoLuckyEvent + { + public GoLuckyEvent(Guid correlationId) + { + CorrelationId = correlationId; + } + + public Guid CorrelationId { get; set; } + } + + + public class EndItAllEvent + { + public EndItAllEvent(Guid correlationId) + { + CorrelationId = correlationId; + } + + public Guid CorrelationId { get; set; } + } + + + public class CommonStateMachine : + MassTransitStateMachine + where T : class, SagaStateMachineInstance + { + // + // ReSharper disable UnassignedGetOnlyAutoProperty + public State Happy { get; } + public State GoLucky { get; } + + public Event OnHappy { get; } + public Event OnGoLucky { get; } + } + + + public class HappyGoLuckyState : + SagaStateMachineInstance + { + public string CurrentState { get; set; } + public Guid CorrelationId { get; set; } + } + + + public class HappyGoLuckyStateMachine : + CommonStateMachine + { + public HappyGoLuckyStateMachine() + { + InstanceState(x => x.CurrentState); + + Initially( + When(OnHappy) + .TransitionTo(Happy), + When(OnGoLucky) + .TransitionTo(GoLucky)); + + During(Happy, GoLucky, + When(OnEndItAll) + .TransitionTo(Finished)); + + SetCompletedWhenFinalized(); + } + + public State Finished { get; } + + public Event OnEndItAll { get; } + } + } +} diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/CatchFault_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/CatchFault_Specs.cs index a3ad84860f6..8110641cc98 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/CatchFault_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/CatchFault_Specs.cs @@ -95,13 +95,16 @@ public async Task Should_receive_the_caught_fault_response() Response startFaulted = await Bus.Request(InputQueueAddress, message, TestCancellationToken, TestTimeout); - Assert.AreEqual(message.CorrelationId, startFaulted.CorrelationId); + Assert.That(startFaulted.CorrelationId, Is.EqualTo(message.CorrelationId)); ConsumeContext context = await serviceFaulted; - Assert.AreEqual(message.CorrelationId, context.CorrelationId); + await Assert.MultipleAsync(async () => + { + Assert.That(context.CorrelationId, Is.EqualTo(message.CorrelationId)); - Assert.That(await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.FailedToStart, TestTimeout), Is.Not.Null); + Assert.That(await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.FailedToStart, TestTimeout), Is.Not.Null); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -172,13 +175,16 @@ public async Task Should_receive_the_caught_fault_response() Response startFaulted = await Bus.Request(InputQueueAddress, message, TestCancellationToken, TestTimeout); - Assert.AreEqual(message.CorrelationId, startFaulted.CorrelationId); + Assert.That(startFaulted.CorrelationId, Is.EqualTo(message.CorrelationId)); ConsumeContext context = await serviceFaulted; - Assert.AreEqual(message.CorrelationId, context.CorrelationId); + await Assert.MultipleAsync(async () => + { + Assert.That(context.CorrelationId, Is.EqualTo(message.CorrelationId)); - Assert.That(await _repository.ShouldNotContainSaga(message.CorrelationId, TestTimeout), Is.Null); + Assert.That(await LoadSagaRepository.ShouldNotContainSaga(message.CorrelationId, TestTimeout), Is.Null); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -191,6 +197,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin TestStateMachine _machine; InMemorySagaRepository _repository; + ILoadSagaRepository LoadSagaRepository => _repository; class Instance : diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/CatchInitial_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/CatchInitial_Specs.cs index 313f580751c..178100f6b0b 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/CatchInitial_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/CatchInitial_Specs.cs @@ -152,9 +152,12 @@ public async Task Should_discard_when_finalized() ISagaStateMachineTestHarness sagaHarness = harness.GetSagaStateMachineHarness(); - Assert.That(await sagaHarness.Consumed.Any(), Is.True); + await Assert.MultipleAsync(async () => + { + Assert.That(await sagaHarness.Consumed.Any(), Is.True); - Assert.That(await sagaHarness.NotExists(id), Is.Null); + Assert.That(await sagaHarness.NotExists(id), Is.Null); + }); } } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEventUpgrade_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEventUpgrade_Specs.cs index c1d41c56ba9..a90e1bebc83 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEventUpgrade_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEventUpgrade_Specs.cs @@ -27,11 +27,14 @@ await InputQueueSendEndpoint.Send(new }); ConsumeContext registered = await handler; Guid? saga = await _repository.ShouldContainSagaInState(memberId, _machine, x => x.Rejected, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var sagaInstance = _repository[saga.Value].Instance; - Assert.AreEqual("Invalid ID", sagaInstance.Result); - Assert.AreEqual("REJECTED!", sagaInstance.Name); - Assert.AreEqual("REJECTED!", sagaInstance.Surname); + Assert.Multiple(() => + { + Assert.That(sagaInstance.Result, Is.EqualTo("Invalid ID")); + Assert.That(sagaInstance.Name, Is.EqualTo("REJECTED!")); + Assert.That(sagaInstance.Surname, Is.EqualTo("REJECTED!")); + }); } [Test] @@ -47,11 +50,14 @@ await InputQueueSendEndpoint.Send(new }); ConsumeContext registered = await handler; Guid? saga = await _repository.ShouldContainSagaInState(memberId, _machine, x => x.Registered, TestInactivityTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var sagaInstance = _repository[saga.Value].Instance; - Assert.AreEqual("Success", sagaInstance.Result); - Assert.AreEqual("Frank", sagaInstance.Name); - Assert.AreEqual("Castle", sagaInstance.Surname); + Assert.Multiple(() => + { + Assert.That(sagaInstance.Result, Is.EqualTo("Success")); + Assert.That(sagaInstance.Name, Is.EqualTo("Frank")); + Assert.That(sagaInstance.Surname, Is.EqualTo("Castle")); + }); } [Test] @@ -67,11 +73,14 @@ await InputQueueSendEndpoint.Send(new }); ConsumeContext registered = await handler; Guid? saga = await _repository.ShouldContainSagaInState(memberId, _machine, x => x.Rejected, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var sagaInstance = _repository[saga.Value].Instance; - Assert.AreEqual("Invalid Name", sagaInstance.Result); - Assert.AreEqual("REJECTED!", sagaInstance.Name); - Assert.AreEqual("Kent", sagaInstance.Surname); + Assert.Multiple(() => + { + Assert.That(sagaInstance.Result, Is.EqualTo("Invalid Name")); + Assert.That(sagaInstance.Name, Is.EqualTo("REJECTED!")); + Assert.That(sagaInstance.Surname, Is.EqualTo("Kent")); + }); } [Test] @@ -95,11 +104,14 @@ await InputQueueSendEndpoint.Send(new }); ConsumeContext registered = await handler; Guid? saga = await _repository.ShouldContainSagaInState(memberId, _machine, x => x.Rejected, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var sagaInstance = _repository[saga.Value].Instance; - Assert.AreEqual("Invalid Surname", sagaInstance.Result); - Assert.AreEqual("Peter", sagaInstance.Name); - Assert.AreEqual("REJECTED!", sagaInstance.Surname); + Assert.Multiple(() => + { + Assert.That(sagaInstance.Result, Is.EqualTo("Invalid Surname")); + Assert.That(sagaInstance.Name, Is.EqualTo("Peter")); + Assert.That(sagaInstance.Surname, Is.EqualTo("REJECTED!")); + }); } InMemorySagaRepository _repository; @@ -257,7 +269,6 @@ public TestStateMachine() }) .PublishAsync(context => context.Init(context.Instance)) .TransitionTo(Registered) - , //-- diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEvent_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEvent_Specs.cs index a4e8df9e393..fcf0e529edd 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEvent_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEvent_Specs.cs @@ -16,16 +16,17 @@ public async Task Should_have_called_combined_event() { var message = new StartMessage(); - Task> received = await ConnectPublishHandler(x => x.Message.CorrelationId == message.CorrelationId); + Task> received = + await ConnectPublishHandler(x => x.Message.CorrelationId == message.CorrelationId); await Bus.Publish(message); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.Waiting, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await Bus.Publish(new FirstMessage(message.CorrelationId)); saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.WaitingForSecond, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await Bus.Publish(new SecondMessage(message.CorrelationId)); diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEventsInInitialState_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEventsInInitialState_Specs.cs index 3238a483f36..dba4978a1ce 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEventsInInitialState_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/CompositeEventsInInitialState_Specs.cs @@ -18,19 +18,19 @@ public async Task Should_have_combined_event() var firstMessage = new FirstMessage(correlationId); var secondMessage = new SecondMessage(correlationId); + Task> received = + await ConnectPublishHandler(x => x.Message.CorrelationId == correlationId); + await InputQueueSendEndpoint.Send(firstMessage); await InputQueueSendEndpoint.Send(secondMessage); Guid? saga = await _repository.ShouldContainSaga(x => x.CorrelationId == correlationId, TestTimeout); - Assert.IsTrue(saga.HasValue); - - Task> received = - await ConnectPublishHandler(x => x.Message.CorrelationId == correlationId); + Assert.That(saga.HasValue, Is.True); await received; } - InMemorySagaRepository _repository; + ISagaRepository _repository; protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) { diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/ConfigureConsumeTopology_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/ConfigureConsumeTopology_Specs.cs index 546aa615985..29faa56d6b4 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/ConfigureConsumeTopology_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/ConfigureConsumeTopology_Specs.cs @@ -16,24 +16,24 @@ public async Task Should_not_bind_the_event_handler() { var sagaId = NewId.NextGuid(); - await Bus.Publish(new Start {CorrelationId = sagaId}); + await Bus.Publish(new Start { CorrelationId = sagaId }); Guid? saga = await _repository.ShouldContainSaga(sagaId, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var handler = await ConnectPublishHandler(); - await Bus.Publish(new Suspend {CorrelationId = sagaId}); + await Bus.Publish(new Suspend { CorrelationId = sagaId }); await handler; saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); - await Bus.Publish(new Stop() {CorrelationId = sagaId}); + await Bus.Publish(new Stop() { CorrelationId = sagaId }); saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Final, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -42,7 +42,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } readonly TestStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; public Specifying_no_topology() { @@ -79,9 +79,7 @@ public TestStateMachine() .TransitionTo(Sus), When(Stopped) .Finalize()); - } - - // ReSharper disable UnassignedGetOnlyAutoProperty + } // ReSharper disable UnassignedGetOnlyAutoProperty public State Running { get; } public State Sus { get; } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/CorrelateGuid_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/CorrelateGuid_Specs.cs index e5c06ff3c55..259ca190b5c 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/CorrelateGuid_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/CorrelateGuid_Specs.cs @@ -20,12 +20,12 @@ public async Task Should_properly_map_to_the_instance() Guid? saga = await _repository.ShouldContainSagaInState(state => state.TransactionId == id, _machine, x => x.Active, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await Bus.Publish(new { TransactionId = id }); saga = await _repository.ShouldContainSagaInState(state => state.TransactionId == id, _machine, x => x.Final, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } InMemorySagaRepository _repository; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/CorrelateUsingTopology_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/CorrelateUsingTopology_Specs.cs index 532037ebe1f..e2c61796a62 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/CorrelateUsingTopology_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/CorrelateUsingTopology_Specs.cs @@ -10,6 +10,9 @@ namespace MassTransit.Tests.SagaStateMachineTests namespace CorrelationEvents { + using System; + + public interface BeginTransaction { Guid TransactionId { get; } @@ -46,12 +49,12 @@ public async Task Should_properly_map_to_the_instance() Guid? saga = await _repository.ShouldContainSagaInState(id, _machine, x => x.Active, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await Bus.Publish(new { TransactionId = id }); saga = await _repository.ShouldContainSagaInState(id, _machine, x => x.Final, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } static Using_topology_for_event_correlation() diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Activity_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Activity_Specs.cs index 3b3a1c3780d..cf30336e5ec 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Activity_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Activity_Specs.cs @@ -10,7 +10,7 @@ public class When_specifying_an_event_activity [Test] public void Should_transition_to_the_proper_state() { - Assert.AreEqual(Running, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(Running)); } State Running; @@ -29,7 +29,7 @@ public void Specifying_an_event_activity() .Event("Initialized", out Initialized) .InstanceState(b => b.CurrentState) .During(builder.Initial) - .When(Initialized, b => b.TransitionTo(Running)) + .When(Initialized, b => b.TransitionTo(Running)) ); _machine.RaiseEvent(_instance, Initialized) @@ -38,10 +38,10 @@ public void Specifying_an_event_activity() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } } @@ -52,7 +52,7 @@ public class When_specifying_an_event_activity_using_initially [Test] public void Should_transition_to_the_proper_state() { - Assert.AreEqual(Running, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(Running)); } State Running; @@ -71,7 +71,7 @@ public void Specifying_an_event_activity() .Event("Initialized", out Initialized) .InstanceState(b => b.CurrentState) .During(builder.Initial) - .When(Initialized, b => b.TransitionTo(Running)) + .When(Initialized, b => b.TransitionTo(Running)) ); _machine.RaiseEvent(_instance, Initialized); @@ -79,10 +79,10 @@ public void Specifying_an_event_activity() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -109,13 +109,13 @@ public class When_specifying_an_event_activity_using_finally [Test] public void Should_have_called_the_finally_activity() { - Assert.AreEqual(Finalized, _instance.Value); + Assert.That(_instance.Value, Is.EqualTo(Finalized)); } [Test] public void Should_transition_to_the_proper_state() { - Assert.AreEqual(_machine.Final, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Final)); } const string Finalized = "Finalized"; @@ -138,7 +138,7 @@ public void Specifying_an_event_activity() .Event("Initialized", out Initialized) .InstanceState(b => b.CurrentState) .During(builder.Initial) - .When(Initialized, b => b.Finalize()) + .When(Initialized, b => b.Finalize()) .Finally(b => b.Then(context => context.Instance.Value = Finalized)) ); @@ -148,11 +148,11 @@ public void Specifying_an_event_activity() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public string Value { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } } @@ -163,25 +163,25 @@ public class When_hooking_the_initial_enter_state_event [Test] public void Should_call_the_activity() { - Assert.AreEqual(_machine.Final, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(_machine.Final)); } [Test] public void Should_have_trigger_the_final_before_enter_event() { - Assert.AreEqual(Running, _instance.FinalState); + Assert.That(_instance.FinalState, Is.EqualTo(Running)); } [Test] public void Should_have_triggered_the_after_leave_event() { - Assert.AreEqual(_machine.Initial, _instance.LeftState); + Assert.That(_instance.LeftState, Is.EqualTo(_machine.Initial)); } [Test] public void Should_have_triggered_the_before_enter_event() { - Assert.AreEqual(Initializing, _instance.EnteredState); + Assert.That(_instance.EnteredState, Is.EqualTo(Initializing)); } State Running; @@ -202,27 +202,28 @@ public void Specifying_an_event_activity() .Event("Initialized", out Initialized) .InstanceState(b => b.CurrentState) .During(Initializing) - .When(Initialized, b => b.TransitionTo(Running)) + .When(Initialized, b => b.TransitionTo(Running)) .DuringAny() - .When(builder.Initial.Enter, b => b.TransitionTo(Initializing)) - .When(builder.Initial.AfterLeave, b => b.Then(context => context.Instance.LeftState = context.Data)) - .When(Initializing.BeforeEnter, b => b.Then(context => context.Instance.EnteredState = context.Data)) - .When(Running.Enter, b => b.Finalize()) - .When(builder.Final.BeforeEnter, b => b.Then(context => context.Instance.FinalState = context.Instance.CurrentState)) + .When(builder.Initial.Enter, b => b.TransitionTo(Initializing)) + .When(builder.Initial.AfterLeave, b => b.Then(context => context.Instance.LeftState = context.Data)) + .When(Initializing.BeforeEnter, b => b.Then(context => context.Instance.EnteredState = context.Data)) + .When(Running.Enter, b => b.Finalize()) + .When(builder.Final.BeforeEnter, b => b.Then(context => context.Instance.FinalState = context.Instance.CurrentState)) ); _machine.RaiseEvent(_instance, Initialized) .Wait(); } + class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public State EnteredState { get; set; } public State LeftState { get; set; } public State FinalState { get; set; } + public Guid CorrelationId { get; set; } } } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/AnyStateTransition_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/AnyStateTransition_Specs.cs index b2966ef1acb..6887908f5b1 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/AnyStateTransition_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/AnyStateTransition_Specs.cs @@ -10,19 +10,19 @@ public class When_any_state_transition_occurs [Test] public void Should_be_running() { - Assert.AreEqual(Running, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(Running)); } [Test] public void Should_have_entered_running() { - Assert.AreEqual(Running, _instance.LastEntered); + Assert.That(_instance.LastEntered, Is.EqualTo(Running)); } [Test] public void Should_have_left_initial() { - Assert.AreEqual(_machine.Initial, _instance.LastLeft); + Assert.That(_instance.LastLeft, Is.EqualTo(_machine.Initial)); } State Running; @@ -43,9 +43,9 @@ public void Setup() .Event("Finish", out Finish) .InstanceState(b => b.CurrentState) .During(builder.Initial) - .When(Initialized, b => b.TransitionTo(Running)) + .When(Initialized, b => b.TransitionTo(Running)) .During(Running) - .When(Finish, b => b.Finalize()) + .When(Finish, b => b.Finalize()) .BeforeEnterAny(b => b.Then(context => context.Instance.LastEntered = context.Data)) .AfterLeaveAny(b => b.Then(context => context.Instance.LastLeft = context.Data)) ); @@ -56,13 +56,13 @@ public void Setup() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public State LastEntered { get; set; } public State LastLeft { get; set; } + public Guid CorrelationId { get; set; } } } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Anytime_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Anytime_Specs.cs index 30724d6ddb4..b332e5c3568 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Anytime_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Anytime_Specs.cs @@ -16,8 +16,11 @@ public async Task Should_be_called_regardless_of_state() await _machine.RaiseEvent(instance, Init); await _machine.RaiseEvent(instance, Hello); - Assert.IsTrue(instance.HelloCalled); - Assert.AreEqual(_machine.Final, instance.CurrentState); + Assert.Multiple(() => + { + Assert.That(instance.HelloCalled, Is.True); + Assert.That(instance.CurrentState, Is.EqualTo(_machine.Final)); + }); } [Test] @@ -26,13 +29,13 @@ public async Task Should_have_value_of_event_data() var instance = new Instance(); await _machine.RaiseEvent(instance, Init); - await _machine.RaiseEvent(instance, EventA, new A + await _machine.RaiseEvent(instance, EventA, new A { Value = "Test" }); + + Assert.Multiple(() => { - Value = "Test" + Assert.That(instance.AValue, Is.EqualTo("Test")); + Assert.That(instance.CurrentState, Is.EqualTo(_machine.Final)); }); - - Assert.AreEqual("Test", instance.AValue); - Assert.AreEqual(_machine.Final, instance.CurrentState); } [Test] @@ -42,8 +45,11 @@ public void Should_not_be_handled_on_initial() Assert.That(async () => await _machine.RaiseEvent(instance, Hello), Throws.TypeOf()); - Assert.IsFalse(instance.HelloCalled); - Assert.AreEqual(_machine.Initial, instance.CurrentState); + Assert.Multiple(() => + { + Assert.That(instance.HelloCalled, Is.False); + Assert.That(instance.CurrentState, Is.EqualTo(_machine.Initial)); + }); } State Ready; @@ -63,31 +69,33 @@ public void A_state_is_declared() .Event("Hello", out Hello) .Event("EventA", out EventA) .Initially() - .When(Init, b => b.TransitionTo(Ready)) + .When(Init, b => b.TransitionTo(Ready)) .DuringAny() - .When(Hello, b => b - .Then(context => context.Instance.HelloCalled = true) - .Finalize() - ) - .When(EventA, b => b - .Then(context => context.Instance.AValue = context.Data.Value) - .Finalize() - ) + .When(Hello, b => b + .Then(context => context.Instance.HelloCalled = true) + .Finalize() + ) + .When(EventA, b => b + .Then(context => context.Instance.AValue = context.Data.Value) + .Finalize() + ) ); } + class A { public string Value { get; set; } } + class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public bool HelloCalled { get; set; } public string AValue { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/AsyncActivity_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/AsyncActivity_Specs.cs index 698db365bfe..bd623d61305 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/AsyncActivity_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/AsyncActivity_Specs.cs @@ -28,12 +28,12 @@ public async Task Should_capture_the_value() await machine.RaiseEvent(claim, Create, new CreateInstance()); - Assert.AreEqual("ExecuteAsync", claim.Value); + Assert.That(claim.Value, Is.EqualTo("ExecuteAsync")); } class TestInstance : -SagaStateMachineInstance + SagaStateMachineInstance { public State CurrentState { get; set; } public string Value { get; set; } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Combine_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Combine_Specs.cs index a978efaed20..ea2ec5fd25f 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Combine_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Combine_Specs.cs @@ -18,7 +18,7 @@ public async Task Should_have_called_combined_event() await _machine.RaiseEvent(_instance, First); await _machine.RaiseEvent(_instance, Second); - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] @@ -30,7 +30,7 @@ public async Task Should_not_call_for_one_event() await _machine.RaiseEvent(_instance, First); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } [Test] @@ -42,7 +42,7 @@ public async Task Should_not_call_for_one_other_event() await _machine.RaiseEvent(_instance, Second); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } State Waiting; @@ -56,14 +56,15 @@ public async Task Should_not_call_for_one_other_event() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public CompositeEventStatus CompositeStatus { get; set; } public bool Called { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } + private StateMachine CreateStateMachine() { return MassTransitStateMachine @@ -74,12 +75,12 @@ private StateMachine CreateStateMachine() .Event("Second", out Second) .CompositeEvent("Third", out Third, b => b.CompositeStatus, First, Second) .Initially() - .When(Start, b => b.TransitionTo(Waiting)) + .When(Start, b => b.TransitionTo(Waiting)) .During(Waiting) - .When(Third, b => b - .Then(context => context.Instance.Called = true) - .Finalize() - ) + .When(Third, b => b + .Then(context => context.Instance.Called = true) + .Finalize() + ) ); } } @@ -95,14 +96,17 @@ public async Task Should_have_called_combined_event() _instance = new Instance(); await _machine.RaiseEvent(_instance, Start); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); await _machine.RaiseEvent(_instance, First); await _machine.RaiseEvent(_instance, Second); - Assert.IsTrue(_instance.Called); + Assert.Multiple(() => + { + Assert.That(_instance.Called, Is.True); - Assert.AreEqual(2, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(2)); + }); } [Test] @@ -112,7 +116,7 @@ public async Task Should_have_initial_state_with_zero() _instance = new Instance(); await _machine.RaiseEvent(_instance, Start); - Assert.AreEqual(3, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(3)); } [Test] @@ -124,7 +128,7 @@ public async Task Should_not_call_for_one_event() await _machine.RaiseEvent(_instance, First); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } [Test] @@ -136,7 +140,7 @@ public async Task Should_not_call_for_one_other_event() await _machine.RaiseEvent(_instance, Second); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } State Waiting; @@ -150,14 +154,15 @@ public async Task Should_not_call_for_one_other_event() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public int CompositeStatus { get; set; } public bool Called { get; set; } public int CurrentState { get; set; } + public Guid CorrelationId { get; set; } } + private StateMachine CreateStateMachine() { return MassTransitStateMachine @@ -168,13 +173,13 @@ private StateMachine CreateStateMachine() .Event("Second", out Second) .InstanceState(b => b.CurrentState) .Initially() - .When(Start, b => b.TransitionTo(Waiting)) + .When(Start, b => b.TransitionTo(Waiting)) .CompositeEvent("Third", out Third, b => b.CompositeStatus, First, Second) .During(Waiting) - .When(Third, b => b - .Then(context => context.Instance.Called = true) - .Finalize() - ) + .When(Third, b => b + .Then(context => context.Instance.Called = true) + .Finalize() + ) ); } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/CompositeCondition_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/CompositeCondition_Specs.cs index f7f731e7fc9..5c4890b18a5 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/CompositeCondition_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/CompositeCondition_Specs.cs @@ -18,8 +18,11 @@ public async Task Should_call_when_met() await _machine.RaiseEvent(_instance, Second); await _machine.RaiseEvent(_instance, First); - Assert.IsTrue(_instance.Called); - Assert.IsTrue(_instance.SecondFirst); + Assert.Multiple(() => + { + Assert.That(_instance.Called, Is.True); + Assert.That(_instance.SecondFirst, Is.True); + }); } [Test] @@ -32,8 +35,11 @@ public async Task Should_skip_when_not_met() await _machine.RaiseEvent(_instance, First); await _machine.RaiseEvent(_instance, Second); - Assert.IsFalse(_instance.Called); - Assert.IsFalse(_instance.SecondFirst); + Assert.Multiple(() => + { + Assert.That(_instance.Called, Is.False); + Assert.That(_instance.SecondFirst, Is.False); + }); } State Waiting; @@ -47,9 +53,8 @@ public async Task Should_skip_when_not_met() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public CompositeEventStatus CompositeStatus { get; set; } public bool Called { get; set; } public bool CalledAfterAll { get; set; } @@ -57,8 +62,10 @@ class Instance : public bool SecondFirst { get; set; } public bool First { get; set; } public bool Second { get; set; } + public Guid CorrelationId { get; set; } } + private StateMachine CreateStateMachine() { return MassTransitStateMachine @@ -68,29 +75,29 @@ private StateMachine CreateStateMachine() .Event("First", out First) .Event("Second", out Second) .Initially() - .When(Start, b => b.TransitionTo(Waiting)) + .When(Start, b => b.TransitionTo(Waiting)) .During(Waiting) - .When(First, b => b.Then(context => - { - context.Instance.First = true; - context.Instance.CalledAfterAll = false; - })) - .When(Second, b => b.Then(context => - { - context.Instance.SecondFirst = !context.Instance.First; - context.Instance.Second = true; - context.Instance.CalledAfterAll = false; - })) + .When(First, b => b.Then(context => + { + context.Instance.First = true; + context.Instance.CalledAfterAll = false; + })) + .When(Second, b => b.Then(context => + { + context.Instance.SecondFirst = !context.Instance.First; + context.Instance.Second = true; + context.Instance.CalledAfterAll = false; + })) .CompositeEvent("Third", out Third, b => b.CompositeStatus, First, Second) .During(Waiting) - .When(Third, context => context.Instance.SecondFirst, b => b - .Then(context => - { - context.Instance.Called = true; - context.Instance.CalledAfterAll = true; - }) - .Finalize() - ) + .When(Third, context => context.Instance.SecondFirst, b => b + .Then(context => + { + context.Instance.Called = true; + context.Instance.CalledAfterAll = true; + }) + .Finalize() + ) ); } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/CompositeOrder_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/CompositeOrder_Specs.cs index 29db9d08d1c..5e56acd7c54 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/CompositeOrder_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/CompositeOrder_Specs.cs @@ -18,7 +18,7 @@ public async Task Should_have_called_combined_event() await _machine.RaiseEvent(_instance, First); await _machine.RaiseEvent(_instance, Second); - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] @@ -31,7 +31,7 @@ public async Task Should_have_called_combined_event_after_all_events() await _machine.RaiseEvent(_instance, First); await _machine.RaiseEvent(_instance, Second); - Assert.IsTrue(_instance.CalledAfterAll); + Assert.That(_instance.CalledAfterAll, Is.True); } [Test] @@ -43,7 +43,7 @@ public async Task Should_not_call_for_one_event() await _machine.RaiseEvent(_instance, First); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } [Test] @@ -55,19 +55,21 @@ public async Task Should_not_call_for_one_other_event() await _machine.RaiseEvent(_instance, Second); - Assert.IsFalse(_instance.Called); + Assert.That(_instance.Called, Is.False); } + class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public CompositeEventStatus CompositeStatus { get; set; } public bool Called { get; set; } public bool CalledAfterAll { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } + State Waiting; Event Start; Event First; @@ -86,21 +88,26 @@ private StateMachine CreateStateMachine() .Event("First", out First) .Event("Second", out Second) .Initially() - .When(Start, b => b.TransitionTo(Waiting)) + .When(Start, b => b.TransitionTo(Waiting)) .During(Waiting) - .When(First, b => b.Then(context => { context.Instance.CalledAfterAll = false; })) - .When(Second, b => b.Then(context => { context.Instance.CalledAfterAll = false; })) + .When(First, b => b.Then(context => + { + context.Instance.CalledAfterAll = false; + })) + .When(Second, b => b.Then(context => + { + context.Instance.CalledAfterAll = false; + })) .CompositeEvent("Third", out Third, b => b.CompositeStatus, First, Second) .During(Waiting) - .When(Third, b => b - .Then(context => - { - context.Instance.Called = true; - context.Instance.CalledAfterAll = true; - }) - .Finalize() - ) - + .When(Third, b => b + .Then(context => + { + context.Instance.Called = true; + context.Instance.CalledAfterAll = true; + }) + .Finalize() + ) ); } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/DataActivity_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/DataActivity_Specs.cs index 9ef6cac84f1..20e70e3981e 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/DataActivity_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/DataActivity_Specs.cs @@ -10,13 +10,13 @@ public class When_specifying_an_event_activity_with_data [Test] public void Should_have_the_proper_value() { - Assert.AreEqual("Hello", _instance.Value); + Assert.That(_instance.Value, Is.EqualTo("Hello")); } [Test] public void Should_transition_to_the_proper_state() { - Assert.AreEqual(Running, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(Running)); } State Running; @@ -45,12 +45,12 @@ public void Specifying_an_event_activity_with_data() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public string Value { get; set; } public int OtherValue { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Declarative_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Declarative_Specs.cs index 55dc74632ec..b17d17e336a 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Declarative_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Declarative_Specs.cs @@ -10,8 +10,11 @@ public class When_an_instance_has_multiple_states [Test] public void Should_handle_both_states() { - Assert.AreEqual(TopGreeted, _instance.Top); - Assert.AreEqual(BottomIgnored, _instance.Bottom); + Assert.Multiple(() => + { + Assert.That(_instance.Top, Is.EqualTo(TopGreeted)); + Assert.That(_instance.Bottom, Is.EqualTo(BottomIgnored)); + }); } State TopGreeted; @@ -34,7 +37,7 @@ public void Specifying_an_event_activity_with_data() .Event("Initialized", out TopInitialized) .InstanceState(b => b.Top) .During(builder.Initial) - .When(TopInitialized, b => b.TransitionTo(TopGreeted)) + .When(TopInitialized, b => b.TransitionTo(TopGreeted)) ); _bottom = MassTransitStateMachine .New(builder => builder @@ -42,28 +45,24 @@ public void Specifying_an_event_activity_with_data() .Event("Initialized", out BottomInitialized) .InstanceState(b => b.Bottom) .During(builder.Initial) - .When(BottomInitialized, b => b.TransitionTo(BottomIgnored)) + .When(BottomInitialized, b => b.TransitionTo(BottomIgnored)) ); - _top.RaiseEvent(_instance, TopInitialized, new Init - { - Value = "Hello" - }).Wait(); + _top.RaiseEvent(_instance, TopInitialized, new Init { Value = "Hello" }).Wait(); - _bottom.RaiseEvent(_instance, BottomInitialized, new Init - { - Value = "Goodbye" - }).Wait(); + _bottom.RaiseEvent(_instance, BottomInitialized, new Init { Value = "Goodbye" }).Wait(); } + class MyState : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State Top { get; set; } public State Bottom { get; set; } + public Guid CorrelationId { get; set; } } + class Init { public string Value { get; set; } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Dependency_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Dependency_Specs.cs index c97045a227d..3ee945309da 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Dependency_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Dependency_Specs.cs @@ -12,7 +12,7 @@ public class Having_a_dependency_available [Test] public void Should_capture_the_value() { - Assert.AreEqual("79", _claim.Value); + Assert.That(_claim.Value, Is.EqualTo("79")); } State Running; @@ -52,11 +52,11 @@ public void Specifying_an_event_activity() class ClaimAdjustmentInstance : ClaimAdjustment, -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public string Value { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/EventObservable_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/EventObservable_Specs.cs index 40d5245299e..ad56c242ae8 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/EventObservable_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/EventObservable_Specs.cs @@ -11,13 +11,13 @@ public class When_an_event_is_raised_on_an_instance [Test] public void Should_have_raised_the_initialized_event() { - Assert.AreEqual(Initialized, _observer.Events[0].Event); + Assert.That(_observer.Events[0].Event, Is.EqualTo(Initialized)); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(1, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(1)); } State Running; @@ -35,7 +35,7 @@ public void Specifying_an_event_activity() .Event("Initialized", out Initialized) .State("Running", out Running) .During(builder.Initial) - .When(Initialized, b => b.TransitionTo(Running)) + .When(Initialized, b => b.TransitionTo(Running)) ); _observer = new EventRaisedObserver(); @@ -43,11 +43,12 @@ public void Specifying_an_event_activity() _machine.RaiseEvent(_instance, Initialized).Wait(); } + class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Event_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Event_Specs.cs index e859064f65d..db554416997 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Event_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Event_Specs.cs @@ -11,31 +11,31 @@ public class When_an_event_is_declared [Test] public void It_should_capture_a_simple_event_name() { - Assert.AreEqual("Hello", _hello.Name); + Assert.That(_hello.Name, Is.EqualTo("Hello")); } [Test] public void It_should_capture_the_data_event_name() { - Assert.AreEqual("EventA", _eventA.Name); + Assert.That(_eventA.Name, Is.EqualTo("EventA")); } [Test] public void It_should_create_configured_events() { - Assert.IsInstanceOf(_eventB); + Assert.That(_eventB, Is.InstanceOf()); } [Test] public void It_should_create_the_proper_event_type_for_data_events() { - Assert.IsInstanceOf>(_eventA); + Assert.That(_eventA, Is.InstanceOf>()); } [Test] public void It_should_create_the_proper_event_type_for_simple_events() { - Assert.IsInstanceOf(_hello); + Assert.That(_hello, Is.InstanceOf()); } Event _hello; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Exception_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Exception_Specs.cs index 449be6eb405..fea6f71b91f 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Exception_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Exception_Specs.cs @@ -11,79 +11,79 @@ public class When_an_action_throws_an_exception [Test] public void Should_capture_the_exception_message() { - Assert.AreEqual("Boom!", _instance.ExceptionMessage); + Assert.That(_instance.ExceptionMessage, Is.EqualTo("Boom!")); } [Test] public void Should_capture_the_exception_type() { - Assert.AreEqual(typeof(ApplicationException), _instance.ExceptionType); + Assert.That(_instance.ExceptionType, Is.EqualTo(typeof(ApplicationException))); } [Test] public void Should_have_called_the_async_if_block() { - Assert.IsTrue(_instance.CalledThenClauseAsync); + Assert.That(_instance.CalledThenClauseAsync, Is.True); } [Test] public void Should_have_called_the_async_then_block() { - Assert.IsTrue(_instance.ThenAsyncShouldBeCalled); + Assert.That(_instance.ThenAsyncShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_exception_handler() { - Assert.AreEqual(Failed, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(Failed)); } [Test] public void Should_have_called_the_false_async_condition_else_block() { - Assert.IsTrue(_instance.ElseAsyncShouldBeCalled); + Assert.That(_instance.ElseAsyncShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_false_condition_else_block() { - Assert.IsTrue(_instance.ElseShouldBeCalled); + Assert.That(_instance.ElseShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_first_action() { - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] public void Should_have_called_the_first_if_block() { - Assert.IsTrue(_instance.CalledThenClause); + Assert.That(_instance.CalledThenClause, Is.True); } [Test] public void Should_not_have_called_the_false_async_condition_then_block() { - Assert.IsFalse(_instance.ThenAsyncShouldNotBeCalled); + Assert.That(_instance.ThenAsyncShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_false_condition_then_block() { - Assert.IsFalse(_instance.ThenShouldNotBeCalled); + Assert.That(_instance.ThenShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_regular_exception() { - Assert.IsFalse(_instance.ShouldNotBeCalled); + Assert.That(_instance.ShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_second_action() { - Assert.IsTrue(_instance.NotCalled); + Assert.That(_instance.NotCalled, Is.True); } State Failed; @@ -147,8 +147,6 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } - public Instance() { NotCalled = true; @@ -171,6 +169,7 @@ public Instance() public bool ElseAsyncShouldBeCalled { get; set; } public bool ThenAsyncShouldBeCalled { get; set; } + public Guid CorrelationId { get; set; } } } @@ -181,31 +180,31 @@ public class When_the_exception_does_not_match_the_type [Test] public void Should_capture_the_exception_message() { - Assert.AreEqual("Boom!", _instance.ExceptionMessage); + Assert.That(_instance.ExceptionMessage, Is.EqualTo("Boom!")); } [Test] public void Should_capture_the_exception_type() { - Assert.AreEqual(typeof(ApplicationException), _instance.ExceptionType); + Assert.That(_instance.ExceptionType, Is.EqualTo(typeof(ApplicationException))); } [Test] public void Should_have_called_the_exception_handler() { - Assert.AreEqual(Failed, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(Failed)); } [Test] public void Should_have_called_the_first_action() { - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] public void Should_not_have_called_the_second_action() { - Assert.IsTrue(_instance.NotCalled); + Assert.That(_instance.NotCalled, Is.True); } State Failed; @@ -248,8 +247,6 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } - public Instance() { NotCalled = true; @@ -260,6 +257,7 @@ public Instance() public Type ExceptionType { get; set; } public string ExceptionMessage { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } } @@ -270,7 +268,7 @@ public class When_the_exception_is_caught [Test] public void Should_have_called_the_subsequent_action() { - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } Instance _instance; @@ -305,9 +303,9 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public bool Called { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } } @@ -318,67 +316,67 @@ public class When_an_action_throws_an_exception_on_data_events [Test] public void Should_capture_the_exception_message() { - Assert.AreEqual("Boom!", _instance.ExceptionMessage); + Assert.That(_instance.ExceptionMessage, Is.EqualTo("Boom!")); } [Test] public void Should_capture_the_exception_type() { - Assert.AreEqual(typeof(ApplicationException), _instance.ExceptionType); + Assert.That(_instance.ExceptionType, Is.EqualTo(typeof(ApplicationException))); } [Test] public void Should_have_called_the_async_if_block() { - Assert.IsTrue(_instance.CalledSecondThenClause); + Assert.That(_instance.CalledSecondThenClause, Is.True); } [Test] public void Should_have_called_the_exception_handler() { - Assert.AreEqual(Failed, _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo(Failed)); } [Test] public void Should_have_called_the_false_async_condition_else_block() { - Assert.IsTrue(_instance.ElseAsyncShouldBeCalled); + Assert.That(_instance.ElseAsyncShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_false_condition_else_block() { - Assert.IsTrue(_instance.ElseShouldBeCalled); + Assert.That(_instance.ElseShouldBeCalled, Is.True); } [Test] public void Should_have_called_the_first_action() { - Assert.IsTrue(_instance.Called); + Assert.That(_instance.Called, Is.True); } [Test] public void Should_have_called_the_first_if_block() { - Assert.IsTrue(_instance.CalledThenClause); + Assert.That(_instance.CalledThenClause, Is.True); } [Test] public void Should_not_have_called_the_false_async_condition_then_block() { - Assert.IsFalse(_instance.ThenAsyncShouldNotBeCalled); + Assert.That(_instance.ThenAsyncShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_false_condition_then_block() { - Assert.IsFalse(_instance.ThenShouldNotBeCalled); + Assert.That(_instance.ThenShouldNotBeCalled, Is.False); } [Test] public void Should_not_have_called_the_second_action() { - Assert.IsTrue(_instance.NotCalled); + Assert.That(_instance.NotCalled, Is.True); } State Failed; @@ -435,8 +433,6 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } - public Instance() { NotCalled = true; @@ -455,6 +451,7 @@ public Instance() public bool ElseShouldBeCalled { get; set; } public bool ThenAsyncShouldNotBeCalled { get; set; } public bool ElseAsyncShouldBeCalled { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Faulted_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Faulted_Specs.cs index 08030dc29b7..c69d860ad34 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Faulted_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Faulted_Specs.cs @@ -20,7 +20,7 @@ public void Should_capture_the_value() Assert.That(async () => await _machine.RaiseEvent(_claim, Create, data), Throws.TypeOf()); - Assert.AreEqual(default, _claim.Value); + Assert.That(_claim.Value, Is.EqualTo(default)); } Event Create; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/FilterExpression_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/FilterExpression_Specs.cs index 9fef498b309..8011813f979 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/FilterExpression_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/FilterExpression_Specs.cs @@ -23,26 +23,26 @@ public async Task Should_transition_to_the_proper_state() .Event("Thing", out Thing) .InstanceState(b => b.CurrentState) .During(builder.Initial) - .When(Thing, context => context.Data.Condition, b => b.TransitionTo(True)) - .When(Thing, context => !context.Data.Condition, b => b.TransitionTo(False)) + .When(Thing, context => context.Data.Condition, b => b.TransitionTo(True)) + .When(Thing, context => !context.Data.Condition, b => b.TransitionTo(False)) ); - await machine.RaiseEvent(instance, Thing, new Data {Condition = true}); - Assert.AreEqual(True, instance.CurrentState); + await machine.RaiseEvent(instance, Thing, new Data { Condition = true }); + Assert.That(instance.CurrentState, Is.EqualTo(True)); // reset instance.CurrentState = machine.Initial; - await machine.RaiseEvent(instance, Thing, new Data {Condition = false}); - Assert.AreEqual(False, instance.CurrentState); + await machine.RaiseEvent(instance, Thing, new Data { Condition = false }); + Assert.That(instance.CurrentState, Is.EqualTo(False)); } class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Group_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Group_Specs.cs index ca9f7272eca..bb5f7abf52c 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Group_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Group_Specs.cs @@ -16,8 +16,11 @@ public void Should_allow_parallel_execution_of_events() [Test] public void Should_have_captured_initial_data() { - Assert.AreEqual("Audi", _instance.VehicleMake); - Assert.AreEqual("A6", _instance.VehicleModel); + Assert.Multiple(() => + { + Assert.That(_instance.VehicleMake, Is.EqualTo("Audi")); + Assert.That(_instance.VehicleModel, Is.EqualTo("A6")); + }); } State BeingServiced; @@ -36,13 +39,13 @@ public void Setup() .Event("VehicleArrived", out VehicleArrived) .InstanceState(b => b.OverallState) .During(builder.Initial) - .When(VehicleArrived, b => b - .Then(context => - { - context.Instance.VehicleMake = context.Data.Make; - context.Instance.VehicleModel = context.Data.Model; - }) - .TransitionTo(BeingServiced)) + .When(VehicleArrived, b => b + .Then(context => + { + context.Instance.VehicleMake = context.Data.Make; + context.Instance.VehicleModel = context.Data.Model; + }) + .TransitionTo(BeingServiced)) ); var vehicle = new Vehicle @@ -56,9 +59,8 @@ public void Setup() class PitStopInstance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State OverallState { get; private set; } public State FuelState { get; private set; } public State OilState { get; private set; } @@ -73,36 +75,38 @@ class PitStopInstance : public decimal OilQuarts { get; set; } public decimal OilPricePerQuart { get; set; } public decimal OilCost { get; set; } + public Guid CorrelationId { get; set; } } + // NOTE: Left in place due to the incompleteness of this test. -// class PitStop : -// MassTransitStateMachine -// { -// public PitStop() -// { -// InstanceState(x => x.OverallState); - -// During(Initial, -// When(VehicleArrived) -// .Then(context => -// { -// context.Instance.VehicleMake = context.Data.Make; -// context.Instance.VehicleModel = context.Data.Model; -// }) -// .TransitionTo(BeingServiced) -//// .RunParallel(p => -//// { -//// p.Start(x => x.BeginFilling); -//// p.Start(x => x.BeginChecking); -//// })) -// ); -// } - -// public State BeingServiced { get; private set; } - -// public Event VehicleArrived { get; private set; } -// } + // class PitStop : + // MassTransitStateMachine + // { + // public PitStop() + // { + // InstanceState(x => x.OverallState); + + // During(Initial, + // When(VehicleArrived) + // .Then(context => + // { + // context.Instance.VehicleMake = context.Data.Make; + // context.Instance.VehicleModel = context.Data.Model; + // }) + // .TransitionTo(BeingServiced) + //// .RunParallel(p => + //// { + //// p.Start(x => x.BeginFilling); + //// p.Start(x => x.BeginChecking); + //// })) + // ); + // } + + // public State BeingServiced { get; private set; } + + // public Event VehicleArrived { get; private set; } + // } class FillTank : diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Introspection_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Introspection_Specs.cs index 5bee5711bef..d0a57cabeeb 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Introspection_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Introspection_Specs.cs @@ -15,35 +15,38 @@ public void The_machine_should_expose_all_events() { List events = _machine.Events.ToList(); - Assert.AreEqual(4, events.Count); - Assert.Contains(Ignored, events); - Assert.Contains(Handshake, events); - Assert.Contains(Hello, events); - Assert.Contains(YelledAt, events); + Assert.That(events, Has.Count.EqualTo(4)); + Assert.That(events, Does.Contain(Ignored)); + Assert.That(events, Does.Contain(Handshake)); + Assert.That(events, Does.Contain(Hello)); + Assert.That(events, Does.Contain(YelledAt)); } [Test] public void The_machine_should_expose_all_states() { - Assert.AreEqual(5, _machine.States.Count()); - Assert.Contains(_machine.Initial, _machine.States.ToList()); - Assert.Contains(_machine.Final, _machine.States.ToList()); - Assert.Contains(Greeted, _machine.States.ToList()); - Assert.Contains(Loved, _machine.States.ToList()); - Assert.Contains(Pissed, _machine.States.ToList()); + Assert.Multiple(() => + { + Assert.That(_machine.States.Count(), Is.EqualTo(5)); + Assert.That(_machine.States.ToList(), Does.Contain(_machine.Initial)); + }); + Assert.That(_machine.States.ToList(), Does.Contain(_machine.Final)); + Assert.That(_machine.States.ToList(), Does.Contain(Greeted)); + Assert.That(_machine.States.ToList(), Does.Contain(Loved)); + Assert.That(_machine.States.ToList(), Does.Contain(Pissed)); } [Test] public void The_machine_should_report_its_instance_type() { - Assert.AreEqual(typeof(Instance), _machine.InstanceType); + Assert.That(_machine.InstanceType, Is.EqualTo(typeof(Instance))); } [Test] public async Task The_next_events_should_be_known() { List events = (await _machine.NextEvents(_instance)).ToList(); - Assert.AreEqual(3, events.Count); + Assert.That(events, Has.Count.EqualTo(3)); } Event Ignored; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Observable_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Observable_Specs.cs index 183062d3c94..39e4d3f7b59 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Observable_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Observable_Specs.cs @@ -11,21 +11,27 @@ public class Observing_state_machine_instance_state_changes [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_second_switched_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(Running)); + }); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(3, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(3)); } State Running; @@ -63,8 +69,8 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } } @@ -75,53 +81,65 @@ public class Observing_events_with_substates [Test] public void Should_have_all_events() { - Assert.AreEqual(2, _eventObserver.Events.Count); + Assert.That(_eventObserver.Events, Has.Count.EqualTo(2)); } [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_fourth_switched_to_finished() { - Assert.AreEqual(Resting, _observer.Events[3].Previous); - Assert.AreEqual(_machine.Final, _observer.Events[3].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[3].Previous, Is.EqualTo(Resting)); + Assert.That(_observer.Events[3].Current, Is.EqualTo(_machine.Final)); + }); } [Test] public void Should_have_second_switched_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(Running)); + }); } [Test] public void Should_have_third_switched_to_resting() { - Assert.AreEqual(Running, _observer.Events[2].Previous); - Assert.AreEqual(Resting, _observer.Events[2].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[2].Previous, Is.EqualTo(Running)); + Assert.That(_observer.Events[2].Current, Is.EqualTo(Resting)); + }); } [Test] public void Should_have_transition_1() { - Assert.AreEqual("Initialized", _eventObserver.Events[0].Event.Name); + Assert.That(_eventObserver.Events[0].Event.Name, Is.EqualTo("Initialized")); } [Test] public void Should_have_transition_2() { - Assert.AreEqual("LegCramped", _eventObserver.Events[1].Event.Name); + Assert.That(_eventObserver.Events[1].Event.Name, Is.EqualTo("LegCramped")); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(4, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(4)); } State Resting; @@ -181,8 +199,8 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -232,60 +250,75 @@ public class Observing_events_with_substates_part_deux [Test] public void Should_have_all_events() { - Assert.AreEqual(2, _eventObserver.Events.Count); + Assert.That(_eventObserver.Events, Has.Count.EqualTo(2)); } [Test] public void Should_have_fifth_switched_to_finished() { - Assert.AreEqual(Running, _observer.Events[4].Previous); - Assert.AreEqual(_machine.Final, _observer.Events[4].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[4].Previous, Is.EqualTo(Running)); + Assert.That(_observer.Events[4].Current, Is.EqualTo(_machine.Final)); + }); } [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_fourth_switched_to_finished() { - Assert.AreEqual(Resting, _observer.Events[3].Previous); - Assert.AreEqual(Running, _observer.Events[3].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[3].Previous, Is.EqualTo(Resting)); + Assert.That(_observer.Events[3].Current, Is.EqualTo(Running)); + }); } [Test] public void Should_have_second_switched_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(Running)); + }); } [Test] public void Should_have_third_switched_to_resting() { - Assert.AreEqual(Running, _observer.Events[2].Previous); - Assert.AreEqual(Resting, _observer.Events[2].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[2].Previous, Is.EqualTo(Running)); + Assert.That(_observer.Events[2].Current, Is.EqualTo(Resting)); + }); } [Test] public void Should_have_transition_1() { - Assert.AreEqual("Running.BeforeEnter", _eventObserver.Events[0].Event.Name); + Assert.That(_eventObserver.Events[0].Event.Name, Is.EqualTo("Running.BeforeEnter")); } [Test] public void Should_have_transition_2() { - Assert.AreEqual("Running.AfterLeave", _eventObserver.Events[1].Event.Name); + Assert.That(_eventObserver.Events[1].Event.Name, Is.EqualTo("Running.AfterLeave")); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(5, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(5)); } State Resting; @@ -350,8 +383,8 @@ public void Specifying_an_event_activity() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/RaiseEvent_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/RaiseEvent_Specs.cs index d0060c338e2..6b9ecb25527 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/RaiseEvent_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/RaiseEvent_Specs.cs @@ -22,31 +22,31 @@ public async Task Should_include_payload() .Event("Thing", out Thing) .Event("Initialize", out Event Initialize) .During(builder.Initial) - .When(Thing, context => context.Data.Condition, b => b - .TransitionTo(True) - .Then(context => context.Raise(Initialize))) - .When(Thing, context => !context.Data.Condition, b => b - .TransitionTo(False)) + .When(Thing, context => context.Data.Condition, b => b + .TransitionTo(True) + .Then(context => context.Raise(Initialize))) + .When(Thing, context => !context.Data.Condition, b => b + .TransitionTo(False)) .DuringAny() - .When(Initialize, b => b - .Then(context => context.Instance.Initialized = DateTime.Now)) + .When(Initialize, b => b + .Then(context => context.Instance.Initialized = DateTime.Now)) ); - await machine.RaiseEvent(instance, Thing, new Data + await machine.RaiseEvent(instance, Thing, new Data { Condition = true }); + Assert.Multiple(() => { - Condition = true + Assert.That(instance.CurrentState, Is.EqualTo(True)); + Assert.That(instance.Initialized.HasValue, Is.True); }); - Assert.AreEqual(True, instance.CurrentState); - Assert.IsTrue(instance.Initialized.HasValue); } class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public DateTime? Initialized { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/SerializeState_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/SerializeState_Specs.cs index d411de1b454..0eff0112cb2 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/SerializeState_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/SerializeState_Specs.cs @@ -23,15 +23,12 @@ public async Task Should_properly_handle_the_state_property() .State("False", out False) .Event("Thing", out Thing) .During(builder.Initial) - .When(Thing, context => context.Data.Condition, b => b.TransitionTo(True)) - .When(Thing, context => !context.Data.Condition, b => b.TransitionTo(False)) + .When(Thing, context => context.Data.Condition, b => b.TransitionTo(True)) + .When(Thing, context => !context.Data.Condition, b => b.TransitionTo(False)) ); - await machine.RaiseEvent(instance, Thing, new Data - { - Condition = true - }); - Assert.AreEqual(True, instance.CurrentState); + await machine.RaiseEvent(instance, Thing, new Data { Condition = true }); + Assert.That(instance.CurrentState, Is.EqualTo(True)); var serializer = new JsonStateSerializer, Instance>(machine); @@ -40,15 +37,15 @@ public async Task Should_properly_handle_the_state_property() Console.WriteLine("Body: {0}", body); var reInstance = serializer.Deserialize(body); - Assert.AreEqual(True, reInstance.CurrentState); + Assert.That(reInstance.CurrentState, Is.EqualTo(True)); } class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/State_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/State_Specs.cs index 8ced5bcea02..be95ed7ce16 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/State_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/State_Specs.cs @@ -2,7 +2,6 @@ { using System; using NUnit.Framework; - using SagaStateMachine; [TestFixture(Category = "Dynamic Modify")] @@ -11,35 +10,36 @@ public class When_a_state_is_declared [Test] public void It_should_capture_the_name_of_final() { - Assert.AreEqual("Final", _machine.Final.Name); + Assert.That(_machine.Final.Name, Is.EqualTo("Final")); } [Test] public void It_should_capture_the_name_of_initial() { - Assert.AreEqual("Initial", _machine.Initial.Name); + Assert.That(_machine.Initial.Name, Is.EqualTo("Initial")); } [Test] public void It_should_capture_the_name_of_running() { - Assert.AreEqual("Running", Running.Name); + Assert.That(Running.Name, Is.EqualTo("Running")); } [Test] public void Should_be_an_instance_of_the_proper_type() { - Assert.IsInstanceOf>(_machine.Initial); + Assert.That(_machine.Initial, Is.InstanceOf.StateMachineState>()); } class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } + State Running; StateMachine _machine; @@ -61,7 +61,7 @@ public class When_a_state_is_stored_another_way [Test] public void It_should_get_the_name_right() { - Assert.AreEqual("Running", _instance.CurrentState); + Assert.That(_instance.CurrentState, Is.EqualTo("Running")); } Event Started; @@ -77,7 +77,7 @@ public void A_state_is_declared() .State("Running", out State Running) .InstanceState(x => x.CurrentState) .Initially() - .When(Started, b => b.TransitionTo(Running)) + .When(Started, b => b.TransitionTo(Running)) ); _instance = new Instance(); @@ -93,13 +93,14 @@ public void A_state_is_declared() /// an ORM that doesn't support user types (cough, EF, cough). /// class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } /// /// The CurrentState is exposed as a string for the ORM /// public string CurrentState { get; private set; } + + public Guid CorrelationId { get; set; } } } @@ -110,7 +111,7 @@ public class When_storing_state_as_an_int [Test] public void It_should_get_the_name_right() { - Assert.AreEqual(Running, _machine.GetState(_instance).Result); + Assert.That(_machine.GetState(_instance).Result, Is.EqualTo(Running)); } State Running; @@ -127,7 +128,7 @@ public void A_state_is_declared() .Event("Started", out Started) .InstanceState(x => x.CurrentState, Running) .Initially() - .When(Started, b => b.TransitionTo(Running)) + .When(Started, b => b.TransitionTo(Running)) ); _instance = new Instance(); @@ -143,13 +144,14 @@ public void A_state_is_declared() /// an ORM that doesn't support user types (cough, EF, cough). /// class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } /// /// The CurrentState is exposed as a string for the ORM /// public int CurrentState { get; private set; } + + public Guid CorrelationId { get; set; } } } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Telephone_Sample.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Telephone_Sample.cs index cbfcdb5daff..2193f5c07a2 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Telephone_Sample.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Telephone_Sample.cs @@ -26,8 +26,11 @@ public async Task Should_be_short_and_sweet() await _machine.RaiseEvent(phone, _model.HungUp); - Assert.AreEqual(_model.OffHook.Name, phone.CurrentState); - Assert.GreaterOrEqual(phone.CallTimer.ElapsedMilliseconds, 45); + Assert.Multiple(() => + { + Assert.That(phone.CurrentState, Is.EqualTo(_model.OffHook.Name)); + Assert.That(phone.CallTimer.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(45)); + }); } PhoneServiceStateModel _model; @@ -86,8 +89,11 @@ public async Task Should_be_short_and_sweet() await _machine.RaiseEvent(phone, _model.TakenOffHold); await _machine.RaiseEvent(phone, _model.HungUp); - Assert.AreEqual(_model.OffHook.Name, phone.CurrentState); - Assert.GreaterOrEqual(phone.CallTimer.ElapsedMilliseconds, 45); + Assert.Multiple(() => + { + Assert.That(phone.CurrentState, Is.EqualTo(_model.OffHook.Name)); + Assert.That(phone.CallTimer.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(45)); + }); } PhoneServiceStateModel _model; @@ -119,8 +125,11 @@ public async Task Should_end__badly() await _machine.RaiseEvent(phone, _model.HungUp); - Assert.AreEqual(_model.OffHook.Name, phone.CurrentState); - Assert.GreaterOrEqual(phone.CallTimer.ElapsedMilliseconds, 45); + Assert.Multiple(() => + { + Assert.That(phone.CurrentState, Is.EqualTo(_model.OffHook.Name)); + Assert.That(phone.CallTimer.ElapsedMilliseconds, Is.GreaterThanOrEqualTo(45)); + }); } PhoneServiceStateModel _model; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Transition_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Transition_Specs.cs index 8d0bca9d090..e1e1d1a5558 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Transition_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Transition_Specs.cs @@ -11,27 +11,33 @@ public class Explicitly_transitioning_to_a_state [Test] public void Should_call_the_enter_event() { - Assert.IsTrue(_instance.EnterCalled); + Assert.That(_instance.EnterCalled, Is.True); } [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_second_moved_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(Running)); + }); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(2, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(2)); } State Running; @@ -49,9 +55,9 @@ public void Specifying_an_event_activity() .Event("Initialized", out Event Initialized) .Event("Finish", out Event Finish) .During(builder.Initial) - .When(Initialized, b => b.TransitionTo(Running)) + .When(Initialized, b => b.TransitionTo(Running)) .During(Running) - .When(Finish, b => b.Finalize()) + .When(Finish, b => b.Finalize()) .WhenEnter(Running, x => x.Then(context => context.Instance.EnterCalled = true)) ); _observer = new StateChangeObserver(); @@ -65,11 +71,11 @@ public void Specifying_an_event_activity() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public bool EnterCalled { get; set; } + public Guid CorrelationId { get; set; } } } @@ -80,40 +86,49 @@ public class Transitioning_to_a_state_from_a_state [Test] public void Should_call_the_enter_event() { - Assert.IsTrue(_instance.EnterCalled); + Assert.That(_instance.EnterCalled, Is.True); } [Test] public void Should_have_first_moved_to_initial() { - Assert.AreEqual(null, _observer.Events[0].Previous); - Assert.AreEqual(_machine.Initial, _observer.Events[0].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[0].Previous, Is.EqualTo(null)); + Assert.That(_observer.Events[0].Current, Is.EqualTo(_machine.Initial)); + }); } [Test] public void Should_have_invoked_final_entered() { - Assert.IsTrue(_instance.FinalEntered); + Assert.That(_instance.FinalEntered, Is.True); } [Test] public void Should_have_second_moved_to_running() { - Assert.AreEqual(_machine.Initial, _observer.Events[1].Previous); - Assert.AreEqual(Running, _observer.Events[1].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[1].Previous, Is.EqualTo(_machine.Initial)); + Assert.That(_observer.Events[1].Current, Is.EqualTo(Running)); + }); } [Test] public void Should_have_third_moved_to_final() { - Assert.AreEqual(Running, _observer.Events[2].Previous); - Assert.AreEqual(_machine.Final, _observer.Events[2].Current); + Assert.Multiple(() => + { + Assert.That(_observer.Events[2].Previous, Is.EqualTo(Running)); + Assert.That(_observer.Events[2].Current, Is.EqualTo(_machine.Final)); + }); } [Test] public void Should_raise_the_event() { - Assert.AreEqual(3, _observer.Events.Count); + Assert.That(_observer.Events, Has.Count.EqualTo(3)); } State Running; @@ -133,9 +148,9 @@ public void Specifying_an_event_activity() .Event("Initialized", out Initialized) .Event("Finish", out Finish) .During(builder.Initial) - .When(Initialized, b => b.TransitionTo(Running)) + .When(Initialized, b => b.TransitionTo(Running)) .During(Running) - .When(Finish, b => b.Finalize()) + .When(Finish, b => b.Finalize()) .BeforeEnter(builder.Final, x => x.Then(context => context.Instance.FinalEntered = true)) .WhenEnter(Running, x => x.Then(context => context.Instance.EnterCalled = true)) ); @@ -151,13 +166,13 @@ public void Specifying_an_event_activity() class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public bool EnterCalled { get; set; } public bool FinalEntered { get; set; } + public Guid CorrelationId { get; set; } } } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/UnobservedEvent_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/UnobservedEvent_Specs.cs index 5a11eefee54..99cd29afbbd 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/UnobservedEvent_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/UnobservedEvent_Specs.cs @@ -26,8 +26,8 @@ public async Task Should_throw_an_exception_when_event_is_not_allowed_in_current class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } @@ -67,9 +67,9 @@ public async Task Should_throw_an_exception_when_event_is_not_allowed_in_current class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public int Volts { get; set; } + public Guid CorrelationId { get; set; } } @@ -109,7 +109,7 @@ public async Task Should_also_ignore_yet_process_invalid_events() await _machine.RaiseEvent(instance, Charge, new A { Volts = 12 }); - Assert.AreEqual(0, instance.Volts); + Assert.That(instance.Volts, Is.EqualTo(0)); } [Test] @@ -119,11 +119,11 @@ public async Task Should_have_the_next_event_even_though_ignored() await _machine.RaiseEvent(instance, Start); - Assert.AreEqual(Running, await _machine.GetState(instance)); + Assert.That(await _machine.GetState(instance), Is.EqualTo(Running)); var nextEvents = await _machine.NextEvents(instance); - Assert.IsTrue(nextEvents.Any(x => x.Name.Equals("Charge"))); + Assert.That(nextEvents.Any(x => x.Name.Equals("Charge")), Is.True); } [Test] @@ -145,9 +145,9 @@ public async Task Should_silently_ignore_the_invalid_event() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } public int Volts { get; set; } + public Guid CorrelationId { get; set; } } @@ -195,8 +195,8 @@ public async Task Should_silenty_ignore_the_invalid_event() class Instance : SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Visualizer_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Visualizer_Specs.cs index 34a1814c7dc..9015ad6e71e 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Visualizer_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Dynamic Modify/Visualizer_Specs.cs @@ -1,9 +1,9 @@ namespace MassTransit.Tests.SagaStateMachineTests.Dynamic_Modify { using System; - using Visualizer; using NUnit.Framework; using SagaStateMachine; + using Visualizer; [TestFixture(Category = "Dynamic Modify")] @@ -12,21 +12,35 @@ public class When_visualizing_a_state_machine [Test] public void Should_parse_the_graph() { - Assert.IsNotNull(_graph); + Assert.That(_graph, Is.Not.Null); } [Test] - public void Should_show_the_goods() + public void Should_show_graphviz_output() { var generator = new StateMachineGraphvizGenerator(_graph); - string dots = generator.CreateDotFile(); + string output = generator.CreateDotFile(); - Console.WriteLine(dots); + Console.WriteLine(output); - var expected = Expected.Replace("\r", "").Replace("\n", Environment.NewLine); + var expected = ExpectedGraphvizFile.Replace("\r", "").Replace("\n", Environment.NewLine); - Assert.AreEqual(expected, dots); + Assert.That(output, Is.EqualTo(expected)); + } + + [Test] + public void Should_show_mermaid_output() + { + var generator = new StateMachineMermaidGenerator(_graph); + + string output = generator.CreateMermaidFile(); + + Console.WriteLine(output); + + var expected = ExpectedMermaidFile.Replace("\r", "").Replace("\n", Environment.NewLine); + + Assert.That(output, Is.EqualTo(expected)); } StateMachine _machine; @@ -46,24 +60,24 @@ public void Setup() .Event("Finished", out Event Finished) .Event("Restart", out Event Restart) .During(b.Initial) - .When(Initialized, (binder) => binder - .TransitionTo(Running) - .Catch(h => h.TransitionTo(Failed)) - ) + .When(Initialized, (binder) => binder + .TransitionTo(Running) + .Catch(h => h.TransitionTo(Failed)) + ) .During(Running) - .When(Finished, (binder) => binder.TransitionTo(b.Final)) - .When(Suspend, (binder) => binder.TransitionTo(Suspended)) - .Ignore(Resume) + .When(Finished, (binder) => binder.TransitionTo(b.Final)) + .When(Suspend, (binder) => binder.TransitionTo(Suspended)) + .Ignore(Resume) .During(Suspended) - .When(Resume, b => b.TransitionTo(Running)) + .When(Resume, b => b.TransitionTo(Running)) .During(Failed) - .When(Restart, context => context.Data.Name != null, b => b.TransitionTo(Running)) + .When(Restart, context => context.Data.Name != null, b => b.TransitionTo(Running)) ); _graph = _machine.GetGraph(); } - const string Expected = @"digraph G { + const string ExpectedGraphvizFile = @"digraph G { 0 [shape=ellipse, label=""Initial""]; 1 [shape=ellipse, label=""Running""]; 2 [shape=ellipse, label=""Failed""]; @@ -89,12 +103,26 @@ public void Setup() 10 -> 1; }"; + const string ExpectedMermaidFile = @"flowchart TB; + 0([""Initial""]) --> 5[""Initialized""]; + 1([""Running""]) --> 7[""Finished""]; + 1([""Running""]) --> 8[""Suspend""]; + 2([""Failed""]) --> 10[""Restart«RestartData»""]; + 4([""Suspended""]) --> 9[""Resume""]; + 5[""Initialized""] --> 1([""Running""]); + 5[""Initialized""] --> 6[""Exception""]; + 6[""Exception""] --> 2([""Failed""]); + 7[""Finished""] --> 3([""Final""]); + 8[""Suspend""] --> 4([""Suspended""]); + 9[""Resume""] --> 1([""Running""]); + 10[""Restart«RestartData»""] --> 1([""Running""]);"; + class Instance : -SagaStateMachineInstance + SagaStateMachineInstance { - public Guid CorrelationId { get; set; } public State CurrentState { get; set; } + public Guid CorrelationId { get; set; } } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/DynamicEvent_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/DynamicEvent_Specs.cs index f816cfd9365..e7d9d1540d3 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/DynamicEvent_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/DynamicEvent_Specs.cs @@ -11,7 +11,7 @@ public class Specifying_dynamic_events_in_a_state_machine : InMemoryTestFixture { readonly TestStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; static Specifying_dynamic_events_in_a_state_machine() { @@ -33,12 +33,12 @@ public async Task Should_handle_a_double_state() await Bus.Publish(new Start { ServiceId = sagaId }); Guid? saga = await _repository.ShouldContainSaga(sagaId, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await Bus.Publish(new Stop { ServiceId = sagaId }); saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Final, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } [Test] @@ -50,7 +50,7 @@ public async Task Should_handle_the_initial_state() Guid? saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/EnterEvent_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/EnterEvent_Specs.cs index 38f28ab6353..e9d306ff929 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/EnterEvent_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/EnterEvent_Specs.cs @@ -16,12 +16,15 @@ public async Task Should_handle_a_double_state() { var sagaId = Guid.NewGuid(); - await InputQueueSendEndpoint.Send(new Start {CorrelationId = sagaId}); + await InputQueueSendEndpoint.Send(new Start { CorrelationId = sagaId }); Guid? saga = await _repository.ShouldContainSagaInState(sagaId, _machine, _machine.RunningFaster, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.Multiple(() => + { + Assert.That(saga.HasValue, Is.True); - Assert.AreEqual(1, _repository[saga.Value].Instance.OnEnter); + Assert.That(_repository[saga.Value].Instance.OnEnter, Is.EqualTo(1)); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Fault_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Fault_Specs.cs index a1c035d9dcb..f6a55b7e7e4 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Fault_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Fault_Specs.cs @@ -18,13 +18,13 @@ public async Task Should_be_able_to_observe_its_own_event_fault() await InputQueueSendEndpoint.Send(message); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.WaitingToStart, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await InputQueueSendEndpoint.Send(new Start(message.CorrelationId)); saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.FailedToStart, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } [Test] @@ -39,7 +39,7 @@ public async Task Should_be_received_as_a_fault_message() ConsumeContext> fault = await faultReceived; - Assert.AreEqual(message.CorrelationId, fault.Message.Message.CorrelationId); + Assert.That(fault.Message.Message.CorrelationId, Is.EqualTo(message.CorrelationId)); } [Test] @@ -58,11 +58,11 @@ public async Task Should_observe_the_fault_message() ConsumeContext> fault = await faultReceived; - Assert.AreEqual(message.CorrelationId, fault.Message.Message.CorrelationId); + Assert.That(fault.Message.Message.CorrelationId, Is.EqualTo(message.CorrelationId)); saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.FailedToStart, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } [Test] @@ -77,7 +77,7 @@ public async Task Should_receive_a_fault_when_an_instance_does_not_exist() ConsumeContext> fault = await faultReceived; - Assert.AreEqual(message.CorrelationId, fault.Message.Message.CorrelationId); + Assert.That(fault.Message.Message.CorrelationId, Is.EqualTo(message.CorrelationId)); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/FilterFault_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/FilterFault_Specs.cs index f472fdb5a2c..30e0382c15a 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/FilterFault_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/FilterFault_Specs.cs @@ -20,7 +20,7 @@ public async Task Should_be_able_to_observe_its_own_event_fault() await InputQueueSendEndpoint.Send(message); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, _machine.WaitingToStart, TimeSpan.FromSeconds(8)); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await InputQueueSendEndpoint.Send(new Start(message.CorrelationId)); @@ -28,7 +28,7 @@ public async Task Should_be_able_to_observe_its_own_event_fault() saga = await _repository.ShouldContainSagaInState(x => x.CorrelationId == message.CorrelationId && x.StartAttempts == 1, _machine, _machine.WaitingToStart, TimeSpan.FromSeconds(8)); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -36,7 +36,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin _machine = new TestStateMachine(); _repository = new InMemorySagaRepository(); - configurator.UseRetry(x => + configurator.UseMessageRetry(x => { x.Ignore(); x.Immediate(2); diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Finalize_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Finalize_Specs.cs index b3967176c17..0e8d0551a4c 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Finalize_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Finalize_Specs.cs @@ -14,6 +14,7 @@ public class Finalize_Specs TestStateMachine _machine; InMemorySagaRepository _repository; + ILoadSagaRepository LoadSagaRepository => _repository; [Test] public async Task Should_remove_saga_when_completed_in_whenenter() @@ -24,12 +25,12 @@ public async Task Should_remove_saga_when_completed_in_whenenter() await InputQueueSendEndpoint.Send(firstMessage); Guid? saga = await _repository.ShouldContainSagaInState(correlationId, _machine, x => x.OtherState, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); _taskCompletionSource.SetResult(true); - saga = await _repository.ShouldNotContainSaga(correlationId, TestTimeout); - Assert.IsFalse(saga.HasValue); + saga = await LoadSagaRepository.ShouldNotContainSaga(correlationId, TestTimeout); + Assert.That(saga.HasValue, Is.False); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Ignore_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Ignore_Specs.cs index de35d0109ea..a23911bf571 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Ignore_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Ignore_Specs.cs @@ -18,15 +18,15 @@ public async Task Should_not_throw_an_exception_when_receiving_ignored_event() { var sagaId = Guid.NewGuid(); - await Bus.Publish(new Start {CorrelationId = sagaId}); + await Bus.Publish(new Start { CorrelationId = sagaId }); Guid? saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); - await Bus.Publish(new Start {CorrelationId = sagaId}); + await Bus.Publish(new Start { CorrelationId = sagaId }); var faultMessage = await GetFaultMessage(TimeSpan.FromSeconds(3)); - Assert.IsNull(faultMessage?.Exceptions.Select(ex => $"{ex.ExceptionType}: {ex.Message}").First()); + Assert.That(faultMessage?.Exceptions.Select(ex => $"{ex.ExceptionType}: {ex.Message}").First(), Is.Null); } protected async Task GetFaultMessage(TimeSpan timeout) @@ -59,7 +59,7 @@ protected override void ConnectObservers(IBus bus) } readonly TestStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; readonly List> _faultMessageContexts; public When_an_event_is_defined_as_ignored_for_state() diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/InMemoryDeadlock_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/InMemoryDeadlock_Specs.cs index e6114e056c0..1c0325c22d3 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/InMemoryDeadlock_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/InMemoryDeadlock_Specs.cs @@ -16,24 +16,24 @@ public async Task Should_not_deadlock_on_the_repository() { var id = NewId.NextGuid(); - await InputQueueSendEndpoint.Send(new {CorrelationId = id}); + await InputQueueSendEndpoint.Send(new { CorrelationId = id }); Guid? saga = await _repository.ShouldContainSagaInState(id, _machine, _machine.Active, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); - await InputQueueSendEndpoint.Send(new {CorrelationId = id}); + await InputQueueSendEndpoint.Send(new { CorrelationId = id }); await Task.Delay(990); await Console.Out.WriteLineAsync("Sending duplicate message"); - await InputQueueSendEndpoint.Send(new {CorrelationId = id}); + await InputQueueSendEndpoint.Send(new { CorrelationId = id }); id = NewId.NextGuid(); - await InputQueueSendEndpoint.Send(new {CorrelationId = id}); + await InputQueueSendEndpoint.Send(new { CorrelationId = id }); Guid? saga2 = await _repository.ShouldContainSagaInState(id, _machine, _machine.Active, TestTimeout); - Assert.IsTrue(saga2.HasValue); + Assert.That(saga2.HasValue, Is.True); } InMemorySagaRepository _repository; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Initiator_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Initiator_Specs.cs index 0e7a70ba236..37a0342cf5f 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Initiator_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Initiator_Specs.cs @@ -23,17 +23,20 @@ public async Task Should_receive_the_published_message() ConsumeContext received = await messageReceived; - Assert.That(received.Message.TransactionId, Is.EqualTo(message.CorrelationId)); + Assert.Multiple(() => + { + Assert.That(received.Message.TransactionId, Is.EqualTo(message.CorrelationId)); - Assert.IsTrue(received.InitiatorId.HasValue, "The initiator should be copied from the CorrelationId"); + Assert.That(received.InitiatorId.HasValue, Is.True, "The initiator should be copied from the CorrelationId"); - Assert.AreEqual(received.InitiatorId.Value, message.CorrelationId, "The initiator should be the saga CorrelationId"); + Assert.That(received.InitiatorId.Value, Is.EqualTo(message.CorrelationId), "The initiator should be the saga CorrelationId"); - Assert.AreEqual(received.SourceAddress, InputQueueAddress, "The published message should have the input queue source address"); + Assert.That(received.SourceAddress, Is.EqualTo(InputQueueAddress), "The published message should have the input queue source address"); + }); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/MissingInstance_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/MissingInstance_Specs.cs index e135c6067a4..3fc69288226 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/MissingInstance_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/MissingInstance_Specs.cs @@ -21,7 +21,7 @@ public async Task Should_publish_the_event_of_the_missing_instance() await notFound; - Assert.AreEqual("A", notFound.Result.Message.ServiceName); + Assert.That(notFound.Result.Message.ServiceName, Is.EqualTo("A")); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/OutboxFault_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/OutboxFault_Specs.cs new file mode 100644 index 00000000000..e29c9586032 --- /dev/null +++ b/tests/MassTransit.Tests/SagaStateMachineTests/OutboxFault_Specs.cs @@ -0,0 +1,234 @@ +namespace MassTransit.Tests.SagaStateMachineTests +{ + using System; + using System.Net.Mime; + using System.Threading.Tasks; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + + + [TestFixture] + public class When_a_send_faults_in_the_outbox + { + [Test] + public async Task Should_handle_the_redelivery_of_a_scheduled_message() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.SetTestTimeouts(testInactivityTimeout: TimeSpan.FromSeconds(5)); + + x.AddConfigureEndpointsCallback((context, name, cfg) => + { + cfg.UseSendFilter(typeof(SomeSendFilter<>), context); + }); + + x.AddHandler((ConsumeContext context) => context.RespondAsync(new SomeResponse + { + Status = context.Message.Count >= 5 ? "Finished" : "Running" + })); + + x.AddSagaStateMachine(); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + var id = NewId.NextGuid(); + + await harness.Bus.Publish(new InitialEvent { CorrelationId = id }); + + Assert.That(await harness.Published.Any(x => x.Context.Message.CorrelationId == id)); + + await harness.Stop(); + } + + + class SomeInstance : + SagaStateMachineInstance + { + public State CurrentState { get; set; } + public Guid? TokenId { get; set; } + public int Count { get; set; } + public Guid CorrelationId { get; set; } + } + + + class SomeSendFilter : + IFilter> + where T : class + { + public Task Send(SendContext context, IPipe> next) + { + if (context.Message is SomeInstanceEvent instanceEvent && instanceEvent.Count == 2) + context.Serializer = new DeathComesMeSerializer(); + + return next.Send(context); + } + + public void Probe(ProbeContext context) + { + } + + + class DeathComesMeSerializer : + IMessageSerializer + { + public ContentType ContentType => new ContentType("application/death"); + + public MessageBody GetMessageBody(SendContext context) + where T1 : class + { + throw new InvalidOperationException("In this scenario, death becomes me."); + } + } + } + + + class SomeStateMachine : + MassTransitStateMachine + { + public SomeStateMachine() + { + InstanceState(x => x.CurrentState); + + Request(() => HandlerRequest, x => + { + x.Timeout = TimeSpan.Zero; + }); + + Schedule(() => ScheduleEvent, x => x.TokenId, x => + { + x.Delay = TimeSpan.FromSeconds(1); + x.Received = r => + { + r.CorrelateById(m => m.Message.CorrelationId); + r.ConfigureConsumeTopology = false; + }; + }); + + Initially( + When(InitialEventReceived) + .Then(context => LogContext.Debug?.Log("Initial event, scheduling instance event")) + .Schedule(ScheduleEvent, context => new SomeInstanceEvent + { + CorrelationId = context.Saga.CorrelationId, + Count = context.Saga.Count++ + }) + .TransitionTo(Running)); + + During(Running, + When(ScheduleEvent.Received) + .Then(context => LogContext.Debug?.Log("Sending request")) + .Schedule(ScheduleEvent, context => new SomeInstanceEvent + { + CorrelationId = context.Saga.CorrelationId, + Count = context.Saga.Count++ + }) + .Request(HandlerRequest, context => new SomeRequest { Count = context.Saga.Count }) + .TransitionTo(Checking) + ); + + During(Checking, + When(ScheduleEvent.Received) + .Then(context => LogContext.Debug?.Log("Scheduled event received while waiting for request")) + .Schedule(ScheduleEvent, context => new SomeInstanceEvent + { + CorrelationId = context.Saga.CorrelationId, + Count = context.Saga.Count++ + }) + .Request(HandlerRequest, context => new SomeRequest { Count = context.Saga.Count }) + .TransitionTo(Suspect) + ); + + During(Suspect, + When(ScheduleEvent.Received) + .Then(context => LogContext.Debug?.Log("Suspect, scheduled event, faulted time")) + .TransitionTo(Failed) + .Publish(context => new InstanceCompleted + { + CorrelationId = context.Saga.CorrelationId, + Result = "Faulted" + }) + ); + + During(Running, Checking, Suspect, + When(HandlerRequest.Completed) + .IfElse(context => context.Message.Status == "Running", running => running + .Then(context => LogContext.Debug?.Log("Response received, back to running")) + .TransitionTo(Running), otherwise => otherwise + .Then(context => LogContext.Debug?.Log("Response received, to completed")) + .Finalize() + ) + ); + + WhenEnter(Final, x => x.Unschedule(ScheduleEvent) + .Publish(context => new InstanceCompleted + { + CorrelationId = context.Saga.CorrelationId, + Result = "Success" + }) + ); + } + + // + // ReSharper disable UnassignedGetOnlyAutoProperty + public Schedule ScheduleEvent { get; } + + public Request HandlerRequest { get; } + + public Event InitialEventReceived { get; } + + public State Running { get; } + public State Checking { get; } + public State Suspect { get; } + public State Failed { get; } + } + + + class SomeSagaDefinition : + SagaDefinition + { + protected override void ConfigureSaga(IReceiveEndpointConfigurator endpointConfigurator, ISagaConfigurator sagaConfigurator, + IRegistrationContext context) + { + endpointConfigurator.UseMessageScope(context); + endpointConfigurator.UseMessageRetry(r => r.Immediate(5)); + endpointConfigurator.UseInMemoryOutbox(context); + } + } + + + public class SomeRequest + { + public int Count { get; set; } + } + + + public class SomeResponse + { + public string Status { get; set; } + } + + + public class SomeInstanceEvent + { + public Guid CorrelationId { get; set; } + public int Count { get; set; } + } + + + public class InstanceCompleted + { + public Guid CorrelationId { get; set; } + public string Result { get; set; } + } + + + public class InitialEvent + { + public Guid CorrelationId { get; set; } + } + } +} diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Partitioning_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Partitioning_Specs.cs index df32cf5fd57..cb9d50455ad 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Partitioning_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Partitioning_Specs.cs @@ -28,7 +28,7 @@ public async Task Should_initiate_the_saga() for (var i = 0; i < Limit; i++) { Guid? guid = await _repository.ShouldContainSaga(ids[i], TestTimeout); - Assert.IsTrue(guid.HasValue); + Assert.That(guid.HasValue, Is.True); } timer.Stop(); @@ -36,7 +36,7 @@ public async Task Should_initiate_the_saga() Console.WriteLine("Total time: {0}", timer.Elapsed); } - InMemorySagaRepository _repository; + ISagaRepository _repository; const int Limit = 100; diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Publish_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Publish_Specs.cs index 72c3c2e85ba..09940e3190b 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Publish_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Publish_Specs.cs @@ -22,17 +22,20 @@ public async Task Should_receive_the_published_message() ConsumeContext received = await messageReceived; - Assert.AreEqual(message.CorrelationId, received.Message.TransactionId); + Assert.Multiple(() => + { + Assert.That(received.Message.TransactionId, Is.EqualTo(message.CorrelationId)); - Assert.IsTrue(received.InitiatorId.HasValue, "The initiator should be copied from the CorrelationId"); + Assert.That(received.InitiatorId.HasValue, Is.True, "The initiator should be copied from the CorrelationId"); - Assert.AreEqual(received.InitiatorId.Value, message.CorrelationId, "The initiator should be the saga CorrelationId"); + Assert.That(received.InitiatorId.Value, Is.EqualTo(message.CorrelationId), "The initiator should be the saga CorrelationId"); - Assert.AreEqual(received.SourceAddress, InputQueueAddress, "The published message should have the input queue source address"); + Assert.That(received.SourceAddress, Is.EqualTo(InputQueueAddress), "The published message should have the input queue source address"); + }); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/RemoveWhen_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/RemoveWhen_Specs.cs index 8571d1b55f7..ddd196c3eff 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/RemoveWhen_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/RemoveWhen_Specs.cs @@ -20,7 +20,7 @@ public async Task Should_handle_the_initial_state() Guid? saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } [Test] @@ -31,12 +31,12 @@ public async Task Should_remove_the_saga_once_completed() await Bus.Publish(new Start { CorrelationId = sagaId }); Guid? saga = await _repository.ShouldContainSaga(sagaId, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await Bus.Publish(new Stop { CorrelationId = sagaId }); saga = await _repository.ShouldNotContainSaga(sagaId, TestTimeout); - Assert.IsFalse(saga.HasValue); + Assert.That(saga.HasValue, Is.False); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -45,7 +45,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } readonly TestStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; public When_a_remove_expression_is_specified() { @@ -122,9 +122,12 @@ public async Task Should_remove_the_saga_once_completed() await Task.Delay(50); Guid? saga = await _repository.ShouldNotContainSaga(sagaId, TestTimeout); - Assert.IsFalse(saga.HasValue); + Assert.Multiple(() => + { + Assert.That(saga.HasValue, Is.False); - Assert.AreEqual(sagaId, response.CorrelationId); + Assert.That(response.CorrelationId, Is.EqualTo(sagaId)); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -133,7 +136,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } readonly TestStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; public When_a_saga_goes_straight_to_finalized() { diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Request2_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Request2_Specs.cs index a8491988ef5..8af22503ece 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Request2_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Request2_Specs.cs @@ -30,7 +30,7 @@ await InputQueueSendEndpoint.Send(new Guid? saga = await _repository.ShouldContainSagaInState(memberId, _machine, x => x.Registered, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var sagaInstance = _repository[saga.Value].Instance; Assert.That(sagaInstance.Name, Is.EqualTo("Frank")); diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Request3_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Request3_Specs.cs index 5695c9579d2..b4bb6065629 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Request3_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Request3_Specs.cs @@ -1,14 +1,14 @@ namespace MassTransit.Tests.SagaStateMachineTests { - using System; - using System.Threading.Tasks; - using MassTransit.Testing; - using NUnit.Framework; - using TestFramework; - - namespace Request3_Specs { + using System; + using System.Threading.Tasks; + using MassTransit.Testing; + using NUnit.Framework; + using TestFramework; + + [TestFixture] public class Sending_a_request_from_a_state_machine : InMemoryTestFixture @@ -30,7 +30,7 @@ await InputQueueSendEndpoint.Send(new Guid? saga = await _repository.ShouldContainSagaInState(memberId, _machine, x => x.Registered, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var sagaInstance = _repository[saga.Value].Instance; Assert.That(sagaInstance.Name, Is.EqualTo("Frank")); @@ -167,9 +167,7 @@ public TestStateMachine() .TransitionTo(NameValidationFaulted), When(ValidateName.TimeoutExpired) .TransitionTo(NameValidationTimeout)); - } - - // ReSharper disable UnassignedGetOnlyAutoProperty + } // ReSharper disable UnassignedGetOnlyAutoProperty public Request ValidateName { get; } public Event Register { get; } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Request_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Request_Specs.cs index 3cbd26cc37a..afc56d6f761 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Request_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Request_Specs.cs @@ -30,7 +30,7 @@ await InputQueueSendEndpoint.Send(new Guid? saga = await _repository.ShouldContainSagaInState(memberId, _machine, x => x.Registered, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); var sagaInstance = _repository[saga.Value].Instance; Assert.That(sagaInstance.Name, Is.EqualTo("Frank")); diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Respond_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Respond_Specs.cs index c213c74b31f..3ac4d658b38 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Respond_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Respond_Specs.cs @@ -3,7 +3,6 @@ using System; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -34,7 +33,7 @@ public async Task Should_start_and_report_status() Response response = await _statusClient.GetResponse(new StatusRequested(start.CorrelationId), TestCancellationToken); - response.Message.Status.ShouldBe(_machine.Running.Name); + Assert.That(response.Message.Status, Is.EqualTo(_machine.Running.Name)); } [OneTimeSetUp] diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/SagaConfigurationObserver_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/SagaConfigurationObserver_Specs.cs index 8de10232b96..8617ac8ab45 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/SagaConfigurationObserver_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/SagaConfigurationObserver_Specs.cs @@ -9,7 +9,7 @@ public class SagaConfigurationObserver_Specs { readonly TestStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; public SagaConfigurationObserver_Specs() { @@ -28,7 +28,7 @@ public void Should_invoke_the_observers_for_each_consumer_and_message_type() cfg.ReceiveEndpoint("hello", e => { - e.UseRetry(x => x.Immediate(1)); + e.UseMessageRetry(x => x.Immediate(1)); e.StateMachineSaga(_machine, _repository, x => { @@ -43,10 +43,13 @@ public void Should_invoke_the_observers_for_each_consumer_and_message_type() }); }); - Assert.That(observer.SagaTypes.Contains(typeof(Instance))); - Assert.That(observer.StateMachineTypes.Contains(typeof(TestStateMachine))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(Instance), typeof(Start)))); - Assert.That(observer.MessageTypes.Contains(Tuple.Create(typeof(Instance), typeof(Stop)))); + Assert.Multiple(() => + { + Assert.That(observer.SagaTypes, Does.Contain(typeof(Instance))); + Assert.That(observer.StateMachineTypes, Does.Contain(typeof(TestStateMachine))); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(Instance), typeof(Start)))); + }); + Assert.That(observer.MessageTypes, Does.Contain(Tuple.Create(typeof(Instance), typeof(Stop)))); } diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Send_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Send_Specs.cs index 057d5646431..cfd5930df29 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Send_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Send_Specs.cs @@ -20,17 +20,20 @@ public async Task Should_receive_the_published_message() ConsumeContext received = await _handled; - Assert.AreEqual(message.CorrelationId, received.Message.TransactionId); + Assert.Multiple(() => + { + Assert.That(received.Message.TransactionId, Is.EqualTo(message.CorrelationId)); - Assert.IsTrue(received.InitiatorId.HasValue, "The initiator should be copied from the CorrelationId"); + Assert.That(received.InitiatorId.HasValue, Is.True, "The initiator should be copied from the CorrelationId"); - Assert.AreEqual(message.CorrelationId, received.InitiatorId.Value, "The initiator should be the saga CorrelationId"); + Assert.That(received.InitiatorId.Value, Is.EqualTo(message.CorrelationId), "The initiator should be the saga CorrelationId"); - Assert.AreEqual(InputQueueAddress, received.SourceAddress, "The published message should have the input queue source address"); + Assert.That(received.SourceAddress, Is.EqualTo(InputQueueAddress), "The published message should have the input queue source address"); + }); Guid? saga = await _repository.ShouldContainSagaInState(message.CorrelationId, _machine, _machine.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/SimpleStateMachine_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/SimpleStateMachine_Specs.cs index 4af97758223..db7ba452a0a 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/SimpleStateMachine_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/SimpleStateMachine_Specs.cs @@ -19,12 +19,12 @@ public async Task Should_handle_a_double_state() await Bus.Publish(new Start { CorrelationId = sagaId }); Guid? saga = await _repository.ShouldContainSaga(sagaId, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await Bus.Publish(new Stop { CorrelationId = sagaId }); saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Final, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } [Test] @@ -36,7 +36,7 @@ public async Task Should_handle_the_initial_state() Guid? saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -45,7 +45,7 @@ protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpoin } readonly TestStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; public Using_a_simple_state_machine() { @@ -114,7 +114,7 @@ public class Using_a_simple_state_machine_with_topology_correlation_id : InMemoryTestFixture { readonly TestStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; public Using_a_simple_state_machine_with_topology_correlation_id() { @@ -130,12 +130,12 @@ public async Task Should_handle_a_double_state() await Bus.Publish(new Start { CorrelationId = sagaId }); Guid? saga = await _repository.ShouldContainSaga(sagaId, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await Bus.Publish(new Stop { CorrelationId = sagaId }); saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Final, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } [Test] @@ -147,7 +147,7 @@ public async Task Should_handle_the_initial_state() Guid? saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -214,7 +214,7 @@ public class Using_a_simple_state_machine_with_custom_correlation_id : InMemoryTestFixture { readonly TestStateMachine _machine; - readonly InMemorySagaRepository _repository; + readonly ISagaRepository _repository; static Using_a_simple_state_machine_with_custom_correlation_id() { @@ -236,12 +236,12 @@ public async Task Should_handle_a_double_state() await Bus.Publish(new Start { ServiceId = sagaId }); Guid? saga = await _repository.ShouldContainSaga(sagaId, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); await Bus.Publish(new Stop { ServiceId = sagaId }); saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Final, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } [Test] @@ -253,7 +253,7 @@ public async Task Should_handle_the_initial_state() Guid? saga = await _repository.ShouldContainSagaInState(sagaId, _machine, x => x.Running, TestTimeout); - Assert.IsTrue(saga.HasValue); + Assert.That(saga.HasValue, Is.True); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/Testing_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/Testing_Specs.cs index 7174fd73501..21e1c196971 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/Testing_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/Testing_Specs.cs @@ -21,12 +21,12 @@ public async Task Should_handle_the_initial_state() await harness.Start(); try { - await harness.InputQueueSendEndpoint.Send(new Start {CorrelationId = sagaId}); + await harness.InputQueueSendEndpoint.Send(new Start { CorrelationId = sagaId }); - Assert.IsTrue(harness.Consumed.Select().Any(), "Message not received"); + Assert.That(harness.Consumed.Select().Any(), Is.True, "Message not received"); var instance = saga.Created.ContainsInState(sagaId, _machine, _machine.Running); - Assert.IsNotNull(instance, "Saga instance not found"); + Assert.That(instance, Is.Not.Null, "Saga instance not found"); } finally { @@ -45,16 +45,16 @@ public async Task Should_handle_the_stop_state() await harness.Start(); try { - await harness.InputQueueSendEndpoint.Send(new Start {CorrelationId = sagaId}); + await harness.InputQueueSendEndpoint.Send(new Start { CorrelationId = sagaId }); - Assert.IsTrue(harness.Consumed.Select().Any(), "Start not received"); + Assert.That(harness.Consumed.Select().Any(), Is.True, "Start not received"); - await harness.InputQueueSendEndpoint.Send(new Stop {CorrelationId = sagaId}); + await harness.InputQueueSendEndpoint.Send(new Stop { CorrelationId = sagaId }); - Assert.IsTrue(harness.Consumed.Select().Any(), "Stop not received"); + Assert.That(harness.Consumed.Select().Any(), Is.True, "Stop not received"); var instance = saga.Created.ContainsInState(sagaId, _machine, _machine.Final); - Assert.IsNotNull(instance, "Saga instance not found"); + Assert.That(instance, Is.Not.Null, "Saga instance not found"); } finally { diff --git a/tests/MassTransit.Tests/SagaStateMachineTests/UncorrelatedMessage_Specs.cs b/tests/MassTransit.Tests/SagaStateMachineTests/UncorrelatedMessage_Specs.cs index ebdc095a3e3..12bc1225c21 100644 --- a/tests/MassTransit.Tests/SagaStateMachineTests/UncorrelatedMessage_Specs.cs +++ b/tests/MassTransit.Tests/SagaStateMachineTests/UncorrelatedMessage_Specs.cs @@ -20,7 +20,7 @@ public async Task Should_retry_the_status_message() Response status = await statusTask; - Assert.AreEqual("A", status.Message.ServiceName); + Assert.That(status.Message.ServiceName, Is.EqualTo("A")); } [Test] @@ -31,7 +31,7 @@ public async Task Should_start_and_handle_the_status_request() Response status = await Bus.Request(InputQueueAddress, new CheckStatus("A"), TestCancellationToken); - Assert.AreEqual("A", status.Message.ServiceName); + Assert.That(status.Message.ServiceName, Is.EqualTo("A")); } [Test] @@ -42,7 +42,7 @@ public async Task Should_start_and_handle_the_status_request_awaited() Response status = await Bus.Request(InputQueueAddress, new CheckStatus("B"), TestCancellationToken); - Assert.AreEqual("B", status.Message.ServiceName); + Assert.That(status.Message.ServiceName, Is.EqualTo("B")); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/SendContextMiddleware_Specs.cs b/tests/MassTransit.Tests/SendContextMiddleware_Specs.cs index 1d570088da3..a6378507739 100644 --- a/tests/MassTransit.Tests/SendContextMiddleware_Specs.cs +++ b/tests/MassTransit.Tests/SendContextMiddleware_Specs.cs @@ -8,7 +8,6 @@ namespace MassTransit.Tests using MassTransit.Testing; using MassTransit.Testing.Implementations; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -149,23 +148,29 @@ public async Task Should_contain_the_same_payloads() ISentMessage a = sendObserver.Messages.Select().FirstOrDefault(); ISentMessage b = sendObserver.Messages.Select().FirstOrDefault(); - a.ShouldNotBeNull(); - b.ShouldNotBeNull(); + Assert.Multiple(() => + { + Assert.That(a, Is.Not.Null); + Assert.That(b, Is.Not.Null); + }); Dictionary ah = a.Context.Headers.GetAll().ToDictionary(x => x.Key, x => x.Value); Dictionary bh = b.Context.Headers.GetAll().ToDictionary(x => x.Key, x => x.Value); - ah.ShouldContainKey("x-send-filter"); - ah.ShouldContainKey("x-send-message-filter"); - ah["x-send-filter"].ShouldBe("send-filter"); - ah["x-send-message-filter"].ShouldBe("send-message-filter"); + Assert.Multiple(() => + { + Assert.That(ah.ContainsKey("x-send-filter"), Is.True); + Assert.That(ah.ContainsKey("x-send-message-filter"), Is.True); + Assert.That(ah["x-send-filter"], Is.EqualTo("send-filter")); + Assert.That(ah["x-send-message-filter"], Is.EqualTo("send-message-filter")); - bh.ShouldContainKey("x-send-filter"); - bh.ShouldContainKey("x-send-message-filter"); + Assert.That(bh.ContainsKey("x-send-filter"), Is.True); + Assert.That(bh.ContainsKey("x-send-message-filter"), Is.True); - // those fails, as while they DO have ",has-consume-context" they don't have access to SomePayload - bh["x-send-filter"].ShouldBe("send-filter,has-consume-context,has-some-payload:hello"); - bh["x-send-message-filter"].ShouldBe("send-message-filter,has-consume-context,has-some-payload:hello"); + // those fails, as while they DO have ",has-consume-context" they don't have access to SomePayload + Assert.That(bh["x-send-filter"], Is.EqualTo("send-filter,has-consume-context,has-some-payload:hello")); + Assert.That(bh["x-send-message-filter"], Is.EqualTo("send-message-filter,has-consume-context,has-some-payload:hello")); + }); } } @@ -425,23 +430,29 @@ public async Task Should_contain_the_same_payloads() IPublishedMessage a = publishObserver.Messages.Select().FirstOrDefault(); IPublishedMessage b = publishObserver.Messages.Select().FirstOrDefault(); - a.ShouldNotBeNull(); - b.ShouldNotBeNull(); + Assert.Multiple(() => + { + Assert.That(a, Is.Not.Null); + Assert.That(b, Is.Not.Null); + }); Dictionary ah = a.Context.Headers.GetAll().ToDictionary(x => x.Key, x => x.Value); Dictionary bh = b.Context.Headers.GetAll().ToDictionary(x => x.Key, x => x.Value); - ah.ShouldContainKey("x-send-filter"); - ah.ShouldContainKey("x-send-message-filter"); - ah["x-send-filter"].ShouldBe("send-filter"); - ah["x-send-message-filter"].ShouldBe("send-message-filter"); - - bh.ShouldContainKey("x-send-filter"); - bh.ShouldContainKey("x-send-message-filter"); - - // those fails, as while they DO have ",has-consume-context" they don't have access to SomePayload - bh["x-send-filter"].ShouldBe("send-filter,has-consume-context,has-some-payload:hello"); - bh["x-send-message-filter"].ShouldBe("send-message-filter,has-consume-context,has-some-payload:hello"); + Assert.Multiple(() => + { + Assert.That(ah.ContainsKey("x-send-filter")); + Assert.That(ah.ContainsKey("x-send-message-filter")); + Assert.That(ah["x-send-filter"], Is.EqualTo("send-filter")); + Assert.That(ah["x-send-message-filter"], Is.EqualTo("send-message-filter")); + + Assert.That(bh.ContainsKey("x-send-filter")); + Assert.That(bh.ContainsKey("x-send-message-filter")); + + // those fails, as while they DO have ",has-consume-context" they don't have access to SomePayload + Assert.That(bh["x-send-filter"], Is.EqualTo("send-filter,has-consume-context,has-some-payload:hello")); + Assert.That(bh["x-send-message-filter"], Is.EqualTo("send-message-filter,has-consume-context,has-some-payload:hello")); + }); } } diff --git a/tests/MassTransit.Tests/SendObserver_Specs.cs b/tests/MassTransit.Tests/SendObserver_Specs.cs index 4a0bd26f6a2..1fb34634613 100644 --- a/tests/MassTransit.Tests/SendObserver_Specs.cs +++ b/tests/MassTransit.Tests/SendObserver_Specs.cs @@ -7,7 +7,6 @@ namespace ObserverTests using System.Threading; using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -27,8 +26,11 @@ public async Task Should_invoke_the_exception_after_send_failure() await observer.PreSent; await observer.SendFaulted; - Assert.That(observer.PreSentCount, Is.EqualTo(1)); - Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(1)); + Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + }); } } @@ -43,8 +45,11 @@ public async Task Should_invoke_the_observer_after_send() await observer.PreSent; await observer.PostSent; - Assert.That(observer.PreSentCount, Is.EqualTo(1)); - Assert.That(observer.PostSentCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(1)); + Assert.That(observer.PostSentCount, Is.EqualTo(1)); + }); } } @@ -58,8 +63,11 @@ public async Task Should_invoke_the_observer_prior_to_send() await observer.PreSent; - Assert.That(observer.PreSentCount, Is.EqualTo(1)); - Assert.That(observer.PostSentCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(1)); + Assert.That(observer.PostSentCount, Is.EqualTo(1)); + }); } } @@ -74,11 +82,14 @@ public async Task Should_not_invoke_post_sent_on_exception() await observer.SendFaulted; - observer.PostSent.Status.ShouldBe(TaskStatus.WaitingForActivation); + Assert.Multiple(() => + { + Assert.That(observer.PostSent.Status, Is.EqualTo(TaskStatus.WaitingForActivation)); - Assert.That(observer.PreSentCount, Is.EqualTo(1)); - Assert.That(observer.PostSentCount, Is.EqualTo(0)); - Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + Assert.That(observer.PreSentCount, Is.EqualTo(1)); + Assert.That(observer.PostSentCount, Is.EqualTo(0)); + Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + }); } } } @@ -100,9 +111,12 @@ public async Task Should_invoke_the_observer_after_send() await observer.PostSent; await observer.SendFaulted; - Assert.That(observer.PreSentCount, Is.EqualTo(2)); - Assert.That(observer.PostSentCount, Is.EqualTo(1)); - Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(2)); + Assert.That(observer.PostSentCount, Is.EqualTo(1)); + Assert.That(observer.SendFaultCount, Is.EqualTo(1)); + }); } } @@ -135,11 +149,14 @@ public async Task Should_not_invoke_post_sent_on_exception() await _observer.SendFaulted; - _observer.PostSent.Status.ShouldBe(TaskStatus.WaitingForActivation); + Assert.Multiple(() => + { + Assert.That(_observer.PostSent.Status, Is.EqualTo(TaskStatus.WaitingForActivation)); - Assert.That(_observer.PreSentCount, Is.EqualTo(1)); - Assert.That(_observer.PostSentCount, Is.EqualTo(0)); - Assert.That(_observer.SendFaultCount, Is.EqualTo(1)); + Assert.That(_observer.PreSentCount, Is.EqualTo(1)); + Assert.That(_observer.PostSentCount, Is.EqualTo(0)); + Assert.That(_observer.SendFaultCount, Is.EqualTo(1)); + }); } SendObserver _observer; @@ -265,8 +282,11 @@ public async Task Should_trigger_the_send_message_observer() await observer.PreSent; await observer.PostSent; - Assert.That(observer.PreSentCount, Is.EqualTo(1)); - Assert.That(observer.PostSentCount, Is.EqualTo(1)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(1)); + Assert.That(observer.PostSentCount, Is.EqualTo(1)); + }); } [Test] @@ -298,8 +318,11 @@ public async Task Should_trigger_the_send_message_observer_for_both_messages() await observer.PreSent; await observer.PostSent; - Assert.That(observer.PreSentCount, Is.EqualTo(2)); - Assert.That(observer.PostSentCount, Is.EqualTo(2)); + Assert.Multiple(() => + { + Assert.That(observer.PreSentCount, Is.EqualTo(2)); + Assert.That(observer.PostSentCount, Is.EqualTo(2)); + }); } } diff --git a/tests/MassTransit.Tests/SendProxy_Specs.cs b/tests/MassTransit.Tests/SendProxy_Specs.cs index 9c7fc588325..740a07bffe0 100644 --- a/tests/MassTransit.Tests/SendProxy_Specs.cs +++ b/tests/MassTransit.Tests/SendProxy_Specs.cs @@ -4,7 +4,6 @@ using System.Threading.Tasks; using MassTransit.Initializers; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -23,9 +22,12 @@ public async Task Should_property_initialize_the_values() InitializeContext context = await MessageInitializerCache.Initialize(values); var message = context.Message; - message.CorrelationId.ShouldBe(values.CorrelationId); - message.Name.ShouldBe(values.Name); - message.Timestamp.ShouldBe(values.Timestamp); + Assert.Multiple(() => + { + Assert.That(message.CorrelationId, Is.EqualTo(values.CorrelationId)); + Assert.That(message.Name, Is.EqualTo(values.Name)); + Assert.That(message.Timestamp, Is.EqualTo(values.Timestamp)); + }); } diff --git a/tests/MassTransit.Tests/SendReceive_Specs.cs b/tests/MassTransit.Tests/SendReceive_Specs.cs index c252a142128..6ce6d7ee68a 100644 --- a/tests/MassTransit.Tests/SendReceive_Specs.cs +++ b/tests/MassTransit.Tests/SendReceive_Specs.cs @@ -2,7 +2,6 @@ namespace MassTransit.Tests { using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; using TestFramework.Messages; @@ -66,7 +65,7 @@ public async Task Should_receive_the_proper_message_as_a_with_request_id() ConsumeContext consumeContext = await handler; - consumeContext.RequestId.ShouldBe(requestId); + Assert.That(consumeContext.RequestId, Is.EqualTo(requestId)); } [Test] @@ -81,7 +80,7 @@ public async Task Should_receive_the_proper_message_type() ConsumeContext consumeContext = await handler; - consumeContext.RequestId.ShouldBe(requestId); + Assert.That(consumeContext.RequestId, Is.EqualTo(requestId)); } [Test] @@ -96,7 +95,7 @@ public async Task Should_receive_the_proper_message_without_type() ConsumeContext consumeContext = await handler; - consumeContext.RequestId.ShouldBe(requestId); + Assert.That(consumeContext.RequestId, Is.EqualTo(requestId)); } } } diff --git a/tests/MassTransit.Tests/SentTime_Specs.cs b/tests/MassTransit.Tests/SentTime_Specs.cs new file mode 100644 index 00000000000..eb26a09c590 --- /dev/null +++ b/tests/MassTransit.Tests/SentTime_Specs.cs @@ -0,0 +1,35 @@ +namespace MassTransit.Tests; + +using System; +using System.Threading.Tasks; +using MassTransit.Testing; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using TestFramework.Messages; + + +[TestFixture] +public class SentTime_Specs +{ + [Test] + public async Task Should_have_sent_time_header_in_utc() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddHandler(async (PingMessage _) => + { + }); + }) + .BuildServiceProvider(true); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new PingMessage()); + + IReceivedMessage consumed = await harness.Consumed.SelectAsync().FirstOrDefault(); + Assert.That(consumed, Is.Not.Null); + + Assert.That(consumed.Context.SentTime.Value.Kind, Is.EqualTo(DateTimeKind.Utc)); + } +} diff --git a/tests/MassTransit.Tests/Serialization/Array_Specs.cs b/tests/MassTransit.Tests/Serialization/Array_Specs.cs index 720df34fbcd..b54b52d952f 100644 --- a/tests/MassTransit.Tests/Serialization/Array_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/Array_Specs.cs @@ -7,7 +7,6 @@ namespace Array_Specs using System.Text; using MassTransit.Serialization; using NUnit.Framework; - using Shouldly; [TestFixture(typeof(NewtonsoftJsonMessageSerializer))] @@ -34,7 +33,7 @@ public void Should_come_from_json_as_null() var result = Return(Encoding.UTF8.GetBytes(source)); - result.Elements.ShouldBe(null); + Assert.That(result.Elements, Is.Null); } [Test] @@ -44,8 +43,11 @@ public void Should_return_a_null_array() var result = SerializeAndReturn(someArray); - someArray.Elements.ShouldBe(null); - result.Elements.ShouldBe(null); + Assert.Multiple(() => + { + Assert.That(someArray.Elements, Is.Null); + Assert.That(result.Elements, Is.Null); + }); } [Test] @@ -58,20 +60,20 @@ public void Should_serialize_a_single_element() var result = SerializeAndReturn(someArray); - result.Elements.ShouldNotBe(null); - result.Elements.Length.ShouldBe(1); + Assert.That(result.Elements, Is.Not.Null); + Assert.That(result.Elements, Has.Length.EqualTo(1)); } [Test] public void Should_serialize_a_single_element_collection() { var someArray = new SomeCollection(); - someArray.Elements = new ArrayElement[1] {new ArrayElement {Value = 27}}; + someArray.Elements = new ArrayElement[1] { new ArrayElement { Value = 27 } }; var result = SerializeAndReturn(someArray); - result.Elements.ShouldNotBe(null); - result.Elements.Count.ShouldBe(1); + Assert.That(result.Elements, Is.Not.Null); + Assert.That(result.Elements, Has.Count.EqualTo(1)); } public A_null_array(Type serializerType) diff --git a/tests/MassTransit.Tests/Serialization/DateTimeConverter_Specs.cs b/tests/MassTransit.Tests/Serialization/DateTimeConverter_Specs.cs new file mode 100644 index 00000000000..50ac22b3fbf --- /dev/null +++ b/tests/MassTransit.Tests/Serialization/DateTimeConverter_Specs.cs @@ -0,0 +1,62 @@ +namespace MassTransit.Tests.Serialization; + +using System; +using MassTransit.Initializers.TypeConverters; +using NUnit.Framework; + + +[TestFixture] +public class DateTimeConverter_Specs +{ + [Test] + public void Should_convert_date_time_min_value() + { + var converter = new DateTimeTypeConverter(); + + var value = DateTime.MinValue; + + Assert.Multiple(() => + { + Assert.That(converter.TryConvert(value, out string text)); + + Assert.That(converter.TryConvert(text, out var result)); + + Assert.That(result, Is.EqualTo(value)); + }); + } + + [Test] + public void Should_convert_date_time_min_value_to_offset() + { + var converter = new DateTimeTypeConverter(); + var offsetConverter = new DateTimeOffsetTypeConverter(); + + var value = DateTime.MinValue.ToUniversalTime(); + + Assert.Multiple(() => + { + Assert.That(converter.TryConvert(value, out string text)); + + Assert.That(offsetConverter.TryConvert(text, out var result)); + + Assert.That(result.UtcDateTime, Is.EqualTo(value)); + }); + } + + [Test] + public void Should_convert_date_time_offset_min_value() + { + var converter = new DateTimeOffsetTypeConverter(); + + var value = DateTimeOffset.MinValue; + + Assert.Multiple(() => + { + Assert.That(converter.TryConvert(value, out string text)); + + Assert.That(converter.TryConvert(text, out var result)); + + Assert.That(result, Is.EqualTo(value)); + }); + } +} diff --git a/tests/MassTransit.Tests/Serialization/ExtensionData_Specs.cs b/tests/MassTransit.Tests/Serialization/ExtensionData_Specs.cs new file mode 100644 index 00000000000..a7b6471d274 --- /dev/null +++ b/tests/MassTransit.Tests/Serialization/ExtensionData_Specs.cs @@ -0,0 +1,114 @@ +namespace MassTransit.Tests.Serialization; + +using System; +using System.Collections.Generic; +using System.Text.Json.Serialization; +using System.Threading.Tasks; +using MassTransit.Serialization; +using MassTransit.Testing; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; + + +[TestFixture(typeof(SystemTextJsonMessageSerializer))] +[TestFixture(typeof(SystemTextJsonRawMessageSerializer))] +public class ExtensionData_Specs +{ + [Test] + public async Task Should_include_the_required_headers() + { + await using var provider = CreateServiceProvider(); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new ExtensiveMessage + { + Extra = new Dictionary + { + { "text", "Value" }, + { "number", 2 } + } + }); + + IReceivedMessage message = await harness.Consumed.SelectAsync().FirstOrDefault(); + + Assert.That(message, Is.Not.Null); + Assert.That(message.Exception, Is.Null); + + Assert.That(message.Context.Message.Extra.ContainsKey("text")); + Assert.That(message.Context.Message.Extra.ContainsKey("number")); + + await harness.Stop(); + } + + readonly Type _serializerType; + + public ExtensionData_Specs(Type serializerType) + { + _serializerType = serializerType; + } + + ServiceProvider CreateServiceProvider() + { + return new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + x.UsingInMemory((context, cfg) => + { + if (_serializerType == typeof(SystemTextJsonMessageSerializer)) + { + cfg.ConfigureJsonSerializerOptions(options => + { + options.SetMessageSerializerOptions(); + + return options; + }); + } + else if (_serializerType == typeof(SystemTextJsonRawMessageSerializer)) + { + cfg.ClearSerialization(); + cfg.UseRawJsonSerializer(); + + cfg.ConfigureJsonSerializerOptions(options => + { + options.SetMessageSerializerOptions(); + + return options; + }); + } + else if (_serializerType == typeof(NewtonsoftJsonMessageSerializer)) + { + cfg.ClearSerialization(); + cfg.UseNewtonsoftJsonSerializer(); + } + else if (_serializerType == typeof(NewtonsoftRawJsonMessageSerializer)) + { + cfg.ClearSerialization(); + cfg.UseNewtonsoftRawJsonSerializer(); + } + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + } + + + public class ExtensiveMessageConsumer : + IConsumer + { + public Task Consume(ConsumeContext context) + { + return Task.CompletedTask; + } + } + + + public class ExtensiveMessage + { + [JsonExtensionData] + public Dictionary Extra { get; set; } + } +} diff --git a/tests/MassTransit.Tests/Serialization/Forward_Specs.cs b/tests/MassTransit.Tests/Serialization/Forward_Specs.cs index 262fd636ee2..0b444d9fa53 100644 --- a/tests/MassTransit.Tests/Serialization/Forward_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/Forward_Specs.cs @@ -25,15 +25,21 @@ public async Task Should_have_the_original_headers() ConsumeContext handled = await _handled; - Assert.That(handled.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(handled.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.Multiple(() => + { + Assert.That(handled.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(handled.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + }); ConsumeContext forwarded = await _forwarded; - Assert.That(forwarded.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(forwarded.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); - Assert.That(forwarded.Message.Crap, Is.EqualTo("All")); - Assert.That(forwarded.ReceiveContext.ContentType.MediaType, Is.EqualTo(NewtonsoftJsonMessageSerializer.ContentTypeHeaderValue)); + Assert.Multiple(() => + { + Assert.That(forwarded.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(forwarded.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.That(forwarded.Message.Crap, Is.EqualTo("All")); + Assert.That(forwarded.ReceiveContext.ContentType.MediaType, Is.EqualTo(NewtonsoftJsonMessageSerializer.ContentTypeHeaderValue)); + }); } Task> _handled; @@ -90,15 +96,21 @@ public async Task Should_have_the_original_headers() ConsumeContext handled = await _handled; - Assert.That(handled.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(handled.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.Multiple(() => + { + Assert.That(handled.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(handled.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + }); ConsumeContext forwarded = await _forwarded; - Assert.That(forwarded.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(forwarded.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); - Assert.That(forwarded.Message.Crap, Is.EqualTo("All")); - Assert.That(forwarded.ReceiveContext.ContentType.MediaType, Is.EqualTo(NewtonsoftJsonMessageSerializer.ContentTypeHeaderValue)); + Assert.Multiple(() => + { + Assert.That(forwarded.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(forwarded.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.That(forwarded.Message.Crap, Is.EqualTo("All")); + Assert.That(forwarded.ReceiveContext.ContentType.MediaType, Is.EqualTo(NewtonsoftJsonMessageSerializer.ContentTypeHeaderValue)); + }); } Task> _handled; @@ -155,15 +167,21 @@ public async Task Should_have_the_original_headers() ConsumeContext handled = await _handled; - Assert.That(handled.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(handled.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.Multiple(() => + { + Assert.That(handled.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(handled.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + }); ConsumeContext forwarded = await _forwarded; - Assert.That(forwarded.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(forwarded.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); - Assert.That(forwarded.Message.Crap, Is.EqualTo("All")); - Assert.That(forwarded.ReceiveContext.ContentType.MediaType, Is.EqualTo(NewtonsoftXmlMessageSerializer.ContentTypeHeaderValue)); + Assert.Multiple(() => + { + Assert.That(forwarded.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(forwarded.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.That(forwarded.Message.Crap, Is.EqualTo("All")); + Assert.That(forwarded.ReceiveContext.ContentType.MediaType, Is.EqualTo(NewtonsoftXmlMessageSerializer.ContentTypeHeaderValue)); + }); } Task> _handled; diff --git a/tests/MassTransit.Tests/Serialization/GivenAComplexMessage.cs b/tests/MassTransit.Tests/Serialization/GivenAComplexMessage.cs index 2459f699fb9..b196ac4114d 100644 --- a/tests/MassTransit.Tests/Serialization/GivenAComplexMessage.cs +++ b/tests/MassTransit.Tests/Serialization/GivenAComplexMessage.cs @@ -6,7 +6,6 @@ using MassTransit.Serialization; using Messages; using NUnit.Framework; - using Shouldly; using TestFramework.Messages; @@ -16,6 +15,7 @@ [TestFixture(typeof(NewtonsoftXmlMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] public class Given_a_variety_of_challenging_messages : SerializationTest { @@ -32,7 +32,7 @@ public void Byte_interface_array() var result = SerializeAndReturn(msg); - result.Contents.SequenceEqual(msg.Contents).ShouldBeTrue(); + Assert.That(result.Contents, Is.EquivalentTo(msg.Contents)); } [Test] diff --git a/tests/MassTransit.Tests/Serialization/IEnumerable_Specs.cs b/tests/MassTransit.Tests/Serialization/IEnumerable_Specs.cs index 9820c154a25..fad9a147e7f 100644 --- a/tests/MassTransit.Tests/Serialization/IEnumerable_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/IEnumerable_Specs.cs @@ -5,7 +5,6 @@ using System.Linq; using MassTransit.Serialization; using NUnit.Framework; - using Shouldly; [TestFixture(typeof(NewtonsoftJsonMessageSerializer))] @@ -14,17 +13,19 @@ [TestFixture(typeof(NewtonsoftXmlMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] public class Deserializing_an_enumerable_property : SerializationTest { [Test] public void Should_deserialize_the_enumerable_type() { - EnumerableMessageType message = new EnumerableMessageTypeImpl {Items = new[] {new MessageItem {Value = "Frank"}, new MessageItem {Value = "Mary"}}}; + EnumerableMessageType message = + new EnumerableMessageTypeImpl { Items = new[] { new MessageItem { Value = "Frank" }, new MessageItem { Value = "Mary" } } }; var result = SerializeAndReturn(message); - result.Items.Count().ShouldBe(message.Items.Count()); + Assert.That(result.Items.Count(), Is.EqualTo(message.Items.Count())); } public Deserializing_an_enumerable_property(Type serializerType) @@ -35,21 +36,57 @@ public Deserializing_an_enumerable_property(Type serializerType) [TestFixture(typeof(NewtonsoftJsonMessageSerializer))] + [TestFixture(typeof(SystemTextJsonMessageSerializer))] [TestFixture(typeof(BsonMessageSerializer))] + [TestFixture(typeof(NewtonsoftXmlMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] + public class Deserializing_a_list_of_key_value_pairs_with_duplicate_keys : + SerializationTest + { + [Test] + public void Should_not_convert_to_a_dictionary() + { + var message = new ListStringObjectMessage + { + Properties = new[] + { + new KeyValuePair("Frank", "Mary"), + new KeyValuePair("Peter", "Mary"), + new KeyValuePair("Frank", "Peter") + }.ToList() + }; + + var result = SerializeAndReturn(message); + + Assert.That(result.Properties.Count(), Is.EqualTo(message.Properties.Count())); + } + + public Deserializing_a_list_of_key_value_pairs_with_duplicate_keys(Type serializerType) + : base(serializerType) + { + } + } + + + [TestFixture(typeof(NewtonsoftJsonMessageSerializer))] + [TestFixture(typeof(BsonMessageSerializer))] + [TestFixture(typeof(EncryptedMessageSerializer))] + [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] public class Using_the_serializer_for_arrays : SerializationTest { [Test] public void Should_deserialize_multi_dimensional_arrays() { - var message = new DoubleTheMessage {Values = new[,] {{1, 2}, {3, 4}, {5, 6}}}; + var message = new DoubleTheMessage { Values = new[,] { { 1, 2 }, { 3, 4 }, { 5, 6 } } }; var result = SerializeAndReturn(message); Assert.That(result.Values, Is.Not.Null); - Assert.That(result.Values.Length, Is.EqualTo(6), "Length"); + Assert.That(result.Values, Has.Length.EqualTo(6), "Length"); Assert.That(result.Values[1, 1], Is.EqualTo(4), "Value"); } @@ -60,6 +97,12 @@ public Using_the_serializer_for_arrays(Type serializerType) } + public class ListStringObjectMessage + { + public List> Properties { get; set; } + } + + public class DoubleTheMessage { public int[,] Values { get; set; } diff --git a/tests/MassTransit.Tests/Serialization/Interface_Specs.cs b/tests/MassTransit.Tests/Serialization/Interface_Specs.cs index 37ee5874dcb..372e1a2935b 100644 --- a/tests/MassTransit.Tests/Serialization/Interface_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/Interface_Specs.cs @@ -2,13 +2,11 @@ namespace MassTransit.Tests.Serialization { using System; using System.Linq; - using System.Reflection; using System.Threading.Tasks; using MassTransit.Configuration; using MassTransit.Serialization; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -18,6 +16,7 @@ namespace MassTransit.Tests.Serialization [TestFixture(typeof(NewtonsoftXmlMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] public class Deserializing_an_interface : SerializationTest { @@ -32,7 +31,9 @@ public void Should_create_a_proxy_for_the_interface() var result = SerializeAndReturn(complaint); - complaint.Equals(result).ShouldBe(true); + #pragma warning disable NUnit2010 + Assert.That(complaint.Equals(result), Is.True); + #pragma warning restore NUnit2010 } [Test] @@ -54,7 +55,7 @@ public async Task Should_dispatch_an_interface_via_the_pipeline() await pipe.Send(new TestConsumeContext(complaint)); - consumer.Received.Select().Any().ShouldBe(true); + Assert.That(consumer.Received.Select().Any(), Is.True); } public Deserializing_an_interface(Type serializerType) @@ -184,7 +185,7 @@ public override bool Equals(object obj) return false; if (ReferenceEquals(this, obj)) return true; - if (!typeof(ComplaintAdded).GetTypeInfo().IsAssignableFrom(obj.GetType())) + if (!typeof(ComplaintAdded).IsAssignableFrom(obj.GetType())) return false; return Equals((ComplaintAdded)obj); } diff --git a/tests/MassTransit.Tests/Serialization/JobDeserialization_Specs.cs b/tests/MassTransit.Tests/Serialization/JobDeserialization_Specs.cs index 6a5f36cbd12..3a31f5440b5 100644 --- a/tests/MassTransit.Tests/Serialization/JobDeserialization_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/JobDeserialization_Specs.cs @@ -37,6 +37,7 @@ public interface ConvertVideo [TestFixture(typeof(NewtonsoftJsonMessageSerializer))] [TestFixture(typeof(SystemTextJsonMessageSerializer))] + [TestFixture(typeof(MessagePackMessageSerializer))] public class JobDeserialization_Specs : SerializationTest { @@ -63,7 +64,7 @@ public async Task Should_deserialize_the_job() var job = startJobContext.GetJob() ?? throw new SerializationException($"The job could not be deserialized: {TypeCache.ShortName}"); - Assert.That(job.Details.Count, Is.EqualTo(2)); + Assert.That(job.Details, Has.Count.EqualTo(2)); } protected async Task> GetConsumeContext(object values) @@ -71,7 +72,7 @@ protected async Task> GetConsumeContext(object values) { var bytes = Serialize((await MessageInitializerCache.Initialize(values)).Message); - var message = new InMemoryTransportMessage(NewId.NextGuid(), bytes, Serializer.ContentType.MediaType, TypeCache.ShortName); + var message = new InMemoryTransportMessage(NewId.NextGuid(), bytes, Serializer.ContentType.MediaType); var receiveContext = new InMemoryReceiveContext(message, TestConsumeContext.GetContext()); var consumeContext = Deserializer.Deserialize(receiveContext); diff --git a/tests/MassTransit.Tests/Serialization/JsonSerialization_Specs.cs b/tests/MassTransit.Tests/Serialization/JsonSerialization_Specs.cs index a4276af6c3f..606a3fd9b88 100644 --- a/tests/MassTransit.Tests/Serialization/JsonSerialization_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/JsonSerialization_Specs.cs @@ -5,7 +5,6 @@ namespace MassTransit.Tests.Serialization using System.Diagnostics; using System.IO; using System.Linq; - using System.Reflection; using System.Text; using System.Xml.Linq; using MassTransit.Serialization; @@ -14,7 +13,6 @@ namespace MassTransit.Tests.Serialization using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; using NUnit.Framework; - using Shouldly; public class When_serializing_messages_with_json_dot_net @@ -56,7 +54,7 @@ public void Serializing_messages_with_json_dot_net() } }; - _envelope = new Envelope {Message = _message}; + _envelope = new Envelope { Message = _message }; _envelope.MessageType.Add(_message.GetType().ToMessageName()); _envelope.MessageType.Add(typeof(MessageA).ToMessageName()); @@ -113,11 +111,14 @@ public void Should_be_able_to_resurrect_the_message() result = _deserializer.Deserialize(jsonReader); } - result.MessageType.Count.ShouldBe(3); - result.MessageType[0].ShouldBe(typeof(TestMessage).ToMessageName()); - result.MessageType[1].ShouldBe(typeof(MessageA).ToMessageName()); - result.MessageType[2].ShouldBe(typeof(MessageB).ToMessageName()); - result.Headers.Count.ShouldBe(1); + Assert.That(result.MessageType, Has.Count.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(result.MessageType[0], Is.EqualTo(typeof(TestMessage).ToMessageName())); + Assert.That(result.MessageType[1], Is.EqualTo(typeof(MessageA).ToMessageName())); + Assert.That(result.MessageType[2], Is.EqualTo(typeof(MessageB).ToMessageName())); + Assert.That(result.Headers, Has.Count.EqualTo(1)); + }); } [Test] @@ -137,7 +138,7 @@ public void Should_be_able_to_suck_out_an_interface() _serializer.Populate(jsonReader, message); - message.Name.ShouldBe("Joe"); + Assert.That(message.Name, Is.EqualTo("Joe")); } } @@ -156,10 +157,13 @@ public void Should_be_able_to_suck_out_an_object() { var message = (TestMessage)_serializer.Deserialize(jsonReader, typeof(TestMessage)); - message.Name.ShouldBe("Joe"); - message.Details.Count.ShouldBe(2); + Assert.Multiple(() => + { + Assert.That(message.Name, Is.EqualTo("Joe")); + Assert.That(message.Details, Has.Count.EqualTo(2)); - message.EnumDetails.Count().ShouldBe(1); + Assert.That(message.EnumDetails.Count(), Is.EqualTo(1)); + }); } } @@ -181,11 +185,14 @@ public void Should_be_able_to_resurrect_the_message_from_xml() ContractResolver = new CamelCasePropertyNamesContractResolver() }); - result.MessageType.Count.ShouldBe(3); - result.MessageType[0].ShouldBe(typeof(TestMessage).ToMessageName()); - result.MessageType[1].ShouldBe(typeof(MessageA).ToMessageName()); - result.MessageType[2].ShouldBe(typeof(MessageB).ToMessageName()); - result.Headers.Count.ShouldBe(1); + Assert.That(result.MessageType, Has.Count.EqualTo(3)); + Assert.Multiple(() => + { + Assert.That(result.MessageType[0], Is.EqualTo(typeof(TestMessage).ToMessageName())); + Assert.That(result.MessageType[1], Is.EqualTo(typeof(MessageA).ToMessageName())); + Assert.That(result.MessageType[2], Is.EqualTo(typeof(MessageB).ToMessageName())); + Assert.That(result.Headers, Has.Count.EqualTo(1)); + }); } [Test] @@ -348,21 +355,21 @@ public class Using_JsonConverterAttribute_on_a_class : [Test] public void Should_use_converter_for_deserialization() { - var obj = new MessageB {Value = "Joe"}; + var obj = new MessageB { Value = "Joe" }; var result = SerializeAndReturn(obj); - result.Value.ShouldBe("Monster"); + Assert.That(result.Value, Is.EqualTo("Monster")); } [Test] public void Should_use_converter_for_serialization() { - var obj = new MessageA {Value = "Joe"}; + var obj = new MessageA { Value = "Joe" }; var result = SerializeAndReturn(obj); - result.Value.ShouldBe("Monster"); + Assert.That(result.Value, Is.EqualTo("Monster")); } @@ -381,10 +388,10 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { reader.Read(); - reader.Value.ShouldBe("value"); + Assert.That(reader.Value, Is.EqualTo("value")); reader.Read(); var value = (string)reader.Value; - return new MessageA {Value = value}; + return new MessageA { Value = value }; } public override bool CanConvert(Type objectType) @@ -409,7 +416,7 @@ public override void WriteJson(JsonWriter writer, object value, JsonSerializer s public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { - return new MessageB {Value = "Monster"}; + return new MessageB { Value = "Monster" }; } public override bool CanConvert(Type objectType) @@ -449,21 +456,21 @@ public class Using_JsonConverterAttribute_on_a_property : [Test] public void Should_use_converter_for_deserialization() { - var obj = new SimpleMessage {ValueB = "Joe"}; + var obj = new SimpleMessage { ValueB = "Joe" }; var result = SerializeAndReturn(obj); - result.ValueB.ShouldBe("Monster"); + Assert.That(result.ValueB, Is.EqualTo("Monster")); } [Test] public void Should_use_converter_for_serialization() { - var obj = new SimpleMessage {ValueA = "Joe"}; + var obj = new SimpleMessage { ValueA = "Joe" }; var result = SerializeAndReturn(obj); - result.ValueA.ShouldBe("Monster"); + Assert.That(result.ValueA, Is.EqualTo("Monster")); } @@ -481,7 +488,7 @@ public override object ReadJson(JsonReader reader, Type objectType, object exist public override bool CanConvert(Type objectType) { - return typeof(string).GetTypeInfo().IsAssignableFrom(objectType); + return typeof(string).IsAssignableFrom(objectType); } } @@ -528,30 +535,30 @@ public class When_serializing_decimals public void Should_deserialize_correctly() { // arrange - var message = new MessageA {Decimal = decimal.MaxValue}; + var message = new MessageA { Decimal = decimal.MaxValue }; // act, assert var serializedMessage = JsonConvert.SerializeObject(message, NewtonsoftJsonMessageSerializer.SerializerSettings); - serializedMessage.ShouldNotBeNull(); + Assert.That(serializedMessage, Is.Not.Null); var deserializedMessage = JsonConvert.DeserializeObject(serializedMessage, NewtonsoftJsonMessageSerializer.DeserializerSettings); - deserializedMessage.ShouldNotBeNull(); - deserializedMessage.Decimal.ShouldBe(message.Decimal); + Assert.That(deserializedMessage, Is.Not.Null); + Assert.That(deserializedMessage.Decimal, Is.EqualTo(message.Decimal)); } [Test] public void Should_deserialize_correctly_with_stj() { // arrange - var message = new MessageA {Decimal = decimal.MaxValue}; + var message = new MessageA { Decimal = decimal.MaxValue }; // act, assert var serializedMessage = JsonConvert.SerializeObject(message, NewtonsoftJsonMessageSerializer.SerializerSettings); - serializedMessage.ShouldNotBeNull(); + Assert.That(serializedMessage, Is.Not.Null); var deserializedMessage = System.Text.Json.JsonSerializer.Deserialize(serializedMessage, SystemTextJsonMessageSerializer.Options); - deserializedMessage.ShouldNotBeNull(); - deserializedMessage.Decimal.ShouldBe(message.Decimal); + Assert.That(deserializedMessage, Is.Not.Null); + Assert.That(deserializedMessage.Decimal, Is.EqualTo(message.Decimal)); } diff --git a/tests/MassTransit.Tests/Serialization/MessageDataSerialization_Specs.cs b/tests/MassTransit.Tests/Serialization/MessageDataSerialization_Specs.cs index f2704654e12..51195adb51d 100644 --- a/tests/MassTransit.Tests/Serialization/MessageDataSerialization_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/MessageDataSerialization_Specs.cs @@ -13,6 +13,7 @@ [TestFixture(typeof(NewtonsoftXmlMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] public class Serialization_a_message_data_property : SerializationTest { diff --git a/tests/MassTransit.Tests/Serialization/MisnamedProperty_Specs.cs b/tests/MassTransit.Tests/Serialization/MisnamedProperty_Specs.cs index a025d5e01df..d716f0bb837 100644 --- a/tests/MassTransit.Tests/Serialization/MisnamedProperty_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/MisnamedProperty_Specs.cs @@ -20,8 +20,11 @@ public async Task Should_carry_the_properties() Response response = await client.GetResponse(request); - Assert.That(response.Message.Message, Is.EqualTo(message)); - Assert.That(response.Message.Name, Is.EqualTo("I am lost!")); + Assert.Multiple(() => + { + Assert.That(response.Message.Message, Is.EqualTo(message)); + Assert.That(response.Message.Name, Is.EqualTo("I am lost!")); + }); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/Serialization/MoreSerialization_Specs.cs b/tests/MassTransit.Tests/Serialization/MoreSerialization_Specs.cs index 62b1f49facd..8accb044a54 100644 --- a/tests/MassTransit.Tests/Serialization/MoreSerialization_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/MoreSerialization_Specs.cs @@ -14,6 +14,7 @@ namespace MassTransit.Tests.Serialization [TestFixture(typeof(BsonMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] public class MoreSerialization_Specs : SerializationTest { diff --git a/tests/MassTransit.Tests/Serialization/NsbInterop_Specs.cs b/tests/MassTransit.Tests/Serialization/NsbInterop_Specs.cs index 6d402b806b6..0676cff72f2 100644 --- a/tests/MassTransit.Tests/Serialization/NsbInterop_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/NsbInterop_Specs.cs @@ -37,8 +37,11 @@ public async Task Should_only_trigger_one_consumer() await harness.Bus.Publish(new PlaceOrder { OrderId = NewId.NextGuid() }); - Assert.IsTrue(await harness.GetConsumerHarness().Consumed.Any()); - Assert.IsFalse(await harness.GetConsumerHarness().Consumed.Any()); + await Assert.MultipleAsync(async () => + { + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.True); + Assert.That(await harness.GetConsumerHarness().Consumed.Any(), Is.False); + }); } diff --git a/tests/MassTransit.Tests/Serialization/Performance_Specs.cs b/tests/MassTransit.Tests/Serialization/Performance_Specs.cs index b4b862dcf1c..b28691af31b 100644 --- a/tests/MassTransit.Tests/Serialization/Performance_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/Performance_Specs.cs @@ -16,6 +16,7 @@ namespace MassTransit.Tests.Serialization [TestFixture(typeof(NewtonsoftXmlMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] [Explicit] public class Serializer_performance : SerializationTest @@ -44,8 +45,7 @@ public void Just_how_fast_are_you() { byte[] data = Serialize(sendContext); - var transportMessage = new InMemoryTransportMessage(Guid.NewGuid(), data, Serializer.ContentType.MediaType, - TypeCache.ShortName); + var transportMessage = new InMemoryTransportMessage(Guid.NewGuid(), data, Serializer.ContentType.MediaType); receiveContext = new InMemoryReceiveContext(transportMessage, TestConsumeContext.GetContext()); Deserialize(receiveContext); diff --git a/tests/MassTransit.Tests/Serialization/PolymorphicProperty_Specs.cs b/tests/MassTransit.Tests/Serialization/PolymorphicProperty_Specs.cs index b84532cf6ff..5b72c7d300b 100644 --- a/tests/MassTransit.Tests/Serialization/PolymorphicProperty_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/PolymorphicProperty_Specs.cs @@ -75,7 +75,7 @@ public async Task Verify_consumed_message_contains_property() ConsumeContext context = await _handled; - Assert.IsInstanceOf(context.Message.Data); + Assert.That(context.Message.Data, Is.InstanceOf()); } Task> _handled; @@ -110,9 +110,9 @@ public async Task Verify_consumed_message_contains_property() ConsumeContext context = await _handled; Assert.That(context.Message.Data, Is.Not.Null); - Assert.That(context.Message.Data.Length, Is.EqualTo(1)); + Assert.That(context.Message.Data, Has.Length.EqualTo(1)); - Assert.IsInstanceOf(context.Message.Data[0]); + Assert.That(context.Message.Data[0], Is.InstanceOf()); } Task> _handled; @@ -149,9 +149,9 @@ public async Task Verify_consumed_message_contains_property() ConsumeContext context = await _handled; Assert.That(context.Message.Data, Is.Not.Null); - Assert.That(context.Message.Data.Count, Is.EqualTo(1)); + Assert.That(context.Message.Data, Has.Count.EqualTo(1)); - Assert.IsInstanceOf(context.Message.Data[0]); + Assert.That(context.Message.Data[0], Is.InstanceOf()); } Task> _handled; diff --git a/tests/MassTransit.Tests/Serialization/PropertyType_Specs.cs b/tests/MassTransit.Tests/Serialization/PropertyType_Specs.cs index 4fea3483823..7439a33ed9b 100644 --- a/tests/MassTransit.Tests/Serialization/PropertyType_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/PropertyType_Specs.cs @@ -3,7 +3,6 @@ namespace MassTransit.Tests.Serialization using System; using MassTransit.Serialization; using NUnit.Framework; - using Shouldly; [TestFixture(typeof(NewtonsoftJsonMessageSerializer))] @@ -12,6 +11,7 @@ namespace MassTransit.Tests.Serialization [TestFixture(typeof(NewtonsoftXmlMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] public class Serializing_a_property_of_type_char : SerializationTest { @@ -22,7 +22,7 @@ public void Should_handle_a_missing_nullable_value() var result = SerializeAndReturn(obj); - result.Value.ShouldBe(obj.Value); + Assert.That(result.Value, Is.EqualTo(obj.Value)); } [Test] @@ -32,7 +32,7 @@ public void Should_handle_a_present_nullable_value() var result = SerializeAndReturn(obj); - result.Value.ShouldBe(obj.Value); + Assert.That(result.Value, Is.EqualTo(obj.Value)); } [Test] @@ -42,7 +42,7 @@ public void Should_handle_a_present_value() var result = SerializeAndReturn(obj); - result.Value.ShouldBe(obj.Value); + Assert.That(result.Value, Is.EqualTo(obj.Value)); } [Test] @@ -52,7 +52,7 @@ public void Should_handle_a_string_null() var result = SerializeAndReturn(obj); - result.Value.ShouldBe(obj.Value); + Assert.That(result.Value, Is.EqualTo(obj.Value)); } @@ -91,7 +91,7 @@ public void Should_handle_a_missing_nullable_value() var result = SerializeAndReturn(obj); - result.Body.ShouldBe(obj.Body); + Assert.That(result.Body, Is.EqualTo(obj.Body)); } diff --git a/tests/MassTransit.Tests/Serialization/ProtoBufAsJson_Specs.cs b/tests/MassTransit.Tests/Serialization/ProtoBufAsJson_Specs.cs index a8950ec9d32..74e853c5fa6 100644 --- a/tests/MassTransit.Tests/Serialization/ProtoBufAsJson_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/ProtoBufAsJson_Specs.cs @@ -13,13 +13,13 @@ public class Serializing_a_protocol_buffer_message : public void Should_return_the_array_values() { var tb = new TradesBookedMT(); - tb.Trades.Add(new TradeBookedMT {Currency = "AUD"}); - tb.Trades.Add(new TradeBookedMT {Currency = "USD"}); + tb.Trades.Add(new TradeBookedMT { Currency = "AUD" }); + tb.Trades.Add(new TradeBookedMT { Currency = "USD" }); var result = SerializeAndReturn(tb); Assert.That(result.Trades, Is.Not.Null); - Assert.That(result.Trades.Count, Is.EqualTo(2)); + Assert.That(result.Trades, Has.Count.EqualTo(2)); } public Serializing_a_protocol_buffer_message(Type serializerType) diff --git a/tests/MassTransit.Tests/Serialization/ReceiveFault_Serialization_Specs.cs b/tests/MassTransit.Tests/Serialization/ReceiveFault_Serialization_Specs.cs index 4842fab2efc..5b429efaf74 100644 --- a/tests/MassTransit.Tests/Serialization/ReceiveFault_Serialization_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/ReceiveFault_Serialization_Specs.cs @@ -5,7 +5,6 @@ using MassTransit.Serialization; using Metadata; using NUnit.Framework; - using Shouldly; [TestFixture(typeof(NewtonsoftXmlMessageSerializer))] @@ -14,6 +13,7 @@ [TestFixture(typeof(BsonMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializer))] [TestFixture(typeof(EncryptedMessageSerializerV2))] + [TestFixture(typeof(MessagePackMessageSerializer))] public class ReceiveFault_Serialization_Specs : SerializationTest { @@ -69,7 +69,7 @@ public class NonSerializableException : Exception void TestCanSerialize(ReceiveFaultEvent fault) { byte[] bytes = Serialize(fault); - bytes.Length.ShouldBeGreaterThan(0); + Assert.That(bytes, Is.Not.Empty); } } } diff --git a/tests/MassTransit.Tests/Serialization/Redelivery_Specs.cs b/tests/MassTransit.Tests/Serialization/Redelivery_Specs.cs new file mode 100644 index 00000000000..76d7590fb8d --- /dev/null +++ b/tests/MassTransit.Tests/Serialization/Redelivery_Specs.cs @@ -0,0 +1,118 @@ +namespace MassTransit.Tests.Serialization; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Internals; +using MassTransit.Serialization; +using MassTransit.Testing; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using TestFramework; + + +[TestFixture(typeof(SystemTextJsonMessageSerializer))] +[TestFixture(typeof(SystemTextJsonRawMessageSerializer))] +[TestFixture(typeof(NewtonsoftJsonMessageSerializer))] +[TestFixture(typeof(NewtonsoftRawJsonMessageSerializer))] +[TestFixture(typeof(MessagePackMessageSerializer))] +public class Redelivery_Specs +{ + [Test] + public async Task Should_include_the_required_headers() + { + await using var provider = CreateServiceProvider(); + + var harness = await provider.StartTestHarness(); + + await harness.Bus.Publish(new FaultyMessage()); + + Assert.That(await harness.Published.Any()); + + IList> messages = await harness.Consumed.SelectAsync().Take(2).ToListAsync(); + + IReceivedMessage faulted = messages.First(); + Assert.That(faulted, Is.Not.Null); + Assert.That(faulted.Context.SupportedMessageTypes, Does.Contain(MessageUrn.ForTypeString())); + + IReceivedMessage consumed = messages.Last(); + + Assert.That(consumed, Is.Not.Null); + Assert.That(consumed.Context.SupportedMessageTypes, Does.Contain(MessageUrn.ForTypeString())); + + await harness.Stop(); + } + + ServiceProvider CreateServiceProvider() + { + return new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + + x.AddConfigureEndpointsCallback((provider, name, cfg) => + { + cfg.UseDelayedRedelivery(r => + { + r.Intervals(5, 10); + r.ReplaceMessageId = true; + }); + }); + + x.UsingInMemory((context, cfg) => + { + if (_serializerType == typeof(SystemTextJsonMessageSerializer)) + { + } + else if (_serializerType == typeof(SystemTextJsonRawMessageSerializer)) + { + cfg.ClearSerialization(); + cfg.UseRawJsonSerializer(); + } + else if (_serializerType == typeof(NewtonsoftJsonMessageSerializer)) + { + cfg.ClearSerialization(); + cfg.UseNewtonsoftJsonSerializer(); + } + else if (_serializerType == typeof(NewtonsoftRawJsonMessageSerializer)) + { + cfg.ClearSerialization(); + cfg.UseNewtonsoftRawJsonSerializer(); + } + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + } + + readonly Type _serializerType; + + public Redelivery_Specs(Type serializerType) + { + _serializerType = serializerType; + } + + + class FaultyConsumer : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + if (context.GetRedeliveryCount() == 0) + throw new IntentionalTestException(); + + await context.Publish(new FinalMessage()); + } + } + + + record FaultyMessage + { + } + + record FinalMessage + { + } +} diff --git a/tests/MassTransit.Tests/Serialization/SeparateSerializer_Specs.cs b/tests/MassTransit.Tests/Serialization/SeparateSerializer_Specs.cs index a1353b5a90e..449c7b382a5 100644 --- a/tests/MassTransit.Tests/Serialization/SeparateSerializer_Specs.cs +++ b/tests/MassTransit.Tests/Serialization/SeparateSerializer_Specs.cs @@ -68,18 +68,21 @@ public async Task Should_handle_any_requested_message_type() ConsumeContext context = await _handled; - Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(NewtonsoftRawJsonMessageSerializer.RawJsonContentType), - $"unexpected content-type {context.ReceiveContext.ContentType}"); + Assert.Multiple(() => + { + Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(NewtonsoftRawJsonMessageSerializer.RawJsonContentType), + $"unexpected content-type {context.ReceiveContext.ContentType}"); - Assert.That(context.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(context.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.That(context.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(context.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + }); } Task> _handled; protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { - configurator.UseRawJsonSerializer(); + configurator.UseRawJsonSerializer(RawSerializerOptions.All); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) @@ -104,6 +107,7 @@ public class BagOfCrap } } + [TestFixture] public class Sending_and_consuming_raw_xml : InMemoryTestFixture @@ -121,18 +125,21 @@ public async Task Should_handle_any_requested_message_type() ConsumeContext context = await _handled; - Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(RawXmlMessageSerializer.RawXmlContentType), - $"unexpected content-type {context.ReceiveContext.ContentType}"); + Assert.Multiple(() => + { + Assert.That(context.ReceiveContext.ContentType, Is.EqualTo(RawXmlMessageSerializer.RawXmlContentType), + $"unexpected content-type {context.ReceiveContext.ContentType}"); - Assert.That(context.Message.CommandId, Is.EqualTo(message.CommandId)); - Assert.That(context.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + Assert.That(context.Message.CommandId, Is.EqualTo(message.CommandId)); + Assert.That(context.Message.ItemNumber, Is.EqualTo(message.ItemNumber)); + }); } Task> _handled; protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { - configurator.UseRawXmlSerializer(); + configurator.UseRawXmlSerializer(RawSerializerOptions.All); } protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) diff --git a/tests/MassTransit.Tests/Serialization/SerializationException_Specs.cs b/tests/MassTransit.Tests/Serialization/SerializationException_Specs.cs deleted file mode 100644 index af207a388de..00000000000 --- a/tests/MassTransit.Tests/Serialization/SerializationException_Specs.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace MassTransit.Tests.Serialization -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using TestFramework; - - - public class When_a_message_deserialization_exception_occurs - : InMemoryTestFixture - { - [Test] - public async Task Should_put_message_in_error_queue() - { - await InputQueueSendEndpoint.Send(new BadMessage("Good")); - - - // - // IEndpoint errorEndpoint = - // LocalBus.GetEndpoint(LocalBus.Endpoint.InboundTransport.Address.AppendToPath("_error")); - // errorEndpoint.InboundTransport.ShouldContain(errorEndpoint.Serializer, typeof(BadMessage)); - // - // LocalBus.Endpoint.ShouldNotContain(); - // - // var errorTransport = LocalBus.Endpoint.ErrorTransport as LoopbackTransport; - // errorTransport.ShouldNotBe(null); - // - // errorTransport.Count.ShouldBe(1); - } - - protected override void ConfigureInMemoryReceiveEndpoint(IInMemoryReceiveEndpointConfigurator configurator) - { - Handled(configurator); - } - - - class BadMessage - { - public BadMessage() - { - throw new InvalidOperationException("I want to be bad."); - } - - public BadMessage(string value) - { - Value = value; - } - - public string Value { get; set; } - } - } -} diff --git a/tests/MassTransit.Tests/Serialization/SerializationTest.cs b/tests/MassTransit.Tests/Serialization/SerializationTest.cs index 1e7219e073d..e1604082439 100644 --- a/tests/MassTransit.Tests/Serialization/SerializationTest.cs +++ b/tests/MassTransit.Tests/Serialization/SerializationTest.cs @@ -3,9 +3,9 @@ namespace MassTransit.Tests.Serialization using System; using Context; using InMemoryTransport; + using Internals; using MassTransit.Serialization; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -101,6 +101,12 @@ public void SetupSerializationTest() Serializer = new EncryptedMessageSerializerV2(streamProvider); Deserializer = new EncryptedMessageDeserializerV2(BsonMessageSerializer.Deserializer, streamProvider); } + else if (_serializerType == typeof(MessagePackMessageSerializer)) + { + var messagePackSerializer = new MessagePackMessageSerializer(); + Serializer = messagePackSerializer; + Deserializer = messagePackSerializer; + } else throw new ArgumentException("The serializer type is unknown"); } @@ -133,21 +139,24 @@ protected byte[] Serialize(T obj) protected T Return(byte[] serializedMessageData) where T : class { - var message = new InMemoryTransportMessage(Guid.NewGuid(), serializedMessageData, Serializer.ContentType.MediaType, TypeCache.ShortName); + var message = new InMemoryTransportMessage(Guid.NewGuid(), serializedMessageData, Serializer.ContentType.MediaType); var receiveContext = new InMemoryReceiveContext(message, TestConsumeContext.GetContext()); var consumeContext = Deserializer.Deserialize(receiveContext); consumeContext.TryGetMessage(out ConsumeContext messageContext); - messageContext.ShouldNotBe(null); + Assert.That(messageContext, Is.Not.Null); - messageContext.SourceAddress.ShouldBe(_sourceAddress); - messageContext.DestinationAddress.ShouldBe(_destinationAddress); - messageContext.FaultAddress.ShouldBe(_faultAddress); - messageContext.ResponseAddress.ShouldBe(_responseAddress); - messageContext.RequestId.HasValue.ShouldBe(true); - messageContext.RequestId.Value.ShouldBe(_requestId); + Assert.Multiple(() => + { + Assert.That(messageContext.SourceAddress, Is.EqualTo(_sourceAddress)); + Assert.That(messageContext.DestinationAddress, Is.EqualTo(_destinationAddress)); + Assert.That(messageContext.FaultAddress, Is.EqualTo(_faultAddress)); + Assert.That(messageContext.ResponseAddress, Is.EqualTo(_responseAddress)); + Assert.That(messageContext.RequestId.HasValue, Is.EqualTo(true)); + Assert.That(messageContext.RequestId.Value, Is.EqualTo(_requestId)); + }); return messageContext.Message; } @@ -157,7 +166,7 @@ protected virtual void TestSerialization(T message) { var result = SerializeAndReturn(message); - message.Equals(result).ShouldBe(true); + Assert.That(message, Is.EqualTo(result)); } } } diff --git a/tests/MassTransit.Tests/StaticProperty_Specs.cs b/tests/MassTransit.Tests/StaticProperty_Specs.cs index b6df15266d9..a469340b9e8 100644 --- a/tests/MassTransit.Tests/StaticProperty_Specs.cs +++ b/tests/MassTransit.Tests/StaticProperty_Specs.cs @@ -41,45 +41,54 @@ public class when_getting_static_properties [Test] public void Can_get_even_with_private_getter() { - IEnumerable props = typeof(StaticsNoGetter).GetAllStaticProperties(); + IEnumerable props = typeof(StaticsNoGetter).GetAllStaticProperties().ToArray(); Assert.That(props.Count(), Is.EqualTo(2)); - IEnumerable names = props.Select(x => x.Name); - CollectionAssert.Contains(names, "ZupMan"); - CollectionAssert.Contains(names, "StaticProp"); + IEnumerable names = props.Select(x => x.Name).ToArray(); + Assert.That(names, Has.Member("ZupMan")); + Assert.That(names, Has.Member("StaticProp")); } [Test] public void Can_get_private_static_properties() { - IEnumerable props = typeof(PrivateStatics).GetAllStaticProperties(); + IEnumerable props = typeof(PrivateStatics).GetAllStaticProperties().ToArray(); Assert.That(props.Count(), Is.EqualTo(2)); - IEnumerable names = props.Select(x => x.Name); - CollectionAssert.Contains(names, "CanWeGetPrivates"); - CollectionAssert.Contains(names, "StaticProp"); + IEnumerable names = props.Select(x => x.Name).ToArray(); + Assert.That(names, Has.Member("CanWeGetPrivates")); + Assert.That(names, Has.Member("StaticProp")); } [Test] public void Can_get_property_on_stand_alone_class() { - IEnumerable props = typeof(SuperTarget).GetAllStaticProperties(); - Assert.That(props.Count(), Is.EqualTo(1)); - Assert.That(props.First().Name, Is.EqualTo("StaticProp")); + IEnumerable props = typeof(SuperTarget).GetAllStaticProperties().ToArray(); + Assert.Multiple(() => + { + Assert.That(props.Count(), Is.EqualTo(1)); + Assert.That(props.First().Name, Is.EqualTo("StaticProp")); + }); } [Test] public void Can_get_single_property_on_super_from_sub() { - IEnumerable props = typeof(SubTarget).GetAllStaticProperties(); - Assert.That(props.Count(), Is.EqualTo(1)); - Assert.That(props.First().Name, Is.EqualTo("StaticProp")); + IEnumerable props = typeof(SubTarget).GetAllStaticProperties().ToArray(); + Assert.Multiple(() => + { + Assert.That(props.Count(), Is.EqualTo(1)); + Assert.That(props.First().Name, Is.EqualTo("StaticProp")); + }); } [Test] public void Can_get_with_no_hierarchy() { - IEnumerable props = typeof(StaticsNoGetter).GetStaticProperties(); - Assert.That(props.Count(), Is.EqualTo(1)); - Assert.That(props.First().Name, Is.EqualTo("ZupMan")); + IEnumerable props = typeof(StaticsNoGetter).GetStaticProperties().ToArray(); + Assert.Multiple(() => + { + Assert.That(props.Count(), Is.EqualTo(1)); + Assert.That(props.First().Name, Is.EqualTo("ZupMan")); + }); } } } diff --git a/tests/MassTransit.Tests/TaskExecutor_Specs.cs b/tests/MassTransit.Tests/TaskExecutor_Specs.cs new file mode 100644 index 00000000000..32f8597edaa --- /dev/null +++ b/tests/MassTransit.Tests/TaskExecutor_Specs.cs @@ -0,0 +1,38 @@ +namespace MassTransit.Tests; + +using System.Threading.Tasks; +using NUnit.Framework; +using Util; + + +[TestFixture] +public class TaskExecutor_Specs +{ + [Test] + public async Task Should_run_a_single_item() + { + await using var executor = new TaskExecutor(10); + + async ValueTask Method() + { + await Task.Delay(100); + } + + await Task.Run(async () => + await executor.Run(Method)); + } + + [Test] + public async Task Should_run_a_single_task() + { + await using var executor = new TaskExecutor(10); + + async Task Method() + { + await Task.Delay(100); + } + + await Task.Run(async () => + await executor.Run(Method)); + } +} diff --git a/tests/MassTransit.Tests/TelemetryMonitor_Specs.cs b/tests/MassTransit.Tests/TelemetryMonitor_Specs.cs new file mode 100644 index 00000000000..886349382b1 --- /dev/null +++ b/tests/MassTransit.Tests/TelemetryMonitor_Specs.cs @@ -0,0 +1,100 @@ +namespace MassTransit.Tests; + +using System.Threading.Tasks; +using MassTransit.Testing; +using Microsoft.Extensions.DependencyInjection; +using NUnit.Framework; +using TestFramework.Messages; + + +[TestFixture] +public class TelemetryMonitor_Specs +{ + [Test] + public async Task Should_wait_until_published_message_is_consumed() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Wait(x => x.Publish(new PingMessage())); + + IPublishedMessage published = await harness.Published.SelectAsync().First(); + + Assert.That(published, Is.Not.Null); + Assert.That(published.Context.RequestId, Is.Null); + } + + [Test] + public async Task Should_wait_until_sent_message_is_consumed() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var endpoint = await harness.GetConsumerEndpoint(); + + await endpoint.Wait(x => x.Send(new PingMessage())); + + IPublishedMessage published = await harness.Published.SelectAsync().First(); + + Assert.That(published, Is.Not.Null); + Assert.That(published.Context.RequestId, Is.Null); + } + + [Test] + public async Task Should_wait_until_the_request_is_consumed() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddConsumer(); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var client = harness.GetRequestClient(); + + Response response = await client.Wait(x => x.GetResponse(new PingMessage())); + + IPublishedMessage published = await harness.Published.SelectAsync().First(); + + Assert.That(published, Is.Not.Null); + Assert.That(published.Context.RequestId, Is.Null); + } + + + class PingHandled + { + } + + + class PingConsumer : + IConsumer + { + public async Task Consume(ConsumeContext context) + { + await context.Publish(new PingHandled()); + + if (context.IsResponseAccepted()) + await context.RespondAsync(new PongMessage(context.Message.CorrelationId)); + } + } +} diff --git a/tests/MassTransit.Tests/Testing/BusPublish_Specs.cs b/tests/MassTransit.Tests/Testing/BusPublish_Specs.cs new file mode 100644 index 00000000000..e27a88150f8 --- /dev/null +++ b/tests/MassTransit.Tests/Testing/BusPublish_Specs.cs @@ -0,0 +1,60 @@ +namespace MassTransit.Tests.Testing +{ + using System.Threading.Tasks; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + + + [TestFixture] + public class BusPublish_Specs + { + [Test] + public async Task Should_observe_published_event() + { + //Arrange + var provider = new ServiceCollection() + .AddScoped() + .AddMassTransitTestHarness() + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + var service = harness.Scope.ServiceProvider.GetRequiredService(); + + await service.Register(); + + Assert.That(await harness.Published.Any()); + } + + + public class MyMessage + { + } + + + public interface IMyApplicationService + { + Task Register(); + } + + + class MyApplicationService : + IMyApplicationService + { + readonly IBus _bus; + + public MyApplicationService(IBus bus) + { + _bus = bus; + } + + public async Task Register() + { + await _bus.Publish(new MyMessage()); + } + } + } +} diff --git a/tests/MassTransit.Tests/Testing/ConsumerMultiple_Specs.cs b/tests/MassTransit.Tests/Testing/ConsumerMultiple_Specs.cs index 51697b4f2db..1813c3f29f7 100644 --- a/tests/MassTransit.Tests/Testing/ConsumerMultiple_Specs.cs +++ b/tests/MassTransit.Tests/Testing/ConsumerMultiple_Specs.cs @@ -4,7 +4,6 @@ namespace MassTransit.Tests.Testing using System.Threading.Tasks; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; public class When_a_consumer_with_multiple_message_consumers_is_tested @@ -33,25 +32,25 @@ public async Task Teardown() [Test] public void Should_have_sent_the_aa_response_from_the_consumer() { - _harness.Sent.Select().Any().ShouldBe(true); + Assert.That(_harness.Sent.Select().Any(), Is.True); } [Test] public void Should_have_sent_the_bb_response_from_the_consumer() { - _harness.Sent.Select().Any().ShouldBe(true); + Assert.That(_harness.Sent.Select().Any(), Is.True); } [Test] public void Should_have_called_the_consumer_a_method() { - _consumer.Consumed.Select().Any().ShouldBe(true); + Assert.That(_consumer.Consumed.Select().Any(), Is.True); } [Test] public void Should_have_called_the_consumer_b_method() { - _consumer.Consumed.Select().Any().ShouldBe(true); + Assert.That(_consumer.Consumed.Select().Any(), Is.True); } diff --git a/tests/MassTransit.Tests/Testing/ConsumerTest_Specs.cs b/tests/MassTransit.Tests/Testing/ConsumerTest_Specs.cs index 0d17cc6f1db..b2892cc2c37 100644 --- a/tests/MassTransit.Tests/Testing/ConsumerTest_Specs.cs +++ b/tests/MassTransit.Tests/Testing/ConsumerTest_Specs.cs @@ -4,7 +4,6 @@ namespace MassTransit.Tests.Testing using System.Threading.Tasks; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; [TestFixture] @@ -13,25 +12,25 @@ public class When_a_consumer_is_being_tested [Test] public void Should_have_called_the_consumer_method() { - _consumer.Consumed.Select().Any().ShouldBe(true); + Assert.That(_consumer.Consumed.Select().Any(), Is.True); } [Test] public void Should_have_sent_the_response_from_the_consumer() { - _harness.Published.Select().Any().ShouldBe(true); + Assert.That(_harness.Published.Select().Any(), Is.True); } [Test] public void Should_receive_the_message_type_a() { - _harness.Consumed.Select().Any().ShouldBe(true); + Assert.That(_harness.Consumed.Select().Any(), Is.True); } [Test] public void Should_send_the_initial_message_to_the_consumer() { - _harness.Sent.Select().Any().ShouldBe(true); + Assert.That(_harness.Sent.Select().Any(), Is.True); } InMemoryTestHarness _harness; @@ -152,27 +151,33 @@ public class When_a_consumer_of_interfaces_is_being_tested [Test] public void Should_have_called_the_consumer_method() { - _consumer.Consumed.Select().Any().ShouldBe(true); + Assert.That(_consumer.Consumed.Select().Any(), Is.True); } [Test] public void Should_have_sent_the_response_from_the_consumer() { - _harness.Published.Select().Any().ShouldBe(true); - _harness.Published.Select().Any().ShouldBe(true); + Assert.Multiple(() => + { + Assert.That(_harness.Published.Select().Any(), Is.True); + Assert.That(_harness.Published.Select().Any(), Is.True); + }); } [Test] public void Should_receive_the_message_type_a() { - _harness.Consumed.Select().Any().ShouldBe(true); + Assert.That(_harness.Consumed.Select().Any(), Is.True); } [Test] public void Should_send_the_initial_message_to_the_consumer() { - _harness.Sent.Select().Any().ShouldBe(true); - _harness.Sent.Select().Any().ShouldBe(true); + Assert.Multiple(() => + { + Assert.That(_harness.Sent.Select().Any(), Is.True); + Assert.That(_harness.Sent.Select().Any(), Is.True); + }); } InMemoryTestHarness _harness; @@ -254,25 +259,25 @@ public async Task Teardown() [Test] public void Should_send_the_initial_message_to_the_consumer() { - _harness.Sent.Select().Any().ShouldBe(true); + Assert.That(_harness.Sent.Select().Any(), Is.True); } [Test] public void Should_have_sent_the_response_from_the_consumer() { - _harness.Sent.Select().Any().ShouldBe(true); + Assert.That(_harness.Sent.Select().Any(), Is.True); } [Test] public void Should_receive_the_message_type_a() { - _harness.Consumed.Select().Any().ShouldBe(true); + Assert.That(_harness.Consumed.Select().Any(), Is.True); } [Test] public void Should_have_called_the_consumer_method() { - _consumer.Consumed.Select().Any().ShouldBe(true); + Assert.That(_consumer.Consumed.Select().Any(), Is.True); } diff --git a/tests/MassTransit.Tests/Testing/HandlerRespond_Specs.cs b/tests/MassTransit.Tests/Testing/HandlerRespond_Specs.cs index 5eaa82e6bbd..877b8c5bcdc 100644 --- a/tests/MassTransit.Tests/Testing/HandlerRespond_Specs.cs +++ b/tests/MassTransit.Tests/Testing/HandlerRespond_Specs.cs @@ -4,7 +4,6 @@ namespace MassTransit.Tests.Testing using System.Threading.Tasks; using MassTransit.Testing; using NUnit.Framework; - using Shouldly; public class When_a_handler_responds_to_a_message @@ -39,15 +38,15 @@ public async Task Should_have_sent_a_message_of_type_b() public async Task Should_have_sent_message_to_bus_address() { ISentMessage message = await _harness.Sent.SelectAsync().First(); - message.ShouldNotBeNull(); + Assert.That(message, Is.Not.Null); - message.Context.DestinationAddress.ShouldBe(_harness.BusAddress); + Assert.That(message.Context.DestinationAddress, Is.EqualTo(_harness.BusAddress)); } [Test] public void Should_support_a_simple_handler() { - _handler.Consumed.Select().Any().ShouldBe(true); + Assert.That(_handler.Consumed.Select().Any(), Is.True); } diff --git a/tests/MassTransit.Tests/Testing/HandlerTest_Specs.cs b/tests/MassTransit.Tests/Testing/HandlerTest_Specs.cs index 541f3026341..b616d9096bf 100644 --- a/tests/MassTransit.Tests/Testing/HandlerTest_Specs.cs +++ b/tests/MassTransit.Tests/Testing/HandlerTest_Specs.cs @@ -1,140 +1,138 @@ -namespace MassTransit.Tests.Testing -{ - using System.Linq; - using System.Threading.Tasks; - using MassTransit.Testing; - using NUnit.Framework; - using Shouldly; - - - [TestFixture] - public class Using_the_handler_test_factory - { - [Test] - public void Should_have_received_a_message_of_type_a() - { - _harness.Consumed.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_have_sent_a_message_of_type_a() - { - _harness.Sent.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_have_sent_a_message_of_type_b() - { - _harness.Sent.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_support_a_simple_handler() - { - _handler.Consumed.Select().Any().ShouldBe(true); - } - - InMemoryTestHarness _harness; - HandlerTestHarness _handler; - - [OneTimeSetUp] - public async Task Setup() - { - _harness = new InMemoryTestHarness(); - _handler = _harness.Handler(); - - await _harness.Start(); - - await _harness.InputQueueSendEndpoint.Send(new A()); - await _harness.InputQueueSendEndpoint.Send(new B()); - } - - [OneTimeTearDown] - public async Task Teardown() - { - await _harness.Stop(); - } - - - class A - { - } - - - class B - { - } - } - - - [TestFixture] - public class Publishing_to_a_handler_test - { - [Test] - public void Should_have_published_a_message_of_type_b() - { - _harness.Published.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_have_published_a_message_of_type_ib() - { - _harness.Published.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_have_received_a_message_of_type_a() - { - _harness.Consumed.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_have_sent_a_message_of_type_a() - { - _harness.Published.Select().Any().ShouldBe(true); - } - - [Test] - public void Should_support_a_simple_handler() - { - _handler.Consumed.Select().Any().ShouldBe(true); - } - - InMemoryTestHarness _harness; - HandlerTestHarness _handler; - - [OneTimeSetUp] - public async Task Setup() - { - _harness = new InMemoryTestHarness(); - _handler = _harness.Handler(); - - await _harness.Start(); - - await _harness.Bus.Publish(new A()); - await _harness.Bus.Publish(new B()); - } - - [OneTimeTearDown] - public async Task Teardown() - { - await _harness.Stop(); - } - - - class A - { - } - - - class B : - IB - { - } - - - interface IB - { - } - } -} +namespace MassTransit.Tests.Testing +{ + using System.Linq; + using System.Threading.Tasks; + using MassTransit.Testing; + using NUnit.Framework; + + [TestFixture] + public class Using_the_handler_test_factory + { + [Test] + public void Should_have_received_a_message_of_type_a() + { + Assert.That(_harness.Consumed.Select().Any(), Is.True); + } + + [Test] + public void Should_have_sent_a_message_of_type_a() + { + Assert.That(_harness.Sent.Select().Any(), Is.True); + } + + [Test] + public void Should_have_sent_a_message_of_type_b() + { + Assert.That(_harness.Sent.Select().Any(), Is.True); + } + + [Test] + public void Should_support_a_simple_handler() + { + Assert.That(_handler.Consumed.Select().Any(), Is.True); + } + + InMemoryTestHarness _harness; + HandlerTestHarness _handler; + + [OneTimeSetUp] + public async Task Setup() + { + _harness = new InMemoryTestHarness(); + _handler = _harness.Handler(); + + await _harness.Start(); + + await _harness.InputQueueSendEndpoint.Send(new A()); + await _harness.InputQueueSendEndpoint.Send(new B()); + } + + [OneTimeTearDown] + public async Task Teardown() + { + await _harness.Stop(); + } + + + class A + { + } + + + class B + { + } + } + + + [TestFixture] + public class Publishing_to_a_handler_test + { + [Test] + public void Should_have_published_a_message_of_type_b() + { + Assert.That(_harness.Published.Select().Any(), Is.True); + } + + [Test] + public void Should_have_published_a_message_of_type_ib() + { + Assert.That(_harness.Published.Select().Any(), Is.True); + } + + [Test] + public void Should_have_received_a_message_of_type_a() + { + Assert.That(_harness.Consumed.Select().Any(), Is.True); + } + + [Test] + public void Should_have_sent_a_message_of_type_a() + { + Assert.That(_harness.Published.Select().Any(), Is.True); + } + + [Test] + public void Should_support_a_simple_handler() + { + Assert.That(_handler.Consumed.Select().Any(), Is.True); + } + + InMemoryTestHarness _harness; + HandlerTestHarness _handler; + + [OneTimeSetUp] + public async Task Setup() + { + _harness = new InMemoryTestHarness(); + _handler = _harness.Handler(); + + await _harness.Start(); + + await _harness.Bus.Publish(new A()); + await _harness.Bus.Publish(new B()); + } + + [OneTimeTearDown] + public async Task Teardown() + { + await _harness.Stop(); + } + + + class A + { + } + + + class B : + IB + { + } + + + interface IB + { + } + } +} diff --git a/tests/MassTransit.Tests/Testing/SagaTest_Specs.cs b/tests/MassTransit.Tests/Testing/SagaTest_Specs.cs index 74275b2232d..13fc0da1c95 100644 --- a/tests/MassTransit.Tests/Testing/SagaTest_Specs.cs +++ b/tests/MassTransit.Tests/Testing/SagaTest_Specs.cs @@ -8,7 +8,6 @@ namespace MassTransit.Tests.Testing using MassTransit.Testing; using Newtonsoft.Json; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -71,40 +70,40 @@ public async Task Teardown() [Test] public void Should_send_the_initial_message_to_the_consumer() { - _harness.Sent.Select().Any().ShouldBe(true); + Assert.That(_harness.Sent.Select().Any(), Is.True); } [Test] public void Should_receive_the_message_type_a() { - _harness.Consumed.Select().Any().ShouldBe(true); + Assert.That(_harness.Consumed.Select().Any(), Is.True); } [Test] public void Should_create_a_new_saga_for_the_message() { - _saga.Created.Select(x => x.CorrelationId == _sagaId).Any().ShouldBe(true); + Assert.That(_saga.Created.Select(x => x.CorrelationId == _sagaId).Any(), Is.True); } [Test] public void Should_have_the_saga_instance_with_the_value() { var saga = _saga.Created.Contains(_sagaId); - saga.ShouldNotBe(null); + Assert.That(saga, Is.Not.Null); - saga.ValueA.ShouldBe(_testValueA); + Assert.That(saga.ValueA, Is.EqualTo(_testValueA)); } [Test] public void Should_have_published_event_message() { - _harness.Published.Select().Any().ShouldBe(true); + Assert.That(_harness.Published.Select().Any(), Is.True); } [Test] public void Should_have_called_the_consumer_method() { - _saga.Consumed.Select().Any().ShouldBe(true); + Assert.That(_saga.Consumed.Select().Any(), Is.True); } diff --git a/tests/MassTransit.Tests/Testing/StandaloneConsumer_Specs.cs b/tests/MassTransit.Tests/Testing/StandaloneConsumer_Specs.cs index 9e6a451498c..c3f263edcec 100644 --- a/tests/MassTransit.Tests/Testing/StandaloneConsumer_Specs.cs +++ b/tests/MassTransit.Tests/Testing/StandaloneConsumer_Specs.cs @@ -22,9 +22,12 @@ public async Task Should_be_able_to_create_standalone_consumer_test_in_memory() { await harness.InputQueueSendEndpoint.Send(new PingMessage()); - Assert.That(harness.Consumed.Select().Any(), Is.True); + Assert.Multiple(() => + { + Assert.That(harness.Consumed.Select().Any(), Is.True); - Assert.That(consumer.Consumed.Select().Any(), Is.True); + Assert.That(consumer.Consumed.Select().Any(), Is.True); + }); } finally { diff --git a/tests/MassTransit.Tests/Testing/StateMachineSagaTest_Specs.cs b/tests/MassTransit.Tests/Testing/StateMachineSagaTest_Specs.cs index 79d62e20072..0634e7ae5c1 100644 --- a/tests/MassTransit.Tests/Testing/StateMachineSagaTest_Specs.cs +++ b/tests/MassTransit.Tests/Testing/StateMachineSagaTest_Specs.cs @@ -7,7 +7,6 @@ namespace MassTransit.Tests.Testing using MassTransit.Testing; using Newtonsoft.Json; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -73,16 +72,22 @@ await _harness.InputQueueSendEndpoint.Send>(new public async Task Should_receive_the_fault_with_data() { await _saga.Exists(_sagaId); - _harness.Consumed.Select().Any().ShouldBeTrue(); - _harness.Consumed.Select>().Any().ShouldBeTrue(); + Assert.Multiple(() => + { + Assert.That(_harness.Consumed.Select().Any(), Is.True); + Assert.That(_harness.Consumed.Select>().Any(), Is.True); + }); var result = (Fault)_harness.Consumed.Select>().Single().MessageObject; - result.FaultId.ShouldBe(_faultId); - result.FaultedMessageId.ShouldBe(_faultedMessageId); - result.Timestamp.ShouldBe(_timestamp); - result.FaultMessageTypes.ShouldBeEquivalentTo(_faultMessageTypes); - result.Message.ShouldNotBeNull(); - result.Exceptions.ShouldNotBeNull(); + Assert.Multiple(() => + { + Assert.That(result.FaultId, Is.EqualTo(_faultId)); + Assert.That(result.FaultedMessageId, Is.EqualTo(_faultedMessageId)); + Assert.That(result.Timestamp, Is.EqualTo(_timestamp)); + Assert.That(result.FaultMessageTypes, Is.EqualTo(_faultMessageTypes)); + Assert.That(result.Message, Is.Not.Null); + Assert.That(result.Exceptions, Is.Not.Null); + }); } [OneTimeTearDown] diff --git a/tests/MassTransit.Tests/Threading_Specs.cs b/tests/MassTransit.Tests/Threading_Specs.cs index 24dcbbb4a2d..3405c3a83b9 100644 --- a/tests/MassTransit.Tests/Threading_Specs.cs +++ b/tests/MassTransit.Tests/Threading_Specs.cs @@ -24,7 +24,7 @@ public void Should_scale_threads_to_meet_demand() { var timer = Stopwatch.StartNew(); Bus.Publish(new A()); - Assert.IsTrue(_before.WaitOne(TimeSpan.FromSeconds(30)), "Consumer thread failed to start"); + Assert.That(_before.WaitOne(TimeSpan.FromSeconds(30)), Is.True, "Consumer thread failed to start"); timer.Stop(); latency.Add(timer.ElapsedMilliseconds); } @@ -34,7 +34,7 @@ public void Should_scale_threads_to_meet_demand() _wait.Set(); for (var i = 0; i < 100; i++) - Assert.IsTrue(_after.WaitOne(TimeSpan.FromSeconds(30)), "Consumer thread failed to complete"); + Assert.That(_after.WaitOne(TimeSpan.FromSeconds(30)), Is.True, "Consumer thread failed to complete"); Console.WriteLine("Elapsed Time: {0}", DateTime.Now - now); } diff --git a/tests/MassTransit.Tests/Timeout_Specs.cs b/tests/MassTransit.Tests/Timeout_Specs.cs index a372cfa2202..8c22bae92a9 100644 --- a/tests/MassTransit.Tests/Timeout_Specs.cs +++ b/tests/MassTransit.Tests/Timeout_Specs.cs @@ -19,11 +19,14 @@ public async Task Should_throw_on_timeout() await InputQueueSendEndpoint.Send(new PingMessage()); await Task.WhenAny(_succeeded, faulted); - Assert.IsTrue(_firstCalled); - Assert.IsTrue(_firstRequested.HasValue); - Assert.IsFalse(_firstRequested.Value); - Assert.IsFalse(_secondCalled); - Assert.IsFalse(_secondRequested.HasValue); + Assert.Multiple(() => + { + Assert.That(_firstCalled, Is.True); + Assert.That(_firstRequested.HasValue, Is.True); + Assert.That(_firstRequested.Value, Is.False); + Assert.That(_secondCalled, Is.False); + Assert.That(_secondRequested.HasValue, Is.False); + }); } Task _succeeded; diff --git a/tests/MassTransit.Tests/Topology/CreateTopology_Specs.cs b/tests/MassTransit.Tests/Topology/CreateTopology_Specs.cs index 4cb71d9f3c7..e2406a7127a 100644 --- a/tests/MassTransit.Tests/Topology/CreateTopology_Specs.cs +++ b/tests/MassTransit.Tests/Topology/CreateTopology_Specs.cs @@ -43,22 +43,31 @@ public async Task Should_have_a_familiar_syntax() ConsumeContext context = await handled; - Assert.IsTrue(context.CorrelationId.HasValue); - Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(context.CorrelationId.HasValue, Is.True); + Assert.That(context.CorrelationId.Value, Is.EqualTo(transactionId)); + }); await harness.InputQueueSendEndpoint.Send(new { CorrelationId = transactionId }); ConsumeContext otherContext = await otherHandled; - Assert.IsTrue(otherContext.CorrelationId.HasValue); - Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(otherContext.CorrelationId.HasValue, Is.True); + Assert.That(otherContext.CorrelationId.Value, Is.EqualTo(transactionId)); + }); await harness.InputQueueSendEndpoint.Send(new { TransactionId = transactionId }); ConsumeContext legacyContext = await legacyHandled; - Assert.IsTrue(legacyContext.CorrelationId.HasValue); - Assert.That(legacyContext.CorrelationId.Value, Is.EqualTo(transactionId)); + Assert.Multiple(() => + { + Assert.That(legacyContext.CorrelationId.HasValue, Is.True); + Assert.That(legacyContext.CorrelationId.Value, Is.EqualTo(transactionId)); + }); } finally { diff --git a/tests/MassTransit.Tests/Topology/MessageSerializer_Specs.cs b/tests/MassTransit.Tests/Topology/MessageSerializer_Specs.cs new file mode 100644 index 00000000000..aef29fba7d5 --- /dev/null +++ b/tests/MassTransit.Tests/Topology/MessageSerializer_Specs.cs @@ -0,0 +1,57 @@ +namespace MassTransit.Tests.Topology +{ + using System.Threading.Tasks; + using MassTransit.Serialization; + using MassTransit.Testing; + using Microsoft.Extensions.DependencyInjection; + using NUnit.Framework; + using TopologyTestSubjects; + + + namespace TopologyTestSubjects + { + public interface JsonMessage + { + string Value { get; } + } + } + + + [TestFixture] + public class Specifying_a_message_serializer_via_topology + { + [Test] + public async Task Should_use_the_serializer_for_messages() + { + await using var provider = new ServiceCollection() + .AddMassTransitTestHarness(x => + { + x.AddHandler(async (JsonMessage message) => + { + }); + + x.UsingInMemory((context, cfg) => + { + cfg.AddRawJsonSerializer(); + + cfg.Send(m => m.UseSerializer("application/json")); + + cfg.ConfigureEndpoints(context); + }); + }) + .BuildServiceProvider(true); + + var harness = provider.GetTestHarness(); + + await harness.Start(); + + await harness.Bus.Publish(new { Value = "Frank" }); + + Assert.That(await harness.Consumed.Any(), Is.True); + + IReceivedMessage received = await harness.Consumed.SelectAsync().First(); + + Assert.That(received.Context.ReceiveContext.ContentType, Is.EqualTo(SystemTextJsonRawMessageSerializer.JsonContentType)); + } + } +} diff --git a/tests/MassTransit.Tests/Transforms/SendTransform_Specs.cs b/tests/MassTransit.Tests/Transforms/SendTransform_Specs.cs index b1d48f7bea7..75e289a2ede 100644 --- a/tests/MassTransit.Tests/Transforms/SendTransform_Specs.cs +++ b/tests/MassTransit.Tests/Transforms/SendTransform_Specs.cs @@ -2,7 +2,6 @@ { using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -13,12 +12,15 @@ public class Transforming_a_message_when_sent_but_published : [Test] public async Task Should_not_affect_published_messages() { - await Bus.Publish(new A {First = "Hello"}); + await Bus.Publish(new A { First = "Hello" }); ConsumeContext result = await _received; - result.Message.First.ShouldBe("Hello"); - result.Message.Second.ShouldBe(null); + Assert.Multiple(() => + { + Assert.That(result.Message.First, Is.EqualTo("Hello")); + Assert.That(result.Message.Second, Is.Null); + }); } Task> _received; @@ -55,12 +57,15 @@ public class Transforming_a_message_when_sent : [Test] public async Task Should_change_the_property() { - await InputQueueSendEndpoint.Send(new A {First = "Hello"}); + await InputQueueSendEndpoint.Send(new A { First = "Hello" }); ConsumeContext result = await _received; - result.Message.First.ShouldBe("Hello"); - result.Message.Second.ShouldBe("World"); + Assert.Multiple(() => + { + Assert.That(result.Message.First, Is.EqualTo("Hello")); + Assert.That(result.Message.Second, Is.EqualTo("World")); + }); } Task> _received; @@ -97,12 +102,15 @@ public class Transforming_a_message_when_published : [Test] public async Task Should_transform_on_publish() { - await Bus.Publish(new A {First = "Hello"}); + await Bus.Publish(new A { First = "Hello" }); ConsumeContext result = await _received; - result.Message.First.ShouldBe("Hello"); - result.Message.Second.ShouldBe("World"); + Assert.Multiple(() => + { + Assert.That(result.Message.First, Is.EqualTo("Hello")); + Assert.That(result.Message.Second, Is.EqualTo("World")); + }); } Task> _received; @@ -139,12 +147,15 @@ public class Transforming_a_message_when_published_but_sent : [Test] public async Task Should_not_transform_on_send() { - await InputQueueSendEndpoint.Send(new A {First = "Hello"}); + await InputQueueSendEndpoint.Send(new A { First = "Hello" }); ConsumeContext result = await _received; - result.Message.First.ShouldBe("Hello"); - result.Message.Second.ShouldBe(null); + Assert.Multiple(() => + { + Assert.That(result.Message.First, Is.EqualTo("Hello")); + Assert.That(result.Message.Second, Is.Null); + }); } Task> _received; diff --git a/tests/MassTransit.Tests/Transforms/SetProperty_Specs.cs b/tests/MassTransit.Tests/Transforms/SetProperty_Specs.cs index 5ded410c4d2..ea555801d53 100644 --- a/tests/MassTransit.Tests/Transforms/SetProperty_Specs.cs +++ b/tests/MassTransit.Tests/Transforms/SetProperty_Specs.cs @@ -2,7 +2,6 @@ { using System.Threading.Tasks; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -13,12 +12,15 @@ public class Setting_a_property_on_the_original_message : [Test] public async Task Should_have_the_message_property() { - await InputQueueSendEndpoint.Send(new A {First = "Hello"}); + await InputQueueSendEndpoint.Send(new A { First = "Hello" }); ConsumeContext result = await _received; - result.Message.First.ShouldBe("Hello"); - result.Message.Second.ShouldBe("World"); + Assert.Multiple(() => + { + Assert.That(result.Message.First, Is.EqualTo("Hello")); + Assert.That(result.Message.Second, Is.EqualTo("World")); + }); } Task> _received; @@ -53,12 +55,15 @@ public class Setting_a_property_on_a_new_message : [Test] public async Task Should_have_the_message_property() { - await InputQueueSendEndpoint.Send(new A {First = "Hello"}); + await InputQueueSendEndpoint.Send(new A { First = "Hello" }); ConsumeContext result = await _received; - result.Message.First.ShouldBe("Hello"); - result.Message.Second.ShouldBe("World"); + Assert.Multiple(() => + { + Assert.That(result.Message.First, Is.EqualTo("Hello")); + Assert.That(result.Message.Second, Is.EqualTo("World")); + }); } Task> _received; @@ -93,18 +98,21 @@ public async Task Should_have_the_message_property() { Task> unmodified = await ConnectPublishHandler(); - await Bus.Publish(new A {First = "Hello"}); + await Bus.Publish(new A { First = "Hello" }); ConsumeContext result = await _received; ConsumeContext original = await unmodified; var tweaked = await _tweaked.Task; - result.Message.First.ShouldBe("Hello"); - result.Message.Second.ShouldBe("World"); - tweaked.Second.ShouldBe("World"); + Assert.Multiple(() => + { + Assert.That(result.Message.First, Is.EqualTo("Hello")); + Assert.That(result.Message.Second, Is.EqualTo("World")); + Assert.That(tweaked.Second, Is.EqualTo("World")); - original.Message.First.ShouldBe("Hello"); - original.Message.Second.ShouldBe(null); + Assert.That(original.Message.First, Is.EqualTo("Hello")); + Assert.That(original.Message.Second, Is.Null); + }); } Task> _received; @@ -150,19 +158,22 @@ public async Task Should_have_the_message_property() { Task> unmodified = await ConnectPublishHandler(); - await Bus.Publish(new A {First = "Hello"}); + await Bus.Publish(new A { First = "Hello" }); ConsumeContext result = await _received; ConsumeContext original = await unmodified; var tweaked = await _tweaked.Task; - result.Message.First.ShouldBe("Hello"); - result.Message.Second.ShouldBe(null); + Assert.Multiple(() => + { + Assert.That(result.Message.First, Is.EqualTo("Hello")); + Assert.That(result.Message.Second, Is.Null); - tweaked.Second.ShouldBe("World"); + Assert.That(tweaked.Second, Is.EqualTo("World")); - original.Message.First.ShouldBe("Hello"); - original.Message.Second.ShouldBe(null); + Assert.That(original.Message.First, Is.EqualTo("Hello")); + Assert.That(original.Message.Second, Is.Null); + }); } Task> _received; diff --git a/tests/MassTransit.Tests/Transforms/TransformClass_Specs.cs b/tests/MassTransit.Tests/Transforms/TransformClass_Specs.cs index c1c6232d800..e47a748b06d 100644 --- a/tests/MassTransit.Tests/Transforms/TransformClass_Specs.cs +++ b/tests/MassTransit.Tests/Transforms/TransformClass_Specs.cs @@ -3,7 +3,6 @@ using System.Threading.Tasks; using MassTransit.Configuration; using NUnit.Framework; - using Shouldly; using TestFramework; @@ -14,12 +13,15 @@ public class Using_a_class_to_define_a_transform : [Test] public async Task Should_have_the_message_property() { - await InputQueueSendEndpoint.Send(new A {First = "Hello"}); + await InputQueueSendEndpoint.Send(new A { First = "Hello" }); ConsumeContext result = await _received; - result.Message.First.ShouldBe("First"); - result.Message.Second.ShouldBe("Second"); + Assert.Multiple(() => + { + Assert.That(result.Message.First, Is.EqualTo("First")); + Assert.That(result.Message.Second, Is.EqualTo("Second")); + }); } Task> _received; diff --git a/tests/MassTransit.Tests/TypeCastRetry_Specs.cs b/tests/MassTransit.Tests/TypeCastRetry_Specs.cs index 43a91cde690..882dce04db7 100644 --- a/tests/MassTransit.Tests/TypeCastRetry_Specs.cs +++ b/tests/MassTransit.Tests/TypeCastRetry_Specs.cs @@ -22,7 +22,7 @@ public async Task Should_receive_the_message() protected override void ConfigureInMemoryBus(IInMemoryBusFactoryConfigurator configurator) { var sec5 = TimeSpan.FromSeconds(5); - configurator.UseRetry(x => x.Exponential(2, sec5, sec5, sec5)); + configurator.UseMessageRetry(x => x.Exponential(2, sec5, sec5, sec5)); base.ConfigureInMemoryBus(configurator); } diff --git a/tests/MassTransit.Transports.Tests/ConsumeBatch_Specs.cs b/tests/MassTransit.Transports.Tests/ConsumeBatch_Specs.cs deleted file mode 100644 index f5fbfb4987e..00000000000 --- a/tests/MassTransit.Transports.Tests/ConsumeBatch_Specs.cs +++ /dev/null @@ -1,88 +0,0 @@ -namespace MassTransit.Transports.Tests -{ - using System; - using System.Linq; - using System.Threading.Tasks; - using NUnit.Framework; - using Testing; - - - public class Consuming_a_batch_of_significant_size : - TransportTest - { - [Test] - public async Task Should_consume_the_sent_message() - { - var limit = 20; - - await Harness.InputQueueSendEndpoint.SendBatch(Enumerable.Range(0, limit).Select(x => new SubmitOrder {Id = NewId.NextGuid()})); - - await Harness.InactivityTask; - - Batch batch = await _batchConsumer[0]; - - Assert.That(batch.Length, Is.EqualTo(limit)); - Assert.That(batch.Mode, Is.EqualTo(BatchCompletionMode.Size)); - } - - TestBatchConsumer _batchConsumer; - - public Consuming_a_batch_of_significant_size(Type harnessType) - : base(harnessType) - { - } - - protected override Task Arrange() - { - _batchConsumer = new TestBatchConsumer(Harness.GetTask>()); - - var batchOptions = new BatchOptions - { - MessageLimit = 20, - TimeLimit = TimeSpan.FromSeconds(20) - }; - - Harness.Consumer(() => _batchConsumer, configurator => - { - configurator.Options(batchOptions); - }); - - Harness.OnConfigureReceiveEndpoint += x => - { - batchOptions.Configure(Harness.InputQueueName, x); - }; - - return Task.CompletedTask; - } - - - class TestBatchConsumer : - IConsumer> - { - readonly TaskCompletionSource>[] _messageTask; - - int _count; - - public TestBatchConsumer(params TaskCompletionSource>[] messageTask) - { - _messageTask = messageTask; - } - - public Task> this[int index] => _messageTask[index].Task; - - public Task Consume(ConsumeContext> context) - { - if (_count < _messageTask.Length) - _messageTask[_count++].TrySetResult(context.Message); - - return Task.CompletedTask; - } - } - - - class SubmitOrder - { - public Guid Id { get; set; } - } - } -} diff --git a/tests/MassTransit.Transports.Tests/ConsumerConcurrency_Specs.cs b/tests/MassTransit.Transports.Tests/ConsumerConcurrency_Specs.cs deleted file mode 100644 index b587ad35342..00000000000 --- a/tests/MassTransit.Transports.Tests/ConsumerConcurrency_Specs.cs +++ /dev/null @@ -1,192 +0,0 @@ -namespace MassTransit.Transports.Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using Testing; - - - public class Setting_a_concurrent_message_limit_of_one : - TransportTest - { - [Test] - public async Task Should_only_consume_one_message_at_a_time() - { - var orderId = NewId.NextGuid(); - - await Harness.InputQueueSendEndpoint.SendBatch(new[] - { - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = NewId.NextGuid() }, - new SubmitOrder { Id = orderId } - }); - - Assert.That(await _consumer.Consumed.Any(x => x.Context.Message.Id == orderId), Is.True); - - await _observer.StopEndpoint(); - - Assert.That(await _observer.ConcurrentDeliveryCount, Is.EqualTo(1)); - } - - ConsumerTestHarness _consumer; - EndpointObserver _observer; - - public Setting_a_concurrent_message_limit_of_one(Type harnessType) - : base(harnessType) - { - } - - protected override Task Arrange() - { - _observer = new EndpointObserver(Harness); - Harness.OnConfigureBus += cfg => - { - cfg.ConnectBusObserver(new BusObserver(Harness, _observer)); - }; - - Harness.OnConfigureReceiveEndpoint += x => x.ConcurrentMessageLimit = 1; - - _consumer = Harness.Consumer(); - - return Task.CompletedTask; - } - - - class BusObserver : - IBusObserver - { - readonly BusTestHarness _harness; - readonly EndpointObserver _observer; - - public BusObserver(BusTestHarness harness, EndpointObserver observer) - { - _harness = harness; - _observer = observer; - } - - public void PostCreate(IBus bus) - { - } - - public void CreateFaulted(Exception exception) - { - } - - public Task PreStart(IBus bus) - { - bus.ConnectReceiveEndpointObserver(_observer); - return Task.CompletedTask; - } - - public Task PostStart(IBus bus, Task busReady) - { - return Task.CompletedTask; - } - - public Task StartFaulted(IBus bus, Exception exception) - { - return Task.CompletedTask; - } - - public Task PreStop(IBus bus) - { - return Task.CompletedTask; - } - - public Task PostStop(IBus bus) - { - return Task.CompletedTask; - } - - public Task StopFaulted(IBus bus, Exception exception) - { - return Task.CompletedTask; - } - } - - - class EndpointObserver : - IReceiveEndpointObserver - { - readonly TaskCompletionSource _concurrentDeliveryCount; - readonly BusTestHarness _harness; - IReceiveEndpoint _receiveEndpoint; - - public EndpointObserver(BusTestHarness harness) - { - _harness = harness; - _concurrentDeliveryCount = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - } - - public Task ConcurrentDeliveryCount => _concurrentDeliveryCount.Task; - - public Task Ready(ReceiveEndpointReady ready) - { - if (ready.InputAddress == _harness.InputQueueAddress) - _receiveEndpoint = ready.ReceiveEndpoint; - - return Task.CompletedTask; - } - - public Task Stopping(ReceiveEndpointStopping stopping) - { - return Task.CompletedTask; - } - - public Task Completed(ReceiveEndpointCompleted completed) - { - if (completed.InputAddress == _harness.InputQueueAddress) - _concurrentDeliveryCount.TrySetResult(completed.ConcurrentDeliveryCount); - - return Task.CompletedTask; - } - - public Task Faulted(ReceiveEndpointFaulted faulted) - { - return Task.CompletedTask; - } - - public async Task StopEndpoint() - { - if (_receiveEndpoint != null) - await _receiveEndpoint.Stop(); - } - } - - - class SubmitOrderConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return Task.CompletedTask; - } - } - - - class SubmitOrder - { - public Guid Id { get; set; } - } - } -} diff --git a/tests/MassTransit.Transports.Tests/Logging/TestOutputLogger.cs b/tests/MassTransit.Transports.Tests/Logging/TestOutputLogger.cs deleted file mode 100644 index 03ea140f205..00000000000 --- a/tests/MassTransit.Transports.Tests/Logging/TestOutputLogger.cs +++ /dev/null @@ -1,78 +0,0 @@ -namespace MassTransit.Transports.Tests.Logging -{ - using System; - using Microsoft.Extensions.Logging; - using NUnit.Framework.Internal; - - - public class TestOutputLogger : - Microsoft.Extensions.Logging.ILogger - { - readonly TestOutputLoggerFactory _factory; - - readonly Func _filter; - object _scope; - - public TestOutputLogger(TestOutputLoggerFactory factory, bool enabled) - : this(factory, _ => enabled) - { - } - - public TestOutputLogger(TestOutputLoggerFactory factory, Func filter) - { - _factory = factory; - _filter = filter; - } - - public IDisposable BeginScope(TState state) - { - _scope = state; - - return TestDisposable.Instance; - } - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) - { - if (!IsEnabled(logLevel)) - return; - - if (formatter == null) - throw new ArgumentNullException(nameof(formatter)); - - var message = formatter(state, exception); - - if (string.IsNullOrEmpty(message)) - return; - - message = $"{DateTime.Now:HH:mm:ss.fff}-{logLevel.ToString()[0]} {message}"; - - if (exception != null) - message += Environment.NewLine + Environment.NewLine + exception; - - var currentContext = TestExecutionContext.CurrentContext; - - if (currentContext.CurrentTest is TestMethod) - _factory.Current = currentContext; - - var executionContext = _factory.Current ??= currentContext; - - executionContext.OutWriter.WriteLine(message); - } - - public bool IsEnabled(LogLevel logLevel) - { - return logLevel != LogLevel.None && _filter(logLevel); - } - - - class TestDisposable : IDisposable - { - public static readonly TestDisposable Instance = new TestDisposable(); - - public void Dispose() - { - // intentionally does nothing - } - } - } -} diff --git a/tests/MassTransit.Transports.Tests/Logging/TestOutputLoggerFactory.cs b/tests/MassTransit.Transports.Tests/Logging/TestOutputLoggerFactory.cs deleted file mode 100644 index dec733cd888..00000000000 --- a/tests/MassTransit.Transports.Tests/Logging/TestOutputLoggerFactory.cs +++ /dev/null @@ -1,32 +0,0 @@ -namespace MassTransit.Transports.Tests.Logging -{ - using Microsoft.Extensions.Logging; - using NUnit.Framework.Internal; - - - public class TestOutputLoggerFactory : - ILoggerFactory - { - readonly bool _enabled; - - public TestOutputLoggerFactory(bool enabled) - { - _enabled = enabled; - } - - public TestExecutionContext Current { get; set; } - - public Microsoft.Extensions.Logging.ILogger CreateLogger(string name) - { - return new TestOutputLogger(this, _enabled); - } - - public void AddProvider(ILoggerProvider provider) - { - } - - public void Dispose() - { - } - } -} diff --git a/tests/MassTransit.Transports.Tests/MassTransit.Transports.Tests.csproj b/tests/MassTransit.Transports.Tests/MassTransit.Transports.Tests.csproj deleted file mode 100644 index d5c91df383c..00000000000 --- a/tests/MassTransit.Transports.Tests/MassTransit.Transports.Tests.csproj +++ /dev/null @@ -1,22 +0,0 @@ - - - - net6.0 - - - - - - - - - - - - - - - - - - diff --git a/tests/MassTransit.Transports.Tests/Send_Specs.cs b/tests/MassTransit.Transports.Tests/Send_Specs.cs deleted file mode 100644 index 1d7abd2d320..00000000000 --- a/tests/MassTransit.Transports.Tests/Send_Specs.cs +++ /dev/null @@ -1,52 +0,0 @@ -namespace MassTransit.Transports.Tests -{ - using System; - using System.Threading.Tasks; - using NUnit.Framework; - using Testing; - - - public class Sending_a_message_directly_to_the_consumer : - TransportTest - { - [Test] - public async Task Should_consume_the_sent_message() - { - var orderId = NewId.NextGuid(); - - await Harness.InputQueueSendEndpoint.Send(new SubmitOrder {Id = orderId}); - - Assert.That(await _consumer.Consumed.Any(x => x.Context.Message.Id == orderId), Is.True); - } - - ConsumerTestHarness _consumer; - - public Sending_a_message_directly_to_the_consumer(Type harnessType) - : base(harnessType) - { - } - - protected override Task Arrange() - { - _consumer = Harness.Consumer(); - - return Task.CompletedTask; - } - - - class SubmitOrderConsumer : - IConsumer - { - public Task Consume(ConsumeContext context) - { - return Task.CompletedTask; - } - } - - - class SubmitOrder - { - public Guid Id { get; set; } - } - } -} diff --git a/tests/MassTransit.Transports.Tests/TransportTest.cs b/tests/MassTransit.Transports.Tests/TransportTest.cs deleted file mode 100644 index 3e39f907ad4..00000000000 --- a/tests/MassTransit.Transports.Tests/TransportTest.cs +++ /dev/null @@ -1,101 +0,0 @@ -namespace MassTransit.Transports.Tests -{ - using System; - using System.Threading; - using System.Threading.Tasks; - using Logging; - using NUnit.Framework; - using NUnit.Framework.Internal; - using Testing; - using Transports; - - - /// - /// Tests a transport capability that should be available on all transports. - /// - [TestFixture(typeof(ActiveMqTestHarnessFactory))] - [TestFixture(typeof(ArtemisTestHarnessFactory))] - [TestFixture(typeof(AmazonSqsTestHarnessFactory))] - [TestFixture(typeof(AzureServiceBusTestHarnessFactory))] - [TestFixture(typeof(GrpcTestHarnessFactory))] - [TestFixture(typeof(InMemoryTestHarnessFactory))] - [TestFixture(typeof(RabbitMqTestHarnessFactory))] - public abstract class TransportTest - { - static readonly bool _enableLog = !bool.TryParse(Environment.GetEnvironmentVariable("CI"), out var isBuildServer) || !isBuildServer; - static readonly TestOutputLoggerFactory LoggerFactory; - - static TransportTest() - { - LoggerFactory = new TestOutputLoggerFactory(_enableLog); - } - - protected BusTestHarness Harness; - TestExecutionContext _fixtureContext; - readonly ITestHarnessFactory _harnessFactory; - - protected TransportTest(Type harnessType) - { - _harnessFactory = (ITestHarnessFactory)Activator.CreateInstance(harnessType); - } - - /// - /// Override this method to setup the test harness features, such as sagas, consumers, etc. - /// - /// - protected abstract Task Arrange(); - - [OneTimeSetUp] - public async Task TransportTestSetUp() - { - Harness = await _harnessFactory.CreateTestHarness(); - - if (_enableLog) - { - Harness.OnConfigureBus += cfg => - { - LogContext.ConfigureCurrentLogContext(LoggerFactory); - - LoggerFactory.Current = default; - }; - } - - await Harness.Clean(); - - await Arrange(); - - _fixtureContext = TestExecutionContext.CurrentContext; - - LoggerFactory.Current = _fixtureContext; - - - await StartTestHarness(); - - await Task.Delay(200); - } - - async Task StartTestHarness() - { - using var source = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - - await Harness.Start(source.Token); - } - - [OneTimeTearDown] - public async Task TransportTestsTearDown() - { - LoggerFactory.Current = _fixtureContext; - - await StopTestHarness(); - - Harness.Dispose(); - } - - async Task StopTestHarness() - { - using var source = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - - await Harness.Stop(); - } - } -} diff --git a/tests/MassTransit.Transports.Tests/Transports/ActiveMqTestHarnessFactory.cs b/tests/MassTransit.Transports.Tests/Transports/ActiveMqTestHarnessFactory.cs deleted file mode 100644 index 0a7491593b6..00000000000 --- a/tests/MassTransit.Transports.Tests/Transports/ActiveMqTestHarnessFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit.Transports.Tests.Transports -{ - using System.Threading.Tasks; - using Testing; - - - public class ActiveMqTestHarnessFactory : - ITestHarnessFactory - { - public Task CreateTestHarness() - { - return Task.FromResult(new ActiveMqTestHarness()); - } - } -} diff --git a/tests/MassTransit.Transports.Tests/Transports/AmazonSqsTestHarnessFactory.cs b/tests/MassTransit.Transports.Tests/Transports/AmazonSqsTestHarnessFactory.cs deleted file mode 100644 index 256c42e7535..00000000000 --- a/tests/MassTransit.Transports.Tests/Transports/AmazonSqsTestHarnessFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit.Transports.Tests.Transports -{ - using System.Threading.Tasks; - using Testing; - - - public class AmazonSqsTestHarnessFactory : - ITestHarnessFactory - { - public Task CreateTestHarness() - { - return Task.FromResult(new AmazonSqsTestHarness()); - } - } -} diff --git a/tests/MassTransit.Transports.Tests/Transports/ArtemisTestHarnessFactory.cs b/tests/MassTransit.Transports.Tests/Transports/ArtemisTestHarnessFactory.cs deleted file mode 100644 index f7861527c00..00000000000 --- a/tests/MassTransit.Transports.Tests/Transports/ArtemisTestHarnessFactory.cs +++ /dev/null @@ -1,27 +0,0 @@ -namespace MassTransit.Transports.Tests.Transports -{ - using System; - using System.Threading.Tasks; - using Testing; - - - public class ArtemisTestHarnessFactory : - ITestHarnessFactory - { - public Task CreateTestHarness() - { - var harness = new ActiveMqTestHarness - { - HostAddress = new Uri("activemq://localhost:61618"), - AdminPort = 8163, - }; - - harness.OnConfigureActiveMqBus += cfg => - { - cfg.EnableArtemisCompatibility(); - }; - - return Task.FromResult(harness); - } - } -} diff --git a/tests/MassTransit.Transports.Tests/Transports/AzureServiceTestHarnessFactory.cs b/tests/MassTransit.Transports.Tests/Transports/AzureServiceTestHarnessFactory.cs deleted file mode 100644 index a8e1e7d3c42..00000000000 --- a/tests/MassTransit.Transports.Tests/Transports/AzureServiceTestHarnessFactory.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace MassTransit.Transports.Tests.Transports -{ - using System; - using System.Threading.Tasks; - using global::Azure; - using NUnit.Framework; - using Testing; - - - public class AzureServiceBusTestHarnessFactory : - ITestHarnessFactory - { - public async Task CreateTestHarness() - { - return new AzureServiceBusTestHarness(Configuration.ServiceEndpoint, new AzureNamedKeyCredential(Configuration.KeyName, Configuration.SharedAccessKey)); - } - - - static class Configuration - { - public static readonly Uri ServiceEndpoint = new Uri($"sb://{ServiceNamespace}.servicebus.windows.net/MassTransit.Transports.Tests"); - - public static string KeyName => - TestContext.Parameters.Exists(nameof(KeyName)) - ? TestContext.Parameters.Get(nameof(KeyName)) - : Environment.GetEnvironmentVariable("MT_ASB_KEYNAME") ?? "MassTransitBuild"; - - public static string ServiceNamespace => - TestContext.Parameters.Exists(nameof(ServiceNamespace)) - ? TestContext.Parameters.Get(nameof(ServiceNamespace)) - : Environment.GetEnvironmentVariable("MT_ASB_NAMESPACE") ?? "masstransit-build"; - - public static string SharedAccessKey => - TestContext.Parameters.Exists(nameof(SharedAccessKey)) - ? TestContext.Parameters.Get(nameof(SharedAccessKey)) - : Environment.GetEnvironmentVariable("MT_ASB_KEYVALUE") ?? "YfN2b8jT84759bZy5sMhd0P+3K/qHqO81I5VrNrJYkI="; - } - } -} diff --git a/tests/MassTransit.Transports.Tests/Transports/GrpcTestHarnessFactory.cs b/tests/MassTransit.Transports.Tests/Transports/GrpcTestHarnessFactory.cs deleted file mode 100644 index 5c4911ceab6..00000000000 --- a/tests/MassTransit.Transports.Tests/Transports/GrpcTestHarnessFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit.Transports.Tests.Transports -{ - using System.Threading.Tasks; - using Testing; - - - public class GrpcTestHarnessFactory : - ITestHarnessFactory - { - public Task CreateTestHarness() - { - return Task.FromResult(new GrpcTestHarness()); - } - } -} diff --git a/tests/MassTransit.Transports.Tests/Transports/ITestHarnessFactory.cs b/tests/MassTransit.Transports.Tests/Transports/ITestHarnessFactory.cs deleted file mode 100644 index 23afc7f5efc..00000000000 --- a/tests/MassTransit.Transports.Tests/Transports/ITestHarnessFactory.cs +++ /dev/null @@ -1,11 +0,0 @@ -namespace MassTransit.Transports.Tests.Transports -{ - using System.Threading.Tasks; - using Testing; - - - public interface ITestHarnessFactory - { - Task CreateTestHarness(); - } -} diff --git a/tests/MassTransit.Transports.Tests/Transports/InMemoryTestHarnessFactory.cs b/tests/MassTransit.Transports.Tests/Transports/InMemoryTestHarnessFactory.cs deleted file mode 100644 index 410200dd828..00000000000 --- a/tests/MassTransit.Transports.Tests/Transports/InMemoryTestHarnessFactory.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace MassTransit.Transports.Tests.Transports -{ - using System.Threading.Tasks; - using Testing; - - - public class InMemoryTestHarnessFactory : - ITestHarnessFactory - { - public Task CreateTestHarness() - { - return Task.FromResult(new InMemoryTestHarness()); - } - } -} diff --git a/tests/MassTransit.Transports.Tests/Transports/RabbitMqTestHarnessFactory.cs b/tests/MassTransit.Transports.Tests/Transports/RabbitMqTestHarnessFactory.cs deleted file mode 100644 index ef97098abc8..00000000000 --- a/tests/MassTransit.Transports.Tests/Transports/RabbitMqTestHarnessFactory.cs +++ /dev/null @@ -1,37 +0,0 @@ -namespace MassTransit.Transports.Tests.Transports -{ - using System; - using System.Net.Http; - using System.Net.Http.Headers; - using System.Text; - using System.Threading.Tasks; - using Testing; - - - public class RabbitMqTestHarnessFactory : - ITestHarnessFactory - { - public async Task CreateTestHarness() - { - var harness = new RabbitMqTestHarness(); - - await EnsureVirtualHostExists(harness); - - return harness; - } - - static async Task EnsureVirtualHostExists(RabbitMqTestHarness harness) - { - var name = harness.GetHostSettings().VirtualHost; - if (string.IsNullOrWhiteSpace(name) || name == "/") - return; - - using var client = new HttpClient(); - var byteArray = Encoding.ASCII.GetBytes($"{harness.Username}:{harness.Password}"); - client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", Convert.ToBase64String(byteArray)); - - var requestUri = new UriBuilder("http", harness.HostAddress.Host, 15672, $"api/vhosts/{name}").Uri; - await client.PutAsync(requestUri, new StringContent("{}", Encoding.UTF8, "application/json")); - } - } -} diff --git a/tests/MassTransit.Transports.Tests/docker-compose.yml b/tests/MassTransit.Transports.Tests/docker-compose.yml deleted file mode 100644 index e7a9696ea3c..00000000000 --- a/tests/MassTransit.Transports.Tests/docker-compose.yml +++ /dev/null @@ -1,41 +0,0 @@ -version: '3' - -services: - rabbitmq: - image: masstransit/rabbitmq:latest - ports: - - "5672:5672" - - "15672:15672" - activemq: - image: masstransit/activemq:latest - environment: - - "ACTIVEMQ_ADMIN_LOGIN=admin" - - "ACTIVEMQ_ADMIN_PASSWORD=admin" - - "ACTIVEMQ_LOGGER_LOGLEVEL=TRACE" - - "ACTIVEMQ_OPTS=-Xms512m -Xms512m" - - "ACTIVEMQ_CONFIG_SCHEDULERENABLED=true" - ports: - - 8161:8161 - - 61616:61616 - - 61613:61613 - artemis: - image: hugoham/artemis:2.16.0 - hostname: artemis - ports: - - '61618:61616' - - '8163:8161' - localstack: - image: localstack/localstack - ports: - - "4566:4566" - - "4571:4571" - - "${PORT_WEB_UI-8080}:${PORT_WEB_UI-8080}" - environment: - - SERVICES=${SERVICES- } - - DEBUG=${DEBUG- } - - DATA_DIR=${DATA_DIR- } - - PORT_WEB_UI=${PORT_WEB_UI- } - - LAMBDA_EXECUTOR=${LAMBDA_EXECUTOR- } - - KINESIS_ERROR_PROBABILITY=${KINESIS_ERROR_PROBABILITY- } - - DOCKER_HOST=unix:///var/run/docker.sock - - HOST_TMP_FOLDER=${TMPDIR}